RTF出力の新視点-STREAMプロシジャの応用力は侮れないという話

RTF出力、実は未だあんまり経験ないんですが、色々大変なのはわかります。

多分おおまかに2つ方法があって、一つはODS RTFを利用する方法、もう一つはputでタグ情報と共にデータを出力して作ってしまう方法だと思います。
どちらも過去のユーザー総会等で素晴らしい発表が多くでているので、そちらを参照してください。

まあ、それで終わりの話なんですが、僕はひねくれているので、何か他に面白い方法はないかなと考えてました。

上記の2つの方法もファイルの新規作成なので、書式等の調整も全てコードでやる必要があります。
要するに、あらかじめ用意したRTFファイルに値を差し込む方法ではないということです。
(一応DDEを使った既存WORDファイルへの値入れする方法もあるみたいですが、少なくとも日本ではあんまり主流ではない気がします)

そこで、書式設定等を済ませたテンプレートRTFにSASから出力する新しい方法について紹介します。

ズバリproc streamを使います! 9.4からの新プロシジャです。一応9.3でも正式版ではないですが実行できます。

proc stream?って方は過去記事
「proc stremの世界」
http://sas-tumesas.blogspot.jp/2015/07/proc-strem.html

を見てください。

要はマクロ変数を展開したテキスト生成ができるプロシジャで、普通の平テキストはもとより、HTMLやXML、RTFなどタグ言語の生成に強みがあるんですね。

さあ、前置きが長くなりましたが、やっと本題に行きます。


まず、ノートパッドでもMS WORDでもいいので、以下の様な出力テンプレートを拡張子RTFで作成、保存してください。(例は「テンプレート.rtf」という名前で保存)



ちなみに値を差し込みたい部分をマクロ変数で記述します。

で、例えば以下の様なデータセットがあった場合







data _NULL_;
set SALES end=eof;
call symputx(ITEM,SALE);
TOTAL+SALE;
if eof then call symputx('TOTAL',TOTAL);
run;

とでも書いて、とりあえずテンプレートのマクロ変数名に合わせて、マクロ変数を作ってやります。

そうしたら、いよいよ次のように書きます

filename in "/home/sasyamasasyama0/テンプレート.rtf" lrecl = 32755;;
filename out "/home/sasyamasasyama0/アウトプット.rtf" lrecl = 32755;;
proc stream outfile=out resetdelim="rdlm" quoting=single;
begin
rdlm; %include in;
;;;;

これだけです。

実行すると、「アウトプット.rtf」ができて、それを開いてみると















と、値が展開されていることがわかります。

どうでしょう?
結構面白いですよね。

resetdlm=で、リセットデリミタを指定しています。リセットデリミタの後に%includeを記述しないと、ただのテキストとして「%include」ってでちゃいます。
%include ファイル名で、指定したファイルをproc streamの中に展開します。そこでその中に含まれるマクロ変数が展開されるわけです。

こういうレイアウトが固定されたrtfに値を出すのには結構向いていると思います。
まあ、多分ご想像されているとおり、表がデータに応じて可変だったり、出力する値の数が多いものには正直オススメできないですね。
(できなくはないけど、それに応じて表を構成するためのタグを作って一緒に出す必要があるので、putで出す際のコードをマクロ化してstreamの中で展開するイメージになるので、お手軽さがなくなる)

定型のちょっとしたレポート向きだと思いますが、知っていて損はない使い方だと思います。

以上です。

monotonic関数の性質。_N_とはちょっと違うという話

monotonic関数は色々注意点もありますが、中々面白い関数です。

例えば以下のデータセットがあります

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

















それで、偶数のオブザベーション番号だけproc printしろっていわれたら、

proc print data=Q1;
 where mod(_N_,2)=0;
run;

なんて書きたくなりますが、もちろん_N_はwhereでは使えません。

ところが

proc print data=Q1;
 where mod(monotonic(),2)=0;
run;

ならいけます









これは何回も紹介している話なんですが、上記のことから
monotonic関数はwhereでも使える_N_みたいなものと認識してしまいがちなのですが
それはちょっと違います

例えば

data A1;
set Q1;
Y=monotonic();
run;



















となり

Y=_N_;とした場合と全く同じですが

以下のコードはどうなるでしょう?

data A2;
set Q1;
if x=1 then Y=monotonic();
if x=2 then Y=monotonic();
if x=3 then Y=monotonic();
if x=4 then Y=monotonic();
run;

実は

















と、Xの値ごとに連番が振られるんですね。

つまりmonotonic関数は、行番号を返しているのではなく、
monotonic関数を通って処理される回数をカウントして返しているようなイメージということなんですね。(理解が間違っていたらご指摘ください)

これは非常に面白い性質ですね。







パッケージ内で別パッケージのメソッドを使用するための方法-コンポジション

今回の記事については説明や用語・概念の説明についてちょっと自信がないです。
SASメインで、他のオブジェクト指向の言語をあんまりやったことがないので、もしもっと上手く説明できる方がいらっしゃれば是非どこかにアップして欲しいです。

さて、苦手ながらも大事な部分なので頑張って書いてみます。

一般的なオブジェクト指向言語でのクラスという概念が、DS2ではパッケージになると思います。
DS2のパッケージには、いわゆる「インヘリタンス(継承)」という機能がありません。

継承は、既存クラスの全機能を引き継いで新たなクラスを定義することで、オブジェクト指向の3大要素とかって言われるぐらい重要なものです。親のメソッドを全部引き継いだ子を作るイメージです

それがないってどうなの?ってことですが、代わりにすべて、同じくオブジェクト指向の「コンポジション(集約)」という概念でまかないます。

インヘリタンスとコンポジションの違いは、ちょっと検索して貰えればたくさん説明がでてくるので、そちらを参考にしてください。
僕がわかりやすかったのは以下のとか?

要するに、単純に他のパッケージの必要なメソッドだけもってきて新規パッケージの中で使えるようにしちゃうってイメージで僕は理解しました。

例えば以下のような、数値に1を加えるメソッド(tasu1メソッド)と2倍するメソッド(kake2)をもったパッケージ(p1パッケージ)があるとします

proc ds2 libs=work;
package p1 /overwrite=yes;
declare double v;
method tasu1(double v) returns double;
return v+1;
end;
 method kake2(double v) returns double;
  return v*2;
 end;
endpackage;
run;
quit;

次に新しく作成するパッケージ(p2パッケージ)の中で、p1のtasu1メソッドの機能を使いたい場合は以下のように書けます

proc ds2 libs=work;
package p2 /overwrite=yes;
declare package p1 pp1();
declare double v;
method p2();
pp1=_new_ p1();
end;
method tasu1(double v) returns double;
return pp1.tasu1(v);
end;
method tasu2(double v) returns double;
return v+2;
end;
endpackage;
run;
quit;

最初のdeclareでパッケージp1をpp1という名前で呼び出している(パッケージp1からインスタンスpp1を生成)のは、普通にパッケージを利用する場合と同様なので自然にかけると思います。
ただ、それだけだと不十分で、

method p2();
pp1=_new_ p1();
end;

の部分、パッケージ名と同じなのでコンストラクタになるわけですが、
ここで謎の式を書いてますが、これも実はパッケージの呼び出し方(インスタンスの生成)の書き方なんですね。p1パッケージからpp1インスタンスを生成するという意味になります。

declareステートメントと同じことしてんじゃんって感じで、なんでこんなのが必要なのかなんですが
呼び出しのコードをみるとわかりやすいかもです

proc ds2 libs=work;
data A1(overwrite=yes);
declare double x y;
declare package p2 p2();
method run();
x=p2.tasu1(1);
y=p2.tasu2(1);
end;
enddata;
run;
quit;

ちなみに結果は





なんですが、ようするにp2パッケージしか呼び出してないのに、内部でp1パッケージの機能も呼び出されている、つまりp1のインスタンスも生成されているのがミソなんです。
パッケージが呼び出された際に、最初に実行されるコンストラクタの部分でp1のインスタンスを作らないと実際使えないよってことなんですね。パッケージを呼び出した際に内部のdecalreステートメントが実行されるわけではないので。


はぁ、自分の説明力のなさに嫌気がさします、、。
ほんとすみません、どなたか、もっとちゃんと説明しなおしてください





DS2でも外部ファイル出力はできます!!な話

ちょっとソース見つけられなかったんですが、だいぶ昔、海外のSASブログだったか何かのサイトで、「DS2にはfileステートメントがないみたいだぜ!」「マジかよ!ヤベェな!ハンドリング専門かよ!DS2はXXXX(なんか汚い言葉)だな!」みたいな外人同士のやりとりがあって、それをボーっと見てて、なんとなく頭に残ってました。

そうなんだぁ~、アウトプット部分は通常のSASコードで書かなきゃいけないんだ、そりゃXXXXだなぁって思ってたんですが、流石にそんなわけなかったですね。
普及し始めはガセじゃないですけど、みんな知らないんで結構間違ったコード上がるんで注意です。僕の記事に関しても間違いが含まれていることは多々あるはずなので鵜呑みは注意です。

話を戻しまして、実際問題ちょっと気づきにくいところなので説明します

じゃあまずはテストデータ

data Q1;
x=1;Y='A';output;
x=2;Y='B';output;
x=3;Y='C';output;
run;

そして、手始めに以下のコードを見てください

proc ds2 libs=work;
data _null_;
method run();
set Q1;
put X Y;
end;
enddata;
run;
quit;

こうすると、ログに






って出力されます。

うん、そりゃそうだろうね。データステップもそうじゃん。

データステップなら、putの前に、file print;ってつければアウトプットウインドウに
出力するし、先にfilenameで外部ファイルを定義してfileで指定すればそこに出力されます。

でも、DS2はfileステートメントないんだよね?じゃあやっぱ無理じゃん。

と思ったところで、次のコード見てください。

proc ds2 libs=work;
data ;
method run();
set Q1;
end;
enddata;
run;
quit;

?何が違うの?って思った方は、もう一度よく見てください。まずdata の後に_null_がなくなってます。旧データステップで、dataの後何もつけないと、何が起こるかは以下の記事を読んでください

「どうてもいい話 データセット名をかかずに実行」
http://sas-tumesas.blogspot.jp/2013/10/blog-post_1261.html

ようは勝手に連番でDATA1とか2とか作られていきます。
次にもう一点、putで何も指定してない!

これを実行すると何が起きるかというと
アウトプットウインドウに以下のような出力がでます







なるほど!そういうことか!SQLプロシジャでcreate tableつけずにselect文流すのとおなじですね。抽出結果がそのままアウトプットになるわけですか。

もうここまできたら読めてきましたね。
つまり、外部ファイルへの出力はアウトプット デリバリー システム、そうODSでやれってことですよ。

例えばcsvで欲しければ

filename outf "aa.csv";
ods tagsets.csv file=outf;
proc ds2 libs=work;
data;
method run();
set Q1;
put X Y;
end;
enddata;
run;
quit;
ods tagsets.csv close;

とすればOKなわけです。

9.4からods excelがあるから、拡張子xlsxにしてtagsets.csvのところをexcelにすれば
みんな大好きEXCELファイルも作れます

以上です!

連続・重複した日付期間データをならして最小オブザベーション数にまとめる方法について

イベント開始日と終了日からなるデータがあって、発生ごとに縦積みされるとします。
ただし、諸々の事情で継続途中でオブザベーションが別れたり、重複して記録されてたりするとします。

たとえば

6/1開始 - 6/3終了
6/2開始 - 6/4終了
6/4開始 - 6/5終了

という3行のにわかれたデータを

6/1開始 - 6/5終了

の1行におこしなおすという処理です。
これは結構苦手な人が多いはず。

今回使うのは以下のデータ(ややこいので欠損は含まれないということにしましょう)
でIDとSTDTでソート済みとしましょう

data Q1;
ID=1;STDT='01JUN2016'd;ENDT='03JUN2016'd;output;
ID=1;STDT='02JUN2016'd;ENDT='04JUN2016'd;output;
ID=1;STDT='04JUN2016'd;ENDT='05JUN2016'd;output;
ID=1;STDT='06JUN2016'd;ENDT='09JUN2016'd;output;
ID=1;STDT='12JUN2016'd;ENDT='15JUN2016'd;output;
ID=1;STDT='13JUN2016'd;ENDT='14JUN2016'd;output;
ID=1;STDT='17JUN2016'd;ENDT='17JUN2016'd;output;
ID=2;STDT='01JUN2016'd;ENDT='05JUN2016'd;output;
ID=2;STDT='02JUN2016'd;ENDT='08JUN2016'd;output;
ID=2;STDT='03JUN2016'd;ENDT='04JUN2016'd;output;
ID=2;STDT='10JUN2016'd;ENDT='11JUN2016'd;output;
format STDT ENDT yymmdds10.;
run;


これを

上記のような形にします。

この手の処理を書く場合は、先にガントチャートみたいな図を書いてから考えると楽ですね。
頭の中だけで組むと、結構泥沼になったりします

ちなみにSAS on demandでは、プロダクト「SAS OR」も使えて、そこに
proc ganttっていうガントチャートかけるプロシジャがあったので、使い方よくわからないけど
適当に指定して流してみました(頑張れば凄い綺麗なのが描けるみたいですけど)。
これのコードは本題じゃないので最後に書きます。


そこで、どうやるかですが、多分以下の感じで書くのが一般的ではないでしょうか?

data _Q1;
format r_STDT r_ENDT yymmdds10.;
set Q1;
by ID;
retain r_STDT r_ENDT ;
if first.ID then do;
r_STDT=STDT;
r_ENDT=ENDT;
end;
if STDT > r_ENDT+1 then do;
r_STDT=STDT;
r_ENDT=ENDT;
end;
if ENDT>r_ENDT then r_ENDT=ENDT;
run;
proc sql noprint;
create table A1 as
select ID,r_STDT as STDT,max(r_ENDT) as ENDT format=yymmdds10.
from _Q1
group by ID ,r_STDT;
quit;

1obs読み込みながらリテインしている開始日と終了日を条件に従って更新していくイメージです。
終了日はリテインしている終了日より後なら置き換える。開始日は、リテインしている終了日+1より
大きければそこで置き換える。
そしてできたデータセットについて、リテインしていた開始日でグループ化して、最も最大のリテインしていた終了日のみ残せばOKです。

sqlの部分に書き方はなんでもよくて、IDとr_STDT r_ENDTでソートしてlast.r_STDTでしぼってもOKなわけです。

上記の処理は2ステップでやってます。SASは基本的に行の先読みがやりにくい言語なので
上記の考え方を1ステップで表現するのは、かなり難しいです。どこで切ってアウトプットするかが、先まで読まないと確定できないですからね。

ところが、ハッシュオブジェクトなら、簡単にできちゃうんですよね。
前の記事で紹介したmultidataとfind_nextを使えば、先読みみたいなことが。

data A2;
length _STDT _ENDT 8.;
retain r_ENDT;
set Q1;
if _N_=1 then do;
call missing(_STDT,_ENDT);
declare hash h1(dataset:'Q1(rename=(STDT=_STDT
                                                                      ENDT=_ENDT))',multidata:'Y');
h1.definekey('ID');
h1.definedata('_STDT','_ENDT');
h1.definedone();
end;
 l_ID=lag(ID);
 if STDT<r_ENDT and ID=l_ID then delete;
 else do rc = h1.find() by 0 while (rc = 0) ;
  if _STDT<=ENDT+1 and _ENDT>ENDT then do;
  ENDT=_ENDT;
  end;
  rc= h1.find_next() ;
 end ;
 r_ENDT=ENDT;
 keep ID STDT ENDT;
run;

実は僕は、この処理を考えるとき、ハッシュでのやり方は一瞬で考えてすぐに書けたんですが
最初の書き方が中々でてこなくて手こずりました。
慣れたらこっちの方がわかりやすいと思うんですが、どうなんでしょ。

ちなみに、最初から最後までSQL一本でも表現できますが、かんなり難しいコードになります。
開始日(条件付き)と終了日の全組み合わせを作ってから、中に含まれる重複期間を除外するイメージですが、興味のある方は「SQLパズル: プログラミングが変わる書き方/考え方 第2版」に全コード載っているので参考にしてください。

最後にproc ganttのコードですが、僕もよくわかっとりません。as=でスタート、af=でエンド時点を指定します。
チャートに重ねる文字とかの制御をラベルデータセットで制御するみたいなんですが、まあ、あんまりSAS OR使う機会もないし、調べるの面倒だったので適当です。

data label;
 format _LVAR $10.; 
 _Y = -1;
 _LVAR = "STDT";
 _XVAR = "STDT";
 _CLABEL = "BLUE";
 _YOFFSET = .8; OUTPUT;
 _LVAR = "ENDT";
 _XVAR = "ENDT";
 _YOFFSET = .8;
 _XOFFSET = -0.3;OUTPUT;
RUN;
data GQ1;
set Q1;
format STDT ENDT day.;
run;
PROC GANTT DATA = GQ1 LABDATA=LABEL;
 CHART / AS = STDT AF = ENDT NOLEGEND NOJOBNUM NOTNLABEL 
         TIMEAXISFORMAT= day.;
 ID ID STDT ENDT;
 ;
RUN;




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

以前の記事、「大半のソートは百害あって一利無しという話」
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になるので、そこに行くまでループしてカウントしていくイメージです。