幾何平均の出し方がちょっとずつ増えてるけど、誰も気づきませんって!という話

9.4からメンテナンスリリースのバージョンによる機能の差がちょっと目立つようになってきた。
プログラムを他の環境に渡して、ゴリゴリ動かすことが想定されているなら
メンテナンスリリースのバージョンまで、移管先とすり合わせておいた方がよくなってきそう。


ちなみに、自分の使っているSASのメンテナンスリリースのバージョンがわからない場合は

proc product_status;
run;

を実行してみるとよいですよ。











9.4_M3と、でてるM3の部分がメンテナンスレベル3を示してます。
だから、BASEのリファレンスをググッて、参照する場合はthird editionとついているやつを見た
方が正確です。
あとSAS/STATのバージョンも重要で14.1とでていますので、BASEにない統計系のプロシジャ
を参照する場合は14.1のものを見てくださいということですね。

9.3→9.4とかの大きなバージョンアップの場合は、多くのユーザーはSASがだしている
what's newを参照してからプログラムしてると思うので(してますよね?)いいんですけど、
メンテナンスリリースは、一応上がるたびに最新に更新してアップされているとはいえ
その辺をくまなくチェックしてるユーザーは少ないかも。

最近ちょっとびっくりしたのが9.4M3で、univariateに幾何平均の指定がこっそりと増えていたこと。
なんでいまさら!しかもメンテナンスリリースレベルのアップでいきなり!
正直に言わせて頂くと…、誰も気づかねぇよ!!

9.1でgeomean関数が追加されて、横持の変数の幾何平均がだせるようになり
9.2でttestプロシジャでだせるようになり
9.4M1でsurveymeansが対応して
9.4M3でunivariateが対応して(信頼区間などはでない)…

いや、まどろっこしいから、普通にproc meansに幾何平均がらみの統計キーワード
全部追加してくださいよ


/*9.4M3から univariateでgeomeanが指定可能*/
proc univariate data=sashelp.class;
var weight;
output out=out1 geomean=geomean;
run;


/*9.4M1からsurveymeansでgomean、その他信頼区間も指定可能*/
proc surveymeans data=sashelp.class geomean gmclm gmstderr lgmclm ugmclm;
var weight;
ods output GeometricMeans=out2;
run;


/*9.2からttestでlognormalを指定して幾何平均と信頼区間がだせる*/
proc ttest data=sashelp.class dist=lognormal;
  var weight;
  ods output Statistics=out3;
run;



/*9.1からgeomean関数で幾何平均がだせる*/
proc transpose data=sashelp.class out=temp1;
var weight;
run;
data out4;
set temp1;
geomean=geomean(of COL:);
keep geomean;
run;



/*9.1以前*/
data temp2;
set sashelp.class;
         logval=log(weight);
RUN;
proc means data=temp2 noprint;
  var logval;
  output out=temp3 lclm=lcl mean=mean uclm=ucl;
run;
data out5;
  set temp3;
  lcl_gm = exp(lcl);
  geomean = exp(mean);
  ucl_gm = exp(ucl);
run;

title "univariate";
proc print data=out1 label;
run;









title "surveymeans";
proc print data=out2 label;
run;




title "ttest";
proc print data=out3 label;
run;




title "転置してからgeomean関数";
proc print data=out4 label;
run;










title "logとってからmeansかけてexp";
proc print data=out5 label;
run;



サンタクロースはいつプレゼントを買うのか

せっかくなんでクリスマスネタをやりたいと思いつつも、ツリー書いたり、雪降らせたり
とてもじゃないですが、matsuさんに太刀打ちできそうにないので、別のネタを。

自分の子供はまだ0歳なんで、あんまり関係ないのですが、世の中のお父さんサンタさん達は、いつ頃プレゼントを仕入れに行ってるのかなと疑問に思いました。

どうしましょうね。

日本にはe-statという、政府統計を誰でも自由に利用できる仕組みがあります。
そこで家計調査を選択し、2015年の二人以上の世帯の「第6-16表 1世帯当たり1か月間の日別支出」を2カ月分ほどEXCELで落としてきます。

ちなみに集計表によってはCSVで落とせたり、API叩いて落とせたりするんですが、今回はEXCELを読み込みます。

ダウンロードしたファイルを開いてみると、












わぁ、なんて素敵な神エクセルでしょう。
ユーザーが読み込んで使うかもしれない統計表をこういうレイアウトにするセンスに脱帽です。

データステップで激しく加工して、品目分類を「玩具」に絞ってプロットしてみます
(ちなみに玩具は「テレビゲーム機」「ゲームソフト等」「他の玩具」を含む分類です)

値自体は、子供のいない世帯も含む平均値なので、絶対的にはあまり意味ないです。
また、事前に予約しておいて、支払いは受け取り時だったりするケースや、クレジットの支払いがどうなっているのかなど
細かい話はおいておいて、傾向をざっくり見てみましょう。


libname ex11 "XXXXXXX\raw\a616_11.xls" mixed=yes header=no;
libname ex12 "XXXXXXX\raw\a616_12.xls" mixed=yes header=no;

data wk1;
set ex11.'6-16二人以上の世帯$'n(drop=F43 in=in1)
      ex12.'6-16二人以上の世帯$'n(in=in2)
;
where F9="玩具";
if in1 then month=11;
if in2 then month=12;

run;

proc transpose data=wk1 out=wk2;
  var F11-F42;
  by month;
run;

data wk3;
set wk2;
val=col1;
year=2015;
day=input(substr(_NAME_,2,2),?? best.)-10;
    date=mdy(month,day,year);
wd=choosec(weekday(date),'日','月','火','水','木','金','土',);
label=cats(put(day,z2.),wd);

if date ne .;
format date yymmdds10.;
keep date val label santa;
run;

title"品目分類835-837 玩具の支出";
proc sgplot data=wk3 noautolegend;
series x=date y=val;
text x=date y=val text=label;

xaxis values=("01NOV2015"d to "31DEC2015"d by 15 );
run;



































やっぱり12月に入ると、土日を中心に活発に活動してるみたいですね。
年比較すると、昔より購入時期が早くなってるのか?なども見えてくるかもですね。
予約時期はこのデータからだと見えませんが。
様式が面倒すぎてあまりやる気がおきませんが…。

データステップ内でプロシジャを動かして、その結果をキー結合するマクロ

最近思いついた、相当しょうもないアイデア。
書くのは恥だが役に…って類いのコード

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

data A;
ID=1;SEX="M";GRP=1;output;
ID=2;SEX="F";GRP=1;output;
ID=3;SEX="M";GRP=3;output;
ID=4;SEX="F";GRP=2;output;
ID=5;SEX="F";GRP=2;output;
run;

data B;
ID=1;SEX="M";WEIGHT=75;output;
ID=2;SEX="F";WEIGHT=55;output;
ID=3;SEX="M";WEIGHT=65;output;
ID=4;SEX="F";WEIGHT=45;output;
ID=5;SEX="F";WEIGHT=85;output;
run;

data C;
GRP=1;HEIGHT=175;output;
GRP=1;HEIGHT=155;output;
GRP=1;HEIGHT=165;output;
GRP=2;HEIGHT=145;output;
GRP=2;HEIGHT=185;output;
run;

BでSEXごとに求めた要約統計量をSEXをキーにしてAに結合、
CでGRPごとに求めた要約統計量をGRPをキーにしてAに結合っていうような
処理を埋め込み系のマクロにして、1ステップ中にいくらでも入れれるようにしたいなと思いました。

ようするにこういうこと
data D;
set A;
%get_summary(dataset=B,key=SEX,var=WEIGHT)
%get_summary(dataset=C,key=GRP,var=HEIGHT)
run;

結果は(画像は途中まで)




で、肝心のマクロの中身は

%macro get_summary(dataset=,key=,var=);
%let qkey  = %sysfunc( tranwrd( %str("&key") , %str( ) , %str(",") ) );
length &var &var._SUM &var._MEAN &var._StdDev &var._MIN &var._MAX 8.;
call missing(of &var &var._SUM &var._MEAN &var._StdDev &var._MIN &var._MAX );
if _N_=1 then do;
rc=dosubl("proc summary data=&dataset nway;class &key;var &var.;
                         output out=temp sum= mean= std= min= max=/autoname");
declare hash h&sysindex.(dataset:"temp");
h&sysindex..definekey(&qkey);
h&sysindex..definedata("&var._SUM","&var._MEAN","&var._StdDev","&var._MIN"
                                         ,"&var._MAX  ");
h&sysindex..definedone();
rc=dosubl("proc delete data=temp;run;");
end;
if h&sysindex..find() ne 0 then call missing(of &var._SUM &var._MEAN &var._StdDev &var._MIN &var._MAX);
drop rc &var. ;
%mend get_summary;

知ってる人からしたら、なんだdosubl関数使うのか、そりゃできるでしょうよって馬鹿にされそう。

けど、まあ、当然に感じる人が大半かもしれないけど、ハッシュに格納する直前で、このタイミングでデータセット作って間に合うっていう感覚は個人的には斬新。

dosubl関数は、call executeと違って、データステップの途中で即時的に処理が開始できる特性があります。
なのでdosubl関数でデータセット作って、そのままハッシュに入れて、すぐ消してるんですね。小賢しい…
だから見かけ上は1ステップでも、速くはないです(mergeよりかは速いけど)。
あらかじめプロシージャ回してデータセット作ってからハッシュに格納するのと同じです。

本来これはDS2でやるか、ハッシュ内でループで計算してから繋ぐ書き方でやる処理なんですね…。

ちなみに、思いつきでパパッと書いたマクロなんで、使うならちゃんと検証してくださいね。
多分、穴はあると思います。まず、再帰ができない、ようするにset対象自身を指定できないとかね。
あと複数変数指定にも対応してないですね

あ、今書いてて思ったんですけど、この記事、SAS忘備録の
「データステップ内でプロシジャを実行する。」の焼き増しですね。
http://sas-boubi.blogspot.jp/2016/05/blog-post_17.html


DATEKEYSプロシジャを使って日付にイベントを関連付ける(なんか凄い応用効く予感)

SASにはholidayname関数、holidaycount関数、その他holidayから始まる
たくさんの関数があります。それらは日付や期間を与えることで祝日の名前や祝日の数を
返したりする機能を持っているのですが…

試しに11月11日を与えて、そこに定義されている祝日の名前を書きだしてみます

data _null_;
dt='11NOV2016'd;
do count= 1 to holidaycount(dt);
name=holidayname(dt,count);
put dt yymmdds10. +2  count= name= ;
end;
run;






と、なんか3つでてきましたね
ちなみに
VETERANS : 退役軍人の日 11月11日
VETERANSUSG : 退役軍人の日(米国政府の祝日)
VETERANSUSPS : 退役軍人の日(米国郵政公社の祝日)
らしいです。

要するに、対応しているのが米国の祝日なんですね。
日本じゃ使い道ね~、工数計算とかスケジュールをSASで作るときに使えたら楽なのに~ってずっと思ってました。
ところが9.4で試用版として導入されたproc datekeysを使うことで話は変わってきます。

proc datekeysは結構深いプロシジャのようなのですが、今回は一番単純に使ってみます

2016年の祝日を直打ちで設定(データセットからも流し込める)して
それを使って5月の祝日判定をしてみましょう

まずdatekeysプロシジャを使って、イベント日の定義情報をデータセット化します

proc datekeys;
   /*元日*/ datekeydef New_Years_Day= '1JAN2016'd / pulse=day;
   /*成人の日*/ datekeydef Coming_of_Age_Day= '11JAN2016'd / pulse=day;
   /*建国記念の日*/ datekeydef National_Foundation_Day= '11FEB2016'd / pulse=day;
   /*春分の日*/ datekeydef Vernal_Equinox_Day= '20MAR2016'd / pulse=day;
   /*振替休日*/ datekeydef Substitute_Holiday= '21MAR2016'd / pulse=day;
   /*昭和の日*/ datekeydef Showa_Day= '29APR2016'd / pulse=day;
   /*憲法記念日*/ datekeydef Constitution_Memorial_Day= '3MAY2016'd / pulse=day;
   /*みどりの日*/ datekeydef Greenery_Day= '4MAY2016'd / pulse=day;
   /*子供の日*/ datekeydef Childrens_Day= '5MAY2016'd / pulse=day;
   /*海の日*/ datekeydef Marine_Day= '18JUL2016'd / pulse=day;
   /*山の日*/ datekeydef Mountain_Day= '11AUG2016'd / pulse=day;
   /*敬老の日*/ datekeydef Respect_for_the_Aged_Day= '19SEP2016'd / pulse=day;
   /*秋分の日*/ datekeydef Autumnal_Equinox_Day= '22SEP2016'd / pulse=day;
   /*体育の日*/ datekeydef Health_Sports_Day= '10OCT2016'd / pulse=day;
   /*文化の日*/ datekeydef Culture_Day= '3NOV2016'd / pulse=day;
   /*勤労感謝の日*/ datekeydef Labour_Thanksgiving_Day= '23NOV2016'd / pulse=day;
   /*天皇誕生日*/datekeydef Emperor_Akihitos_Birthday= '23DEC2016'd / pulse=day;

   datekeydata out=MyHolidays ;
run;

次にeventds=オプションで定義データセットを指定します。ここでnodefaultsも追加しておくと
デフォルトの定義(=アメリカの祝日)を外すことができます。日米両方の祝日をだしたければnodefaultsはいりません。

options eventds=(nodefaults MyHolidays);
data MAYHOLI;
length dt 8. name $50.;
do dt='1MAY2016'd  to '31MAY2016'd ; 
call missing(name);
if holidaycount(dt) = 0 then output;
else do count= 1 to holidaycount(dt);
name=holidayname(dt,count);
output;
end;
end;
format dt yymmdds10.;
keep dt name;
run;






















というわけです(画像は途中まで)。
ちなみにイベントデータは、週単位や月単位、あるいは幅を持って設定もできますし、
リファレンス見てるともっと色々できそうです。

これ、祝日とか判定する以外にもなんか使えそうじゃないですか?

特に、開始と終了日を与えてその間の祝日回数を数えるHOLIDAYCK関数とか応用できそうじゃないですか?

頭のいい人は何か使い道を編み出して発表してください


ハッシュオブジェクト感動物語(おまけ付き)

タイトル適当です。

たまたまSAS初心者の方が書かれている以下のブログ記事を読んで、感動しました。
ついに、ついに、mergeステートメントを覚えるより先にハッシュオブジェクトを覚えて、データ結合をハッシュオブジェクトでやる方が現れだしましたよ。

「SAS初心者のメモ」
hashの世界へようこそ①
http://sasbiginner.seesaa.net/article/442279733.html

ぜひこれからも頑張ってください(組織でハブられないようにほどほど気をつけて…)。

やっぱり新しい人がピュアな判断で良いと感じれる技術は、
ある程度確かな「正しさ」を持っていて、必ず定着化すると思うんですよね。

長くやってると自分のプログラミングスタイルで価値観が曇りがちじゃないですか。

ハッシュオブジェクトが一部のマニアなおっさん(自分を筆頭)にしか使われない裏技程度の位置づけになったら嫌だなぁ、スタンダードになるべき技術なのに…
と感じていたので、本当に感動しました。

別に「ハッシュは俺が育てた!」と言うつもりはないですが、もう何年もハッシュ使え~ハッシュ使え~と、わめいていた身としては感無量です。

ハッシュオブジェクトのメリットと仕事への具体的な導入法がわからないという方は一例として
以下の記事を読んでみてください。

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

さて、ここからはおまけ
最近Luaに首ったけでしたが
たまにはハッシュオブジェクトの新しい話でもしましょうか。

9.4から追加されたdo_overメソッドは超イケてますよ。
なにせ、findメソッドとfind_nextメソッドを組み合わせて書いていたmultidataの処理が
一撃ですからね。find_nextメソッドの存在価値なくなった気がしますね

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

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












data B;
x=1;y='A';val=1;output;
x=2;y='B';val=2;output;
x=2;y='C';val=3;output;
x=2;y='D';val=4;output;
x=2;y='E';val=5;output;
x=4;y='F';val=6;output;
x=4;y='G';val=7;output;
x=4;y='';val=.;output;
x=6;y='H';val=8;output;
x=6;y='I';val=9;output;
run;


















Bのデータセットをxの値でグループ化して、文字変数は「,」で連結、数値変数は
合計と平均をだしてAにくっつけるという処理を1ステップでかけと言われても

data C;
length x 8. z $100.;
if 0 then set B;
set A;
if _N_=1 then do;
declare hash h1(dataset:"B",multidata:"Y");
h1.definekey("x");
h1.definedata("y","val");
h1.definedone();
end;
call missing(count,total);
do while(h1.do_over() eq 0);
 z=catx(',',z,y);
 count+val ne .;
 total +val;
end;
mean = divide(total,count);
keep x z total mean;
run;



で終わりです。

いままでなら、

data D;
length x 8. z $100.;
if 0 then set B;
set A;
if _N_=1 then do;
declare hash h1(dataset:"B",multidata:"Y");
h1.definekey("x");
h1.definedata("y","val");
h1.definedone();
end;
call missing(count,total);
rc=h1.find()
do while( rc eq 0);
 z=catx(',',z,y);
 count+val ne .;
 total +val;
 rc=h1.find_next();
end;
mean = divide(total,count);
keep x z total mean;
run;


と書かなきゃならんような処理です。

それにしてもハッシュオブジェクトのコードはやっぱり良いですね。本当はDS2のハッシュの方が美しいですが、データステップのハッシュも、それでもやっぱり美しい。

この気持ちをできるだけ多く共有したいですね。
やっぱりコード書いて、読んで、美しい楽しいと感じれるのは
人生の充実に繋がると思うので、これからも普及させてたいですね(怪しい宗教みたいですね)



ちょっとした情報共有:明らかにFALSE(TRUE)なWHERE句です(9.4から)

かなりの小ネタというか、発見なのですが

data A;
set sashelp.class;
where 2 < 1;
run;

data B;
set sashelp.class;
where 2 > 1;
run;

のようにwhere句が、論理式が定数で作れていて、どうあがいてもTrueかFalseしかとりえない
場合、9.4から




って、感じでNOTEの中に/* */のコメントの形でアナウンスがでるようになったみたいですよ。

NOTEとかWARNINGとかのSASが生成するメッセージの中にコメントステートメント入ることって今までありましたっけ??

変な出方なんで、もし、会社でログチェック系のプログラムとかツールを管理してる人は一応正しく処理できるか気をつけてくださいね。

マクロ変数の結果などで意図的にそうなるように組む場合もなきにしもあらずなので(別データセットのobs)、チェック対象にするかどうかは各自の判断ですけど、一応注意喚起のため拾った方がいいメッセージかもですね。



解析対象集団とかのフロー図を一緒に書いてみよう!

たまには役に立つ話をしましょうか!!

各解析対象集団のフロー図は、いろんな業界で頻繁に書きますよね
でもだいたいの業界では、SASでは数字だけ出して図は別のソフトとかで書いちゃいます

ただ、医薬系は色々あって、手で数字の切り貼りはあんまりやらないし、
いろんなソフトを組み合わせるのも、ちょっとやりにくかったりします。

完全にSASでやれと言われると結構面倒ですよね。
出力がEXCELならまあ、エクセルの罫線でテンプレート書いといて、セルにDDEやらLIBNAMEで出せばいいかな。

RTFなら?
まあ、EXCELと同じようにWORDで表作って、そこにマクロ変数埋めてproc streamするとか、
proc reportでも頑張れば書けるらしいので、もし先人の作った偉大なマクロがあればそれを使いますね。
putでタグを頑張って表を作るのもありでしょう。

画像で作って欲しいと言われたら?
GPLOTとかSGPLOTで書くのか~。annotateいるのかな~、なんか面倒そうだな~って感じします。

みんなどうしてるんでしょうね

最近の感想としては、9.4からsgplotに一筆書きの感覚でどんな図形でも書き放題のpolygonステートメントができて、四角の枠が書きやすくなったことと(まあ、以前でも直線4つで箱書けばいいんですけどね。)、textステートメントが追加(以前はscatterのマーカーにテキストあててじゃないとできなかったはず)されたことで、エクセル芸を駆使するような特殊なものでなければsgplotで書くのが一番楽なんじゃないかなと思うようになりました

一気に書くと何やってるかわかりにくいので、一個ずつ手順を踏んで一緒に素敵なフロー図をつくましょう

まずは箱を一個作ってみましょう

/*1個目の箱*/
data hako1;
boxid=1;box_x=10;box_y=100;output;
boxid=1;box_x=30;box_y=100;output;
boxid=1;box_x=30;box_y=90;output;
boxid=1;box_x=10;box_y=90;output;
run;

title "Step1 箱を書いてみよう";
proc sgplot data=hako1 noborder noautolegend;
  polygon id=boxid x=box_x y=box_y;
  xaxis display=none min=0 max=100 offsetmin=0 offsetmax=0;
  yaxis display=none min=0 max=100 offsetmin=0 offsetmax=0;
run;



はい、適当な解説をいれます。
polygonステートメントは1個のをまとまりを作るためにid=でIDを指定する必要があります。
後はx軸の値をy軸の値で四角形になるように座標取りすればいいのですが注意点は
オブザベーションの並びです。四角形の左上→右上→右下→左下

次は箱にテキスト入れてみましょう

/*1個目のテキスト*/
data text1;
text="将棋経験者#N=100";text_x=20;text_y=95;output;
run;

data hakotext1;
set hako1 text1;
run;

title "Step2 文字を入れてみよう";
proc sgplot data=hakotext1 noborder noautolegend;
  polygon id=boxid x=box_x y=box_y;
  text x=text_x y=text_y text=text / textattrs=(size=12 )  splitchar='#' splitpolicy=splitalways;
  xaxis display=none min=0 max=100 offsetmin=0 offsetmax=0;
  yaxis display=none min=0 max=100 offsetmin=0 offsetmax=0;
run;




楽ですね~。座標と内容を書くだけですね。 split系のオプションを使うことで改行を制御できます

次は矢印ですね!これはseriesにarrow系オプションを併用することでできます

/*1個目の矢印*/
data yaji1;
yajiid=1;yaji_x=20;yaji_y=90;output;
yajiid=1;yaji_x=20;yaji_y=80;output;
run;

data hakotextyaji1;
set hako1 text1 yaji1;
run;

title "Step3 矢印を入れてみよう";
proc sgplot data=hakotextyaji1 noborder noautolegend;
  polygon id=boxid x=box_x y=box_y;
  text x=text_x y=text_y text=text / textattrs=(size=12 )  splitchar='#' splitpolicy=splitalways;
  series x=yaji_x y=yaji_y/ group=yajiid  lineattrs=(color=black) arrowheadpos=end  arrowheadscale=0.3;

  xaxis display=none min=0 max=100 offsetmin=0 offsetmax=0;
  yaxis display=none min=0 max=100 offsetmin=0 offsetmax=0;
run;



でけたでけた。

じゃあその調子で2段目に2個ブロックを作って、そこに矢印引いてみましょうか

/*2段目の箱*/
data hako2;
boxid=2;box_x=10;box_y=80;output;
boxid=2;box_x=30;box_y=80;output;
boxid=2;box_x=30;box_y=70;output;
boxid=2;box_x=10;box_y=70;output;

boxid=3;box_x=40;box_y=80;output;
boxid=3;box_x=60;box_y=80;output;
boxid=3;box_x=60;box_y=70;output;
boxid=3;box_x=40;box_y=70;output;
run;
/*2段目のテキスト*/
data text2;
text="振飛車党#N=50";text_x=20;text_y=75;output;
text="居飛車党#N=50";text_x=50;text_y=75;output;
run;

/*2段目の矢印★途中で折り曲げてるので3つで1データ*/
data yaji2;
yajiid=2;yaji_x=20;yaji_y=85;output;
yajiid=2;yaji_x=50;yaji_y=85;output;
yajiid=2;yaji_x=50;yaji_y=80;output;
run;

data hakotextyaji2;
set hako1 text1 yaji1 hako2 text2 yaji2;
run;

title "Step4 もう要領つかんだよね";
proc sgplot data=hakotextyaji2 noborder noautolegend;
  polygon id=boxid x=box_x y=box_y;
  text x=text_x y=text_y text=text / textattrs=(size=12 )  splitchar='#' splitpolicy=splitalways;
  series x=yaji_x y=yaji_y/ group=yajiid  lineattrs=(color=black) arrowheadpos=end  arrowheadscale=0.3;

  xaxis display=none min=0 max=100 offsetmin=0 offsetmax=0;
  yaxis display=none min=0 max=100 offsetmin=0 offsetmax=0;
run;



矢印をカギ形に曲げたりするのも普通に3点指定するだけなので、そんなに難しくないはず。

ここまで、できた人は、チャレンジで振飛車を中飛車、四間飛車、三間飛車、向かい飛車に分類するフロー図を書いてみましょう!!

僕は面倒なのでここまでで!

クロージャってなんじゃろ?

今日もまたSASマクロの悪口、Proc Lua導入のメリットを紹介する記事です。

今回はクロージャです。

ちなみにクロージャやコルーチン(いずれ紹介)などスコープ絡みの部分が、Luaのメリットのひとつであるということは、かなり初期の頃からfukiyaさんやmatsuさんが示唆されてました。僕はその時点では全く意味がわかってませんでした。本当尊敬です。

wikipediaでクロージャと調べると
「引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決することを特徴とする。関数とそれを評価する環境のペアであるともいえる」
う~ん、やっぱり、なんじゃろ??

closure → 直訳すると閉鎖、閉店ガラガラみたいな感じ クローズ。

この閉じてるというのがクロージャのポイントなんですね。

SASマクロで何か処理をして、その結果を保持して、次にそのマクロを実行するときに参照して処理したいとなったらどうします?

例えば、データセットをパラメータとして与えると、オブザベーション数をカウントして返す。呼び出されるたびにそれまでにカウントした合計に足していくマクロを作れといわれたらどうしますか?

以下のようにかけると思います。

%let tobs=0;

/*マクロ定義部分*/
%macro m(ds);
data _null_;
 call symputx('tobs',nobs+&tobs);
 if 0 then set &ds nobs=nobs;
 stop;
run;
%put 合計=  &tobs;
%mend;

/*実行部分*/
%m(sashelp.class)
%m(sashelp.iris)
%m(sashelp.cars)

結果は







何がいいたいかと言いますと、値の保持、受け渡しの手段としてグローバルマクロ変数
(&tobs)を作成して使ってますね。

SASの場合、データステップ内であればretainを使って値を保持できますが、ステップをまたぐ場合、データセットを作ってその中に入れるか、あるいはグローバルマクロに入れるかしか方法がありません。
(まあ、フォーマット作るとか外部ファイルにだすとかもあるかもしれませんが…)

SAS使いにとっては疑いのない当たり前の手段ですが、これは本当はちょっと怖いことやってるんですね。

値を受け渡す前に、意図せずマクロ変数の中身が変えられていたら?参照するデータセットが意図せず更新されたら?
マクロを呼び出すときに、参照するものが、本当に前回までの結果なのか保証できますか?

例えばプログラムをどんどん増築したり、複数のプログラムをincludeしまくる等した時、プログラム作成者が複数人になればグローバルマクロ変数の名前やデータセット名がかぶる事は珍しいことではないです。

値がオープンで、処理と結びついていないところで参照・更新できうるということが問題なわけです。

なんとなく話が見えてきたかと思いますが、それに対してのクロージャはというと

/*関数定義部分*/
proc lua;
 submit;
  function L(ds)
   local nobs= 0
   return function(ds)
    local dsid = sas.open(ds)
    nobs = nobs+sas.nobs(dsid)
    sas.close(dsid)
    return nobs
   end
  end
endsubmit;
quit;

/*実行部分*/
proc lua;
 submit;
  L1=L()
  print ("合計=",L1("sashelp.class"))
  print ("合計=",L1("sashelp.iris"))
  print ("合計=",L1("sashelp.cars"))
 endsubmit;
quit;

結果は同じ






ですが

proc lua;
 submit;
  print(nobs)
 endsubmit;
quit;

としても、




中身は空、つまり定義されていない。
nobsという変数は、Lの中でのみ存在・保持されていて、外からは触れられないんですね。

これにより、他の処理でnobsという変数名を使っても衝突することがなくなります。
完全に閉じてるわけです。

Lを代入して変数であり関数のL1(インスタンスみたいなもん)を作って実行しているわけですが、例えばL2 L3を作ってそれぞれ
独立してカウントすることもできます

proc lua;
submit;
 L2=L()
 L3=L()
 print ("合計=",L2("sashelp.class"))
 print ("合計=",L2("sashelp.iris"))
 print ("合計=",L3("sashelp.class"))
 print ("合計=",L2("sashelp.class"))
 print ("合計=",L3("sashelp.class"))
endsubmit;
quit;









さて、肝心の解説(たぶん正確ではないから詳しい人に聞いてね)

  function L(ds)
   local nobs= 0
   return function(ds)
    local dsid = sas.open(ds)
    nobs = nobs+sas.nobs(dsid)
    sas.close(dsid)
    return nobs
   end
  end
L1=L()

関数Lはそのローカルの範囲に変数nobsと戻り値として名前をもたない関数(return function(ds)の部分ね。無名関数という)をもってますよね。、
このとき戻り値(関数)には自分の上層にいるL(エンクロージャという)のローカル変数nobsも環境としてくっついているんですね。
しかし、L1に代入されたとき、戻り値の関数から見た変数nobsは、ローカル変数ではなく・グローバル変数でもないという謎の状態になります
ローカル変数ではないので、関数終了後に消えるという運命は適応されません。、かといってグローバルではないので他からアクセスもできません
関数実行時にのみ内側でしかアクセスできずに残る。このようにグローバルやローカルではなく、処理ブロック自体がスコープを閉じ込めているのをレキシカルスコープ(静的スコープ)といい、今回のnobsは「レキシカル変数」といわれるのです

小ネタ Proc fslistでテキストファイルの中身をすぐ確認できるよ

CSVつくったり、プログラムの自動生成とか、なんしかテキストファイルの類を出力した後、ちゃんと出力できてるかの確認ってどうやってます?

いちいちファイルを開いてもいいですが

proc fslist file="\XXXX.txt";
run;

fslistプロシジャを使うと、専用のfslistウインドウが開いてすぐに確認できますよ。
それだけですけど、意外と便利ですよ。

たぶん古いプロシジャ過ぎるのと、最近、固定長とか扱う人が減ったのであんまり知られてないのかも。

最近proc streamでプログラムの自動生成をトライ&エラーでやってるときとか楽に感じますね。

fslistウインドウが開いた状態でコマンドウインドウにnums または colsとかを打つことで、列・行位置に番号振ってくれるので、固定長ファイルを読み込む前のちょっとした確認とかにもよろしいですよ。

温故知新な感じでいいですね。sas on demandで使えないので画像なしですが、まあ試してください



別のオプション空間を作ってSASを実行する話。個人的には微妙な仕様だけども

sasのオプションはoptionsステートメントで定義できます。
これはいわゆるグローバルな定義で、一旦設定をいじると、それ以降ずっとそのオプションが
反映されます。

options missing = - ;
とすれば数字欠損値の表示がハイフンになり、
options obs=1;とすると、1オブザベーションしか読み込まれなくなります。

よくある失敗として、そのオプションを適用したい部分が終わっても、元に戻すoptionsステートメント
を書き忘れて、それ以降の処理に全部適用されちゃったってパターンがあります。

処理が終わったらかならず、

options missing = . obs=max;

などとして、戻してやらなければいけません

まあ、この程度のオプションならいいですが、非常に込み入った設定をごく一部の処理にだけ
適用したい場合、戻すのも面倒なんですね。

それをパパっとやりたい、そんなあなたに朗報!

以下のコードを実行してみます

data a;
x=.;output;
x=1;output;
x=2;output;
run;

proc lua restart;
 submit;
 sas.submit[[
 title "Luaのsas.submitの中①";
 options missing=- obs=1;
 proc print data=a;
 run;
 ]]
 endsubmit;
run;

 title "普通のsas";
 proc print data=a;
 run;

 proc lua;
 submit;
 sas.submit[[
 title "Luaのsas.submitの中②";
 proc print data=a;
 run;
 ]]
 endsubmit;
run;

 proc lua restart;
 submit;
 sas.submit[[
 title "restartしてリセットされたLuaのsas.submitの中";
 proc print data=a;
 run;
 ]]
 endsubmit;
run;











































と、luaのsas.submitの中で定義したオプションが
その中でのみ働いていることがわかります。

便利ですね。proc luaの使い道の一つかも知れません。


でも個人的には
う~ん、この仕様、正直いいんだか悪いんだか、ちょっと僕には微妙です。
ちょっとしたときにはいいけど、オプション空間が2つあるのは混乱を招くような気も…
ちなみにLuaのSAS.submit内のoptionは、最初に実行した際のグローバルの設定がベースになりますからね。

SAS.submit内でoptionを設定した場合と、Luaのsas.submit初回実行後にsasの方でoptionいじったときに初めて、環境がズレていくわけです。


小ネタ proc streamでクリップボードに出力できた~!できたよ~!できただけだよ~!

proc streamのoutfile=って出力先の外部ファイルを指定するものですが、
もしかしてクリップボード指定できるんじゃない?って思いつきました。

できました。狙い通りに動いてニヤりなんですが、使い道はしらないです。

9.4使われている方は、以下のコード実行した後に[Ctr]+[V]してみてください。

%let person1=おじいさん;
%let person2=おばあさん;
%let place1=山;

filename clip clipbrd;
proc stream outfile=clip resetdelim="goto";
begin
むかしむかし &person1.と&person2.が&place1 に住んでおったそうだ goto NEWLINE;
&person1.は山に芝刈りにいき goto NEWLINE;
&person2.は川に洗濯にいった。
;;;;


できた~!!わ~い!!

どうして、いつもいつも何の役にも立たないことばかり閃くのか…

Compareプロシジャの結果が一致か不一致か、何が不一致かをマクロ変数で取得する話

コンペアプロシジャで、解析用データセットとかをいっぱいコンペアするときに、ひとつなぎで結果がでてくると見にくいし、スクロールだるいし、そもそもSASアウトプットもhtmlも見づらいので
僕はデータセットごとにコンペア結果をodsでPDFファイルにしたりします。
コンペアすると、いつもだいたい僕が間違ってますが、ダブルやってるもう一人の人が間違ってる場合に印刷してあげやすいしね。

そのときに、一致している場合はファイル名の頭にOK、してない場合はNGをつけるって処理を書いてたんですね。

あらじめOK_XX.pdfで出力しといて、NGの場合に
rc=rename(...\OK_XX.pdf,...\NG_XX.pdf,'file')みたいな感じです。

それはいいんですけど、コンペアが一致したかどうかの判定をods outputで
CompareSummaryとか諸々をだして、その中の文言で判定するという方法でやるしか知りませんでした。
バージョンによってなんか文言が微妙に変わるし、効率的じゃないなと思ってましたが、
最近、コンペアプロシジャの結果を自動マクロ変数で取得できるっていうことを教えていただきました。いや~、しらんかった。

&sysinfoを使います。

こいつはコンペアプロシジャの実行後に展開した場合、完全に一致してれば0が入り、値や属性など何かしら不一致であれば0以外の数がでます。

では見てみましょう。

データセット3つ用意します

data a;
x=1;
run;

data b;
x=1;
run;

data c(label="aaa");
x=2;y=1;output;
x=2;y=1;output;
format x yymmdds10.;
run;

一致している場合、

proc compare base=a comp=b;
run;

%put sysinfoの中身は → &sysinfo;

結果







不一致の場合、

proc compare base=a comp=c;
run;

%put sysinfoの中身は → &sysinfo;





となります。

で、気になるのが不一致だった場合、どんな不一致がでているのかを全部だせるのかという話ですが、band関数という2つの引数のビットごとの論理積を返す関数を使ってメッセージを分離できます。

では実際に、それを組み込んだコンペアマクロを見てみましょう。

%macro compare(main,sub);

 %if %sysfunc(exist(&main)) ne 1 %then %do;
  %put WARNING:メイン側&main.がないぞ;
 %end;
 %if %sysfunc(exist(&sub)) ne 1 %then %do;
  %put WARNING:サブ側&sub.がないぞ;
 %end;

 %if %sysfunc(exist(&main)) and %sysfunc(exist(&sub)) %then %do;
proc compare base=&main comp=&sub;
run;
%put NOTE:&main.と&sub.コンペアしました;
%if %sysfunc(band(&sysinfo,1)) eq 1
         %then %put WARNING:データセットラベルが違うぞ
[Data set labels differ];
%if %sysfunc(band(&sysinfo,2)) eq 2
%then %put WARNING:データセットのタイプが違うぞ ビューとかコンペアしてんのか?
[Data set types differ];
%if %sysfunc(band(&sysinfo,4)) eq 4
%then %put WARNING:インフォーマット違うぞ
[Variable has different informat];
%if %sysfunc(band(&sysinfo,8)) eq 8
%then %put WARNING:フォーマット違うぞ
[Variable has different format ];
%if %sysfunc(band(&sysinfo,16)) eq 16
%then %put WARNING:レングス違うぞ
[Variable has different length];
%if %sysfunc(band(&sysinfo,32)) eq 32
%then %put WARNING:ラベル違うぞ
[Variable has different label];
%if %sysfunc(band(&sysinfo,64)) eq 64
%then %put WARNING:メイン側なんかobs数多いぞ
[Base data set has observation not in comparison];
%if %sysfunc(band(&sysinfo,128)) eq 128
%then %put WARNING:メイン側なんかobs数少ないぞ
[Comparison data set has observation not in base];
%if %sysfunc(band(&sysinfo,256)) eq 256
%then %put WARNING:BY変数でメインにだけあるやつあんぞ
[Base data set has BY group not in comparison];
%if %sysfunc(band(&sysinfo,512)) eq 512
%then %put WARNING:BY変数でメインにないやつあんぞ
[Comparison data set has BY group not in base];
%if %sysfunc(band(&sysinfo,1024)) eq 1024
%then %put WARNING:メイン側変数多いぞ
[Base data set has variable not in comparison];
%if %sysfunc(band(&sysinfo,2048)) eq 2048
%then %put WARNING:メイン側変数少ないぞ
[Comparison data set has variable not in base];
%if %sysfunc(band(&sysinfo,4096)) eq 4096
%then %put WARNING:値が違うね
[A value comparison was unequal];
%if %sysfunc(band(&sysinfo,8192)) eq 8192
%then %put WARNING:型の違う変数があるね
[Conflicting variable types];
%if %sysfunc(band(&sysinfo,16384)) eq 16384
%then %put WARNING:BY変数一致してないね
[BY variables do not match];
%if %sysfunc(band(&sysinfo,32768)) eq 32768
%then %put WARNING:コンペア自体コケてるやん。論外
[Fatal error: comparison not done] ;
%if &sysinfo eq 0 %then %put OKおめでとう!!;
 %end;
%mend compare;

%compare(a,c)



というわけです。
[]の中の英語がSASの公式のメッセージで、日本語はメインに非があるという体で僕が適当に書きました。これをうまく使うと、差異を把握しやすいのでコンペア・修正・すり合わせの作業がちょっと楽になるかもしれませんね。

コンペア状況一覧とか作って、細かく進捗管理してるぜ~って感じだすと、
あんまり進んでなくても、ウケがよかったりします

あ、Luaでもかけますからね

割と画期的なアイデアでは??SASプログラムの自動生成について

ちょっと、今日ぱっと思いついたんですけど、
STREAMプロシジャは性質として、内包されるマクロ変数を展開、そして内包されるマクロを実行コードレベルまで展開するけど実行はしないというものを持ってるじゃないですか?
そしてLuaプロシジャは内包するマクロ変数を展開しないという性質がありますよね。

ということはLuaプロシジャでマクロを定義して、それを入れ込んだSTREAMをsubmitする流れをうまく使えば、例えば仕様書的な情報から効率的にプログラム生成できそうじゃないですか?

業界を問わず、仕様書からのプログラム自動生成って割とみんなの夢だと思うんですが、
既存のSASだと、put文で一生懸命作るやり方で、もちろん部分部分をマクロ化して構造的にしていたけども、全体として煩雑なものになりがちだったと思います。
あと地味にSASマクロのクォーティングを気にしてテキスト生成する必要があったりして結構うざい思いをしたことがあると思います。

例えば、以下のデータセットがあって、ここからプログラムを生成するとしたら

data define_table;
titleno='Table-1-1';title='体重の要約統計量';
table='SASHELP.CLASS';target='WEIGHT';method='UNIVARIATE';output;
titleno='Table-2-1';title='性別の頻度集計';
table='SASHELP.CLASS';target='SEX';method='FREQ';output;
run;






以下のように書ける訳です


proc lua;
submit;

--仕様書から情報を抜きつつループ
local dsid = sas.open("define_table")
while sas.next(dsid) do
  local titleno=sas.get_value(dsid,"titleno")
  local title  =sas.get_value(dsid,"title")
  local table  =sas.get_value(dsid,"table")
  local target  =sas.get_value(dsid,"target")
  local method  =sas.get_value(dsid,"method")

--プログラムヘッダー部分
sas.submit
([[
%macro title;
%nrstr(*---------------------------------------) goto newline;
   帳票番号  : @titleno@ goto newline;
   表名      : @title@   goto newline;
   自動生成日: &SYSDATE9 goto newline;
%nrstr(---------------------------------------*)
%mend title;
]])

--プログラム本体部分
local var="VAR"
if method=='FREQ' then
 local var="TABLE"
end
sas.submit
([[
%macro main_pg;
PROC @method@ DATA = @table@; goto newline;
@var@ @target@;goto newline;
RUN;
%mend main_pg;
]])

--実際にファイルを生成する部分
sas.submit
([[
filename outf "D:\temp\@titleno@.sas";
proc stream outfile=outf resetdelim="goto";
begin
/%title/
goto newline;
goto newline;
%main_pg
;;;;
]])

end
sas.close(dsid)
endsubmit;
run;


上記を実行すると2つのSASファイルができて、中身は以下のとおりです。

【Table-1-1.sas】

【Table-2-1.sas】










おのおのの部分をつくるマクロを生成しているLuaの部分を関数化などしてやれば
拡張性に富んだ自動生成コードが作れると思うんですよ。
条件分岐とかループも今までのようにマクロで制御するより、Luaで制御し放題なほうが楽だし。

そもそも21世紀にもなって、ずっとputで平テキスト打ってることずっと違和感あったんですよね

医薬業界はもはや言うまでもなくですが、データセットや解析仕様の標準化が進んでいる環境において活かせそうな発想だと思いますけど、どうでしょう?



proc Luaの世界⑥proc Luaとマクロ変数のカンケイ

珍しく記事連続投稿です。
一つ前の記事「パラメータマクロでやっていた処理をLuaで組むのはどういう感じななのか?の話」
http://sas-tumesas.blogspot.jp/2016/10/lua.html
も是非読んでもらえると嬉しいです。

僕は、処理をまるごとマクロにしてパラメータ与えて実行するマクロは、基本的にLuaで置き換えることができるし、そちらの方が良いと思っています。
%macroをSASコードから1行残らず駆逐してやる!とまでは思わないですが、マクロ一党支配の現状はちょっと変えていきたいですね。何年かかるかわかりませんが。

そんな%macroあんまり好きでない僕ですが、マクロ変数の仕組みは割と好きです。シンプルで強力なテキスト展開なので、、。

今日はProc Lua内でマクロ変数の値を取得する&Proc Lua内でマクロ変数を作成する話です。

さて、今、ふたつのマクロ変数を作ります。

%let m1=I;
%let m2=SAS;

通常、SASコード内に「&マクロ変数名」があると実行時に展開されますが、
以下のコードを実行してみると

proc lua restart;
submit;
print("&m1".. " love ".. "&m2")
endsubmit;
run;


はい、結果はこんな感じです




ダブルコーテーション使ってるのに&のままです。
proc luaの中は、鉄壁の不可侵領域で、一切マクロ変数の展開はおきません。
この仕様、僕は大好きですね。変にクォート処理とかに頭使わなくていいので気が楽です。

じゃあ、proc lua内にマクロ変数持ち込めないんですか?って話になりますけど
もちろんそんなわけないです。

ひとつは、普段のデータステップでもおなじみのsymget関数を使う。

proc lua restart;
submit;
print(sas.symget("m1").. " love ".. sas.symget("m2"))
endsubmit;
run;

結果は




或いは、以下のように

proc lua restart ;
submit "m1='&m1'; m2='&m2'";
print(m1 .. " love " .. m2)
endsubmit;
run;

submitの後のコーテーションの中で
割り当てることにより、Luaのローカル変数にマクロ変数の値を渡せます。
テキストとして渡したいので'&m1'と、コーテーションでくくっていることに注意です。

もしコーテーションでくくらずに、文字を渡した場合、それ自体がLuaの変数を指定していると
みなされるので、例えば以下のようなこともできます

proc lua restart;
submit;
I='YOU'
SAS=' LOVE Lua'
endsubmit;
run;
proc lua;
submit "m1=&m1;m2=&m2";
print(m1 .. m2)
endsubmit;
run;




ちなみにちょっと注意ですが、Proc Lua内ではマクロ変数の展開はおきませんが


proc lua restart;
submit;
sas.submit([[data _null_;
put "&m1 love &m2";
run;
]])
endsubmit;
run;

のようにsas.submitで実行されるSASコードは、実行前に通常と同じくマクロ変数の展開がおきますからね


さて、次は、Lua内でマクロ変数に値をいれる話ですが、簡単なのでさらりと


proc lua restart;
submit;
local x = 1+2+3
local y = 'aaa'

sas.symput('_x',x)
    sas.symput('_y',y)

endsubmit;
run;

%put &_x;
%put &_y;

要はsas.symputでいけるという話です。

なので、データセットのオブザベーション数や変数数をマクロ変数に取得するのも超簡単

proc lua;
submit;
      local dsid = sas.open("sashelp.class")
      local nobs = sas.nobs(dsid)
sas.symput('obs',nobs)
  local nvars = sas.nvars(dsid)
      sas.symput('vars',nvars)
      sas.close(dsid)
endsubmit;
run;

%put オブザベーション数は &obs;
%put 変数の数は &vars;

です

やっぱProc Lua楽しいですね


パラメータマクロでやっていた処理をLuaで組むのはどういう感じななのか?の話

最近いただいた意見で
「Luaをマクロの代わりにできるというイメージがわきません。
例えば、患者背景の一覧表作成に際して,数値データに対して要約統計量をだし、文字データに対しては頻度集計を行うといったマクロを最初にそれぞれ作って、適宜実行し、結果を成型して結合していますが、同じことがLuaでもできますか?」
というのがありました。

どっちでやるのがいいのかという議論はさておき、当然可能です。
細部まで作りこむのが面倒だったので、ざっくりこんな感じかなっていうのを作ってみました。

最初にマクロを定義して、適宜実行ということなので、プログラムの途中途中で呼び出すのだと思いますから、パラメータマクロと同じ機能を、Luaの関数でグローバルにして定義しています。
プログラムごとに管理するなら最初のproc luaにrestartをつけて、そこで一度リセットするようにしておけばよいでしょう。


/*Luaでパラメターマクロっぽく関数を定義*/
proc lua restart;
 submit;
 --カテゴリデータに対して頻度集計する関数
 function freq(ds,var)
  local code = [[proc freq noprint
data=@ds@;
tables @var@/out=temp
(rename=(@var@=out2 count=out3));
quit;
data out_@var@;
set temp;
out1="@var@";
run;
  ]]
  sas.submit(code)
 end

 --数値データに対して要約統計量をだす関数
  function summary(ds,var)
  local code = [[proc summary noprint
data=@ds@;
var @var@;
output out=temp(drop=_TYPE_ _FREQ_)
mean=Mean std=Std min=Min median=Median max=Max;
quit;
proc transpose data=temp out=out_@var@;
run;
data out_@var@;
set out_@var@;
out1="@var@";
out2=_NAME_;
out3=col1;
run;
  ]]
  sas.submit(code)
 end

 endsubmit;
quit;

で、あとは実際使用する場所で、いかのようにパラメータを指定して実行します

マクロを実行するようにluaの関数を実行する

proc lua;
submit;
 freq("sashelp.class","sex")
 summary("sashelp.class","age")
 summary("sashelp.class","height")
 summary("sashelp.class","weight")
endsubmit;
quit;

/*ただのデータステップで結果を結合すれば*/
data output;
length out1 out2 $50.;
set out_:;
keep out:;
run;




























round入れるべきでしたが、まあこんなもんでしょう。

データセットを指定したら、全変数に対して自動で数値型ならsummary 文字ならfreqをかけて
結果の結合までやる関数が欲しければ、以下のようにすればいいでしょう


proc lua;
submit;
  function autoreport(ds)
 local dsid = sas.open(ds)
 for var in sas.vars(dsid) do
  if var.type == "C" then
freq(ds,var.name)
  end
  if var.type == "N" then
summary(ds,var.name)
  end
 end
      sas.close(dsid)

sas.submit([[
data auto_output;
length out1 out2 $50.;
set out_:;
keep out:;
run;
]])
  end
endsubmit;
run;


proc lua;
submit;
 autoreport("class")
endsubmit;
run;

結果は上とだいたい同じですが、NAMEもカテゴリ集計されちゃうので、そういう部分の処理の工夫は必要ですね。

どうでしょう?作ってる感じとしてはマクロ作るのと、感覚的に大差ないので、スムーズに移行できると思いますが。

proc Luaの世界⑤SASデータセットをLuaのテーブルに取り込んでみる話

しばらく間があいてしまいましたが、僕のProcLua魂はまだ燃えてます
自分の何に変えても必ずやProc Luaを普及しなければならないという謎の使命感があります。
SASのやりすぎで、ちょっと頭のどこかがおかしくなったのかもしれませんね。

さて、僕は今のところproc luaを実行されるsasコードの処理分岐に使うことが多く、
SASデータセットの存在を確認したり、オブザベーション数や、含まれる変数の数、変数名など、いわゆるメタデータを取得して、実行SASコードを合成するといった、SASマクロの代替法としての使い方を主としてます。SASマクロの在り方に美しさを感じれない性があるので…

しかし、処理コードをどうこうするのではなく
Proc Lua自体でSASデータセットそのものを操作したり
データセットをLuaのテーブルに取り込んで処理をして、そこからLuaのテーブルを
SASデータセットにして戻すといったことも当然できます。

今日はSASデータセットをLuaのテーブルに取り込む話をします。

ただ、個人的な感触としてはがっつりとしたデータハンドリングは、敢えてLuaテーブルに取り込んでやるよりSASコードで捌いた方がいいかなぁとは思います。
なんでかっていうと、Luaのテーブルはメモリ上にあるので、あんまり大きいデータセットをテーブルにするのはパフォーマンス的に危険じゃないかなって点と、単純にSASデータセットの操作するのはSASコードが一番書きやすいですしね…
あとLuaのテーブルはちょっと柔軟すぎて、僕も含めてSASデータセットに慣れた頭には結構ついていけないとこもあるのです。

とはいえLuaのテーブルならではの処理の仕方も今後紹介する予定なので、
データの大きさに留意するという点を踏まえつつ、見てきましょう。

まずはテストデータ

data a;
x=1;y='A';output;
x=2;y='B';output;
label x='ラベル';
format x z2.;
run;

ではLuaからこのデータセットaにアクセスして、
xとyの値をログに書きだして見ましょう

proc lua;
submit;
--データセット内の変数を指定してプリントしてみる
local dsid = sas.open("a")
  while sas.next(dsid) do
    print(sas.get_value(dsid,"x"), sas.get_value(dsid,"y"))
  end
 sas.close(dsid)
endsubmit;
quit;

結果は以下のとおり




普段通常のデータステップにはSCL関数系を使い慣れている人にとっては、
書き方が似ているのでピンとくるかと思います。

まずsas.open("a")でデータセットを開いて、そのデータセットIDを取得します。
sas.next(対象データセットID) は順番にobsを移動していって、最終obsまでループします。
sas.get_value(対象データセットID,変数名)で指定した変数に入っている値を取得できます。
取得した値をそのままluaのprint関数ではいているわけです。
あと、これは鉄則ですがopenで開いたsasデータセットは必ず sas.close(dsid)で閉じなければ
いけません。

さて、毎度、getvalueに変数名を直打ちして指定するには正直面倒です。
存在する変数全ての名前を自動的に取得して、値を取りたくなります。
変数情報はどうやって取得するのかというと

proc lua;
submit;
--イテレータ関数sas.varsを使って、全変数の変数情報をプリントしてみる
  local dsid = sas.open("a")
  local table_1 = {}
  for var in sas.vars(dsid) do
    print(var.name)
    print(var.label)
    print(var.length)
    print(var.format)
  end
  sas.close(dsid)
endsubmit;
run;

結果は以下のとおり







変数yにはラベルもフォーマットもついていないので変数名だけがプリントされています。

for var in sas.vars(dsid) do の書き方についてですが以前紹介した pairsと同じでイテレータ関数というもので、こう書くことによって、varにデータセット内の変数が順番に入っていきます。

さてさて、そういえば今回やりたかったのはSASデータセットをLuaのテーブルにすることでしたね。
今まででてきたことに加えて、Lua備え付けのテーブルライブラリ関数のtable.insertを使ってそれを実現してみましょう

proc lua;
submit;
--応用してSASデータセットをLuaのテーブルに入れてみる
--変数名ごとにobsをキーにした子テーブルを作っている構造
  local dsid = sas.open("a")
  local table_1 = {}
  for var in sas.vars(dsid) do
    table_1[var.name] = {}
  end
  while sas.next(dsid) do
    for i, v in pairs(table_1) do
      table.insert(table_1[i], sas.get_value(dsid, i))
    end
  end
  sas.close(dsid)

  print(table.tostring(table_1))
   
endsubmit;
run;

結果は


















あ、ちなみにtostringで吐いた場合、出力順は固定じゃないので、xの前にyがきていても不思議じゃないですからね

構造を説明すると、今回table_1というLuaのテーブルにデータセットaの内容を入れたわけですが

まずLuaのtable_aは{ x={},y={} }というようにxとyのテーブルを中にもっていて、
xとyの中身はx[1]=1 x[2]=2 y[1]='A' y[2]='B'となっているわけです。
つまりデータセットを表現するのに、変数ごとに子テーブルを作り、オブゼベーション番号を要素キーにして表現したわけです。


別のやり方を見てみましょう。実はもっとお手軽にsas.load_ds(データセット名)で一発でSASデータセットをLuaのテーブルにできます。

しかし、その取り込まれ方にはちょっと癖があります

proc lua;
submit;
--load_dsでデータセットを読み込んでみる
table_2=sas.load_ds("a")
print(table.tostring(table_2))
endsubmit;
quit;

結果、


さっきとは少し違い、まずオブザベーション番号ごとに子テーブルをつくって、そこの変数名をキーとしてデータを持たせています。
さらにvarsテーブルというのが自動で作られ、それは変数名ごとの子テーブルを持ち、そこには変数タイプやラベルフォーマットなどのメタ情報が入っています。
さらにnameテーブルにはデータセット名、nvarsテーブルには変数の数が入ります。

要するにsas.load_dsでデータセットを読み込むと本体のデータ部分以外にコンテンツ情報も子テーブルにして入れてくれるというわけです。

はい、今日はここまで、次はもうちょっと簡単で具体的な話をいれたいなぁと思います

proc Luaの世界④Luaの関数は柔軟で奥が深いな~って話

Fukiyaさんの新作記事「Proc Luaを勉強してみての感想とProc Luaをオブジェクト指向プログラミグの試み」が激アツいです。

今日は関数のお勉強になるんですが、これがまた深いところなんで、いつも言ってますが
とりあえず簡単なとこだけやります。
僕もまだまだよくわかってなくて、本当はFukiyaさんかmatsuさんに解説してもらってから記事書きたいぐらいです。間違ったこと言ってたら殴ってくださいね

さて

と、そのまえに前回記事の補足です

sas.submit([[SASコード]],{置換パラメータ指定})
置換パラメータ指定の部分ですが、別にsubmitの中で指定しなくても
実行時点で置換パラメータがLuaの中で変数として定義されていればそれが適応されます

ので以下のコードも成立します。

proc lua;
 submit;
 local ds="b"
 local var="y"
 local code=[[data @ds@;
              @var@=1;
             run;]]
 sas.submit(code)
endsubmit;
quit;

あと、さらに唐突な補足です。Luaの論理式はFalseとnilが偽でそれ以外が真という話をしました。それに絡んで面白い性質があって、以下のような代入式(SASでいう割り当てステートメント)において左辺をorで結ぶと、左から順に評価して、偽である値はスキップされ、初めて真である値が代入されます
コードと結果を見てください


proc lua;
 submit;
local a = 99;
local b = nil;

local v1 = b or a
local v2 = (1>2) or a
local v3 = d or e or f or a or 88
local v4 = g or h

print(v1)
print(v2)
print(v3)
print(v4)

 endsubmit;
quit;

結果






それがなんじゃい?なにに使うんじゃい?と思われた方はちょっと甘い!
 X=Y or 99 は、YがあればYの値を使うし、Yがなければ99を使う。初期化されていない変数にはnilが入っている。それらの性質を使うと、パラメータのデフォルト値を設定できるということなんですよ。
SASマクロにも、キーワードパラメータが指定されなかったときにはこの値を使うみたいな書き方できるでしょう、あれと同じことができるんですね

はい、いよいよ本題です。

Luaの関数ですが、以下のように作って、使います

proc lua;
 submit;
--定義
function add(a,b)
  return a+b
end

--型もみてみよう
print(type(add))

--早速使ってみる
print(add(1,2))
print(add(1,5))
 endsubmit;
quit;

結果






文法は
function 関数名(引数)
戻り値があるならreturn 戻り値
end

です。簡単ですね。

ただ、細かい話なんですが、正確に言うと、Luaには「関数名」っていう考え方は間違ってるんです。
関数名というものは存在しません。

はぁ?って話なんですが、以下のコードを見てください

proc lua;
 submit;
 --[[厳密にいうとLuaの関数に名前はない(無名関数)、
   関数を変数に入れて使っており、変数に名前がついている
]]--
add=function(a,b)
  return a+b
end
 endsubmit;
quit;

実は最初に紹介したのは、こういう書き方もできるって話で糖衣構文みたいなもんです。
本質的には

変数名 = function(引数)
戻り値があるならreturn 戻り値
 end

なんです。これは無名関数といって、本当は関数自体には名前がなくて、その名前のない処理を
変数に入れる。変数には名前がある。ので関数の入ってる変数名がいわゆる関数名みたいになるということです。

だからそれがどうしたって話なんですけど、一応知っておいてください。
いずれ複雑なことをするときに必要になるので。

さて、わけのわからん話はやめて、続きです。
冒頭に紹介したorを使えば、関数の引数を省略した場合の挙動を制御できますよという話

proc lua;
 submit;
 function add(a,b)
  local a= a or 3
  local b= b or 5
    return a+b
 end
 print(add(aaa))
 print(add(1))
 print(add(nill,1))
 endsubmit;
quit;

結果






そして、引数の部分について以下のようにハッシュ型の変数定義も使うことができます
こうすると本当キーワードパラメータマクロのようですね

proc lua;
 submit;
function add(para)
para.a= para.a or 3
para.b= para.b or 5
  return para.a+para.b
end
print(add({a=2,b=2}))
print(add({a=2}))
print(add({b=2}))
 endsubmit;
quit;

結果






戻り値のない関数も全然ありなので
たとえば、以下のようなSASコードの実行を関数にすることもできますよっと

proc lua;
 submit;
function ds_make(para)
local ds= para.ds or "aaa"
local var= para.var or "bbb"
sas.submit([[data @ds@;@var@=1;run;]])
end

ds_make({ds="ds1",var="var1"})
ds_make({})
 endsubmit;
quit;



次に、引数の数がが可変の場合の定義です。
以下のように...とかきます。その後、関数の中でテーブルに入れて
捌いてやればOK。
引数がいくつでも動く関数ができます

proc lua;
 submit;
function add3(...)
  list = {...}
  result=0
  for val in pairs(list) do
     result =result+val
  end
  return (result)
end
print(add3(1,2))
print(add3(1,2,3,4))
 endsubmit;
quit;

結果




・・・で引数を定義するのが面白いですね、受け取ったらまずテーブルに入れて捌いてます。
ループで足してます。sum関数みたいなのができましたね。


逆に、戻り値が複数あるパターン。
海外論文にも載ってた(論文のコードには誤植あるけど)、本日日付を年月日に分解して
3つの戻り値をえるコードです

proc lua;
 submit;
function split_date()
local date = sas.date()
return sas.day(date), sas.month(date), sas.year(date)
end

local day, month, year = split_date()
print("d=", day, "m=", month, "y=",year)

 endsubmit;
quit;

結果(実行したのは2016/8/30です)





さらに、戻り値をテーブルにしたり、テーブルを引数にするパターンを見てみましょう。
まず、与えた文字列を1文字ずつ分解してテーブルを返すsplit_char関数をつくってみて

その後、テーブルを受け取ってまた1つの文字にするcats_table関数を作ってみました。

ただテーブルから文字をつくるのは普通にtable.concat関数というものがあって、これだと連結文字や対象範囲も指定できて、絶対こっちの方がいいです。車輪の再発明も勉強にはなりますけどね

proc lua restart;
 submit;
    --文字列を1文字ずつ取り出してテーブルにする関数(戻り値はテーブル)
function split_char(char)
local result_table={}
local len=string.len(char)
for i =1,len do
result_table[i]=string.sub(char, i,i)
     end
return(result_table)
end

local tb1=split_char("abc")
print("★文字列が分解されてテーブルになりました→",table.tostring(tb1))

--テーブルを引数にして、テーブルの中身を連結して1つの文字値にする関数
function cats_table(table)
local result=''
for i, item in pairs(table) do
  result = result..item
    end
return result
end

local st1=cats_table(tb1)
print("★テーブルが連結されて文字列にになりました→",st1)

local st2=table.concat(tb1,"/")
    print("★concat関数はcatxみたいで便利だな→",st2)


 endsubmit;
quit;

結果

なんかFCMPよりだいぶ書きやすいなぁって思うのは僕だけしょうか。


--おまけ

ちょっと難しい話になりますが、
関数は、変数に過ぎないので、関数の中に関数を入れることができ
それを使うと関数を作る関数とかも定義できます

proc lua;
submit;

function concat3_func(x,y,z)
   print(x..' '..y..' '..z)
end

function make_func(x,y)
   return function (target)
    concat3_func(x, y, target)
  end
end

i_love_func =make_func("I","love")
you_love_func =make_func("You","love")
i_study_func =make_func("I","study")

print(i_love_func("SAS"))
print(you_love_func("SAS"))
print(i_study_func("Lua"))

endsubmit;
quit;









こういうのの詳しい話はまたいずれ。とりあえず今日はここまで!!