2016年7月27日水曜日

マクロ言語入門10:マクロ変数と&&



「&&マクロ変数」という指定方法を紹介します。





例1

%let  VAR1 = abcde;
%let  X = 1;

%put  &&VAR&X ;

ログ
abcde



「&&」と書くと、「&」を1つに置き換える命令になります。
この機能の何が便利なのか解説していきます。


  • もし「&VAR&X」と書いた場合「&VAR」「&X」という2つのマクロ変数が指定されていると解釈されますが、
  • 「&&VAR&X」とすると初手では「&&」「VAR」「&X」という3つに分けて解釈されて、以下のように展開されます。
    • 「&&」→「&
    • 「VAR」→「VAR
    • 「&X」→「1
    • この3つの展開結果をくっつけると「&VAR1」になって、これがさらに展開されて「abcde」が返されます。


初手で「VAR」の部分がマクロ変数として解釈されないようにするテクニックです。




例2

%let  VAR1 = aaa;
%let  VAR2 = bbb;
%let  VAR3 = ccc;

%macro TEST;
    %do i = 1 %to 3;
          %put  &&VAR&i ;
    %end;
%mend;

%TEST;

ログ
aaa
bbb
ccc

%DOループと&&マクロ変数の組み合わせはよく用います。




実践例

以下に&&の実践的な使い方の記事リンクを貼っておきます。
慣れるまでややこしい感じのプログラムなので、余裕があったらそのうち、、という感じで参考にして頂ければと思います。

変数値をマクロ変数に格納する方法「CALL SYMPUTX編」




記事一覧

1. マクロ変数とは
2. マクロの登録と実行
3. 定位置パラメータ
4. キーワードパラメータ
5. クォート処理
6. クォート処理2
7. ループ処理
8. 条件分岐処理
9. ドット
10. &&
11. 演算評価
12. 補足


2016年7月26日火曜日

マクロ言語入門9:マクロ変数とドット




まず今後マクロを使ううえで便利な%PUTステートメントについて紹介します。
以下構文の通り、%PUTステートメントで指定した文字列をログに表示する事が出来ます。



構文

 %PUT   文字列;



例1
%let X = ABCDE;
%put  &X;

ログ
ABCDE

マクロ変数Xの値をログに出力しています。



例2
%let X = ABCDE;
%put  マクロ変数 X に "&X" を指定しました。;

ログ
マクロ変数 X に "ABCDE" を指定しました。

文字列とマクロ変数を組み合わせてログに出力しています。




ドットの活用



以下は例2と同じようなプログラムですが、WARNINGが出てしまいます。

%let X = ABCDE;
%put  マクロ変数 X に&Xを指定しました。;

ログ
WARNING: Xを指定しました。のシンボリック参照を解決できません。


これは 「&Xを指定しました。」 という名前のマクロ変数があるのだと解釈してしまったため、「そんな名前のマクロ変数存在しないぞ!」 というWARNINGです。

「&X」 と 「を指定しました。 」というように分けて解釈させたい場合、マクロ変数名の後に ドット「.」を入れてあげます。


%let X = ABCDE;
%put  マクロ変数 X に&X.を指定しました。;

ログ
マクロ変数 X にABCDEを指定しました。


マクロ変数名の後のドットは展開後に消えます。




ドットの活用2

%let LIB=WORK;
data &LIB.DT1;
   A=1;
run;

ログ
NOTE: データセットWORK.WORKDT1は1オブザベーション、1変数です。


上記はデータセットDT1を保存するライブラリをマクロ変数LIBで指定するプログラムのつもりです。
しかしログを見ると実際できたデータセットは「WORK.WORKDT1」 となっています。


これは「&LIB.DT1」という文をSASが見たときに、「&LIB.」はマクロ変数の後にドットがついてるから、マクロ変数展開後にドットが消えて「WORKDT1」となってしまうからです。

以下のようにドットを2つ入れてあげると、展開後に1つだけドットが残ります。


%let LIB=WORK;
data &LIB..DT1;
   A=1;
run;
ログ
NOTE: データセットWORK.DT1は1オブザベーション、1変数です。




記事一覧

1. マクロ変数とは
2. マクロの登録と実行
3. 定位置パラメータ
4. キーワードパラメータ
5. クォート処理
6. クォート処理2
7. ループ処理
8. 条件分岐処理
9. ドット
10. &&
11. 演算評価
12. 補足




2016年7月25日月曜日

「SASユーザー総会2016」に行ってきました。


神戸で開催されたSASユーザー総会2016に行ってきました。



今年は、SAS関連でネット上で交流を持たせていただいた方達とも顔を合わせる事が出来てすごく楽しかったです。

(バタバタしててあまり話せなかったのがちょっと残念。)


ちなみに今年からユーザー総会Onlineというシステムが出来ました!当日の発表の様子がオンライン上で見れるそうです。

お金はかかってしまいますが、実際しゃべってるとこ見たほうが、論文やパワーポイントの資料だけ見るより、分かり易いかも。





帰りにアンケートに答えたら、SASメモがもらえました。
SAS社のキャッチコピー 「THE POWER  TO KNOW」 と書かれていて、いい感じですね。








来年の話ですが、今度は東京開催になります。
(神戸開催は5年に1回とのこと)


2016年7月20日水曜日

SASユーザー総会2016で発表の「SASによる迷路の自動生成」の関連記事リンク


7月21・22日に神戸で開催のSASユーザー総会2016にてポスター展示で発表します。
場違い感のある発表内容ですが、こういうのがあってもいいですよね。。


B1Fでひっそりと展示しているので、是非見に来ていただけると嬉しいです。


今回発表する内容とそれに関連する記事を以下にまとめました。

【発表内容の関連記事】
 ・ RWIで迷路を作る
 ・ RWIで迷路を作る2


【RWIの解説】
 ・ レポート作成インターフェイス(RWI)入門1


【迷路のアルゴリズムで参考にさせて頂いた書籍】
 ・ 柳井 政和(2014) 「プログラマのためのコードパズル ~JavaScriptで挑むコードゴルフとアルゴリズム」技術評論社


2016年7月19日火曜日

RWIで迷路を作る2



以下記事では、棒倒し法という方法を使って迷路を作りました。

RWIで迷路を作る
http://sas-boubi.blogspot.jp/2015/08/rwi.html


今回は穴掘り法という方法を使って迷路を描画してみます。
それと、もうちょっと見た目を迷路っぽく改良してみます。



迷路作成の初期値を設定

%let n    = 37;
%let init = 1234;

n    ・・・ 迷路のマスの数。(必ず奇数を設定する事)
init  ・・・ 迷路の壁と床を決めるために生成する乱数のシード値

初期値を変えるたびに異なる迷路を作ることが出来ます。



迷路の生成と描画

SAS9.4より前のバージョンでは、 format_cellメソッドの「style_attr」を「overrides」に置き換える。

※ SAS9.2および9.3ではRWIが評価版のため、ERRORやWARNINGが出ます。
また出力される迷路も9.4と多少異なります。あくまでこんな雰囲気の迷路が出来るという参考程度としてください。

ods noresults;
ods html path="★出力先のパスを指定" file="maze2.html";



*--- 穴掘り法による迷路の生成 ----------------------------------------------;
data _NULL_;


  * 乱数のシード ;
  call streaminit(&init);

  * 迷路のブロックを入れておく箱を用意 ;
  dcl hash hs(ordered:'yes');
  hs.definekey("x","y");
  hs.definedata("block");
  hs.definedone();

  * 始点候補のブロックを入れておく箱を用意 ;
  dcl hash st(ordered:'yes');
  st.definekey("no");
  st.definedata("x","y");
  st.definedone();


  * ブロックを作って箱に放り込む ;
  do y2=1 to &n;
      do x2=1 to &n;
   
         * 外側を道ブロック,それ以外壁ブロックを詰める(1:道ブロック 2:壁ブロック) ;
         if x2 in (1,&n) or y2 in (1,&n) then BLOCK=1;
         else BLOCK=2;

         * ブロックを放り込む ;
         hs.add(key:x2, key:y2, data:block);
       
         * 始点候補のブロックを放り込む ;
         if BLOCK=2 and mod(x2*y2,2)=1 then do;
            no+1;
            st.add(key:no, data:x2, data:y2);
         end;
      end;
  end;



  do i=1 by 1 while (1);

      * 始点候補更新 ;
      if i>1 then do;
          no=0;
          st.clear();
          do y2=1 to &n;
          do x2=1 to &n;
       
               * 始点候補のブロックを放り込む ;
               if x2^in (1,&n) and y2^in (1,&n) and mod(x2*y2,2)=1 then do;
                    hs.find(key:x2, key:y2);
                    if block=1 then do;
                        * 上下左右いずれか2マス先が壁ブロックだったら始点候補にする ;
                        block_num=0;
                        hs.find(key:x2-2, key:y2  ); block_num+(block=2);
                        hs.find(key:x2+2, key:y2  ); block_num+(block=2);
                        hs.find(key:x2,   key:y2-2); block_num+(block=2);
                        hs.find(key:x2,   key:y2+2); block_num+(block=2);
                        if block_num>0 then do;
                              no+1;
                              st.add(key:no,data:x2,data:y2);
                        end;
                    end;
               end;
       
          end;
          end;
      end;

      if st.num_items=0 then leave;


      * 始点をランダムに決めてブロックの位置を取得 ;
      no = ceil(rand('uniform') * st.num_items);
      st.find(key:no);
      x2=x; y2=y;

      * 進めなくなるまで道ブロックを詰めこんでいく ;
      do while (1);
   
          * 進めるか確認 ;
          block_num=0;
          hs.find(key:x2-2, key:y2  ); block_num+(block=2);
          hs.find(key:x2+2, key:y2  ); block_num+(block=2);
          hs.find(key:x2,   key:y2-2); block_num+(block=2);
          hs.find(key:x2,   key:y2+2); block_num+(block=2);
          if block_num=0 then leave;

          * 進行方向を決める ;
          RAND = ceil(rand('uniform')*4);


          * 進行方向の2マス先が壁なら掘る ;
          if RAND=1 then do; x_move=0;  y_move=-1; end;  * 上 ;
          if RAND=2 then do; x_move=0;  y_move=1;  end;  * 下 ;
          if RAND=3 then do; x_move=1;  y_move=0;  end;  * 右 ;
          if RAND=4 then do; x_move=-1; y_move=0;  end;  * 左 ;

          if i=1 then hs.replace(key:x2, key:y2, data:1) ;
          hs.find( key:x2+x_move*2 , key:y2+y_move*2);
          if block=1 then continue;

          hs.replace(key:x2+x_move  , key:y2+y_move  , data:1) ;
          hs.replace(key:x2+x_move*2, key:y2+y_move*2, data:1) ;

          * 2マス先に移動 ;
          x2=x2+x_move*2;
          y2=y2+y_move*2;

      end;
  end;



  * ブロックを箱から出して迷路組み立て(迷路の描画) ;
  length WID HEI $10.;
  dcl odsout ob();
  ob.table_start();

  do y=1 to &n;
      ob.row_start();

      do x=1 to &n;

         hs.find(key:x, key:y);
       
         wid="7mm";
         hei="7mm";
         if (x=2 and y=3) or (x=&n-1 and y=&n-2) then do;
            block=1;
         end;
         if mod(x,2)=0 then wid="1mm";
         if mod(y,2)=0 then hei="1mm";

         *** 壁と床を描画 ;
          if (x=3 and y=3) or (x=&n-2 and y=&n-2) then do;
             block=1;
          end;
          ob.format_cell(style_attr: " bordercolor="|| choosec(block,"white","black") ||
                                     " background=" || choosec(block,"white","black") ||
                                     " height="|| hei || " width=" || wid);
      end;

      ob.row_end();
  end;

  ob.table_end();

  stop;
run;


ods html close;
ods html;
ods results;



プログラム書いてる時は、楽しくていっきに書き上げたけど、長いですね。。

2016年7月12日火曜日

【RENAMEステートメント】変数名の変更



※ データステップのMODIFYステートメントなど、特定の機能を用いるとRENAMEステートメントが動作しないことがあるので注意。



以下の構文で変数名を変更することができます。



構文

RENAME
        変更前の変数名1 = 変更後の変数名1
        変更前の変数名2 = 変更後の変数名2
        ・・・
 ;

💬 
  • 通常のステートメントはPDVという「一時的な処理スペース」で実行されますが、RENAMEステートメントは「一時的な処理スペース」の結果を「出力データセットへ書き込む時」に実行します
  • つまり、最終的に適用されるイメージです。また適用されるタイミングが決まっているので、RENAMEステートメントを記述する位置は関係ありません(プログラムの先頭や最後に書いても挙動は変わらない)






data DT2;
   set SASHELP.CLASS;
   rename AGE=AGE2;
run;

変数AGE を AGE2に変更しています。




注意点

RENAMEステートメントを DROP、KEEPステートメントと併用して使う場合、以下のルールを理解しておいた方が良いです。


【ルール】
ステートメントの実行順は、DROP → KEEP → RENAME


このルールに従うと、以下のプログラムは正しく動かない事が分かると思います。

data DT2;
  set SASHELP.CLASS;
  rename AGE=AGE2;
  drop AGE2;
run;

2016年7月6日水曜日

マクロ言語入門8:マクロ内での条件分岐処理【%IF】




マクロ内で「%IFステートメント」を使って、処理を条件によって分岐させる方法を紹介します




構文

%IF  条件式  %THEN  %DO;

          条件式がTRUEになった場合の処理

%END;
%ELSE %DO;

          条件式がFALSEになった場合の処理

%END;

  • %IFの条件式の判定法に注意! 例えば、
    • 「%IF 120>13」の条件は想定通り「TRUE」になりますが、
    • 「%IF 120.0>13.0」の条件は想定に反し「FALSE」になります
    • 理由は以下リンク記事で説明
    • https://sas-boubi.blogspot.com/2018/10/if.html
    • (SAS社の%IFのページにはこの事について触れていないので気づきにくい。。)




例1

%macro TEST( FLG );

   %if &FLG = Y %then %do;
       proc print data=SASHELP.CLASS;
       run;
   %end;

%mend;

%TEST();
%TEST(Y);


マクロ変数FLG に 「Y」 と入っていたらPRINTプロシジャが実行されます。
気をつけたいのがデータステップと勝手が違い、「%if &FLG = "Y"」というようにダブルクォーテーションで囲う必要はありません。

%IFステートメントではクォーテーションもテキストとして解釈されます。つまり「Y」という文字ではなく「"Y"」という文字として解釈されてしまいます。




例2

%macro TEST( FLG );

   data DT1;
       VAL1 = 1;
       %if &FLG = Y %then %do;
           VAL2=1;
       %end;
   run;

%mend;

%TEST();

 VAL1 
    1  


マクロ変数FLG に 「Y」 と入っていたら変数VAL2を作って「1」を代入するマクロです。
これ実はかなり便利です。なんでかというと、以下のように通常のIF文で同じようなことをやろうとすると、、


%let YN=;

data DT1;
    VAL1 = 1;
    if "&YN"="Y" then do;
        VAL2=1;
    end;
run;

 VAL1 
 VAL2  
    1      .


作りたくないVAL2が出来てしまいます。
これはたとえ実行されない文でもデータステップ内に書かれている変数はまず自動で変数の枠だけ作られてしまうという性質があるからです。

一方、%IFは通常のプログラムとは別のところで動くので、この動きを回避できちゃいます。




取り扱い注意


①「マクロ変数Xが欠損値だったら~」という%IFを書きたい場合

%if &X = %then %do;


少し違和感ありますがこれでOK(未確認ですが、昔のバージョンだとこの書き方ではERRORが出たらしい?)



② %IFが誤作動する例

%if &x = eq %then %do;


マクロ変数Xの値が「eq」かどうかを条件にしていますが、「eq」は「=」を意味しているので、演算子として解釈されて誤作動を起こして正しい結果が得られません。


他に誤作動を起こす文字は多数ありますが、それらは以下リンク記事で解説しているような「クォート処理」をする必要があります。


以下、適当にクォート処理を入れてみた例ですが、マクロ変数に格納される値によってはまだ誤作動を起こす危険があります。

%if %bquote(&x) = %str(eq) %then %do;

状況にあわせてどのクォート関数を使うのか、どのタイミングでクォート処理をするのかを判断する必要があります!




IF と %IF の大きな違い


  •  IFはデータステップ内で実行するステートメント
  •  %IFはデータステップ内とか外とかそういう概念がありません。


つまり%IFでは以下のようにデータセットの中にある変数の値を評価することは出来ません。

X 勘違い注意 X
%macro TEST;

data DT1;
    A=1;
    %if A=1 %then %do;
        B=1;
    %end;
run;

%mend;
%TEST;

%IFでは上記のプログラムを「A」という文字列と「1」という文字列がイコールかどうかを評価しちゃいます。
当然イコールじゃないので中の 「B=1;」 は実行されません。




2016年7月4日月曜日

FedSQLプロシジャで変数名が予約語と被る場合の解決策


ネタ提供いただきました、ありがとうございます!



【質問】 以下FedSQLプロシジャを実行するとERRORが出ます。SQLプロシジャで動いてたのに何故?
data DT1;
  OF=1;
run;

proc fedsql;
   select OF
   from DT1;
quit;

ログ
ERROR: 構文エラー at or near "OF"


【回答】 FedSQLには予約語が存在します。
変数名をOFにしていますが、このOFが予約語だったのでERRORが出てしまったわけです。

解決策は以下のようにダブルクォーテーションで囲ってあげるだけです。

proc fedsql;
   select "OF"
   from DT1;
quit;

FedSQLにどんな予約語があるかはSAS社のリファレンスを確認してみてください。

2016年7月1日金曜日

SGPLOTでTEMPLATEプロシジャのコードを自動生成する【TMPLOUT=オプション】





SGPLOTプロシジャに「TMPLOUT=オプション」をつけると、TEMPLATEプロシジャのコードを自動生成してくれます。


  • 自動生成されるコードの出力先を「TMPLOUT=」にtxt形式等でフルパスで指定する。
  • 生成されるコードの中身は環境によって少し異なる。あと後述しますが注意点あり

proc sgplot data=SASHELP.FISH  tmplout="出力先をフルパス指定" ;
      hbox WEIGHT / category=SPECIES;
run;


自動生成されたコード
proc template;
define statgraph sgplot;
dynamic _ticklist_;
begingraph / collation=binary;
layout overlay / yaxisopts=(labelFitPolicy=Split) y2axisopts=(labelFitPolicy=Split) yaxisopts=(discreteOpts=(sortOrder=ascendingFormatted)) yaxisopts=(type=Discrete reverse=true discreteOpts=(tickValueList=_ticklist_ tickValueListPolicy=Union)) y2axisopts=(discreteOpts=(sortOrder=ascendingFormatted)) y2axisopts=(type=Discrete reverse=true discreteOpts=(tickValueList=_ticklist_ tickValueListPolicy=Union));
   BoxPlot X='Species'n Y='Weight'n / primary=true orient=horizontal LegendLabel="Weight" NAME="HBOX";
endlayout;
endgraph;
end;
run;



💬 注意点

例えば、以下のSGPLOTからTEMPLATEプロシジャのコードを自動生成すると、、

proc sgplot data=SASHELP.CLASS  tmplout="出力先をフルパス指定" ;
      vbar SEX;
run;


自動生成されたコード
proc template;
define statgraph sgplot;
dynamic _NEGATIVE_;
dynamic _ticklist_;
begingraph / collation=binary;
layout overlay / yaxisopts=(labelFitPolicy=Split) y2axisopts=(labelFitPolicy=Split) xaxisopts=(type=Discrete discreteOpts=(tickValueList=_ticklist_ tickvaluefitpolicy=SplitRotate tickValueListPolicy=Union)) x2axisopts=(type=Discrete discreteOpts=(tickValueList=_ticklist_ tickvaluefitpolicy=SplitRotate tickValueListPolicy=Union)) yaxisopts=(linearopts=(Integer=true));
   BarChartParm X='Sex'n Y=_FREQUENCY_ / primary=true LegendLabel="度数" NAME="VBAR";
endlayout;
endgraph;
end;
run;


自動生成されたコードの青文字で示した部分に注目!
プロットに使用したデータセット「SASHELP.CLASS」には「_FREQUENCY_」なんて変数はないし、DYNAMICステートメントにも「_FREQUENCY_」なんていうDYNAMIC変数は定義されていません。
ナニコレ??

どうやら、グラフに含まれる要約値(今回の場合は度数)は、自分で計算しないといけない場合があるようです。



例えば今回の場合では、以下のような対応が必要
  • SEXごとの度数をPROC FREQとかで求めてデータセットに出力
  • 自動生成されたコードの「Y=_FREQUENCY_」の部分を「Y=自分で求めた度数が入った変数」に置き換える
  • SGRENDERでは上記で作成したデータセットを使用する



まとめ

SGPLOTでは不可能なカスタマイズをしたい場合にTEMPLATEプロシジャを使いますが、TMPLOUT=オプションでコードを自動生成してしまえば、自分で一から書くより楽な場合があります。

ただし、自動生成なので改行とかインデントつけたりして、見やすくする必要がありますし、
前述したとおり、想定外のコードが生成される場合があるので、ちゃんと中身を確認・理解したうえで適宜カスタマイズする必要があります。