くらべてみました。ハッシュオブジェクトのfindメソッドとDS2ハッシュパッケージのfindメソッド。

ハッシュオブジェクトとDS2のハッシュパッケージについて、大体おんなじ感覚でかけますが、
DS2の方がより仕組みが洗練されている感じがします。

感覚的な話で終わってもいけないので、代表的なfindメソッドでその違いを見てみましょう。

以下の2つのデータセットがあって

data Q1;
X=1;Y=1;output;
X=1;Y=2;output;
X=2;Y=2;output;
X=3;Y=1;output;
run;










data Q2;
A=2;B=2;C=1;D=4;output;
A=3;B=1;C=2;D=3;output;
A=1;B=1;C=3;D=2;output;
A=3;B=3;C=4;D=1;output;
run;










ここでQ1のX,YとQ2のA Bをひも付けて、Q1にQ2からCとDを取得して、
それぞれV Wという変数に割り当てろという処理を考えてみます。

ハッシュオブジェクトなら、以下のようになります。

data A1;
if 0 then set Q2;
set Q1;
if _N_=1 then do;
declare hash h1(dataset:'Q2');
h1.definekey('A','B');
h1.definedata('C','D');
h1.definedone();
end;
if h1.find(key:X,key:Y) ^=0 then do;
call missing(of C D);
end;
V=C;
W=D;
drop A B C D;
run;










if h1.find(key:X,key:Y) ^=0 then do;
call missing(of C D);
end;

の箇所について、ちょっと小洒落た書き方をしていますが、そもそも
なぜこんな書き方をするのかというと

単純にh1.find(key:X,key:Y);とすると、キーが見つからない2行目がエラーになって
データセットが作成されません。

rc=h1.find(key:X,key:Y);とすればエラーにはなりませんが、直前の成功行の値が引延されてしまいます。

まあ、そのへんの話は
記事:ハッシュオブジェクトの世界①
などで断片的に説明しているので今回は割愛。
ただ、キーマッチしない場合のことを考えなきゃならなくて面倒だということはわかってください。

また、注目なのはV=C;W=D;の2行。割当ステートメントではなくrenameで処理してもいいんですが
昔から、このリネーム処理を果てしなく無駄に感じていました。keyの方はkey:指定で、データステップでの変数とハッシュオブジェクトのkey変数の変数名が異なっても寄せることはできますが、dataの方はできないんですよね。

さて、いよいよDS2で書いてみます。

proc ds2 libs=work;
data A4(overwrite=yes);
declare double A B C D X Y V W;
declare package hash h1();
drop A B C D;
method init();
h1.dataset('Q2');
h1.keys([A B]);
h1.data([C D]);
h1.definedone();
end;
method run();
set Q1;
h1.find([X Y],[V W]);
end;
enddata;
run;
quit;

う、美しい。
h1.find([X Y],[V W]);のように、最初の引数でハッシュのキーに紐づく変数、次にデータに紐付ける変数を指定できます。
さらにキーマッチしない場合の処理を書かなくても、エラーにもならず、引き伸ばしも起きません。
この素晴らしさをどう伝えていいのかわからない。






PROC SQL では LIMIT 句が使えない けどPROC FEDSQLでは使えるよ。ついでにOFFSET句も。の話

データ解析備忘録の記事
【SAS】PROC SQL では LIMIT 句が使えない
http://y-mattu.hatenablog.com/entry/2016/04/20/153534

を見て、なんかFEDSQLではできそうだなぁと思って調べたら、やっぱりできました。
ちなみに PROC FEDSQLっていうのは9.4から導入されたプロシジャで、PROC SQLの拡張版みたいなもんです(適当)。
SAS on demand で実行する場合、DS2と同じでlibs=workがいりますが、製品版だといらないみたい。

data A;
do X=1 to 1000;
output;
end;
run;

proc fedsql libs=work;
select X from A limit 100;
quit;

これで100obsだけアウトプットされます。

ちなみにoffsetというものもあって、これは最初の指定オブザベーションをスキップするオプションです

proc fedsql libs=work;
select X from A offset 100;
quit;

とするとX=101からアウトプットされます。

ちなみにDS2では、データセットの指定部分に直接SQLを書けますが、
そこで書くSQLはFEDSQLになります。

そのため以下のようなコード書いても通ります。

proc ds2 libs=work;
data B;
method run();
set {select X from A limit 100};
end;
enddata;
run;
quit;

ハッシュオブジェクトとDS2ハッシュパッケージの差異_定義周りについて

さて、通常のデータステップではハッシュオブジェクトを利用することができます。
一方、DS2にはハッシュパッケージというものがあります。

実際に使ってみるとわかりますが、まあ、基本ほぼ同じといってしまっていいと思います。
データステップからDS2は随分違うというか、別言語ですが、ハッシュオブジェクトとハッシュパッケージはノリが同じです。
ハッシュオブジェクトをある程度使っている方であれば、ほとんど勉強しなおさなくても
同じ感覚で使いこなせると思います。

しかし、全く同じというわけではないので、あまり体系だてては無理ですが、、少しずつ解説していきたいと思います。

ただ、残念ながら僕もまだ勉強中で、半分、自分の学習のために記事を書いていくので、内容に誤りがあったり、もっといいやり方を見つけた場合は、後日、別記事で修正、更新しますのでそこはあしからずです。

今日はハッシュオブジェクトの宣言部分と、ハッシュパッケージの宣言部分を並べて見比べてみましょう。

例えば以下のデータセットQ1があって、

data Q1;
A=1;B=2;C='A';D=5;output;
A=2;B=3;C='B';D=6;output;
A=3;B=4;C='C';D=7;output;
A=1;B=2;C='D';D=8;output;
run;

そのデータセットをハッシュオブジェクト(パッケージ)の中にA Bをキー、 A B C Dをデータとして定義して、重複キーを許容した上でキー値でソートして格納して、別データセットに出力という、ソートプロシジャ使えよっ!ていう感じの、処理としてはあまり意味のないものを例にします(結果はすべて同じなので画像は割愛)。

まずハッシュオブジェクトで書くなら以下の感じでしょうか

data _NULL_;
if 0 then set Q1;
declare hash h1(dataset:'Q1',multidata:'YES',ordered:'YES');
h1.definekey('A','B');
h1.definedata('A','B','C','D');
h1.definedone();
h1.output(dataset:'A1');
stop;
run;

一方、DS2で書く場合、以下のようにかけます

proc ds2 libs=work;
data _NULL_;
declare double A B D;
declare char C;
declare package hash h1();
method init();
h1.dataset('Q1');
h1.definekey('A');
h1.definekey('B');
h1.definedata('A');
h1.definedata('B');
h1.definedata('C');
h1.definedata('D');
h1.multidata('YES');
h1.ordered('YES');
h1.defineDone();
h1.output('A2');
end;
enddata;
run;
quit;

おおっ、なかなか面白いですね。ハッシュオブジェクトでdataset:のように指定していた(タグパラメータ)の部分が独立してメソッドになっているんですね。これはこっちの方が書きやすいし、構造を把握しやすいです。

しかし、multidataやorderedのようなオプションで1回設定するようなものはさておき、keyやdataを1個1個メソッドで指定するのは流石に面倒なので、そこについてはkeysメソッドとdataメソッドというものが別途準備されていて、これは変数リストを指定できるので、以下のようにかけます。

proc ds2 libs=work;
data _NULL_;
declare double A B D;
declare char C;
declare package hash h1();
method init();
h1.dataset('Q1');
h1.keys([A B]);
h1.data([A B C D]);
h1.multidata('YES');
h1.ordered('YES');
h1.defineDone();
h1.output('A3');
end;
enddata;
run;
quit;

さらに僕はこの書き方はあまり好きではありませんが、ハッシュパッケージを宣言するdeclare packageのところでカッコ内に順番に引数を設定することで、以下の定義メソッドでやることと同じことをまかなえるようになってます

文法としては

declare package hash 任意のハッシュ名(キー変数,データ変数,ハッシュ割当サイズ,格納するデータセット名または{SQLソース},ordered,duplicate,suminc,multidata);

となっています。つまり今回の場合であれば以下のようにもかけます。

proc ds2 libs=work;
data _NULL_;
declare double A B D;
declare char C;
declare package hash h1([A B],[A B C D],8,'Q1','YES','','','YES');
method init();
h1.output('A4');
end;
enddata;
run;
quit;

上記の引数で指定する書き方は、キー変数とデータ変数の部分を省略したり、データ変数省略したりなど亜種の書き方があって計4種類の指定の仕方があるのですが、その辺については興味のある方はリファレンスのなどで調べてみてください。
引数が何を意味しているかわかりにくいのでこの書き方はあんまりだと思いますが。

僕がまだ解決できていない疑問はoutputメソッドの際に、overwrite指定ができないのかなってとこです。
それは困るけどなぁ。事前にproc deleteでもしろってことかなぁ。どなたか何か知ってたら教えてください。



DS2のtermメソッドの話

ハッシュオブジェクトやってた時もそうなんですが、実際に現場で活用できるプログラムを書けるようになるまで、勉強のため、基礎的で単純な処理のコードを延々と書きました。

正直、やってる時は、今までのデータステップでもやれるようなことを敢えて、別の文法で一人で黙々と書いて、どこに使うわけでもなく、何か意味あんのかなって気持ちにもなりました。
けど、結局そういった不毛に思える練習をやっておこないと、いざ今までのデータステップを超える処理を書こうって時に書けない、というか超えるということを発想することすらできないんですよね。

というわけで、今日もDS2の基礎的で面白くない話をしましょう。

ハッシュパッケージに入る前にtemメソッドについて少しだけ。

おさらいですが、DS2は基本的にメソッドというものをうまく組み合わせてプログラム全体を構築していきます。
メソッドはユーザーが自由に定義できるユーザー定義メソッドと、あらかじめ用意されているメソッドに分けられます。
あらかじめ用意されているメソッドのうち、特定のパッケージに依存するもの(hashパッケージやsqlstmtパッケージ等)を除いた、基本となるメソッドをシステムメソッドというそうです。

システムメソッドはinitruntermsetparmsです。

initとrun、termメソッドの説明については忘備録を参考にしてください。

DS2プロシジャ入門1:基本構文
http://sas-boubi.blogspot.jp/2015/05/ds2.html

setparmsはスレッドのところでちらっとでてきました。
http://sas-tumesas.blogspot.jp/2016/04/ds2threadset.html

で、今回はtermメソッドを取り上げます。

例えば、以下のようなデータセットがあって

data Q1;
do x = 1 , 3 , 4 , 7 , 8  , 9 ;
output;
end;
run;













そこから、平均である






のデータセットを作れと言われたら、まあ方法は山ほどあって

例えばプロシジャで

proc summary data=Q1;
var x;
output out=A1(keep=mean_x) mean=mean_x;
run;

とか

proc sql noprint;
create table A2 as
select mean(x)as mean_x
from Q1;
quit;

でいけますが、もしデータステップでやれと言われたなら

data A3;
set Q1 end=eof;
sum_x+x;
count+1;
if eof then do;
mean_x=sum_x/count;
output;
end;
keep mean_x;
run;

みたいにかけます。少し丁寧に書きましたが、もっと手をぬくなら

data A3;
set Q1 end=eof;
mean_x+x;
if eof then do;
mean_x=mean_x/_N_;
output;
end;
keep mean_x;
run;

でもいけますね。


さて、これをDS2で書くならどうなるかという話です。
データステップでは、setステートメントにend=オプションをつけることで
ファイル終端にフラグ変数を当てることができて、これによって、
最終行の際に処理を分岐することができました。

いっぽうDS2にはendオプションはありません。代わりにtermメソッドが存在し、
そこに書いたものが最後の処理として実行されます。
ただ、後で説明してますが、厳密にはendで書く処理とtermメソッドは実行タイミングがちょっと違うので注意が必要です。

以下のコードを見てください。

proc ds2 libs=work;
data A4(overwrite=yes);
declare double sum_x mean_x count test;
retain sum_x count;
method init();
count=0;
sum_x=0;
test=99;/*いらないけど説明用*/
end;
method run();
set Q1;
sum_x =sum_x + x;
count =count + 1;
end;
method term();
put _ALL_;
mean_x = sum_x / count;
output;
end;
enddata;
run;
quit;


実行すると、目的のデータセットが得られるコードで、正解です。
termとは反対に、処理の一番最初に実行されるinitメソッドで初期値0をいれ、
runメソッドで通常のデータステップと同じ処理を書き、
最後にtermメソッドで計算してoutputしています。

ちょっと説明したいことがあったのでinitメソッドでtestという変数に99を割り当てています。

termメソッドでput _ALL_としているので、その時点で(PDVにって言っていいのかな??)存在する変数をログにだします。
ログを見てみると、




まず、initで値を入れてるのに、testがnullになっているのは何故でしょうか?

それは、terrmメソッドには、グローバル変数をリセットする働きがあるからです。
SAS忘備録でも解説されています
http://sas-boubi.blogspot.jp/2015/06/ds22.html

リセットされないのは以下になります
・ SAS側があらかじめ定義している_N_などの変数
・ RETAINのような値を保持するようにしてる変数
・ パッケージ変数

sum_xとcountがnullになっていないのはretainステートメントで指定していたからなんですね。


で、次に注目なのは、_N_=7ですね。nullじゃないのは、_リセットされない変数ルールの1つめに該当するからですね。Q1は6オブザベーションなので、もしデータステップでend=オプションの分岐で_N_をputしていれば6です。
Termメソッドは、仮想的に最終オブザベーションの次の、存在しないオブザベーションにポイントしている感じになります。

なので、データステップの時のようにsum_x/_N_で平均が取れると思ってると間違ってしまいます。
やるならsum_x/(_N_-1)ですね。

もしretainステートメントを直接書かなくても以下のように
sum_x + x; count + 1;のように合計ステートメントで

proc ds2 libs=work;
data A5(overwrite=yes);
declare double sum_x mean_x count test;
method run();
set Q1;
sum_x + x;
count + 1;
end;
method term();
mean_x = sum_x / count;
end;
enddata;
run;
quit;

としてもretainと同じで、値の引き継ぎ効果ということでtermメソッドのリセットを免れます。

そして、もう一つ、別の方法としてパッケージを使い、そのパッケージ内で定義された変数をメソッドで取得してもOKです。

proc ds2 libs=work;
package pac1/overwrite=yes;
declare double sum_x count;
method 
pac1();
count=0;
sum_x=0;
end;
method msum(double inx);
   count = count + 1;
   sum_x = sum_x + inx;
end;
method avg() returns double;
return(sum_x/count);
end;
endpackage;

data A6(overwrite=yes);
declare double mean_x;
declare package pac1 p1();
method run();
set Q1;
p1.msum(x);
end;
method term();
put _ALL_;
mean_x = p1.avg();
output;
end;
enddata;
run;
quit;

結果は同じになります。
pac1というパッケージを作り、オブザベーション数合計(count)と値合計(sum_x)を格納するグローバル変数を定義、コンストラクタ(method pac1)に値への0セット。
msumメソッドにcountとsum_xへの加算機能を持たせ、avgメソッドはsum_xとcountから平均を計算して戻り値とする機能を持たせています。

パッケージを呼び出しているdata A6のステップだけみると、メソッドにxを与え、最後に値を取得するだけで、処理の中身自体は完全に分離していることがよくわかります。DS2っぽいですよね。

ちなみにオマケですが、DS2はsetにSQLを入れ込めるので、以下のコードも通りますからね。

proc ds2 libs=work;
data A7(overwrite=yes);
declare double mean_x;
method run();
set {select mean(x) as mean_x from Q1};
end;
enddata;
run;
quit;



大半のソートは百害あって一利無しという話

最近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度だけ実行されますよね?そこでハッシュパッケージからインスタンスを生成すればいいんです。
とても自然な流れです。コードと思想が一致しています


どうでしょう?少しは興味をそそることができたでしょうか?




monotonic関数でアイタタタ

monotonic関数は超便利です。
SQLプロシジャの中でいわゆるrow_number的、SASで言うなら_N_的な処理ができる(ちょと違うけど)。
通常のデータステップでも、例えば、3,5,8オブザベーションを抽出って時に

where _N_ in (3,5,8)って書き方はできないから
if _N_ in (3,5,8) になるけど、データを絞る場合サブセットifよりwhereの方が
効率的なわけで、where使いたいな~って思うんだけど

monotonic関数なら

where monotonic() in (3,5,8)で実現できてしまう。

もちろん、プロシジャに流すデータセットも絞れる優れもの。
proc print data=XX;
where monotonic() in (3,5,8)
run;

そんな手軽さもあって日ごろ愛用しているですが、この関数はちょっと色々と癖というか謎がありまして、
そのことについてはSAS忘備録のa.matsuさんが注意喚起してくれています。
http://sas-boubi.blogspot.jp/search/label/%E9%96%A2%E6%95%B0%3A%20MONOTONIC

今回はそこに注意事項を追加することになりそうです。


いや、monotonic関数については完全に把握している、複雑な処理じゃなきゃ問題ないでしょって驕っていたせいで原因箇所の特定にかなりてこずってしまいました。
生兵法は大怪我のもとってやつですよ。ほんと。

以下のようなデータセットがあり

data Q1;
do x = 1 to 10;
output;
end;
run;




















まずは以下を実行

data A1;
set Q1;
where monotonic() >= 3;
run;
















問題なし。

次に以下を実行

data A2;
set Q1;
where monotonic() <= 6;
run;














問題なく、狙い通り。

じゃあ次は上記二つをあわせて、3~6を抽出してみましょう

data A3;
set Q1;
where 3 <= monotonic() <= 6;
run;














なんで、7とか8が入ってくんの!?

ヒントはログの
NOTE: データセット WORK.Q1 から 6 オブザベーションを読み込みました。
      WHERE (3<=MONOTONIC()) and (MONOTONIC()<=6)」という記述です。
これは、where 3 <= monotonic() <= 6が実行時にSASによって
 WHERE (3<=MONOTONIC()) and (MONOTONIC()<=6)と直されて実行されたことを示します。


当然以下も同じ結果でアウト

data A4;
set Q1;
where 3 <= monotonic() and monotonic() <= 6;
run;

さて、アホな僕は何が起きたかすぐにはわかりませんでしたが、
聡明なみなさんはこのカラクリに既に気がついてますね?


whereステのmonotonicはそこが実行された時点からカウント再スタートな感じで、次のmonotonicはその時点からのカウントなんですね。
つまり3オブザベーション目も含めて6オブザベーション取得するから上記の結果になってるわけですよ。

なるほどね。関数実行時に動的に値が決まるのか、その決まり方を僕がわかってなかったんですね

つまり以下のように書けばいいのか

data A5;
set Q1;
where monotonic() between 3 and 6;
run;











betweenが嫌いならinで書いてもよし

data A6;
set Q1;
where monotonic() in ( 3 : 6 );
run;

なるほどね~。
しかし、これは危ないな。

where 3 <= monotonic() and monotonic() <= 6;

where monotonic() between 3 and 6;

とで結果が違うんだもんなぁ。

僕がSASの試験問題マニアックに作るなら入れたいような問題ですね

DS2のthreadでマルチスレッド処理を実行する際に内部にsetを含む場合と含まない場合の話

termメソッドの使い方で思うところがあったので、それを説明しようと考える

テストデータ作ろう

そういや今までデータセットはDS2の外で普通に作ってたな。せっかくだからDS2で作ろうか

テストデータ大量に作って、パフォーマンス測定したい人もいそうだからどうせなら
スレッド(thread)で作るか

そういや前スレッドの説明したときはsetステートメントを含むスレッドだったな~。
含まない時と挙動違うんだよな。
参考記事:http://sas-tumesas.blogspot.jp/2016/03/ds2thread.html


ということで今回はsetを含まないスレッドの話になりました。


スレッド内にsetがあった場合、SASがその参照先を適当に分けて、平行で処理して、後で縦にガッチャンコされます。
(ちなみに分割→処理→縦結合なので、スレッド内の処理書く時に、全体を通して値を縦に引き継いだりするretain系の処理いれてると分断されて、思ったとおりにならないから気をつけて)

前回はスレッド作成と呼び出しを分けましたが、今回は1つのDS2プロシジャステップ内にいれてます(どっちでもいいです)
さて、以下のコードを実行した場合、Q1は何オブザベーションになるでしょうか?

proc ds2  libs=work;
/*スレッド定義*/
thread mkdata/overwrite=yes;
declare double i x;
method init();
streaminit(777);
do i = 1 to 5;
x=rand('uniform');
output;
end;
end;
endthread;
run;
/*スレッド使用*/
data Q1(overwrite=yes);
dcl thread mkdata mkdata;
method run();
set from mkdata threads=3;
end;
enddata;
run;
quit;























正解は15obsでした。
1スレッド5obs×3スレッドでした。へ~。
わざとiをdropしてないので何が起きてるかわかりやすいはずです。
あと、横道にそれますが、コールルーチンがDS2にはないので
call streaminit()ではなくstreaminit関数でいいのです。違和感ですね

ただ、位置はここでいいのかな~?スレッド分重複で実行される?まあそれでも問題ないけど。
実行の方で設定する?でもそっちのinitでやってもスコープ?違うくて影響ないみたい。

まあいいや、本筋に戻ります。
当然、1スレッドのオブザベーション数をパラメーターにしたいという要望もあるでしょう。
その場合、スレッド定義にメソッドのときのように括弧でパラメータを設定します。
そして、使用する前にパラメータに値をセットするのはserparamsメソッドというものを使います。


proc ds2  libs=work;
/*スレッド定義*/
thread mkdata(double obs)/overwrite=yes;
declare double i x ;
method init();
streaminit(777);
do i = 1 to obs;
x=rand('uniform');
output;
end;
end;
endthread;
run;
/*スレッド使用*/
data Q2(overwrite=yes);
dcl thread mkdata mkdata;
method init();
mkdata.setParms(10000);
    end;
method run();
set from mkdata threads=3;
run;
quit;


で30000obsできます。

以下はオマケ、以下2つスレッドを作ってますが、スレッドの中に合計ステートメントを入れるか外に入れるかでどう違ってしまうかを確認できます。

proc ds2  libs=work;
/*スレッド定義1*/
thread omake1/overwrite=yes;
declare double sum ;
method run();
set Q2;
sum + x;
end;
endthread;
run;
/*スレッド定義2*/
thread omake2/overwrite=yes;
method run();
set Q2;
end;
endthread;
run;

/*スレッド使用1*/
data A1(overwrite=yes);
dcl thread omake1 omake1;
method run();
set from omake1 threads=3;
end;
enddata;
run;

/*スレッド使用2*/
data A2(overwrite=yes);
dcl thread omake2 omake2;
dcl double sum;
method run();
set from omake2 threads=3;
sum+x;
end;
enddata;
run;

quit;

ちなみにA2の最終obsのsumは

proc means data=Q2 sum;
var x;
run;

と一致します。


じゃあ、なんだかこれを見てるとスレッドを利用してグループ集計処理とかもできないのって?感じですが、
どうもそうでもないようで、いずれ紹介しますが、byでlast.出力で、byの途中の中途半端なところでスレッドが切れないようにやってくれるみたいですが、まだ検証中なので乞うご期待です。
(その認識であってますか?知ってたら情報ください)

DS2におけるキーワードthisとforward

SASの通常のデータステップには、変数のスコープ(有効範囲)って概念がありません。
というか変数の宣言がないです。

まあ、マクロ変数での%local %globalがあって、難しいちゃ難しい世界ですけど、そんなに複雑なマクロ考えない場合はあんまり関係ないし、よくわからないまま問題なくプログラム書けてる場合も多いはずです。

ところがDS2でプログラム書く時には、スコープを強く意識しないといけません。

次のコードは、1変数1obsで変数xに0の値が入ったデータセットA1を作成するためのコードですが
実行すればエラーになります。

proc ds2 libs=work;
data A1(overwrite=yes);
method run();
dcl double x;
    x = 0;
output;
end;
enddata;
run;
quit;





なぜなのか?
dcl(declare)ステートメントでxを宣言してますが、それがrun()メソッドの中に書かれています。
メソッド内でのdcl、或いはパラメータとして宣言された変数はローカル変数となります。
ローカル変数はスコープがメソッド内に限定されるため、その外側には存在し得ないので
データセットに残ることもできません。なので、何も変数なしでデータセット作ろうとしたのでエラー
ということです。

なので、以下のように宣言の位置をメソッドの外にだしてやります。

proc ds2 libs=work;
data A1(overwrite=yes);
dcl double x;
method run();
    x = 0;
output;
end;
enddata;
run;
quit;

これでOKです。

このあたりは、やっぱり忘備録の記事が秀逸なので、不安のある方はそちらで復習してください。
・DS2プロシジャ入門2:変数の宣言
http://sas-boubi.blogspot.jp/2015/06/ds22.html


で、やっとここから本題への繋ぎなんですが、ひとつのステップの中で、グローバル変数とローカル変数に同じ名前をつけたいケースがでてきます。
例えば、何らかのメソッドで値をパラメーターで受け取ったりして、加工して、他のメソッドに渡したい場合などにメソッドの外に、消さずに出したいわけですので、グローバル変数に格納するわけです。
メソッドを経由する都度、別の名前になるようにしていたら、わけがわからなくなります。
意味として同一のものは同じ名前にしておかないといけません。

ところが実際コーディングしてみると、変数名は同じなので、それがグローバル変数をさしているのか、ローカルをさしているのかがわからなくなります。
そこで、グローバル変数を明示的に指定する場合に変数名の前にthis.というものをつけて区別しようというわけです。

以下のコードはSASのリファレンスから抜き出して一部加工したサンプルです

proc ds2 libs=work;
data A2(overwrite=yes);
declare double x; /* declare global x */
method run();
declare double x; /* declare local x */
this.x = 1; /* assign 1.0 to global x */
    x = 0; /* assign 0.0 to local x */
output; /* global x with 1.0 output */
end;
enddata;
run;
quit;







グローバル、ローカル両方でxを定義し、両方に値を代入していますが、this.をつけたxだけが
グローバル変数と解釈され、処理されるので結果にも1の方が残ります。


さて、ここからが本題です。

例えば以下のデータセットがあって

data Q1;
x=1;output;
x=3;output;
x=5;output;
x=8;output;
x=2;output;
x=8;output;
x=1;output;
x=4;output;
run;


順次オブザベーションを読み込んでいき、1obsごとにそれまでのxの値の累積合計を
ログにputする。ただしx=8となったときは累積合計を0にリセットするという仕様のプログラムを
作りたいと思います。しかもその仕組みは流し込むデータセットを変えたり、他の処理も後で
追加したいため、パッケージ化したいという要望があるとします。

さて、どんな風にメソッドを作りパッケージを組み、どうやって呼び出してやりましょうか?

書き方はいくらでもあるでしょうが、基本的な考え方として、機能ごとにメソッドを分けてやれば
大体正解の場合が多いです。

そこで、累積合計の計算で1メソッド、ログへのputで1メソッドの構成を考えます。
ついでに、実は今回は無くてもいいのですが、コンストラクタの実行について追加で説明しておきたいことが
あるのでコンストラクタに処理が開始される旨のメッセージと、累積合計の初期化(=0)を入れておきましょう。
別に無くてもいいといったのは、今回合計の計算にsum関数使うので、初期時点の累積合計値が欠損でも大丈夫なのでということです

はい、じゃあできました、こんな感じかな

proc ds2 libs=work;
package pkg_mes /overwrite=yes;
dcl varchar(100) message;
dcl double total;

method pkg_mes();
total=0;
setmes('コンストラクタでの生成メッセージ:total=',0);
putmes();
end;

method setmes(varchar(90) message , double num );
total = sum(total,num);
if num= 8 then  total=0;
this.message = message || total;
end;

method putmes() ;
put message;
end;
endpackage;

run;
quit;











はい、エラー!コンパイルできずにパッケージは作られませんでした。

なんでか?

それはコンストラクタpkg_mesメソッドの中でsetmes(計算部分)とputmes(出力部分)を呼び出しているのですが、この時点でsetmesとputmesはまだ定義コードが読み込まれていないので、SASはメソッドだと思えずに、括弧があるから関数だと思って呼び出そうとするけど、そんな関数ないからエラーになります。その下にすぐメソッド定義書いてるから、ちょっと待ってといっても聞いてくれません。

なので、コードの順番を入れ替えてpkg_mesメソッドを他2つのメソッドの下に記述すればいいんですけど、それは気持ち悪い!なんで、一番最初に実行される部分を、最後にかかなきゃならんのか。
だいたいメソッドは入れ子になることが多いのに、その度に並び替えゲームするわけにいかないですよ。
SASマクロは、どれだけ入れ子にしても展開される時点で存在してればよく、コーディングの順番を意識する必要がなかっただけに違和感がありますね。

で、解決法はいたって簡単、後で記述されているメソッドの中身を先行するメソッド内で記述する場合、グローバルの領域に「forward メソッド名」と記述して、あらかじめSASに、forward指定しているのは後で定義してるメソッドだから慌てんなといってあげればよい。

ということで、書きなおし

proc ds2 libs=work;
package pkg_mes /overwrite=yes;
dcl varchar(100) message;
dcl double total;

forward setmes;
forward putmes;

method pkg_mes();
total=0;
setmes('コンストラクタでの生成メッセージ:total=',0);
putmes();
end;

method setmes(varchar(90) message , double num );
total = sum(total,num);
if num= 8 then  total=0;
this.message = message || total;
end;

method putmes() ;
put message;
end;
endpackage;

run;
quit;





でOK


やっと呼び出しまで来た。
今回はinitメソッドはいらないんだけど、initメソッドとコンストラクタの実行だと
コンストラクタの方が早い(dclの時点だから)を再確認して欲しいので、あえていれてます

proc ds2 libs=work;
data _null_;
dcl package pkg_mes p();
method init();
p.setmes('init内での生成メッセージ:',0);
p.putmes();
end;
method run();
set Q1;
p.setmes('run内での生成メッセージ:',x);
p.putmes();
end;
enddata;
run;
quit;

結果、















というわけです。

今回はただ、putするだけで、データセットを作成しないのでメソッドにreturnをつけないサンプルでしたが今後はもう少し実務的なサンプルも上げれればいいかなと思ってます。

あ~、長い記事だった