ill-identified diary

5 million yen salary expected.

[SAS] 日付でオブザベーションを抜き出す方法について

概要

SAS の日付の扱い方について. 本当に小ネタ.

実行環境は SAS® University Edition.

SAS日付値

データから特定の日付 (または期間) のオブザベーション (レコード) だけ抜き出したいという場面は結構多いはず. SAS では日付は文字列では認識せず, SAS内部で日付を処理するため, SAS日付という値に変換する必要があるが, その方法として input() 関数を使って

input('2015/5/20',yyymmdd10.)

みたいに文字を変換する必要がある (Technical Support). 逆に日付を文字に変えるには put() 関数を使う.

そして, 実はもう1つ楽な方法がある.

'20MAY2015'd

である. クオーテーションの後に d または D を付けると SAS日付に変換してくれる. もちろん "&today"d のようにすればマクロ変数も使える. ただし北米式の日付表記 ’DDMMMYYYY’d の形式でないと変換してくれない*1.

SAS日付の利点

input() を使うよりも, ’DDMMMYYYY’d のほうがタイプする文字が少なくて済む. また,

input()’DDMMMYYYY’d とでフィルタを掛けた場合, 後者のほうが早くなる可能性がある. 以下はその検証コードになる (乱数の部分がちょっと間違っているが本筋には影響がないので許して欲しい).

/*本日(2015/5/20) から10日後の間のランダムな日付を作成*/
data testdata;
    attrib date length=8 format=yymmdds10. label='日付';
    do i=1 to 10**7;
        date = '20MAY2015'd + int(ranuni(1)*10+.5);
        output;
    end;
run;

ここで, SAS日付についてもう1つ重要な仕様として, SAS日付は数値でもある, ということがある. 実は SAS日付は UNIX時間のように, ある起点となる日をゼロとして, そこからの相対的な日数として記録される (参考: SAS9.4から、yearcutoff オプションのデフォルト値が 1926 になっていた | data Memorandum ; set Memory ; run ;). なので, 数値と同じように演算ができる (整数じゃないとエラーになるが) ので,

'20MAY2015'd + 1

は 2015年5月21日を表すSAS日付になる.

ここで作成した 10,000,000 件の日付から, 2015年5月20日のものだけでフィルタをかける. まずは whereif 文それぞれを使った場合を比較する.

/*2015/5/20だけフィルタ*/
/*sas日付利用*/
data query1;
    set testdata;
    where date = '20MAY2015'd;
run;
/*input()関数利用*/
data query2;
    set testdata;
    where input('2015/5/20',yymmdd10.)=date;
run; 
/*指定の日付だけを抜き出せたか確認*/
proc freq data=query1;
    table date;
run;
proc freq data=query2;
    table date;
run;
/*if  の場合*/
/*sas日付利用*/
data query1;
    set testdata;
    if date = '20MAY2015'd;
run;
/*input()関数利用*/
data query2;
    set testdata;
    if input('2015/5/20',yymmdd10.)=date;
run;

抜き出せているか簡単に確認するために freq プロシジャを使った.

f:id:ill-identified:20150521003454p:plain

どちらも 2015/5/20 だけ抜き出されている.

処理時間は where の場合はほとんど変わらない.

 1          OPTIONS NONOTES NOSTIMER NOSOURCE NOSYNTAXCHECK;
 42         ;
 43         /*本日(2015/5/20) から10日後の間のランダムな日付を作成*/
 44         data testdata;
 45             attrib date length=8 format=yymmdds10. label='日付';
 46             do i=1 to 10**7;
 47                 date = '20MAY2015'd + int(ranuni(1)*10+.5);
 48                 output;
 49             end;
 50         run;
 
 NOTE: データセットWORK.TESTDATAは10000000オブザベーション、2変数です。
 NOTE: DATAステートメント処理(合計処理時間):
       処理時間           1.82 秒
       CPU時間            0.87 秒
       
 
 51         
 52         /*2015/5/20だけフィルタ*/
 53         /*sas日付利用*/
 54         data query1;
 55             set testdata;
 56             where date = '20MAY2015'd;
 57         run;
 
 NOTE: データセットWORK.TESTDATAから499784オブザベーションを読み込みました。 
       WHERE date='20MAY2015'D;
 NOTE: データセットWORK.QUERY1は499784オブザベーション、2変数です。
 NOTE: DATAステートメント処理(合計処理時間):
       処理時間           0.31 秒
       CPU時間            0.33 秒
       
 
 58         
 59         /*input()関数利用*/
 60         data query2;
 61             set testdata;
 62             where input('2015/5/20',yymmdd10.)=date;
 63         run;
 
 NOTE: データセットWORK.TESTDATAから499784オブザベーションを読み込みました。 
       WHERE date=20228;
 NOTE: データセットWORK.QUERY2は499784オブザベーション、2変数です。
 NOTE: DATAステートメント処理(合計処理時間):
       処理時間           0.30 秒
       CPU時間            0.32 秒

一方, if の場合は結構差が出てくる. これは, 入力データに対するwhere は, 入力時点でどのようなデータが入っているかわかっているため, それに最適化した処理ができる一方で, if はデータステップ中に1レコードづつ判定され, かつデータステップ中では他の処理によって判定対象の変数の値が変わるため, 最適化処理ができないということで発生している(はず).

73         
 74         /*if  の場合*/
 75         /*sas日付利用*/
 76         data query1;
 77             set testdata;
 78             if date = '20MAY2015'd;
 79         run;
 
 NOTE: データセットWORK.TESTDATAから10000000オブザベーションを読み込みました。 
 NOTE: データセットWORK.QUERY1は499784オブザベーション、2変数です。
 NOTE: DATAステートメント処理(合計処理時間):
       処理時間           0.41 秒
       CPU時間            0.40 秒
       
 
 80         
 81         /*input()関数利用*/
 82         data query2;
 83             set testdata;
 84             if input('2015/5/20',yymmdd10.)=date;
 85         run;
 
 NOTE: データセットWORK.TESTDATAから10000000オブザベーションを読み込みました。 
 NOTE: データセットWORK.QUERY2は499784オブザベーション、2変数です。
 NOTE: DATAステートメント処理(合計処理時間):
       処理時間           1.61 秒
       CPU時間            1.62 秒
       
 
 86         
 87         
 88         ;
 89         OPTIONS NONOTES NOSTIMER NOSOURCE NOSYNTAXCHECK;
 99         ;

なお, 特定の日付ではなく, ある期間を抜き出す場合については, SAS日付が数値として扱えることを利用して,

if '20MAY2015'd <= date <= '25MAY2015'd;

のように不等式にすればよい*2. 結果は省略するが, これも input() を使わないほうが早くなる (そしてコードの見た目もすっきりする).

また, N週間, Nヶ月のような単位で抜き出したい場合, 公式サポートで紹介されている, 年齢を intck() 関数で計算する方法 (Technical Support) を応用するという手もある. 以下は, 2015年5月20日から1ヶ月以内かどうかを判定する.

if intck('month', '20MAY2015'd, date) = 1; 

intck() 関数の第1引数は, ’day’, ’week’, ... が指定できるが, さらに ’week.2’, ’week.3’, ... とすると, 「1周間の月曜日から」「1周間の火曜日から」判定するようになるので, けっこう複雑な条件にも対応できる*3.

*1:もしかするとロケール関連の設定をいじると他の形式でもできるのかもしれないが, 未確認.

*2:蛇足だが, たいていの言語だと a <= x && x <=b のように書くのにSASはこれで問題ない. 便利だが不安な感じがしないでもない.

*3:詳しくはSAS日付関連の関数 - CatTail Wiki*や公式リファレンス参照