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=⊂
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もカテゴリ集計されちゃうので、そういう部分の処理の工夫は必要ですね。

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