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をつけないサンプルでしたが今後はもう少し実務的なサンプルも上げれればいいかなと思ってます。

あ~、長い記事だった

DS2のパッケージにおけるコンストラクタの概念の話

まずDS2のpackageって何って方は忘備録の記事を読んでください。

・DS2プロシジャ入門8:ユーザー定義パッケージ
http://sas-boubi.blogspot.jp/2015/08/ds28.html

DS2のリファレンス(現時点では英語版しかないので辛い)で、packageのところを読んでいると
やたらConstructor(コンストラクタ)っていう単語がでてくる。純粋にSASしか経験してないと、前に紹介したオーバーロードとかもそうだけどオブジェクト指向の用語をいきなりマニュアルで出して、DS2でコードではこうなるからって説明されても、みんな困ると思う。

で、コンストラクタって何かって言うと
「オブジェクト指向の言語において、クラスからオブジェクトを作成した際に、
自動実行されるメソッドで、メンバ変数の初期化などについて行います。」みたいな説明になる。

それをDS2で考えるなら、pakkageからデータステップ内でinstance(インスタンス)を生成するタイミング、つまりは、declareでパッケージに参照名をつけて使えるようにした時点で先に自動実行されるメソッドだと考えればいいはず。

忘備録のサンプルにコンストラクタを加えてみると以下のような感じになる


*** パッケージの作成 ;
proc ds2 libs=work;
   package MYPACK / overwrite=yes;
   dcl double VAR1 ;
       method MYPACK() ;
          put 'NOTE:コンストラクタが実行されたんだよ';
 VAR1=1;
       end;


       method TEST () returns double;
          return VAR1+10;
       end;
       method TEST (double VAR1) returns double;
          return VAR1+10;
       end;

   endpackage;
   run;
quit;


*** パッケージを使う ;
proc ds2 libs=work;
   data _NULL_;
       dcl package MYPACK mp();
       method init();
           dcl double OUTVAR;
           OUTVAR = mp.TEST();
           put '******' OUTVAR '******';     
           OUTVAR = mp.TEST(100);
           put '******' OUTVAR '******';     
       end;
   enddata;
   run;
quit;


結果は以下。










パッケージの作成の部分で
       method MYPACK() ;
          put 'NOTE:コンストラクタが実行されたんだよ';
 VAR1=1;
       end;
     
というコードを追加している。
大事なのは、このメソッドの親になってるパッケージの名前もMYPACKだということ。
つまりパッケージと同じ名前のメソッドを中に入れれば、それがコンストラクタになるということ(のはず)
で、そのコンストラクタの中で、メッセージをputするのと、V1に1という初期値を代入している。

以下、前回紹介したオーバーロードの話で、引数がある場合とない場合、両方についてメソッドを定義する
【参考】
★SASマクロに比べてDS2のメソッドが優れている点★の一つ、オーバーロード(多重定義)が可能なこと
http://sas-tumesas.blogspot.jp/2016/03/sasds2.html

       method TEST () returns double;
          return VAR1+10;
       end;
       method TEST (double VAR1) returns double;
          return VAR1+10;
       end;
     
メソッドにパラメータがあれば+10するし、なければ、その時点のVAR1に+10した値を返すということです。


で、パッケージを使う部分で最初にdcl package MYPACK mp();(これがインスタンスを作るってこと)でコンストラクタが動いて、V1に1がセットされます。
だから、最初のOUTVAR = mp.TEST(); で返ってくる値が11なんですね。

あんまり、うまい説明ではないですが、何か間違ってるところがあれば教えてください。

使い方を見てると、パッケージの構成として、メインの処理を実行するメソッドと、それに必要なパラメータをセットするメソッドを分けて設定しているものが多く、コンストラクタではそのパラメータの初期値をいれているという使い方が一般的のようです。
(次の記事もパッケージ絡みの予定なので、またそこで例示します)

まだ何とも言えませんが、多分、集団開発においても、全員がDS2をきちんと理解さえしていればパッケージを共有して、うまく管理できればマクロより作業効率あがる気がします。
SASマクロは複雑になってくると、展開形を頭の中で描くのがかなり困難になりますが、DS2の場合はトレースしやすい感じがします。ただ、全員がDS2を理解するっていうのがとても大変そう。


あと、少し文句を言わせていただくと、DS2プロシジャのエラーメッセージがわかりにくすぎです。なんでそのコードが駄目なのか
さっぱりわからない。もうちょっと優しく指南して欲しい




DS2のifステートメントの話

DS2のifステートメントが、通常のデータステップでの用法に加えてさらに柔軟な書き方ができるようなので自分用メモ。

proc ds2 libs=work;
data _null_;
declare double a b c;
declare char(10) d;

method init();

/*例1*/
a=1;
b=2;
c = b + (if a < 2 then + a else - a );
put ' ①-1 ' c=;

a=3;
b=2;
c = b + (if a < 2 then + a else - a );
put ' ①-2 ' c=;

/*例2*/
a=4;
b=2;
c= if b ne 0 then a / b else null;
put ' ②-1 ' c=;

a=4;
b=0;
c= if b ne 0 then a / b else null;
put ' ②-2 ' c=;


/*例3*/
a=3;
d = if . < a <= 2 then 'C'
else if 2< a <=3 then 'B'
else if 3 < a then 'A'
else NULL
;

put ' ③-1 ' d=;

a=4;
d = if . < a <= 2 then 'C'
else if 2< a <=3 then 'B'
else if 3 < a then 'A'
else NULL
;
put ' ③-2 ' d=;

/*例4*/
a=1;
b=2;
if a=1 then
if b=2 then put 'メッセージA';
else put 'メッセージB';
else put 'メッセージC';

end;
enddata;
run;
quit;

















例1 c = b + (if a < 2 then + a else - a );

は今までのSASだと
if a < 2 then c= b + a;
else c = b -a;

例2 意味は例1と同じだけど0除算防止はよく使う

例3 これも使えそう、

例4 then doなくていいんだ。すごく違和感だけど通る

注意点として1~3共通だけど、変数=if文って表現やるときは
かならずelseステートメントつけて、文法上でパターンを網羅した形に
しとかないとエラーになるみたい(実際にelseを通ることがありえなかったとしても)


ちなみにselect whenでも同じようにかける
例1なら
c = b + select when(a<2) + a otherwise - a end;
みたいに書ける。

ちなみにちなみにelse ifはelifと書ける。

今回の話とはあんまり関係ないですが
なんかDS2って最早、他のオブジェクト指向の言語で使う表現がすっと通って、旧来のデータステップの表現だとはじかれるイメージ(他言語知らないくせに言うのもアレだけど)があります

他の言語習得してからSASやるのにDS2を介すといいかも。あるいはSASから他言語学ぶのも
DS2をちらっとやってからだとスムーズなのかも。

もしかしたらDS2が普及すると、他言語からの学習コストが一気に下がり、ユーザーも増えて
SASプログラマーの特殊な立ち位置というか、ある種の希少価値や既得権益(笑)がなくなるのかもしれませんね。
若い人とかに「あのオッサン、未だに全部古いデータステップで書いてて、勘弁して欲しいよ」とか言われたらどうしよ。

あんまり宣伝しない方がいいか(笑)

★SASマクロに比べてDS2のメソッドが優れている点★の一つ、オーバーロード(多重定義)が可能なこと

良くも悪くもSASはプログラミング言語としてシンプルかつ、かなりトラディッショナル(上品な言い方にしてます)なとこがあります。
SASに慣れた人からするとSASマクロの仕組みにそれほど疑問は抱かないかもしれないですが、モダンな他言語をやっている人から見ると結構、えぇ~ってとこがあるみたいです。

そのひとつが今回紹介するオーバーロードという機能です。
簡単にいうと、引数の数が違ったり、型は違うんだけど、処理の内容は似ている機能を
同名で多重に定義してまとめることができるということです。
それによって呼び出された際に引数に応じて適切な処理が行われるので、呼び出し側で余計なこと考えなくていいし、似た名前の機能が乱立しない、条件分岐ではなく独立しているので管理しやすい等のメリットがあります

SASマクロは、同じ名前で定義するとフツーに上書きされてしまうので、マクロ処理の中に引数の数や型を判定する分岐を入れるか、別マクロを増やすしかないんですね。極端な例を見てみましょう。

ある部署では、2つの数をかけるという処理が多かったので、以下のようなマクロを作っていました

%macro v1(p1,p2);
 &p1 * &p2;
%mend v1;

次のように使っています。

data a;
x=%v1(3,2)
run;

ところが、ある時に3つの数をかける処理もでてきました。

ここで選択肢は2つあります。

①今まで使っていた%v1の内容を以下のように変える

%macro v1(p1,p2,p3);
%if &p3 ne %str() %then &p1 * &p2 * &p3;
%else &p1 * &p2;
%mend v1;

上記でOKですが、他のメンバーから、色んなところで使っている%v1マクロの中身を変えたら、今まで正常に動いていた箇所もチェックする必要があって怖いという意見がでます。

じゃあ、②新しいマクロを増やす

%macro v2(p1,p2,p3);
 &p1 * &p2 * &p3;
%mend v2;

を作って引数3の時はv2を使おうとなりました。

ところがしばらくすると、引数4の場合もでてきました。
さらに引数が文字のケースも増えてきました。さらに.....
といった度に、分岐を増やすか、マクロを新規にたてるかとやっているうちに、パラメーターの化け物かっていうマクロが生まれたり、共有マクロライブラリの中身が似たような名前のマクロでカオスにっていうのはよくある話ですよね。


これをDS2で考えて見ましょう。マクロカタログはパッケージに、マクロはメソッドになるとイメージしてください。

data Q1;
a=2;
b=3;
c=4;
d='1';
e='9';
run;

というテストデータがあります。

そして以下のコードでpackという名前のパッケージの中に、vという名前のメソッドを3ついれています。これが今までの%macro ~%mendで定義していたような箇所にあたると考えておいてください。
今はパッケージをworkにつくっているので、SASを閉じれば消えます。消したくなければ永久ライブリに作ってください。

proc ds2 libs=work;
package pack/overwrite=yes;

method v(double p1, double p2) returns double;
return p1 * p2 ;
end;

method v(double p1,  double p2,  double p3) returns double;
return p1 * p2 * p3 ;
end;

method v(char p1,  char p2) returns double;
return inputn(p1,'best.') * inputn(p2,'best.');
end;

endpackage;
run;
quit;

で注目は同じ名前でvを3つ作ってますがv( )の括弧内の部分、ここが引数の設定なのですが、そこがそれぞれ違います。
1つ目は引数が二つで、数値の場合
2つ目は引数が三つで、数値の場合
3つ目は引数が二つで、文字の場合
です

さて、では実際、定義したメソッドを呼び出して使ってみましょう

proc ds2 libs=work;
data A1/overwrite=yes;
declare double x1 x2 x3;
declare package pack p();
keep x1 x2 x3;
method run();
set Q1;
x1=p.v(a,b);
x2=p.v(a,b,c);
x3=p.v(d,e);
end;
enddata;
run;
quit;

結果は






となって、引数ばらばらなのに全部vメソッドで処理できました。ということです。

もし、エラーが起きたら、どの引数パターンの時に起きたかをたどれば、どのメソッドに問題があるかわかるのでデバックも楽ですし、同名で追加するだけなので、今までの正常に動いているメソッドに触る必要もありません。

どうです?そろそろDS2、気になってきませんか?

★DS2導入のわかりやすいメリット★-マルチスレッド処理が超簡単、でかいデータの処理やシュミレーションとかに良し!thread

趣味で書いたり、完全に一人で仕事してる人を除いて、組織の中でSASを書くというのは常に何かしらルールに縛られるものですよね。

コードは共有物であったりするわけで、やれハッシュオブジェクトやFCMPやSQLやと様々なやり方を組み合わせてコードを書くと仮にそれが処理上の最善手であったとしても「おい、他のメンバーが読めないもの書くな」ってなるわけです。当然です。

新しい書き方を導入するには、何故それを使うべきなのか、メリットデメリット、学習コスト等を偉い人に説明して、OKもらったら、メンバーにも説明しなきゃならないわけですね。

DS2プロシジャは、個人的には中々イケてる仕組みだと思いますが、メリットをうまく説明して導入にOK貰うのが大変そうだな~って思いました。学習にかかる時間はやっぱりそれなりに大きそうですからね。
で、メリットを説明するのに、HadoopがとかJSONがとかって言っても、うちそんなん使ってないからとか、それなに?で終わる上司もいる気がするし、FedSQLが便利なんですといったって、パススルーでsql飛ばせるだろ、libnameエンジンでいけるだろうと言ってきそうだし、パッケージだメソッドだオブジェクトだと言ったところで、何それ?全部SASマクロで頑張れるだろうって言われちゃうかもなわけです。

まあ、それはそれでいいんですけどね。こっちも、好き勝手DS2で書けたら楽しそうだな~っていう程度の気持ちで薦めてて、細かい理由は後付けだったりするんで。

で、僕が思うのは、そういうごり押ししたい時の簡単な説明として、難しい話はせず、ポイントも1点に絞ってみるのはどうでしょう。以下の感じです。

マルチスレッド処理が簡単に書けて、処理時間がすげー短くなって、効率いいっすよ。
マルチスレッドがわからない?ひとつの処理を後ろでいくつかに分けて同時並行で処理して後で統合するんですわ。シュミレーションとかもいっぱい回せますよ!っていう切り口はどうでしょう?

なんでかというとSPD Serverとか使ってない限り、SASでマルチスレッドの処理書くのってすげー大変なんですね(対応してるプロシジャ除く)。そして、古い人ほどそれをよく知ってるからです。


threadはあくまでDS2でできることの一側面に過ぎないし、それだけでブレイクスルーになるか?
っていう意見はわかりますが、まあいいじゃないですか。入れてしまえばこっちの勝ち?なんだから。

あ、ちなみに全部冗談ですからね。

さて本題、以下のデータセットがあるとします。

data A;
array ar{100};
do i= 1 to 10000000;
do j = 1 to 100;
ar{j}=rand('uniform');
end;
output;
drop i j;
end;
run;

100万obsの100変数です

普通のSASの書き方で、セットします。

data A1;
set A end=eof;
count+1;
if eof then put count "obs読み込みました";
drop count;
run;

ログで処理時間を見てみます。


















んん?結構かかってますね。まあSAS on Demandじゃなくて製品版ならもっと早いはずですけどね。

同じことを普通にDS2で書いて見ます

proc ds2 libs=work;
data A2/overwrite=yes;
   dcl double count;
   drop count;
   method run();
set A;
count +1;
   end;
   method term();
    put count 'obs読み込みました';
   end;
enddata;
run;
quit;

ログで処理時間を見てみます。




















すでにだいぶ早いなぁ、海外のPaperとか見る限り、シングルの処理で書いたら通常ステップと同じくらいのはずなんでけどなぁ。
まあいいや。

次にマルチスレッドで処理するために、一旦処理をthreadというもので定義します。
これはpakageと同じ概念で、一旦workや永久ライブラリに処理内容をデータ化したものをおきます。

proc ds2 libs=work;
/*スレッド処理定義部分*/
thread th/overwrite=yes;
   dcl double count;
   drop count;
   method run();
set A;
count +1;
   end;
   method term();
      put 'スレッド番号' _threadid_ 'が' count 'obs読み込みました';
   end;
endthread;
run;
quit;

上記の実行は0.0何秒で終わります。実際に処理をしているわけじゃなく、処理内容を定義しただけだからです。

そして以下のコードで上で定義したthreadを呼び出して展開します

proc ds2 libs=work;
/*スレッド処理の呼び出し*/
data A3(overwrite=yes);
   dcl thread th t;
   method run();
   set from t threads=3;
   end;
enddata;
run;
quit;

ここで注目はthreads=3のところで、ここで実際に分割する数を指定して見ます。

で結果、






















「スレッド番号〇がXXXXobs読み込みました」

に注目です。全部で100万の処理を、SASが適当に分けてやったことが確認できます。
(この分け方はSASが適当に決めるので毎回変わります)

処理時間も18秒が11秒なので、4割近く減ってます。ただのsetですらね。

ただ、ちょっと注意なのはSAS on demandはブラウザ経由でどっかのサーバーで回してるから
実行する時によって結構時間にムラがあります。
うまく差が出た時のスクショとっておきましょう。逆転することがざらにあるので、、。
あと調子悪いと実行終わらなかったりするので、その時は10万くらいでやってみましょう。
まあ、本来こういうでかい処理はon demandでやるもんじゃないよね。

コードの説明を少しだけ

スレッド処理定義部分の
thread th/overwrite=yes;
は、以下の処理をthというスレッド処理としてworkに保存しますよ、もう一回実行したら
上書きしますよって意味です。
_threadid_ は、自動変数で、実際実行された時、のスレッド番号が0から振られます

スレッド処理の呼び出しの方で  dcl thread th t; でthにtという名前を振って定義しています
そしてスレッドを使ってデータを取得する場合 set from として、次に定義した t で threads=3;です

かなり簡単じゃないですか?
今回はただのsetですが、ds2がわかっていれば基本どんな処理でもかけるので、大規模データ処理やシュミレーションに持って来いです。

まあ、マルチスレッドを活かすにはやっぱりハード性能もそこそこ要りますが、そこは後でこっそり言っておきましょう。