最近DS2の話が多くて、全然興味ないな~って人が多いと思うので、ちょっとだけ挑発的なタイトルにしてみました。
大げさに言ってみただけで悪意はないです。sortプロシジャ大好きです。
一般的に、SASプログラムの実行速度を上げたいとか、コードの見通しをよくしたいとかって場合、環境によって対応は大きく違います。
ただ、どんな環境でも大体共通で、一番手っ取り早く効果的なのはproc sortを削ることなんじゃないかと僕は考えています。
SASのデータステップは1オブザベーション読み込んで、1オブザベーション出力する動作の連続を基本としているため、インプットデータの格納順、つまりソート状態をとっかかりにした仕掛けが多いです。
象徴的なのはmergeステートメントです。
by変数を使って、いわゆるマージキーを指定する場合、
merge対象のデータセットが全てby変数で事前にソートされている必要があります。
そのため、SASプログラムにはやたらとproc sort がでてきます。
それはある意味SASプログラムの特徴といえますし、mergeステートメントはデータステップの基本であり、様々な結合条件も表現できるので必須の存在です。
ただし、ただしですね、実務において、データハンドリングの結合処理の大半は、片方のデータセットを全て残しつつ、キーが一致したオブザベーションのみもう片方からデータを取得するという片側外部結合で実現できることが多くないでしょうか?
メインとなるデータに対して、別のマスタからID的なもので必要な情報をくっつけていくみたいな。
そういった極めて単純な結合、全てに対して、ソート→ソート→マージを繰り返すことが本当に必要なんでしょうか?
例えば以下に3つのデータセットがあります
data Q1;
X=3;Y=2;output;
X=1;Y=3;output;
X=2;Y=2;output;
X=3;Y=1;output;
X=1;Y=1;output;
run;
data Q2;
X=1;Y=1;Z='A';A=1;B=2;output;
X=1;Y=3;Z='B';A=1;B=2;output;
X=1;Y=2;Z='C';A=1;B=2;output;
X=3;Y=1;Z='D';A=1;B=2;output;
X=2;Y=1;Z='E';A=1;B=2;output;
run;
data Q3;
Z='B';W=1;C='A';output;
Z='C';W=2;C='A';output;
Z='A';W=3;C='A';output;
run;
Q1のオブザベーションを全て残した上で、X YをキーにしてQ2のZを
取得、取得したZを使ってQ3からWを取得、XとWを足して変数Vを追加、
最終的なデータセットはX Yでソート、なおA B Cは不要
という要求を実現する場合、以下のように書く人が多いはずです。
proc sort data = Q1;
by X Y;
run;
proc sort data = Q2(drop=A B) out=_Q2;
by X Y;
run;
data A1;
merge Q1(in=in1)
_Q2;
by X Y;
if in1;
run;
proc sort data=A1;
by Z;
run;
proc sort data=Q3(drop=C ) out=_Q3;
by Z;
run;
data A2;
merge A1(in=in1)
_Q3;
by Z;
if in1;
V=sum(X,W);
run;
proc sort data=A2;
by X Y;
run;
ちなみにソートプロシジャのdata=で指定しているデータセットにオプションでkeepやdropを加えることで余計な変数を落とし、パフォーマンスを上げれることは比較的広く知られているテクニックです
上記の処理は7ステップで、うち5ステップはproc sortです。
しかも最初のソートと最後のソートは全く同じソート条件です。
まあ、データ量が多くなければ、全て一瞬で終わる話なのでいいのです。
SASを知っていれば、さほど読みにくいということもないでしょう。
別にこれでいいんです。
いいんですけど、僕はずっとこういう書き方が正解だとは到底思えないなぁと感じながら仕事してました。やたらsortばっかしてんなぁっと。
あくまで僕の意見ですが、こういった結合が普段から業務でたくさんでてくるのであれば、共通で利用するマクロライブラリに1個
%macro get(master=,key=,var=);
%let name = &sysindex;
%let qkey = %sysfunc( tranwrd( %str("&key") , %str( ) , %str(",") ) );
if 0 then set &master(keep= &key &var);
if _N_=1 then do;
declare hash h&name.(dataset:"&master(keep= &key &var)", duplicate:'E');
h&name..definekey(&qkey);
h&name..definedata(all:'Y');
h&name..definedone();
end;
if h&name..find() ne 0 then do;
call missing(of &var);
end;
%mend get;
のようなマクロを加えておけば
今後全て、以下の2ステップで済むわけです。
(赤字の部分、追記しました。マスターにキー重複がある場合、安全のためエラーにしちゃった方がいいかなと思って。僕が実際使っているのはそうしてたので)
出力順に指定がなければsortプロシジャ一回もかかずともいけます
上記マクロは結合先、元ともにソートされている必要がありません。
data AA1;
set Q1;
%get(master=Q2,key=X Y,var=Z)
%get(master=Q3,key=Z,var=W)
V=sum(X,W);
run;
proc sort data = AA1;
by X Y;
run;
結果は同じです
(上記マクロは説明用の簡易な例です。var=で指定した変数がバッティングしている
場合、keyマッチしないobsの該当部分は欠損になるので、マスターのvar=で指定している変数がsetでしている変数にもともとある場合は使わないでね。
実際、僕が業務で使っているのは、かなり多機能にしてゴテゴテしいので、お好みに応じて
好きにカスタマイズしてください)
次の場合をみてみましょう。以下の2つのデータセットがあります
data Q4;
X=1;Y=2;output;
X=2;Y=2;output;
X=3;Y=3;output;
X=4;Y=1;output;
X=4;Y=5;output;
run;
data Q5;
Y=2;Z=1;output;
Y=2;Z=3;output;
Y=1;Z=1;output;
Y=5;Z=5;output;
Y=5;Z=5;output;
run;
ここでQ4について、Yの値が、Q5にも存在する場合、新規変数FLGに1を、存在しない場合0を代入して新しいデータセットを作りたいと思ったとします。
その場合、mergeとin=オプションを利用すればいいわけですが、そのままだと
「NOTE: MERGE ステートメントに BY 値を繰り返すデータセットが複数あります。」になって
うまくいかないので、以下のように書くと思います
proc sort data=Q4;
by Y;
run;
proc sort data=Q5(keep=Y) out=_Q5 nodupkey;
by Y;
run;
data A3;
merge Q4
_Q5(in=ina)
;
by Y;
FLG=ina;
run;
proc sort data=A3;
by X;
run;
これも、個人的にはひっかかります、存在有無を調べて、0-1変数作るだけなのに
なぜマージが必要なのかと。
例えば以下のようなマクロをつくっておけば、チェックするのは1行のコードで
事足りるわけです。
%macro chk(master=,key=,fl=);
%let name = &sysindex;
%let qkey = %sysfunc( tranwrd( %str("&key") , %str( ) , %str(",") ) );
if 0 then set &master(keep= &key);
if _N_=1 then do;
declare hash h&name.(dataset:"&master(keep= &key)", multidata:'Y');
h&name..definekey(&qkey);
h&name..definedone();
end;
&fl = ifn(h&name..check()=0,1,0);
%mend chk;
data AA2;
set Q4;
%chk(master=Q5,key=Y,fl=FLG)
run;
proc sort data=Q4;
by X;
run;
さらに次のケースは、ID値などに重複が発生してないかをみるパターンです。
先ほどのQ4ついて、XとYを足してZという変数を追加したい。
ついでにYの値に重複がないかをみたい、しかし、最終的なデータセットはXでソートしていて欲しい。
という要求があった場合、以下のようにかけます。
proc sort data=Q4;
by Y;
run;
data A4;
set Q4;
by Y;
if first.Y ^=1 then put "WARNING:重複あり" Y=;
Z=X+Y;
run;
proc sort data=A4;
by X;
run;
が、結局、重複をみるためだけにソートを打っているのが無駄です。もしQ4が巨大なデータだったら2回もソートするのはかなり時間かかるはずです。
これもよくある処理なので、マクロ化しておけば、一発です。
%macro dupchk(key);
%local name qkey;
if _N_=1 then do;
%let name = &sysindex;
%let qkey = %sysfunc( tranwrd( %str("&key") , %str( ) , %str(",") ) );
declare hash h&name();
h&name..definekey(&qkey);
h&name..definedone();
end;
if h&name..check() = 0 and cmiss(of &key) = 0
then put "WARNING:重複あり" +2 (&key.) (=);
else if cmiss(of &key) = 0 then do;
h&name..add();
end;
%mend dupchk;
proc sort data=Q4;
by X;
run;
data AA3;
set Q4;
%dupchk(Y);
Z=X+Y;
run;
と、結局、ハッシュオブジェクト万歳の記事じゃないか!って感じですけど(笑)。
まあ、でも実際、ソート削リたいけど、SQLだと書きにくい、データステップ的に
書きたいって時にはやっぱりハッシュいいんですよ。
得意な人が幾つかマクロ化してあげて、とりあえずはブラックボックスでもいいから使ってみるのがいいと思います。
さて、今回ハッシュオブジェクトをたくさん取り上げたのですが、実はこれは次からDS2における
ハッシュパッケージを説明する布石だったのです。
ハッシュオブジェクトも充分凄いんですが、オブジェクト指向でないSAS言語に無理やり
ぶち込んでる感があるため、ちょっと不自然に感じる部分もあります(不具合ではない)。
ところがDS2はオブジェクト指向を考えて構成されているので、ハッシュとの親和性が高く、
とても自然です。
不自然さの一例をあげると、例えば今回紹介したマクロなんかでも、if _N_=1 then do;の中で
ハッシュオブジェクトを生成したりしています。これは、ほっとくとSASの性質上、1オブザベーション読み込む度に生成されてしまうため、しょうがなく、SASの都合に合わせて、そう書いてるわけです。
だから、ifステートメントを書いて、その中には今回のマクロたちを入れてはいけません。
それが_N_=1のオブザベーションをスルーする条件の場合、宣言がされないままメソッドを
実行しようとしてエラーになります(回避する書き方はありますが)。
ところが、DS2にはinitメソッドっていうのがあって、かならずrunメソッドの前に
1度だけ実行されますよね?そこでハッシュパッケージからインスタンスを生成すればいいんです。
とても自然な流れです。コードと思想が一致しています
どうでしょう?少しは興味をそそることができたでしょうか?