2018年11月22日木曜日

FIRSTOBS=, OBS=, WHERE を組み合わせたときの挙動




データセットオプション「FIRSTOBS=」と「OBS=」の紹介と、WHEREステートメントを組み合わせて使用した時の挙動について、まとめておきます。



データの状態によっては結果がバグる事があって、その辺の注意点については最後に別記事のリンク貼ってるので参照下さい。



Sample data
data a;
   do i=1 to 10;
     output;
   end;
run;





FIRSTOBS=オプション

・処理(読込み)を開始するオブザベーションを指定できます。

proc print data=a (firstobs=3);
run;




OBS=オプション

・何オブザベーション目まで処理するか(読み込むか)を指定できます。

proc print data=a (obs=3);
run;




FIRSTOBS=, OBS=, WHERE の組み合わせ

ここから本題。以下のプログラムで結果が予測できればバッチリです。

proc print data=a (firstobs=2 obs=3);
  where i>5;
run;


解説
・まずWHEREでオブザベーションを抽出。
・抽出したオブザベーションに対して、FIRSTOBS=とOBS=を適用。
・FISTOBS=とOBS=で共通する範囲にあるオブザベーションが処理対象となる。


以下にイメージを載せておきます。


ちなみに

SETステートメントとかでもこれらのオプションが使えます。

data b;
  set a (obs=3);
run;



注意


以下の記事で、FISTOBS=オプション自体の落とし穴も紹介しています。
FIRSTOBSオプションの落とし穴




2018年11月20日火曜日

FORMATの落とし穴【デフォルト長】




PROC FORMAT の VALUEステートメントでありがちな落とし穴。
以下のプログラムと結果をご覧ください。結果は想定通りでしょうか?

proc format;
   value $FMT
       "a"    = "x"
       "abc" = "y"
   ;
run;

data DT1;
   format VAR1 $FMT.;
   VAR1 = "abc";
run;

  VAR1  
  x 

変数VAR1は "abc" なのでフォーマット$FMTを貼りつけると "y" と表示されるはずが、 "x" が表示されてしまいました。



原因

これは「デフォルト長 (Default Length)」というものが関係してます。
例えば、今回の例で

proc format;
   value $FMT
       "a"    = "x"
       "abc" = "y"
   ;
run;


等号の右側に定義した値 ("x" と "y") の文字の長さは1バイトです。
この長さが、そのフォーマットの「デフォルト長」というものになります。
(定義した値が複数ある場合は、右側の文字の長さが最も大きいものがそのフォーマットの「デフォルト長」になる)



そしてこのフォーマット$FMTは、以降のFORMATステートメントでデフォルト長となった1バイトしか視界に入らなくなります。
どういう事かというと、、

data DT1;
   format VAR1 $FMT.;
   VAR1 = "abc";
run;

変数VAR1 の値 "abc" から先頭の1バイトの値 "a" だけを見て対応するフォーマット値を表示しようとします。



ちょうど PROC FORMAT で "a" = "x" と定義してたので、この値が表示されちゃったというわけですね。

proc format;
   value $FMT
       "a"    = "x"
       "abc" = "y"
   ;
run;


ちなみに...

以下のように、PROC FORMATから1つ目の「"a" = "x"」をコメントアウトして定義から外すと、、、

proc format;
   value $FMT
/*       "a"    = "x" */
       "abc" = "y"
   ;
run;

data DT1;
   format VAR1 $FMT.;
   VAR1 = "abc";
run;

  VAR1  
  a 

まずは変数VAR1 の値 "abc" から先頭の1バイトの値 "a" だけを見て対応するフォーマット値を表示しようとしますが、
対応するフォーマットがないので、そのままの値 "a" が表示されちゃってるわけですね。
この辺の挙動を知らないと「なんじゃこりゃ??」となってしまいますね。



解決策

変数VAR1 の値 "abc" の文字の長さは3バイトなので、3バイト全部読み込んで表示する必要があります。
解決策としては以下の2つの方法があります。


解決策①
proc format;
   value $FMT (default=10)
       "a"    = "x"
       "abc" = "y"
   ;
run;

data DT1;
   format VAR1 $FMT.;
   VAR1 = "abc";
run;

  VAR1  
  y 

「DEFAULT=オプション」によってこのフォーマットのデフォルト長を 10 に広げています。


解決策②
proc format;
   value $FMT (max=10)
       "a"    = "x"
       "abc" = "y"
   ;
run;

data DT1;
   format VAR1 $FMT10.;
   VAR1 = "abc";
run;

  VAR1  
  y 

「format VAR1 $FMT10.」として、FORMATステートメントで、フォーマット名の後ろに幅を指定することも出来ます。
今回は幅を10に設定して問題なく動作していますが、「出力形式〇〇に指定した幅は無効です」みたいなERRORが出る事があります。そういった場合はFORMATプロシジャで「value $FMT (max=10)」というように、指定可能な幅の最大値を設定する必要があります。




See also

インフォーマットでも同様の落とし穴があります。
INFORMATの落とし穴【デフォルト長】


ポイント
・文字値から数値に変換するINVALUEステートメントでは「左側」に定義した文字の長さがデフォルト長になる
・VALUEステートメントでは「右側」に定義した文字の長さがデフォルト長になる


2018年11月1日木曜日

SQLビューは同じ変数名が複数あってもエラーにならない




SQLビューの落とし穴です。以下をご覧ください。

proc sql;
   create view myview as
   select  name,
              111 as var1,
              222 as var1
   from  sashelp.class;
quit;


proc print data=myview noobs;
run;


なにこれ、VAR1が2つ存在しちゃってる、エラーにもなってない。。
実はCREATE VIEWでは、同じ名前の変数がいくつも存在する状態を作り出すことが出来てしまうんです。



では、先ほどのSQLビューに対して、VAR1を選択してPROC PRINTすると、、

proc print data=myview noobs;
   var var1;
run;


1個目のVAR1がプリントされました。2個目はどうした。



私自身、この挙動を知らずにミスったことがあります。
その時の状況として、変数が数十個あるデータセットに対して、

select  *, x+y as var1

みたいなselect文を書いたSQLビューを作りました。ですが元々データセットにVAR1っていう変数が存在していて、それを知らずにSQLビューの中でもVAR1っていう変数を作ってしまっていました。

このSQLビュー内で作ったVAR1に対して、

proc means data=○○;
   var var1;
run;

みたいな事をやろうとしたところ、元々データセットに存在しているVAR1に対して集計が行なわれてしまい、結果がおかしい!?ってなりました。



See also...
SQLビューの作成と落とし穴①

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のように読み込むテキストを指定したいんだ!」という質問を頂きます。


面倒な方法になってしまいますが、以下解決法の一例。

%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で書きだした値を読み込み




2018年10月9日火曜日

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





文字列の比較で使える「=:」を紹介します。
非常に重要な性質・注意点も含めて紹介していきます。



簡単な例


Sample data
data TEST;
length X $3.;
input X;
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" で始まる値かどうか」を判定したいなら、以下のように他の方法を使ったほうが安全です。
(以下で使用しているINDEX関数とFIND関数は両方ともシングルバイト専用の関数で日本語等のマルチバイトには対応していないので注意)

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.33 秒
       ユーザーCPU時間    0.02 秒
       システムCPU時間    0.32 秒


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

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

ログ
       処理時間           0.53 秒
       ユーザーCPU時間    0.12 秒
       システムCPU時間    0.41 秒

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





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


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

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

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

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



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

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

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





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


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

data out2;
  set dt1;
  c = a + b;
  if c >= 10;
run;

この例ではデータステップ内で計算した変数Cをサブセット化IFで使用しています。
WHEREステートメントだと「where c >= 10」と書くことは出来ませんよね(データを読み込む前には、まだ変数Cが導出されていないので)