割と画期的なアイデアでは??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;









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




proc Luaの世界③SASコードを実行する話 四六時中遊べる異常な面白さ間違いなし!!

いよいよproc Luaの中でSASコードを実行する話です。多分これも後にさらに掘り下げて
記事を書くと思いますが、とりあえず触りです。

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

proc lua;
 submit;
  --sas.set_quiet(true) 実行されるコードをログにださない場合:マクロのnomprintみたいなもん
  sas.submit([[data a;x=1;run;]])
endsubmit;
quit;








こんな感じです、ログに実行されたSASコードがでて、実行されます。
マクロと違ってデフォルトでは、実行された実際のコードがログに出力される設定sas.set_quiet(false )になってます。
nomprintのように実行コードをログにだしたくない場合はsas.set_quiet(true )としましょう。

sas.submit([[ SASコード ]])が基本の書き方になります。sas.submtのタイミングで記述したSASコードがsubmitされます。

次に以下のコードをみてください。

proc lua;
 submit;
 local code=[[data a;x=1;run;]]
 sas.submit(code)
endsubmit;
quit;

結果は同じです。つまり一旦変数の中にSASコードを格納し、それをsas.submitで指定することでも同じことになります。

Luaのテーブルは柔軟なので、例えば再利用性の高い処理であれば
カテゴリでまとめたりしてテーブルにいれて、

proc lua ;
 submit;
 makeds={} --グローバルでテーブルmakedsを作って、その中にコードを入れている
 makeds.mds1=[[data ds1;x=1;run;]]
 makeds.mds2=[[data ds2;x=1;run;]]
 makeds.mds3=[[data ds3;x=1;run;]]
endsubmit;
quit;

以下のように使いたい時に取り出して実行することができます。

proc lua ;
 submit;
 sas.submit(makeds.mds2) --makdedsテーブル内のmds2のコードが実行される
endsubmit;
quit;

以前の記事で、グローバルで定義したものはnilを代入しないと消えないといった話をしたのですが
あれはどうやら、ちょっと間違いです。

proc luaの後にrestartと記述すると一旦、Luaの環境をリセットし、グローバル定義も全て初期化されます。
つまり、以下のコードを実行すると

proc lua restart;
--[[restartは記述したproc lua開始時点で一旦Luaの環境をリセットしてメモリ上に滞留してるグローバル変数などを全部殺すよ]]--
--terminateは記述したproc luaが終わった時点でリセットするよ
 submit;
 sas.submit(makeds.mds2) --makdedsテーブル内のmds2のコードが実行される
endsubmit;
quit;

makedsはnilに初期化されているので参照エラーになります

ちなみにproc lua terminateとすると、そのprocが終わるまでは環境が保持されてますが、終わる時点でリセットがかかります。restartとのタイミングの違いに注意しましょう。

つづいて、マクロパラメータのように、可変的部分を実行時に与えて実行する方法について紹介します。

以下のコードを実行すると
proc lua;
 submit;
 --置換(可変)部分を@で括って定義できる
 local code=[[data @ds@;
              @var@=1;
             run;]]
 --実行時に置換パラメータを指定して実行する
 sas.submit(code,{ds="b",var="y"})
endsubmit;
quit;











となります。
実行時に置換される部分を@置換変数@と書いて定義しておき、実行時に
sas.submit([[SASコード]] ,{置換変数1= , 置換変数2= ,,,, })として実行します。

さて、次に話です。call executeのように、実行するSASコードを刻んでいきたい場合、つまり
sas.submit_([[data c;x=1;run;]])を、

sas.submit_([[data c;)]]
sas.submit_([[x=1;)]]
sas.submit_([[run;)]]
としたいな~って思った場合、そのままだとエラーになります。
なぜかというと、submitすると、そこを部分実行したのと同じで
x=1;というのだけ実行しても、それはエラーですよね。
ちなみに、data c;の部分は暗黙的runが補完されたような形になって一応動きますけど。

さて、ではどうすればいいのかという話ですが、
subimit_と、アンダースコアをつけるのです。
subimit_で実行されたコードは、submitとアンダースコア無しのsubmitが実行されるまでは
ストックされた状態で、待機したような形になります。

proc lua;
submit;
 sas.submit_([[data c;]]) --submit_とアンダースコアがついてることに注意!
 sas.submit_([[x=1;]])    --アンスコなしのsubmitが実行されるまでアイドリング状態になる
 sas.submit([[run;]])     --ここで一気に実行される
endsubmit;
quit;

と書けるということです。

これが、何を意味するのかというと、Luaで実行するSASコードを可変的に
生成する場合、全文コードを生成してからsubmitしなくても純粋に可変部分のみ
で済むということ、すなわち!

proc lua;
submit;
 sas.submit_([[data d;]])
 for i = 1,4 do
  sas.submit_([[x=@val@;output;]],{val=i})
 end
sas.submit([[run;]])
endsubmit;
quit;

というコードが成立し、
ログに出ている実際に実行されたコードをみると以下のようになっています









まだまだ紹介したいことはあるんですが、とりあえずここまで!

面白い。僕は心から面白いと感じるんですけど、この感動、情動をどうやったらうまく表現できるのか。どうすれば流行らせられるか、次世代のスタンダードな知識にできるのか、いいアイデアがあれば是非教えて下さい



Proc Luaの世界②-自由奔放なLuaのテーブルの話

さて、Luaの文法的な話ばかりだと面白くないので、今回の記事で紹介する機能を使ってできる小技を先に紹介します。

他の方のブログでも何度か言及されていますがSASマクロの%doループはちょっと使いにくいところがあります。

[データ解析備忘録]
アルファベットや飛び飛びの値でもループしたい

たとえばSASHELPライブラリにあるCLASS FISH CARSの3つのデータセットをループ処理で
順番にproc printにかけたいなと思えば

%macro mloop;
 %let loopval=CLASS FISH CARS;
 %do i=1 %to %sysfunc(countw(&loopval));
  %let target=%scan(&loopval,&i);
      proc print data=sashelp.⌖run;
 %end;
%mend;
%mloop;

とかけます。
はい、わかりにくいですね(別にSASマクロをディスってるわけじゃないですよ!)

proc luaの場合、色々と書けるんですが、とっつきやすい1パターン紹介です。

proc lua;
submit;
local target = {'CLASS','FISH','CARS'}
for i, item in ipairs(target) do
sas.submit("proc print data=sashelp."..item..";run")
end
endsubmit;
run;

と、これでOKです。うん、発想的にも見かけ的にもcall executeと同じなので、何やってるかはわかりますね。

上のように実行コードが単純な1行程度なら文字列連結でもいいですけど、複雑になってくると
手に負えないので、キーワードパラメータもちのマクロのように、パラメータを分離して以下のように書くほうが綺麗かな

proc lua;
submit;
local target = {'CLASS','FISH','CARS'}
local code =
[[
proc print data=sashelp.@dataset@;
run;
]]
for i, item in ipairs(target) do
sas.submit(code, { dataset = item } ) --ここでパラメータ指定して実行
end
endsubmit;
quit;

うん、発想はマクロと同じですが、こっちの方が美しいと僕は思いますけどね。
まあ、Luaの中でSASコードを生成して実行する話はまた今度詳しくやります。

さて、今回勉強したいのは target = {'CLASS','FISH','CARS'}の部分で「テーブル」といいます。

Luaにおいて、とても特徴的なのが、データ構造が唯一その「テーブル」というもので管理されるという点です。
「テーブル」と聞いて一般的に想像する形式とは違い、Luaのテーブルは、入れ子にできたり関数を保持できたり、かなり何でもアリな概念だと思ってください。

多分、何回か掘り下げることになりうと思いますが、まずポイントとして、Luaのテーブルは
配列(ARRAY)と連想配列(HASH)の両方の性質を持つという点です。
いや正確には内部的に「Array」管理パートと「Hash」管理パートがあるといった感じですかね。
まあ、まだそんなに難しく考える必要はないです。なんでも入れれると思ってください


proc lua;
 submit;
 --配列的なテーブル定義の例 とりあえずなんか突っ込んでみる
 local sampletable = {'りんご', 999, 'ABCD', 123}

 --型を見てみる
 print("sampletableの型は",type(sampletable),"だ")
 print("===================================")


 --ループで1から4番目の要素をプリントする
 for i =1 , 4 do
   print(i,"=",sampletable[i])
 end

 --1番目の値を変えて、5番目に値を追加する
 sampletable[1]='みかん'
 sampletable[5]=456

 print("===================================")

 --配列テーブルのループ参照には、ipairs関数というものを使って以下の書き方をする。
 --とりあえず丸覚えしちゃえばいいと思う
 for i, val in ipairs(sampletable) do
  print(i, "=",val)
 end

endsubmit;
quit;

結果は

















文字と数値混ぜれたり、途中で要素増やしたり、なんかSASのarrayをゆるゆるにした感じですね。
代入の仕方は簡単ですね。{}の中に値を区切っていれるだけです。
参照の仕方も、テーブル名[要素番号]でシンプル。

ちなみに
table ={}と空でつくっておいてから
table[1]=1と値をいれるのも当然OK


Luaの通常のインデックスループは
for インデックス変数 = 開始値 , 終了値 ,加算値 do
 処理
end
といったごく普通。

ただ、テーブルの中を参照する場合、決まり文句のようなものがあって

for i, 変数 in ipairs(テーブル名) do
変数の中にテーブルの値が入っていく
end

といった書き方をします。上記の例だとvalの中にsampletableの要素が順番にはいる形で
ループが発生します。

ipairs関数はイテレータ関数と呼ばれるもので、配列としてのテーブル(keyが1から始まる数字で連続しているならハッシュでもいいけど)には
これを使います。


つぎに同じテーブルでもハッシュ的にkeyと値で管理してみます


proc lua;
 submit;
  --ハッシュ的テーブル定義の例 とりあえずなんか突っ込んでみる
  local sampletable2 = {fruits='りんご', no1=999, valc='ABCD',[1]=1}

  --キーがkey1のデータと、キーが99のデータを追加してみる
  sampletable2["key1"] = "data1"
  sampletable2[99] = 100

  print("===================================")
  --キーがvalcのデータを参照
  print("キーがvalcのデータは",sampletable2.valc)
  --キーが99のデータを参照
  print("キーが99のデータは",sampletable2[99])
  print("===================================")

  --ハッシュテーブルのループ参照には、pairs関数というものを使って以下の書き方をする。
  --とりあえず丸覚えしちゃえばいいと思う
 for key, val in pairs(sampletable2) do
  print(key, "=",val)
 end

endsubmit;
quit;

結果は














local sampletable2 = {fruits='りんご', no1=999, valc='ABCD',[1]=1}
について
sampletable2テーブルのキーは
fruits
no1
valc
1
で、それぞれにデータが入ってます。

参照はsampletable2.valc でもsampletable2['valc']でもOK

ハッシュのように管理されているテーブルの場合
ipairsではなくpairs関数を使います。「key」っていう変数名はなんでもいいんですが、
慣習的にipairsではi、pairsではkeyって名前にするみたいです。

多分、もうおなかいっぱいだと思うんですけどもう少しだけ
今まで説明したのはいわゆる1行データ、1次元配列のようなパターンだけでした。
ただ、それだけだと、今後SASデータセットをLuaのテーブルにしたり
Luaのテーブルからデータセットを作る際にちょっとわかりにくくなると思います。
SASデータセットは変数とOBS、つまり2次元になるので。

多次元構造のつくり方はいたって簡単で、テーブルの要素をテーブルにしちゃえばOKですつまり

proc lua;
 submit;
 local samp3 = {};
 samp3[1] = {1,2,3};
 samp3[2] = {4,5,6};
 samp3[3] = {7,8,9};
 --local samp3 = {{1,2,3},{4,5,6},{7,8,9}};でも当然OKよ

 --中身を確認するためLuaのテーブル関数tostringを使ってみる
 print(table.tostring(samp3))

 endsubmit;
quit;






















としてみます。以下のようにきちんと2重ループにしてプリントしてもいいけど
for i, val in ipairs(samp3) do
for j, val2 in ipairs(val) do
print(i..'-'..j, "=",val2)
end
end

面倒なので、Luaのテーブル関数(またいずれ説明)tostringを使って、テーブルの中身をみます。
階層構造で表現されます。結果は以下のとおり

ハッシュタイプで階層的に表現したいのなら以下の感じかな
proc lua;
 submit;
 local samp4 = {};
 samp4.row1={col1=1,col2=2,col3=3};
 samp4.row2={col1=4,col2=5,col3=6};
 samp4.row3={col1=7,col2=8,col3=9};
 print(table.tostring(samp4))
 endsubmit;
run;























ごめんなさい、もう少しだけ。
sasのdim関数のようなものはないのかということなんですが#テーブル名かtable.size関数があります。
通常の配列的に使用されるテーブルなら結果は同じですが、キー値が文字、あるいは数字であっても1から始まって連番でなければ#の結果は狂います。狂うというか、#はテーブルのArrayパートの1以上の数値の最大値しか参照していない仕様のようです

どういうことかというと

proc lua;
 submit;
 --ハッシュ的テーブル定義の例 とりあえずなんか突っ込んでみる
  local samp5 = {1,2,3,4}
  print ('#samp5=',#samp5);
  print ('table.size=',table.size(samp5));

 --飛び番で定義してみる
  samp5[6] = 6
  print ('#samp5=',#samp5);
  print ('table.size(samp5)=',table.size(samp5));

  --0とか文字のキーとかたしてみる
  samp5[0] = 0
  samp5.key1 ="data"
  print ('#samp5=',#samp5);
  print ('table.size(samp5)=',table.size(samp5));

endsubmit;
quit;










ということです
これは気をつけないと駄目ですね


ところで最後にYoshihiro Fukiyaさんが嬉しい記事を書いてくれました!!

[みんなでLuaを勉強してみよう!!]

凄く嬉しいです。
参照の論文もおすすめです(ただ、ちょっと気になるところもあって、get関数じゃなくてget_valueじゃない?とか、書かれた時期のメンテナンスバージョンのせいなのかそのまま通すとエラーになる部分がある気がします。そんなことない?)