standardプロシジャやstdizeプロシジャで色んな標準化の話

なんかスコアデータみたいのがあって、最低スコアを0にして
最大スコアを100になるように値を変換してということがありました。

z得点だしたり、偏差値だしたり、はしょっちゅうするんで同じノリでproc standardでできるかなと
思ったけど、よく考えてみると、あれ?ってなりました。

proc standardで行ういわゆる標準化は、平均と標準偏差が一定の値になるように変換するわけで
今回みたいに無理やり最低値と最高値を決まった上限下限値にするのと全然違うよな~、平均も標準偏差も
使わないし。

もういいや~、面倒だからsummaryで各変数の最大、最小だして
変換値 =(元の値 - 最小値) / (最大値 - 最小値) * (設定上限値 - 設定下限値 ) + 設定下限値
でデータステップで片づけてしまいました。

で後で、対応するプロシジャってなんになるんだろう、多分stdizeだろうなと思って調べると
やぱりstdizeでした。

stdizeは広い意味での様々な標準化変換を行えるもので、かなり奥が深いです。

さわりだけ見てみましょう

今、以下のようなデータがあります

data q1;
x=1;y=20;z=-5;output;
x=6;y=10;z=5;output;
x=4;y=0;z=-1;output;
x=8;y=0;z=-3;output;
x=2;y=100;z=8;output;
run;











とりあえず基本統計量を見てみましょう
proc means data = q1  n mean std max min median sum maxdec=3;
run;








例えば、データを中心化したいから、平均0に寄せてと言われたら
以下のように各変数の平均を引けばいいわけです。

data q2;
set q1;
 x = x - 4.2;
 y = y - 26;
 z = z - 0.8;
run;

中身と統計量は以下になります。ちゃんと0になってますね

これをstandardプロシジャで行う場合は以下のように書きます
m=0というのが平均0にしている部分ですね

proc standard data=q1 out=a1 m=0;
 var x y z;
run;





















同じ結果になることが確認できました。

次はいわゆる偏差値です std=は標準偏差の指定です
proc standard data=q1 out=a2 m=50 std=10;
 var x y z;
run;

確認






















つづいてm=0 std=1 でz得点 標準得点ってやつですね
proc standard data=q1 out=a2 m=0 std=1;
 var x y z;
run;





















さて一方stdizeプロシジャとはなんなのかという話で、
まずは以下のように何も指定せずにまわしてみます

proc stdize data= q1 out = a3;
 var x y z;
run;




















さっきstandardプロシジャでzスコアだしたのと同じですね。
つまり何も指定しないとデフォルトで標準得点に変換するということがわかりました

実は、何も指定しなかった場合、内部的には以下のコードを実行しているのと同じになります

proc stdize data= q1 out = a3
 method=std
 add =0
 mult=1
;
 var x y z;
run;

method=std というのが標準偏差を使った標準化(変な言い方ですが)であること
add はこの場合、指定する平均
multiは指定する標準偏差になります

ですので偏差値をだすのであれば以下のようにかけます。
methodは省略可です。


proc stdize data= q1 out = a4
 method=std
 add=50
 mult=10
;
 var x y z;
run;



さて、ここまでだとstandardプロシジャと何が違うんだという話ですが
stdizeの味噌はmethod=の箇所にいろいろ指定できて、広い意味でのいろんな基準化ができるというところです

例えば冒頭の例のように0から100にデータを変換する場合

変換値 =(元の値 - 最小値) / (最大値 - 最小値) * (設定上限値 - 設定下限値 ) + 設定下限値にしたがって


data q3;
set q1;
x = ( x - 1 ) / ( 8 - 1 ) * (100 - 0) + 0;
y = ( y - 0 ) / ( 100 - 0 ) * (100 - 0) + 0;
z = ( z -(-5) ) / ( 8 - (-5) ) * (100 - 0) + 0;
run;

と書くところを、stdizeであれば以下のようにできます
mehodがrangeになっているところがポイントです

proc stdize data= q1
out = a5
        method=range
add = 0
mult = 100
;
 var x y z;
run;

結果は同じで




















最低値20 最大値100であれば

data q4;
set q1;
x = ( x - 1 ) / ( 8 - 1 )  * (100 - 20) + 20;
y = ( y - 0 ) / ( 100 - 0 ) * (100 - 20) + 20;
z = ( z -(-5) ) / ( 8 - (-5) ) * (100 - 20) + 20;
run;

を以下のように指定できます。
proc stdize data= q1
out = a6
        method=range
add = 20
mult = 80
;
 var x y z;
run;





















stdizeプロシジャの挙動は、わかると明快で
一貫した以下のルールに従って変換を行うんですね。

変換値 = add指定値 + mult指定値 ×[(元の値 - location(method毎に決まってる)/scale(method毎に決まってる]

method=stdの場合locationは平均値 scaleは標準偏差になります
method=rangeの場合locationは最小値 scaleはレンジになります

メソッドごとのlocationとscaleの設定は以下にまとまっております
http://support.sas.com/documentation/cdl/en/statug/63033/HTML/default/viewer.htm#statug_stdize_sect012.htm

methodに関わらず addのデフォルトは0 multのデフォルトは1になります。

なので、例えば中央値を使って標準化をする場合

proc stdize data= q1
out = a7
    method=median
add = 50
;
 var x y z;
run;




















最後に平均と標準偏差を使った標準化に対して
ノンパラ版として平均の代わりに中央値、標準偏差の代わりに正規化四分位範囲を使った方法が工業分野とかでよくあるらしいです。
すみません、まったくアホなんで詳しくないですが

四分位範囲はmethod=IQR で指定できるので、それに正規化のnormオプションを付けて

proc stdize data= q1
out = a8
    method=iqr
norm
;
 var x y z;
run;
















な感じですかね


ユニークなIDとか作れって言われたらの話

多分需要ないし、方法に工夫もないけど、与えらえた文字列から指定の長さで
ユニークなIDを作成するマクロです。

すでに発行済のIDが入ったデータセットを指定することで
過去に発行したものとも重複しない作成が可能です。

使用可能文字の数と、作成IDの長さの設定によって、生成できるIDの限界数が
決まります。
例えば100万の生成限界なのに90万のIDを発行したりすると実行効率遅いです。
生成限界はできるだけ余裕を持たせてください。

アルゴリズムは単純ですが
生成限界がぶっとんだ値であれば、何十万発行しようが結構速いと思います。

すでに存在するデータセットに付与して作りたい場合は
マクロをちょっと書き換えてsetをいれて、do untilのループをとっちゃえばいいです。


%macro unique_make(outds=,obs=,idlength=,seed=,moji=,ban=);
/*--------------------------------------------------------
outds=作成されるデータセット
obs =作成するオブザベーション数
idlength=作成される文字列の長さ
seed =作成のための乱数シード
moji =使用する文字列(半角英数字)
ban  =データセット指定(ここにあるidは作成されない)
----------------------------------------------------------*/
data &outds(keep = id);
length id $&idlength..;
if _N_ = 1 then do;
declare hash h1
%if %length(&ban) =0 %then %do;();%end;
%else %do;(dataset:"&ban");%end;
h1.definekey('id');
h1.definedone();
end;
x="&moji";
n=length(x);
kumi=n**&idlength;

put 'NOTE:与えられた文字の数は ' n;

put 'NOTE:生成限界は ' kumi;

call streaminit(&seed);

do until(obs=&obs);
do until(okfl=1);
do i = 1 to &idlength;
r=int(rand('uniform') * n +1);
id = cats(id , char(x,r));
if i = 5 then do;
if h1.check() ne 0 then do;
okfl=1;
h1.add();
output;
obs+1;
end;
else id='';
end;
end;
end;
end;
run;
%mend;

以下実行例です。


/*英数字で5桁のIDを10000発行*/

%unique_make(outds=A1
 ,obs=10000
 ,idlength=5
 ,seed=2345
 ,moji=abcdefghijklmnopqrstuvwxyz0123456789
);

/*先に発行したA1とかぶらないようにさらに10000発行*/

%unique_make(outds=A2
 ,obs=10000
 ,idlength=5
 ,seed=6789/*シード変えた方がよい*/
 ,moji=abcdefghijklmnopqrstuvwxyz0123456789
 ,ban=A1
);

引数指定の際のofとカンマ区切りの細かい話

最近ちょっと更新が途絶えてましたが元気です。

さて、昔、人にプログラムをあげた時に
s = sum(of a -numeric- e) ;
みたいな書き方を入れていたんですが、なんですかこれ??って
質問が来たことがあります。

特定の関数で、複数の引数を指定するときの方法として
カンマで区切って列記する方法と、ofで指定する方法があります
(併用もできます)。

が、うっかりしていると慣れた人でもハマりやすい罠があるので
おさらいしてみようと思います。

テストデータ

data Q1;
 x1=1;x2=2;x3=3;x4=4;x5=5;
run;

まずは基本。
x1からx5まで全部の合計をだすには例えば以下のように書きます。

data A1;
 set Q1;
 y1 = sum(x1,x2,x3,x4,x5);
 y2 = sum(of x1-x5);
 put (y1-y2) (=/);
run;

ログに
y1=15
y2=15

とでます。あってます。

y1は基本で、対象の変数を全部カンマ区切りで指定します。
y2はofを使用しています。
これから詳しく見ていきますが、ofを使うと

①of x1-x10  (変数の末尾の連番指定)
②of id--y (データセットの格納順 idからyまでの間にある変数全部)
③of _all_ や of _numeric_; や of _character_;(全変数や全数値変数や全文字変数)
④of ax{*} (配列を指定)
⑤of x: (コロンモディファイア指定→xから始まる変数名のもの全て)
⑥of x1 x3 x5 (変数列記 変数だけでなく①から⑤までのものを全て空白区切りで列記できる)

といった指定が可能になります(他にもあったら教えてください)。

で、とても便利なのですが、よくやってしまうのが

data A3;
 set Q1;
 y3 = sum(x1-x5);
 put y3=;
run;

結果は
y3=-4

なんで値がマイナスになっているかというと
of をつけないと x1からx5を引いた単一の値がsum関数の対象になってしまってるんですね。

続いて、複数の変数リストを指定する場合ですが

data A4;
 set Q1;
 y4 = sum(of x1-x2 x4-x5);
 y5 = sum(x1-x2 , x4-x5);
 y6 = sum(of x1-x2 , of x4-x5);
 y7 = sum(of x1-x2 , x4 , x5);
 put (y4-y7) (=/);
run;

結果は
y4=12
y5=-2
y6=12
y7=12

y4のようにof の後、空白でリストを区切ればOKです。
先ほどと同様にy5のようにしてしまうと x1引くx2とx4引くx5の値の合計値がでてしまいます。
意外と盲点なのが、y6のようにカンマで区切っても、その中ごとにofをつけてやれば
コードとして正しいということなんですね。
同様にy7のようにof指定とカンマ区切りを併用することもできます。
y4のようにカンマつけなきゃ一つのofで済む話なのに、なんで敢えてこういうことを書くかというと
例えば、プログラム仕様書の条件式や指定変数からコードを自動生成したりする場合、
仕様書の記載法と生成ロジックを考えなきゃいけないわけですが、そういう時に
こういう書き方でも式が正しく成立している(していない)といったケースを多く知ってると意外と役立つと思います。

で、次は、対象の変数に個々にマイナスをかけて合計したい場合です。
質問方向が反転の項目とかでそういうことありますよね。

以下を実行すると

data A5;
 set Q1;
 y8 = sum(-x1,-x2,-x3);
 y9 = sum(of -x1 -x2 -x3);
 put (y8-y9) (=/);
run;

結果は
y8=-6
y9=.

となってy8は正しいですがy9は正しく計算されません。
ofの場合、カンマ区切りと違ってマイナスつけはできないのです。

マイナスつけができないというより、もっと広く言えば
以下のように

data A6;
 set Q1;
 y10 = sum(x1+x2,x1*x2);
 y11 = sum(of x1+x2 x1*x2);
 put (y10-y11) (=/);
run;

結果は(y11のせいでエラーになりますが個別にだすと)

y10=5
y11=.

つまり、ofは変数の一括指定のためのキーワードで計算式を引数には
とれないうことです


さて次は、of使うといろいろできるよという例ですね

data A7;
 set Q1;
 array x{*} x: ;
 y11 = sum(of x{*});
 y12 = sum(of x{*} , x2 , of x3-x5  , of x:);
 put (y11-y12) (=/);
run;

結果
y11=15
y12=44

y11についてはカンマで区切らなければofは一個で、後は空白区切りでOKですからね。


最後は冒頭で述べた例に戻ります。
以下のテストデータがあり

data Q2;
 a=1;b=2;c=3;d='AAA';e=4;
run;

--を使うことによって変数の格納順を利用できます。

data A8;
 set Q2;
 y13= sum(of a--c);
 put y13 = ;
run;

結果
y13=6

で、以下の例のy13のように_numeric_としてデータセット内の
全数値型変数を対象にできるのですが、

さらにそれを--と組み合わせてy14のようにかくと


data A9;
 set Q2;
 y14 = sum(of _numeric_);
 y15 = sum(of a -numeric- e);
 put (y14-y15) (=/);
run;

y14=10
y15=10

aからeの間に格納されている変数のうち、数値型のものに
限るという指定ができます。

ながながと細かい話でした