転置して連結してマージみたいな処理を一切ソートせずに行ってみる

以前の記事、「大半のソートは百害あって一利無しという話」
http://sas-tumesas.blogspot.jp/2016/04/blog-post.html

が結構反響あって、最近よくハッシュオブジェクトについて質問されることが多いです。

昔はよく、職場で提案したら「導入のメリットがわからない」「コードが難しそうだから」とかで一蹴されちゃいました!とかってネガティブな報告をもらってました。

近頃では、やっと導入するようになりました!という話や、単純な使用法から一歩踏み込んで、multidataやハッシュ反復子オブジェクト関連の質問までもくるので、日本でもようやく普及してきたのかなぁと嬉しく思います。次の僕の興味はDS2に傾いちゃってますが…。

ともあれ、まだハッシュオブジェクト使っていない方に、興味を持ってもらうには、具体的な使用例をできるだけ多く上げていくのがいいのかなと思ったので、最近質問があってハッシュを使った例を紹介してみます

以下の2つのデータセットがあったとします

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











data Q2;
x=1;y=1;output;
x=1;y=2;output;
x=1;y=3;output;
x=3;y=4;output;
x=3;y=5;output;
x=4;y=6;output;
run ;













Q1に、xをキーにしてQ2のyをzっていう名前で取得する、複数ある場合は「,」で連結してねっていう処理を考えます。
出力したいのは以下の形です











多分、以下のような流れで書く方が多いんではないでしょうか

proc sort data=Q1;
by x;
run;
proc sort data=Q2;
by x;
run;
proc transpose data=Q2 out=_Q2 prefix=y_;
var y;
by x;
run;
options missing='';
data A0;
merge Q1(in=in1)
 _Q2;
by x;
if in1;
z=catx(',',of y_:);
keep x z;
run;
options missing='.';

上記の場合、4ステップで、sortが2回、transposeが1回入っています。

これをハッシュオブジェクトを使うと1ステップで記述できます

data A1 ;
length x 8. z $200.;
 if _n_ = 1 then do ;
 if 0 then set Q2 ;
 dcl hash h1 (dataset: "Q2", multidata: "y") ;
 h1.definekey ("x") ;
 h1.definedata ("y") ;
 h1.definedone () ;
 end ;
 set Q1 ;
 do rc = h1.find() by 0 while (rc = 0) ;
  z=CATX(',',z,y);
  rc= h1.find_next() ;
 end ;
keep x z;
run ;

結果は同じです。

ハッシュオブジェクトのmultidataは昔に一度紹介したことがありますがそれと同じですね。

「ハッシュオブジェクトの世界⑧ keyの重複を許容する multidata find_nextメソッド」


 do rc = h1.find() by 0 while (rc = 0) ;の部分は
ちょっと小洒落た、というかスカした書き方ですね。意味がわからない場合は過去記事のように
単純にわけて書いてOKです

ちょっと説明すると、ようはfindメソッドと、fined_nextメソッドの戻り値判定を一つにまとめてるんですね。
こういう風にifとかdoにメソッドの戻り値をもってくる書き方してもメソッドは実行されるんですね。
by 0にしてるのは、これをつけないとデフォルトで1ずつ増えてしまうからです。
rc=0、つまりメソッドがキーを見つけれている間はずっとのループを表現してるんですね。

前準備のためのソートや転置が一切不要になっているので、コードとしては効率化できたといえると思います。

例では文字列連結しましたが
catxの部分の処理を変えれば、例えばyの合計値や平均値をマージするプログラムも簡単にかけるわけですね。(DS2ならもっと簡単で、sqlで計算してからdataset指定できますけど)

また何かいい例があれば紹介したいと思います


正規表現でパターンマッチングを行い、マッチした数を返すマクロ

アクセス数とか見てみると、DS2系の記事がぶっ飛ぶほど人気なくてちょっと笑えます。
DS2凄いし、便利だし、今後の解析環境的にシフトしていくと思うんですけど、なかなかね。
もしちょっとでも興味のある方は今年のユーザー総会でDS2入門があるはずなので是非。

たまにはDS2以外の話でもと思っていたところ、いつ作ったかわからない便利マクロがでてきたので、ちょっと紹介します。

SASではPerlの正規表現が使用でき、わりと色んな所で紹介されているので利用している方も多いと思います。(2バイト文字に対してダメダメなので使えないケースも多いですが)

SASで正規表現-世界の切りとり方
http://d.hatena.ne.jp/O_Kohsuke/20141216/1419430432

【SAS】正規表現の取り扱い-ネットをさまよう実験室
http://tetchi-kun.hatenablog.com/entry/2015/01/13/183205

正規表現-Welcome to データ分析・マイニングの世界 by SAS
http://wikiwiki.jp/cattail/?%C0%B5%B5%AC%C9%BD%B8%BD

いろんな関数が用意されてるんですが、テキスト中に正規表現でマッチする部分が
いくつあるかをカウントする関数がありそうでなかったので、作ってみました。
多分正しくいけるはずですが間違ってたらご指摘ください

%macro prxcount (pattern, text, countvar);
 %local prx start stop pos length;
 %let prx = prxcount__prx_&sysindex;
 %let start = prxcount__start_&sysindex;
 %let stop = prxcount__stop_&sysindex;
 %let pos = prxcount__pos_&sysindex;
 %let length = prxcount__len_&sysindex;

 &prx = prxparse (&pattern);
 &start = 1;
 &stop = length (&text);
 &countvar. = 0;

 do while (&start <= &stop);
  call prxnext (&prx, &start, &stop, &text, &pos, &length);
  if &pos < 1 then leave;
  &countvar+1;
 end;

 drop prxcount__:;

%mend;

例えば

data A1;
text='cat rat bat pat';
%prxcount ("/[crb]at/",text, count1);
%prxcount ("/c.t/",text, count2);
run;

とすると、count1はcかrかbで始まって、次がatの部分なので3
count2はcの次が任意の一文字で、その次がtなのでcatの部分のみのマッチで1







となります。

肝になっている「call prxnext」ルーチンは
文字列内でパターンの一致があった位置と長さを都度指定した変数に返していくコールルーチンです。
詳しくはSASの公式を参照してください。
http://support.sas.com/documentation/cdl_alternate/ja/lefunctionsref/67960/HTML/default/n1obc9u7z3225mn1npwnassehff0.htm

パターンマッチしなくなった際に第4引数が0になるので、そこに行くまでループしてカウントしていくイメージです。


DS2ハッシュパッケージのdataset指定にSQL文書けるのは便利すぎるという話

DS2ではsetステートメントに set { SQL文 }; っていう書き方が通るという話は何回もしてます。
(正確にはFEDSQLプロシジャで使用できるSQL文)

それだけでも結構すごいことだと思いますが、個人的に一番助かったというか、感激したのはハッシュテーブルのdataset指定の部分にSQL入れれるってことなんですね。待ってましたよこの機能!

たとえば以下の2つのデータセットがあるとします。

data Q1;
ID=1;SEX='M';VAL=10;output;
ID=2;SEX='F';VAL=.;output;
ID=3;SEX='F';VAL=15;output;
ID=4;SEX='M';VAL=.;output;
run;










data Q2;
ID='A';SEX='M';VAL=10;output;
ID='B';SEX='M';VAL=14;output;
ID='C';SEX='M';VAL=13;output;
ID='D';SEX='M';VAL=15;output;
ID='E';SEX='F';VAL=9;output;
ID='F';SEX='F';VAL=8;output;
ID='G';SEX='F';VAL=11;output;
ID='H';SEX='F';VAL=16;output;
run;
















最初のデータセットQ1についてVALが欠損の箇所がありますが、
ここを後のデータセットQ2で算出した性別ごとの平均値を使って補完したいとします。

正道で行くならDS2をmeansプロシジャでも何でもつかって性別と平均値のテーブル作って、
結合して処理でしょう。最低2ステップは必要になります。

1ステップでやるなら、ひとつはハッシュオブジェクトですが、詳細は割愛しますが結構面倒になります。
まあ、実はこの程度であればSQLで普通にかけるんですが、それも今回は置いておきましょう。

DS2プロシジャのハッシュパッケージだと、以下のように

proc ds2 libs=work;
data OUT1(overwrite=yes);
declare package hash h1();
declare double ID mean;
drop mean;
method init();
h1.dataset('{select SEX,mean(VAL) as mean from Q2 group by SEX}');
h1.keys([SEX]);
h1.data([mean]);
h1.definedone();
end;
method run();
set Q1;
h1.find();
if VAL=. then VAL=mean;
end;
enddata;
run;
quit;










余計なデータセットも作らずに1ステップで完了です。
ミソはh1.dataset('{select SEX,mean(VAL) as mean from Q2 group by SEX}'); の部分ですね
SQLの結果がそのままデータセットとしてみなされているわけですね。

以下は、まだ未解決の問題なんですが、これが例えばQ1の平均でQ1を補完するみたいな場合、
Q1を2重に開けなくて、つまりロック状態でエラーになるんですね。
データセットオプションにlocktable=shareっていうのが指定できるみたいなんですが、それをSQLで
指定しているQ1に適用するのってどうやるのかな??
誰か知っていたら教えて下さいな。

それができればさらに便利なんだけど




DS2プロシジャでは、byステートメントを入れるだけで、自動でソートが発生する。ただ、細かいけど、そのソートはproc sortじゃなくてsqlのorder byという話

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

data Q1;
x=2;y=6;output;
x=1;y=1;output;
x=2;y=5;output;
x=2;y=7;output;
x=3;y=1;output;
x=1;y=2;output;
run;














以下のようにx yでsortして、first lastを利用して条件式をかくと

proc sort data=Q1 out=_Q1;
by x y;
run;
data A1;
set _Q1;
by x y;
if ^first.x and ^last.x then FL=1;
run;













といった処理ができます。

これをDS2でやる場合ですが、DS2では事前にデータセットをsortプロシジャにかけておかなくても
byステートメントに指定するだけで、勝手にsetに指定されているデータセットがsortされます。

つまり

proc ds2 libs=work;
data A2(overwrite=yes);
declare double FL;
method run();
set Q1;
by x y;
if ^first.x and ^last.x then FL=1;
end;
enddata;
run;
quit;

で同じ結果になります。

書きやすいですね~!ただ、コード上でproc sortがなくなったとはいえ、それで実行が早くなりはしません。結局、裏でsetするデータセットにソートかけてますから。

イメージ的にはbyをつけると、set Q1がset {select * from Q1 order by X,Y}に解釈されて
実行されるイメージですね。

で、勘のよい方は気づいているかもしれませんが、実はproc sortとproc sql のorder byのソートアルゴリズムは異なります。

それはSAS社のFAQでも紹介されています

http://www.sas.com/offices/asiapacific/japan/service/technical/faq/list/body/ba093.html

でDS2のソートも含めて、SAS社の例で3つのソートの結果を比較してみましょう

DATA test1 ;
     INPUT var1 var2 $ var3 $ ;
     DATALINES ;
     2 BBB  2_BBB_3
     1 BBB  1_BBB_1
     2 AAA  2_AAA_2
     2 AAA  2_AAA_1
     1 BBB  1_BBB_2
     2 BBB  2_BBB_4
     2 BBB  2_BBB_2
     2 BBB  2_BBB_1
     1 AAA  1_AAA_2
     1 AAA  1_AAA_1
     1 BBB  1_BBB_3
     ;
     RUN ;

     PROC SORT DATA=test1 OUT=test2;
       BY var1 var2 ;
     RUN ;
     title "sortプロシジャ";
     proc print data=test2 noobs;
     run;

     title "sqlプロシジャ order by";
     PROC SQL ;
       SELECT * FROM test1 
       ORDER BY var1,var2 ;
     QUIT ;
     
     title "DS2プロシジャ by";
proc ds2 libs=work;
data test3(overwrite=yes);
method run();
set test1;
by var1 var2;
end;
enddata;
run;
quit;
proc print data=test3 noobs;
run;

結果をみると



















DS2でbyを指定した場合の自動ソートがSQLソートであることが確認できました。

くらべてみました。ハッシュオブジェクトの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でもしろってことかなぁ。どなたか何か知ってたら教えてください。