proc planを使って定義から、それっぽい値のテストデータを生成する

新しいプログラムを作るときに、使用するデータの仕様はある程度決まっているけど
実際テストデータはまだ存在していないみたいな状況で、
動き始めなければならないことってあると思います。なければそれは幸せです。
下手すりゃ初めて実行するのが、実際の本番データってこともあるかもです。

やっぱり作り手としては、データ無しで書けなくもないけど
ある程度、テストデータ使って、回しながらコード書きたいですよね。

例えば、

変数名値リスト
sampleid数値
sex文字男性/女性
btype文字A型/B型/AB型/O型
age数値20-65
ques1数値1,3,5,99


みたいなデータ定義書があって、これに沿ったデータが100件ほしければ

proc plan seed=1234;
factors sampleid = 100 ordered
sex      = 1 of 2;
treatments btype = 4
age   = 46
ques1 = 4;
output out= Q1
sampleid
sex   cvals =('男性' '女性')
btype cvals =('A型' 'B型' 'AB型' 'O型')
age   nvals =(20 to 65)
ques1 nvals =(1,3,5,99);
run;
quit;

とすれば






























の通りです。

ちなみに各値の出現頻度は
























のようカテゴリ数で大体等分されます。サンプル数少ないからちょっと綺麗になってませんが。
(発現割合をいじったりはこのやり方ではできないはず…)

もともと実験計画とか割付用の特殊なプロシジャを無理やりテストデータ作りに
使っているのでステートメントに違和感がありますが、要はfactorのところで
サンプルID的な変数を指定しときます。sexはキーじゃないのですが、factorステートメントは
2変数以上指定しないと実行できないので、とりあえず放り込んでおきます。

後はtreatmentsで各変数のカテゴリ数、output=データセット名以下で
文字数字を決めて、その発現リストを指定します。

上記の例は、1サンプル1オブザベーションのデータですが、例えば
企業の売り上げデータが月ごとに積みあがっている以下のような場

変数名値リスト
sampleid数値
month数値1-12
uriage数値0-9999


であれば

proc plan seed=1234;
factors sampleid = 100 ordered
month    = 12 ordered;
treatments uriage = 1001;
output out= Q2
sampleid
month  nvals= (1 to 12)
uriage nvals=(0 to 9999)
;
run;
quit;






























と先ほどsex      = 1 of 2としていたところをmonth    = 12というように
1 of を抜いてやればOKです。

あと、僕がよくやるのは、実際データって欠損が混じりますよね。ここはシステムで制御してるから
絶対入りませんと言われていたところが何故か本番で欠損だったこともあるので、基本そういうのは
信用せずにID以外は全部ブランクが入る可能性があると考えてコード書くようにしてます。

なので

data _Q1;
 set Q1;
 call streaminit(1234);

 array NV _numeric_;
 do over NV;
  if rand('uniform') <0.1 & vname(NV) NOTIN ('sampleid') then NV =.; 
 end;
 array CV _character_;
 do over CV;
  if rand('uniform') <0.1 & vname(CV) NOTIN ('sampleid') then CV =''; 
 end;
run;





として、ここが欠損なら話になんねぇってところ以外は、ランダムに穴ぼこにしてからテスト実行してます。


マクロ内でcardsステートメントは使えない

連投。

1年に1回のペースで、間違えて、思い出して、忘れてを繰り返していて、嫌気がさしたので自分の備忘のため。


%macro dsmake;
data Q1;
input X;
cards;
1
2
3
;
run;
%mend;

%dsmake

このコードはエラーになります。

なぜでしょう?


答えは公式ページで
http://www.sas.com/offices/asiapacific/japan/service/technical/faq/list/body/ba243.html



まあ、要はマクロの中ではcardsは、改行がうまく認識できなくなって
仕様上つかえないんですって。





ods tagsets.excelxpで作成されたエクセルもどき(XML)をちゃんとしたEXCELファイルに変換するマクロの紹介

今回はただの公式情報紹介です。

以前、「9.4からODS EXCELでエクセルファイルが作れる話」
ods tagsets.excelxp絡みの記事で、

ods tagsets.excelxpだと、作成されるのはエクセルで開くことのできる
XML形式で、純粋なEXCEL形式じゃないから、一回開いてEXCEL形式で保存する必要があるとかって
話をしたことがあります。

9.4があればods excelでいいんですが、実際まだ9.4使っている現場ってそんな多くないと思います。多分。

で、最近知ったのですが、SASの公式で、
XML形式をEXCEL形式に変換するマクロが公開されていて、これを使えば一応ods tagsets.excelxpで作った
ファイルをEXCELにしてくれます。

使い方等も記述されているので、読んでその通りにやれば再現できるはずです。


見てみるとわかりますが、要はXMLをEXCELに変換するVBScriptをputで生成して、それを走らせて変換して貰ってるってことですね。
もはやSAS関係ないですね。

ただし、この方法でもods excelのようにグラフの画像を含めることはできないのでそこだけ注意してください。

最も近い値を持つデータを別のデータセットからマッチングする方法について考える話

まずデータセットを2つ

data Q1;
call streaminit(777);
do ID=1 to 40;
X = round(rand('uniform')*1000,0.1);
output;
end;
run ;












































data Q2 ;
call streaminit(888);
do ID2=1 to 40;
Y = round(rand('uniform')*1000,0.1);
output;
end;
run ;












































それぞれのデータセットについて XとYの値はデータセット内でたまたま一意であることを確認しています。

で、やりたいことは
Q1のXの値に最も近いYの値を持つQ2のデータをQ1の各オブザベーションに結合するということです。
最も近いというのはX-Yの絶対値が最小になるデータです。

求めるべき結果は































さて、どうするかという話で、まず思いつくのはsqlやデータステップで
今回の場合、40obs×40obs=1600obsの総当たりの直積をつくってIDとabs(X-Y)でソートして
first.IDをとるような処理ですね。

それもありなんですが、例えばデータが1000や10000、さらにもっと増えた場合、処理時間がえらいことになっていくので今回は別の方法を検討してみます。

まずは、

data _Q2;
   set Q2 end=eof;
   CLUSTER=_n_;
   if eof then call symputx('nobs',_N_);
run;

proc fastclus data=Q1 out=_Q1
              seed=_Q2(rename=(Y=X))  maxclusters=&nobs
              noprint maxiter=0 ;
     var X;
run;

proc sort data=_Q1;
by cluster;
run;
proc sort data=_Q2;
by cluster;
run;
data A2;
informat ID X ID2 Y;
     merge _Q1(in=ina) _Q2;
     by cluster;
if ina;
keep ID X ID2 Y distance;
run;

で先ほどの結果になります。
何をしているかというと、クラスター分析ができるfastclusプロシジャを使って、Q2のYをシード値にして、最大クラスタ数をQ2の総オブザベーション数にします。

あとはclusterでマージすれば終わりです。

続きましては

data A1;
  if _N_ = 1 then do ;
if 0 then set Q2;
  declare hash h1 (dataset:'Q2', ordered:'A') ;
  declare hiter hi ('h1');
  h1.definekey('Y');
h1.definedata('Y','ID2');
  h1.definedone () ;
end ;
  set Q1;
rc = h1.find(key:X);
  if rc ^=0 then do;
  h1.add(key:X,data:X,data:ID);
  if hi.setcur(key:X) = 0 and hi.prev() = 0 then do;PID = ID2;PY = Y ;end;
  if hi.setcur(key:X) = 0 and hi.next() = 0 then do;NID = ID2;NY = Y ;end;
if PY=. and NY=. then put ID=;
if PY =. then do;Y=NY;ID2=NID;end;
else if NY =. then do;Y=PY;ID2=PID;end;
else if abs(X-PY)<=abs(X-NY) then do;Y=PY;ID2=PID;end;
else if abs(X-NY)<abs(X-PY)  then do;Y=NY;ID2=NID;end;
  h1.remove(key:X);
end;
distance = abs(X-Y);
run ;


で、適当に
data A1;
informat ID X ID2 Y;
set A1;
keep ID X ID2 Y distance;
run;

成型すれば先ほどの結果と同じです。

ハッシュオブジェクトなわけなんですが、何が面白いかって、総当たり比較を行っていないってことなんですね。ハッシュオブジェクトにQ2を入れて、データステップでXを読み込んで、まずfindメソッドで差が0のものがないかをみます。
それがなかった場合、X自身をハッシュオブジェクトに入れます。ハッシュオブジェクトは定義時に値で昇順に格納されるようにしているため、X自身が格納された場所の前のデータと後のデータを取得することで、最近値候補の2つをゲットできます。あとはその2つとの差を比べて、どっちを選択するかだけです。
setcurメソッドは指定したキー値の値で、ハッシュ反復子オブジェクト内のポインタをそこに持っていきます。prevメソッドとnextメソッドはポインタの前後のデータを取得するメソッドです。
処理が終了したらハッシュにいれたXはremoveで取り除いておきます。
個人的に勉強中で、謎なのがremoveメソッドは通常、ハッシュ反復子オブジェクトで再定義されているハッシュオブジェクトを対象にして、かつ以前にハッシュ反復子オブジェクト対象のメソッドが実行されていた場合、ロックがかかってエラーになるはずなんですか、
今回のようにif 条件式の中で判定のために使うと、メソッドの結果を得つつ、ロックを回避できるんですよね。
もし、このことについて知っている方がいらっしゃれば情報求むです。
※後日追記:コメントでamatsuさんに突っ込んで貰いました。僕の勘違いです。条件式うんぬんではなくハッシュ反復子のポインタの位置があるキーをremoveできないのです。アホデシタ。


今はオブザベーション数40ですが、これを1000や10000にしてみると(今回はhashにmultikeyつけてないので値は一意になるように)、ハッシュオブジェクトの処理時間が相当短いことが確認できます(SAS雲丹だと厳しいけど)。さらに10万や100万規模になってもhashexpでハッシュオブジェクトのサイズを調整すれば充分優秀です。

byにgroupformatをつけて、フォーマット値によるfirst last処理をする話

ソートされたデータをbyで指定してsetするとfirst lastを利用した処理ができます。first lastはあくまでby変数の値によって0 1が立つのですが、groupformatオプションを使うと、値そのものではなくフォーマットされた値の切り替わりに依存させるととができます。

例えば

proc format;
value FMT 1-<4 ='GR1'
 4-<8 ='GR2'
 8-10 ='GR3'
;
run;

適当にフォーマット作って

data Q1;
call streaminit('777');
do i=1 to 15;
 X=round(rand('uniform')*10,0.1);
 _X=X;
 drop i;
 output;
end;
run;






















データ作って

proc sort data=Q1;
by X;
run;

ソートして

data A1;
format X FMT.;
set Q1;
by X groupformat;
FIRST=first.X;
LAST=last.X;
run;






















と、こんな感じです。

これを使うとフォーマットで区切った範囲でのfirst lastを使った処理ができるわけですね。
あんまり使ったことないですが、工夫すると、なかなか使えそうな機能ですね。

FCMPでデータセットを配列に入れて色々する

ちょっとからかわれた話です。

data Q1;
input X1 X2 X3;
cards;
1 2 3
4 5 6
;
run;







みたいなデータセットから


を作る場合、

proc transpose data=Q1 out=A0(drop=_NAME_);
run;

で終わりなんですが、ある人が、proc fcmpで転置すると
データステップやIML、transposeで転置するより速いよ。って言ってきました。

FCMPを使うと行列を扱えるという話は
SAS忘備録の記事「FCMPプロシジャと行列計算」
で読んで、知ってたんですが、実際やったことなかったです。

proc fcmp;
   array X[2,3] / nosymbols;
   array Y[3,2] / nosymbols;
   rc=read_array('Q1',X);
    call transpose (X,Y);
   rc=write_array('A1',Y);
run;

で、先ほどと同じ結果を導けます。

で、一応データセットの大きさとか色々変えて試してみたんですが
どうも大して速くない気が…。むしろ2-3割くらい遅い?環境のせいかなと思って、訊いてみると、
「あれ嘘だよ。」と一言。悔しいので、一応記事にしました。

もう一ネタ。

data Q2;
input X1 X2 X3 X4;
cards;
1 . . .
2 5 . .
3 6 8 .
4 7 9 10
;
run;










みたいなのがあって、










を作れというような問題があって、IMLのライセンスがない場合、

proc fcmp;
   array X[4, 4] / nosymbols;
   rc=read_array('Q2', X);
   do i = 1 to 4;
      do j = 1 to 4;
         if missing(X[i, j]) then X[i, j] = X[j, i];
      end;
   end;
   rc=write_array('A2', X);
quit;

でいけるんですね。


指定した変数以外の全ての変数に特定の値を割り当てる

実戦ではあまり使わない筋なので、ちょっとした遊びです。

今適当なデータセットがあって

data Q1;
X1=1;X2=1;X3=1;output;
X1=.;X2=2;X3=4;output;
X1=3;X2=3;X3=.;output;
run;








指定した変数以外の変数について値を全て99に強制変換せよというのが
課題だとします。

今回はX2の内容はそのままに、それ以外の変数を99にします。


data A1;
if 0 then set Q1;
retain _numeric_ 99;
set Q1(keep=X2);
run; 










とこんな感じです。

if 0 then setで変数情報だけ読み込ませているからretainでの指定が活きてくるわけですね。
そのあとにsetすれば、変数があればretainを掻き消すわけですね。

なんというかSASっぽいコードですね。




ハッシュオブジェクトで、出力データセット名を動的に制御する。しかもdataステートメントでの指定が不要という話

今年もよろしくお願いいたします。

僕はSASハッシュオブジェクトが大好きです。処理効率がいいとか、柔軟性が高いとか、コードの見通しがよくなるとか、マクロと相性いいとか、お勧めする理屈は僕なりに多々あります。
ただ個人的心情として、ぶっちゃけハッシュオブジェクトはコード書いてて面白いんですよね。

なんで面白いんかな?って考えると、既存のデータステップの枠を超えた処理がかけるので、想像力が試されてる気がして、それが刺激的なんですよね。

例えば、サンプルとして類型の多い有名なコードですが

data Q1;
X=1;Y=1;output;
X=1;Y=2;output;
X=1;Y=3;output;
X=2;Y=4;output;
X=2;Y=5;output;
X=3;Y=6;output;
X=3;Y=7;output;
run;














例えば、上記のようなデータセットを、Xの値で3つのデータセットに分ける場合、

data OUT1 OUT2 OUT3;
 set Q1;
 if X=1 then output OUT1;
 if X=2 then output OUT2;
 if X=3 then output OUT3;
run;

とかけます。
ルールとして、データステップ内でoutputされるデータセットはdataステートメントで指定されている
必要があります。
またOUT1 OUT2 OUT3としていますが、ここをXの値から取得して可変的に増減させることは
通常のステップでは難しいです。
恐らくマクロを使って、まずXの値のパターンをとって、データセット名を生成して、みたいな流れにせざるをえないはずです。

ところが、ハッシュオブジェクトだと

data _null_;
    if _n_=1 then
        do;
            dcl hash hid (ordered:'Y', multidata:'Y');
            hid.definekey ('X');
            hid.definedata ('X','Y');
            hid.definedone ();
        end;
    hid.clear();

    do until (last.X);
        set Q1;
        by X;
        hid.add();
    end;
    hid.output (dataset:cats('OUT',put(X, best.-L)));
run;

で結果は
【OUT1】







【OUT2】






【OUT3】






となります。

かなり変態コードなので少し補足します。
まずDOWループの理屈を使って、by値のXごとにhid.clearとhid.outputが起きるようにしてます。
つまり、まずハッシュオブジェクトの中身をclearメソッドで空にして、by値のグループに対して処理をします。処理は単純にハッシュオブジェクトにそのグループのデータを追加していくだけの処理です。

その処理がおわったらoutputメソッドでデータセット化するわけですが、その時点のXの値を使って合成した文字列をデータセット名にしている。つまり動的に生成できてるわけです。

う~ん、心がいきいきしますね!楽しい!!

ただ、今回はわざと趣味でアクロバティックな例を出しましたが、本当はもっとわかりやすいものなので、わけわからんから業務に導入しない!という結論にならないようにお願いします!