2018年10月26日金曜日

マクロステートメント「%IF」の落とし穴





落とし穴の例


以下、%IFで「1」と「1.0」が等しいかどうかを評価しています。(結果は等しくないって出ちゃいます)

%macro test;
        %if 1 = 1.0  %then %put Equal; %else %put Unequal;
%mend;
%test

ログ
Unequal





以下、%IFで「3 / 2」と「1」が等しいかどうかを評価しています。(結果は等しいって出ちゃいます)

%macro test;
        %if (3 / 2) = 1  %then %put Equal; %else %put Unequal;
%mend;
%test

ログ
Equal 




%IFの挙動

・評価する値がすべて整数の場合、「数値」として評価が行われる。
例)「2 < 3」「(1 + 2) = 3」


・評価する値がすべて整数でも、割り算等の式が含まれていて計算結果が小数になる場合、小数部分を破棄してから評価が行われる。
例)「(3 / 2) = 1」… 3 / 2 → 1.5 → 1


・評価する値に小数や文字を含む場合、「文字値」として評価が行われる。
例)「1 < 1.0」「a < b」




小数を扱う場合、以前紹介した「%sysevalf関数」を使います。
http://sas-boubi.blogspot.com/2016/08/11-evalsysevalf.html

%macro test;
        %if %sysevalf( 1 = 1.0 ) %then %put Equal; %else %put Unequal;
%mend;
%test

ログ
Equal



%macro test;
        %if %sysevalf( (3 / 2) = 1 )  %then %put Equal; %else %put Unequal;
%mend;
%test

ログ
Unequal

2018年10月15日月曜日

マクロ内でCARDSが使えないので工夫してみる。




そろそろコタツみかんが出来る時期ですね。



さて、タイトルの通りマクロ内ではCARDSが使えません。
以下の例を見てみましょう。

%macro test;

data out1;
   infile mytemp lrecl=30000;
cards;
1 2 aaa
3 4 bbb
5 6 ccc
;

%mend;
%test

ログ
 ERROR: マクロTESTがCARDS(データ行)を生成しました。不当な結果の原因になるかもしれません。DATAステップとマクロの実行を中止します。
 ERROR: マクロTESTの実行を中止します。




原因はSAS社のFAQやデータステップ100万回でも言及されてますね。

https://www.sas.com/offices/asiapacific/japan/service/technical/faq/list/body/ba243.html
http://sas-tumesas.blogspot.com/2015/01/cards.html

マクロの仕様上、マクロソースは半角スペース(空白)が詰められてリフォーマットされる、
それによって改行位置がわからなくなってしまう、、
なので仕様上の制限として使用できないようにしている、、だそうです。


「それでも、マクロの中でCARDSのように読み込むテキストを指定したいんだ!」という質問を頂きます。
色々方法はありますが、今回2通りの解法を載せておきます。



SAS9.3以前の場合
%macro test;

filename mytemp temp;
data _null_;
   file mytemp;
   put "1 2 aaa";
   put "3 4 bbb";
   put "5 6 ccc";
run;

data out1;
   infile mytemp lrecl=3000;
   input x y z:$10.;
run;

%mend;
%test

プログラムの流れ
・filenameで一時的なファイル参照を作って、
・file/putでこのファイル参照に値を書き出して、
・infile/inputで書きだした値を読み込み



SAS9.4以降の場合
%macro test;

filename mytemp temp;
proc stream resetdelim="_reset" outfile=mytemp;
begin    _reset;
1 2 aaa  _reset newline;
3 4 bbb  _reset newline;
5 6 ccc  _reset;
;;;;

data out1;
   infile mytemp lrecl=3000;
   input x y z:$10.;
run;

%mend;
%test

プログラムの流れ
・filenameで一時的なファイル参照を作って、
・proc streamでこのファイル参照に値を書き出して、
・infile/inputで書きだした値を読み込み

リセットデリミタの設定がちょっとめんどくさいです。



2018年10月9日火曜日

「=:」を使った前方一致による比較と落とし穴





文字列の比較で使える「=:」を紹介します。
落とし穴もあるので、それも含めて紹介していきます。



簡単な例


Sample data
data TEST;
input X:$3.;
cards;
ab
abc
cab
;




data OUT1;
    set TEST;
    if X =: "ab" then FLG1=1;
run;



変数Xが "ab" で始まる値かどうか、という比較を行っています。


詳しく説明すると、「=:」の両辺に指定した変数または文字列のうち、LENGTHが大きい方は、LENGTHが小さい方の長さに切り捨てて比較を行ないます。


ここでいうLENGTHとは以下を指します。
 文字列の場合 

文字列の長さ」
 変数の場合

 固定長の文字変数の場合
「文字列と末尾の空白を含む長さ(つまり変数に定義した長さ)」



上の例では、「X =: "ab"」で、
・左辺に指定した変数Xに定義されたlengthは「3」
・右辺に指定した "ab" は2バイトなのでlengthは「2」

この2つのlengthで小さいのは「2」なので、左辺の変数値を2バイトに切り捨てると以下の通り。

ab → ab
abc → ab
cab → ca

この切り捨てた値と右辺の "ab" がイコールか否か、という判定を行います。






注意が必要な例



data OUT2;
    set TEST;
    if X =: "abcd" then FLG1=1;
run;



「X =: "abcd"」で
・左辺の変数Xに定義されたlengthは「3」、
・右辺の "abcd" は4バイトなのでlengthは「4」、

lengthが小さいのは「3」なので、右辺の文字 "abcd" を3バイトで切り捨てると "abc" となります。


この "abc" が変数Xとイコールか否か、という判定を行います。
この切り捨てルールを知らないと、一見「変数Xが "abcd" で始まる値かどうか」と思ってしまいがちですが、間違いです!





今回の例のデータでもし「変数Xが "abcd" で始まる値かどうか」を判定したいなら、以下のように他の方法を使ったほうが安全です。

data OUT3;
    set TEST;
    if index( X, "abcd" )=1 then FLG1=1;    * 方法1 ;
    if find( X, "abcd" )=1 then FLG2=1;       * 方法2 ;
run;




2018年10月1日月曜日

【効率を意識したプログラム】WHEREとサブセット化IFの使い分け




「WHERE」と「サブセット化IF」の違いを知って使い分けようっていう話です。



「WHERE」と「サブセット化IF」の比較


Sample data
data test;
   length col1-col200 $200.;
   array ar(*) col:;
   do i=1 to dim(ar);
       ar(i) = "abcdefg";
   end;
   do i=1 to 50000;
      output;
   end;
run;



サンプルデータから「WHEREステートメント」を使ってオブザベーションを抽出したときの処理時間とCPU時間を見てみます。

data out1;
   set test;
   where i<100;
run;

ログ
       処理時間           0.40 秒
       CPU時間            0.39 秒



同様のオブザベーションの抽出を「サブセット化IF」を使って処理時間とCPU時間を見てみます。

data out1;
   set test;
   if i<100;
run;

ログ
       処理時間           0.53 秒
       CPU時間            0.54 秒


WHEREステートメントの方が処理時間が短いですね。何故このような違いが起きたのでしょうか。





「WHERE」と「サブセット化IF」の違い


・実行されるタイミングが違う
 WHERE

 データを読み込む時に実行される
 サブセット化IF 

 データを読み込んだ後のデータステップ内で実行される

WHEREの方が実行されるタイミングが早いんで、処理時間が短くなるというわけですね。



・実行できる環境の違い
 WHERE

 データステップおよび(ほとんどの)PROCステップで実行できる
 サブセット化IF 

 データステップでのみ実行できる





「サブセット化IF」の利点


サブセット化IFが効果的な例として、以下のサブセット化IFについての記事の例②で紹介しているようなケースです。
【サブセット化IF】条件に一致するオブザベーションのみ処理を継続する。

data out2;
  set dt1;
  bmi = weight / ((height/100)**2);
  if bmi>=25;
run;


身長と体重からBMIを計算したあと、「if bmi>=25」でBMIが25以上のオブザベーションを残すようにしていますが、
このBMIはデータステップ内で計算しているので、「where bmi>=25」と書くことは出来ませんよね(データを読み込む時には、まだBMIが計算されていないので)