例えば、以下のデータセット
data Q1;
X='い';Y=1;Z='A';W=1;output;
X='い';Y=2;Z='B';W=1;output;
X='い';Y=3;Z='C';W=2;output;
X='は';Y=4;Z='D';W=2;output;
X='は';Y=5;Z='E';W=.;output;
X='ろ';Y=6;Z='E';W=3;output;
run;
proc sort data=Q1;
by X;
run;
を使って
のようにXを起点にしてその他の変数を全て転置したデータセットを作成せよと言われたらどうしますか?
多分、すぐに思いつくのが
proc transpose data=Q1 out=_Y(keep=X Y_:) prefix=Y_;
var Y;
by X;
run;
proc transpose data=Q1 out=_Z(keep=X Z_:) prefix=Z_;
var Z;
by X;
run;
proc transpose data=Q1 out=_W(keep=X W_:) prefix=W_;
var W;
by X;
run;
data A0;
merge _Y _Z _W;
by X;
run;
だと思います。
ただし、この方法だと、転置する変数分だけtransposeを書くため、どうしても処理に無駄があるように僕はずっと感じてきました。
特に以前、仕事で、臨床検査値と臨床検査基準値を、検査日と基準値適応開始日を比較しながらマージすることが多かったため、その過程で上記のコードをよく書いていたため、もっと改良できないかと常に思っていました。
それに対する一つの答えが、今年ユーザー総会で発表したハッシュオブジェクトに関する論文で、ハッシュオブジェクトを利用することで、そもそも上記のようなデータセット加工を行わずに、ダイレクトにマッチングができるという内容でした。
それで少しは気が晴れたのですが、それはあくまで、連続transposeと再マージ処理を避けるテクニックなので、まだひっかかりがありました。
そこで今回、2ステップにはなりましたがdo untilの終了条件にlast.変数を使う特殊なループ法でやってみました。
ちなみにこのループ法、海外ではDow loopと呼ばれていて、「SAS dow loop」で検索すると、その応用法がたくさんでてきます。
Dowの「w」の文字はIan Whitlockという方の名前からつけられました。2000年にこの方が、初めてSAS-Lというコミュニティで、この方法を発表し、それがあまりに画期的であったため、リスペクトを込めて、ループの終了条件にfirstやlast等を使用するような変則ループ全般をDOWループと呼ぶようになったそうです。いい話だな~、うらやましいなぁ。
僕はa matsuさんにこのブログのコメントで初めDowの書き方を教えていただきました。
さて、前置きが長くなりましたが、コードです。
proc sql noprint;
select max(A)into:TNMAX from
(select count(*) as A from Q1 group by X);
quit;
data A1;
informat X Y_: Z_: W_:;
array Y_(&TNMAX) 8. ;
array Z_(&TNMAX) $2.;
array W_(&TNMAX) 8. ;
do until(last.X);
set Q1;
by X;
if first.X then i=0;
i+1;
Y_{i}=Y;
Z_{i}=Z;
W_{i}=W;
end;
drop Y Z W i;
run;
で、結果は先述の通りになります。
まあ、満足。
ただ、まだ、もうちょっと面白い手順があるんじゃないかと思うのでもう少し考えてみます。
SASのデータステップについて深く勉強すると、思いついた面白い構想や、突飛なアイデアを強引に実現できる力がついてきて、結局それがモチベーションになってきてる気がします。
ちょっとした話 配列要素にコロンとループ変数の初期値
もしかしたら既に別の投稿で触れていたかもしれませんが、最近はっとした、小話です。
data Q1;
array X{5}(1:5);
run;
とすると
こうなります。
おぉ~、in演算子のコロン範囲指定と同じ理屈か~。
コロン範囲指定については
SAS忘備録の「条件式(IF/WHERE)におけるINオペレータの小技」
http://sas-boubi.blogspot.jp/2014/01/in.html
ちなみに全部に1を入れたければ
data _Q1;
array X{5}(5*1);
run;
と書けば、1を5要素に入れるという指定になります。
もう一つは、もしかしたら常識なのかも
data Q2;
do until(X=2);
output;
X+1;
end;
run;
の結果は
です。
あ~、いっつもループ変数に初期値割り当ててから回してたから、意識したことなかったけど
なにも指定しないと0から始まるんだ、そうだっけ?知ってたような、初めて知ったような。
data Q1;
array X{5}(1:5);
run;
とすると
こうなります。
おぉ~、in演算子のコロン範囲指定と同じ理屈か~。
コロン範囲指定については
SAS忘備録の「条件式(IF/WHERE)におけるINオペレータの小技」
http://sas-boubi.blogspot.jp/2014/01/in.html
ちなみに全部に1を入れたければ
data _Q1;
array X{5}(5*1);
run;
と書けば、1を5要素に入れるという指定になります。
もう一つは、もしかしたら常識なのかも
data Q2;
do until(X=2);
output;
X+1;
end;
run;
の結果は
です。
あ~、いっつもループ変数に初期値割り当ててから回してたから、意識したことなかったけど
なにも指定しないと0から始まるんだ、そうだっけ?知ってたような、初めて知ったような。
do to by ループのtoを消してもループは成立するので、それを使って1手得を狙っていく話
以前、「do until(end変数)set end=を利用して、複数のデータステップを1つにまとめて共存させる方法」
http://sas-tumesas.blogspot.jp/2013/12/do-untilendset-end1.html
で、do loopの中にsetコマンドを突っ込むやりかたを紹介しました。
また、その方法については「SASインタリーブ(interleave)の紹介と再帰的インタリーブで、SQL再マージ処理をデータステップで疑似的に表現する」
http://sas-tumesas.blogspot.jp/2014/01/sasinterleavesql.html
でも少しふれましたし
最近、「詰めSAS10回目_変数内の1文字ごとにオブザベーションを起こす」でa matsuさんが
提案してくれた方法もdo ループとsetをつかっています。
http://sas-tumesas.blogspot.jp/2014/01/sas101.html
で、これとほぼ同じ内容にはなるのですが
ループの中でカウンタとする変数を増加する場合、
ループの中で変数+1と割り当てる方法があるのですが、最近、それを省略してもかけるんじゃないかと思いつきました。
ちょっと細か過ぎて、この話題で喜ぶ人がどれくらいいるか謎ですが、
たとえば
data Q1;
X='A';output;
X='A';output;
X='Z';output;
X='B';output;
X='Z';output;
X='A';output;
X='B';output;
X='C';output;
X='Z';output;
run;
こういうデータがあって、
Xの値に「Z」が出現するまでのオブザベーション数をカウントし、出現した行の番号と
ともに出力するというプログラムをかくとします。
求めたい結果は
元データの3,5,9行目でZが出現しているのでROWには3,5,9が入り、
そこまでに出現するZ以外の文字のカウントがCOUNTに入ります。
さて、どう切り込みますか?
今回の場合、元データの格納順を崩せないので
ソートを利用したfirst last系の分岐を利用できないんですね。
多分、第一感はretainだと思います。
data A0;
set Q1;
retain COUNT 0;
COUNT+1;
if X='Z' then do;
ROW=_N_;
output;
COUNT=0;
end;
drop X;
run;
で詰みですね。
これでも全然いいんですが、do loopを使うなら
data A1;
do COUNT=0 by 1;
set Q2;
ROW=monotonic();
if X='Z' then leave;
end;
drop X;
run;
これでも詰みなんです。
do loopの to条件を省くことでループ変数そのものをカウンタ変数として使っちゃうわけですね。
setがループされるため、ここで_N_つかっても1,2,3とはいるだけで元データの行番号をとるうえではポンコツなのでmonotonic関数でとります。
ちなみにここでのleaveはoutputでもいいです。leaveでもオブザベーション起きるのが面白いので
leaveにしてます。
ちなみにifで抜けなくても、until終了条件でも当然OK
data A2;
do COUNT=0 by 1 until(X='Z');
set Q2 end=eof;
ROW=monotonic();
end;
drop X;
run;
http://sas-tumesas.blogspot.jp/2013/12/do-untilendset-end1.html
で、do loopの中にsetコマンドを突っ込むやりかたを紹介しました。
また、その方法については「SASインタリーブ(interleave)の紹介と再帰的インタリーブで、SQL再マージ処理をデータステップで疑似的に表現する」
http://sas-tumesas.blogspot.jp/2014/01/sasinterleavesql.html
でも少しふれましたし
最近、「詰めSAS10回目_変数内の1文字ごとにオブザベーションを起こす」でa matsuさんが
提案してくれた方法もdo ループとsetをつかっています。
http://sas-tumesas.blogspot.jp/2014/01/sas101.html
で、これとほぼ同じ内容にはなるのですが
ループの中でカウンタとする変数を増加する場合、
ループの中で変数+1と割り当てる方法があるのですが、最近、それを省略してもかけるんじゃないかと思いつきました。
ちょっと細か過ぎて、この話題で喜ぶ人がどれくらいいるか謎ですが、
たとえば
data Q1;
X='A';output;
X='A';output;
X='Z';output;
X='B';output;
X='Z';output;
X='A';output;
X='B';output;
X='C';output;
X='Z';output;
run;
こういうデータがあって、
Xの値に「Z」が出現するまでのオブザベーション数をカウントし、出現した行の番号と
ともに出力するというプログラムをかくとします。
求めたい結果は
元データの3,5,9行目でZが出現しているのでROWには3,5,9が入り、
そこまでに出現するZ以外の文字のカウントがCOUNTに入ります。
さて、どう切り込みますか?
今回の場合、元データの格納順を崩せないので
ソートを利用したfirst last系の分岐を利用できないんですね。
多分、第一感はretainだと思います。
data A0;
set Q1;
retain COUNT 0;
COUNT+1;
if X='Z' then do;
ROW=_N_;
output;
COUNT=0;
end;
drop X;
run;
で詰みですね。
これでも全然いいんですが、do loopを使うなら
data A1;
do COUNT=0 by 1;
set Q2;
ROW=monotonic();
if X='Z' then leave;
end;
drop X;
run;
これでも詰みなんです。
do loopの to条件を省くことでループ変数そのものをカウンタ変数として使っちゃうわけですね。
setがループされるため、ここで_N_つかっても1,2,3とはいるだけで元データの行番号をとるうえではポンコツなのでmonotonic関数でとります。
ちなみにここでのleaveはoutputでもいいです。leaveでもオブザベーション起きるのが面白いので
leaveにしてます。
ちなみにifで抜けなくても、until終了条件でも当然OK
data A2;
do COUNT=0 by 1 until(X='Z');
set Q2 end=eof;
ROW=monotonic();
end;
drop X;
run;
SASインタリーブ(interleave)の紹介と再帰的インタリーブで、SQL再マージ処理をデータステップで疑似的に表現する
インタリーブと聞くと、なんだっけそれ?となってしまいますが、たいしたことではなく
例えば
data Q1;
do X=1,3,5;
output;
end;
run;
proc sort data=Q1;
by X;
run;
data Q2;
do X=2,4,6;
output;
end;
run;
proc sort data=Q2;
by X;
run;
のようにソート変数の一致したデータセットが複数存在する場合
全てのデータセットを縦結合してからソートしなおす必要なく、ソートされた縦結合データセットが
上記の感じです。
SQL再マージで書くなら
proc sql noprint;
create table A0 as
select X,Y,sum(Y) as TOTAL
from Q3
group by X;
quit;
で詰みですね。
あるいはdo untilによる複数データステップの併合を利用して
data A00;
do until(last.X);
set Q3;
by X;
retain TOTAL;
if first.X then TOTAL=.;
TOTAL+Y;
end;
do until(last.X);
set Q3;
by X;
output;
end;
run;
でも同じ結果になります。
【参考】do until(end変数)set end=を利用して、複数のデータステップを1つにまとめて共存させる方法
http://sas-tumesas.blogspot.jp/2013/12/do-untilendset-end1.html
ぱっと思いつきませんでしたが、call executeでもかけそうです。
脱線しました、他の方法を考えていると、きりがないです。
インタリーブで書くと、以下のようになります。
data A2;
set Q3(in=in1)
Q3;
by X;
retain TOTAL;
if in1 then do;
if first.X then TOTAL = .;
TOTAL = sum(TOTAL,Y, 0);
end;
else do;
output;
end;
run;
で詰みです。
ポイントは、setで敢えて元データを倍加重複させておいた上でインタリーブしてるところですね。
で、そのままだといけないので、in=で要約統計を出す部分と、実出力部分を分けているのですね。
タネさえ解れば、どうってことない処理ですね
例えば
data Q1;
do X=1,3,5;
output;
end;
run;
proc sort data=Q1;
by X;
run;
data Q2;
do X=2,4,6;
output;
end;
run;
proc sort data=Q2;
by X;
run;
のようにソート変数の一致したデータセットが複数存在する場合
data A1;
set Q1
Q2;
by X;
run;
全てのデータセットを縦結合してからソートしなおす必要なく、ソートされた縦結合データセットが
得られるという技術のことです。
特に巨大なデータセットの場合、巨大なやつをつくってからproc sortするよりも、
小さい部品となるデータセットごとにソートしておいてインタリーブした方が、処理効率が
よいらしいです。
で今回のメインは処理効率の話ではなく
インタリーブをつかってSQL再マージと同様の自己参照処理をやってみようという話です。
SQL再マージ?という方はSAS忘備録の「SQL「再マージ」入門」を先に読んでください
http://sas-boubi.blogspot.jp/2013/12/sql_26.html
今、以下のデータセットがあったとします。
data Q3;
X='い';Y=1;output;
X='い';Y=5;output;
X='い';Y=9;output;
X='ろ';Y=2;output;
X='ろ';Y=3;output;
X='は';Y=2;output;
X='は';Y=4;output;
X='は';Y=6;output;
run;
proc sort data=Q3;
by X;
run;
今、以下のデータセットがあったとします。
data Q3;
X='い';Y=1;output;
X='い';Y=5;output;
X='い';Y=9;output;
X='ろ';Y=2;output;
X='ろ';Y=3;output;
X='は';Y=2;output;
X='は';Y=4;output;
X='は';Y=6;output;
run;
proc sort data=Q3;
by X;
run;
上記の感じです。
SQL再マージで書くなら
proc sql noprint;
create table A0 as
select X,Y,sum(Y) as TOTAL
from Q3
group by X;
quit;
で詰みですね。
あるいはdo untilによる複数データステップの併合を利用して
data A00;
do until(last.X);
set Q3;
by X;
retain TOTAL;
if first.X then TOTAL=.;
TOTAL+Y;
end;
do until(last.X);
set Q3;
by X;
output;
end;
run;
でも同じ結果になります。
【参考】do until(end変数)set end=を利用して、複数のデータステップを1つにまとめて共存させる方法
http://sas-tumesas.blogspot.jp/2013/12/do-untilendset-end1.html
ぱっと思いつきませんでしたが、call executeでもかけそうです。
脱線しました、他の方法を考えていると、きりがないです。
インタリーブで書くと、以下のようになります。
data A2;
set Q3(in=in1)
Q3;
by X;
retain TOTAL;
if in1 then do;
if first.X then TOTAL = .;
TOTAL = sum(TOTAL,Y, 0);
end;
else do;
output;
end;
run;
で詰みです。
ポイントは、setで敢えて元データを倍加重複させておいた上でインタリーブしてるところですね。
で、そのままだといけないので、in=で要約統計を出す部分と、実出力部分を分けているのですね。
タネさえ解れば、どうってことない処理ですね
do i=1 to ○○ until();のようにdo toループとuntilやwhileが併用できることを今まで知らなくてとても悲しかった話
とてもショックです。
data A1;
X=20;
do i=1 to 99 until (X=40);
X+2;
output;
end;
run;
99回ループする。ただし途中終了条件としてX=40になれば終了する。
この処理を上記のコードでかけるのです。
こんな単純な書き方なのに気づきませんでした。ショックです。悲しいです。
以前は以下のように書いていました。
data A2;
X=20;
do until(i=99 or X=40);
X+2;
i+1;
output;
end;
run;
最近、leaveステートメントを知って、その時もショックでしたが、
以来、以下のように書けるようになりました。
data A3;
X=20;
do i=1 to 99;
X+2;
output;
if X=40 then leave;
end;
run;
SASのデータステップの知識の全てを100としたら、多分そのうち理解できている部分は
1以下だろうと思い知りました。
性根を叩き直して勉強しなおします
data A1;
X=20;
do i=1 to 99 until (X=40);
X+2;
output;
end;
run;
99回ループする。ただし途中終了条件としてX=40になれば終了する。
この処理を上記のコードでかけるのです。
こんな単純な書き方なのに気づきませんでした。ショックです。悲しいです。
以前は以下のように書いていました。
data A2;
X=20;
do until(i=99 or X=40);
X+2;
i+1;
output;
end;
run;
最近、leaveステートメントを知って、その時もショックでしたが、
以来、以下のように書けるようになりました。
data A3;
X=20;
do i=1 to 99;
X+2;
output;
if X=40 then leave;
end;
run;
SASのデータステップの知識の全てを100としたら、多分そのうち理解できている部分は
1以下だろうと思い知りました。
性根を叩き直して勉強しなおします
do until(end変数)set end=を利用して、複数のデータステップを1つにまとめて共存させる方法
例えばあるデータステップを実行して、その最終結果として得られる値を、
次のデータステップで使用したい場合、常識的に考えれば当然2ステップが必要になります。
しかしdo untilループとファイル終端の際に1がたつend変数を利用することで、
複数のステップを繋いだ、かなり柔軟なデータステップを書くことができます。
今回は例として、単純なものを紹介します。
たとえば以下のように2つのデータセットがあったとします。
data Q1;
do X=1 to 5;
output;
end;
run;
data Q2;
do Y=1 to 10;
output;
end;
run;
そこでQ2の全オブザベーションを足して求められる値(1から10までの足し算なので55です)を
Q1の各オブザベーションにそれぞれ足したいとします。
その場合、普通はまずQ2の合計を、何らかのプロシージャやあるいはデータステップで導出し
Q1の全obsにマージしたり、或いはマクロ変数を介して足し算したりすることが浮かぶと思います。
(SQLならサブクエリで1発ですが今回はおいといてください)
ところが、なんと以下のように1ステップで書くことができます。
data A1;
do until(eofQ2);
set Q2 end=eofQ2;
Y_TOTAL+Y;
end;
do until(eofQ1);
set Q1 end=eofQ1;
TOTAL=X+Y_TOTAL;
output;
end;
run;
で結果は
となります。
do until(eofQ2)はdo until(eofQ2=1)の略で、set Q2の際にend=でeofQ2と変数をあてているので
Q2が全て読み込まれて処理されるまで、このループが続き、抜ける頃にはY_TOTALを含む最終obsのみが
残っています。
そこからQ1のループに入りますが、今回Q1をベースとして全obs残したいのでこちらにoutputをいれています。
このように書くことで、複数のデータステップを1つにまとめることができ、
前のステップで生成された値を、リレーのバトンタッチのように使用できます。
海外のコミュニティのやりとりなどで、何回も見たことがあるので、向こうでは定跡化されている
方法なのかもしれません。
あ、ちなみに多分今回の投稿で100回目です。
あと1万倍投稿すればデータステップ100万回になるので、がんばります。
もう多分10回目の投稿ぐらいから慢性的にネタ切れな感じなので、何か意見、要望、突っ込み、アイデア、質問、なんでもください、まっています。
次のデータステップで使用したい場合、常識的に考えれば当然2ステップが必要になります。
しかしdo untilループとファイル終端の際に1がたつend変数を利用することで、
複数のステップを繋いだ、かなり柔軟なデータステップを書くことができます。
今回は例として、単純なものを紹介します。
たとえば以下のように2つのデータセットがあったとします。
data Q1;
do X=1 to 5;
output;
end;
run;
data Q2;
do Y=1 to 10;
output;
end;
run;
そこでQ2の全オブザベーションを足して求められる値(1から10までの足し算なので55です)を
Q1の各オブザベーションにそれぞれ足したいとします。
その場合、普通はまずQ2の合計を、何らかのプロシージャやあるいはデータステップで導出し
Q1の全obsにマージしたり、或いはマクロ変数を介して足し算したりすることが浮かぶと思います。
(SQLならサブクエリで1発ですが今回はおいといてください)
ところが、なんと以下のように1ステップで書くことができます。
data A1;
do until(eofQ2);
set Q2 end=eofQ2;
Y_TOTAL+Y;
end;
do until(eofQ1);
set Q1 end=eofQ1;
TOTAL=X+Y_TOTAL;
output;
end;
run;
で結果は
となります。
do until(eofQ2)はdo until(eofQ2=1)の略で、set Q2の際にend=でeofQ2と変数をあてているので
Q2が全て読み込まれて処理されるまで、このループが続き、抜ける頃にはY_TOTALを含む最終obsのみが
残っています。
そこからQ1のループに入りますが、今回Q1をベースとして全obs残したいのでこちらにoutputをいれています。
このように書くことで、複数のデータステップを1つにまとめることができ、
前のステップで生成された値を、リレーのバトンタッチのように使用できます。
海外のコミュニティのやりとりなどで、何回も見たことがあるので、向こうでは定跡化されている
方法なのかもしれません。
あ、ちなみに多分今回の投稿で100回目です。
あと1万倍投稿すればデータステップ100万回になるので、がんばります。
もう多分10回目の投稿ぐらいから慢性的にネタ切れな感じなので、何か意見、要望、突っ込み、アイデア、質問、なんでもください、まっています。
登録:
投稿 (Atom)











