2014年3月28日金曜日

1行プログラムその4:カテゴリ変数の作成



今回は1行でカテゴリ変数 (数値) に置き換える方法。

[年齢]
20歳未満               → 1
20歳以上30歳未満 → 2
30歳以上               → 3


サンプルデータ

data DT1;
input AGE;
cards;
17
23
19
37
27
;

データセットDT1
AGE
17
23
19
37
27

求めたい結果
AGE  AGEGRP
17       1
23       2
19       1
37       3
27       2




1行で書く方法。

普通に書くとこんな感じですが、
data DT2;
   set DT1;
   if  0   <   AGE <20      then  AGEGRP = 1 ;
   if  20 <= AGE <30      then  AGEGRP = 2 ;
   if  30 <= AGE            then  AGEGRP = 3 ;
run;


WHICHN関数で1行でいける。
data DT2;
  set DT1;
  if AGE^=. then AGEGRP = whichn(1, 0<AGE<20, 20<=AGE<30, 30<=AGE);
run;


  • WHICHN関数の構文は「whichn(Y, X1,X2…)」で、Yと同じ数値がXの何番目に初登場するか返してくれます。
  • Yと同じ数値がどのXにもない場合「0」が返されます。
  • Yが欠損値の場合、欠損値が返される。
  • リンクさせて頂いてる「データステップ100万回」の「whichn関数はwhereステートメントでも使える話とその応用をだらだら」で最後に提案されてるアイディアに再インスパイアされて今回の問題に当てはめて利用させて頂きました。


今回の使用法の注意点
・カテゴリ値が1~5くらいまでならいいけど、「年齢=NULL → 100」とか飛び離れたカテゴリ値を設定したい場合、関数の引数を100個指定しなきゃいけなくなるので実用的じゃなくなる → whichn(Y, X1,X2…X100)


2014年3月25日火曜日

1行プログラムその3:数値を任意の文字に置き換える。

その2」の続き、、というかやってる事は同じ。
今回は「1→なし」「2→あり」「3→不明」と文字に置き換えたい場合、CHOOSEC関数を使うと1文でいけます。

1 → なし
2 → あり
3 → 不明

サンプルデータ

data DT1;
input V1;
cards;
1
3
2
;
run;

データセットDT1
V1
 1
 3
 2

求めたい結果
V1  V2
 1    なし
 3    不明
 2    あり



1行で書く方法。

まじめに書くとこんな感じですが、
data DT2;
   length V2  $6.;
   set DT1;
   if V1 = 1 then V2="なし";
   if V1 = 2 then V2="あり";
   if V1 = 3 then V2="不明";
run;


CHOOSEC関数の場合。
data DT2;
   length V2  $6.;
   set DT1;
   if V1^=. then V2=choosec(V1, "なし","あり","不明");
run;

CHOOSEC関数の構文は「choosec(N, X1,X2…)」でN番目のXの値を取得することが出来ます。
Xは文字型の変数または値である必要があります。


今回の使用法の注意点
前回と注意点は同じで、
V1に入る値は1~10くらいまでならいいけど、100とか飛び離れた値があると関数の引数を100個指定しなきゃいけなくなるので実用的じゃなくなる → choosec(N, X1,X2…X100)

1行プログラムその2:数値を任意の数値に置き換える。

その1」の続きです。
前回は1行のプログラムで「1→2」「2→1」と置き換えたけど、「1→10」「2→15」のように任意の値に置き換えたい場合どうすればいいでしょうか?

1 → 10
2 → 15
3 → 100

サンプルデータ

data DT1;
input V1;
cards;
1
3
2
;
run;

データセットDT1
V1
 1
 3
 2

求めたい結果
V1  V2
 1    10
 3    100
 2    15



1行で書く方法。

まじめに書くとこんな感じですが、
data DT2;
   set DT1;
   if V1 = 1 then V2=10;
   if V1 = 2 then V2=15;
   if V1 = 3 then V2=100;
run;


CHOOSEN関数を使うと1文でいけます。
data DT2;
   set DT1;
   if V1^=. then V2=choosen(V1, 10,15,100);
run;

CHOOSEN関数の構文は「choosen(N, X1,X2…)」でN番目のXの値を取得することが出来ます。
Xは数値型の変数または値である必要があります。

最近までこの関数なめてたけど、考えてみると色々使い道があっておもしろいです。


今回の使用法の注意点
V1に入る値は1~10くらいまでならいいけど、100とか飛び離れた値があると関数の引数を100個指定しなきゃいけなくなるので実用的じゃなくなる → choosen(N, X1,X2…X100)

2014年3月23日日曜日

MERGEステートメントの落とし穴




むかし、MERGEでやってしまったミスです。


サンプルデータ

data A;
input NO$ NO2 V1;
cards;
001 1 1
001 2 1
001 3 1
002 1 1
002 2 1
;

data B;
input NO$ V1 V2;
cards;
001 0 1
002 0 2
;

 AB
 



問題

サンプルデータより以下のプログラムを実行すると、①②どちらのデータが出来るでしょうか?

data C;
   merge A  B;
   by  NO;

   if  V2=1 then  V2=10;
   else  V2=20;
run;

 ①

正解は②です。①だと思ったら要注意!


解説

はじめに、内部処理の説明は自分なりの解釈になってる可能性もあるので、あしからず。。


仕組み①
まずMERGEの仕組みを知る必要があります。
内部では、一旦プログラムデータベクトル(PDV)という入れ物に読み込んで加工してから、データセットに出力します。

以下は、サンプルデータの1行目をPDVに読み込むイメージ。




仕組み②
次が最大のポイントで、一対多のマージにおいて、
一側データセットBの変数は、BYグループ毎に1回しかPDVに読み込まれません。
(BYグループが変わるまで、PDVに残った値が保持される。。。RETAINのようなイメージ)

上記イメージの続きで、2行目をPDVに読み込むイメージ。




もし①のような結果が欲しい場合は、以下のように工夫する必要がある。
data C;
   merge  A(drop=V1)  B;
   by  NO;

   if   V2=1 then _V2=10;
   else  _V2=20;
run;




利用

この動きを面倒なものと思わず、逆に利用してやりましょう。
以下はマージの動きを利用してBYグループ毎に連番をふってます。
data B_;
  set B;
  SEQ=0;
run;

data D;
  merge A B_;
  by NO;
  SEQ+1;
run;




以上、説明がうまく出来た気がしないので、分かりにくい等のコメントを頂ければ、
さらに詳しく解説したいと思います。

2014年3月20日木曜日

欠損値の置き換え。



SASのFAQや色々なサイトですでに紹介されていますが、
面白いなぁと思ったので、こちらでも紹介したいと思います。


指定した数値変数について、欠損値をゼロや任意の値に置き換えてくれる小技です。

*** サンプルデータ ;
data DT1;
input A B C;
cards;
1 . 2
3 4 .
. . 5
;

データセットDT1
A   B   C
1    .    2
3   4    .
.     .    5




上記に対して、指定した数値変数が欠損値だったら「0」に置き換えたい場合、
以下のようにARRAYを使って書くのがスタンダードだと思います。

data DT2;
  set DT1;
  array AR(*) A B C;
  do i = 1 to dim(AR);
     if AR(i) =. then AR(i) = 0;
  end;
run;

データセットDT2
A   B   C
1    0    2
3    4    0
0    0    5

このプログラムで何をやってるかは以下記事を見ていただくと理解が深まると思います。
配列(ARRAYステートメント)入門





そしてここから本題。面白いと思ったのが、以下のやり方。

proc stdize data=DT1 reponly missing=0 out=DT3;
   var A B C;
run;

STDIZEプロシジャは、データの標準化をするSTANDARDプロシジャの進化版みたいなもの。
「reponly」と「missing=」オプションを指定することで、欠損値の置き換えのみを行ってくれる。


こういうプロシジャの面白い使い方をどんどん発掘していきたいです。


2014年3月18日火曜日

関数の落とし穴



シングルバイト専用の関数に、マルチバイト文字を含む引数を指定しちゃダメという話です。
今回の内容はWindowsのSAS9.3 日本語 (shift-jis) の環境下での結果とします。他の環境下では挙動が異なります。


上記環境下で以下のプログラムを実行すると変数BとCの値は変わった動きをします。

data DT1;
   length A B $20.;

   A = "予防";
   B = compress(A, "\");
   C = index(A, "\");
run;

結果
A        B         C
予防    蘭h       2



これは文字コードが関係しています。この辺は詳しくないですが、
  • "予防"の"予"という字は文字コードだと「975C」、
  • "\" は文字コードだと「5C」になるようです。

「5C」の部分が共通なので、"予" に "\" が入っているものとして扱われてしまいます。
「5C」に限らず、マルチバイト文字だと上記のように文字コードで見たときに共通する部分が出てきてしまうため、関数の結果がおかしくなります。


たとえば以下の場合とかもおかしくなるはず。
「VAR1 = compress("右", "E");」


これを回避するのが、「K」を頭につけたマルチバイト対応の関数達です。
data DT2;
   length A B $20.;

   A = "予防";
   B = kcompress(A, "\");
   C = kindex(A, "\");
run;

結果
A        B        C
予防    予防    0



注意点として、以下の例のように、シングルバイト専用の関数はバイト数で判断するのに対して、K関数達は文字数で判断します(K関数でもバイト数で判断するものもある)

 INDEX("あいう","い") -> 3
 KINDEX("あいう","い") -> 2

2014年3月14日金曜日

変数名を取得する「CALL VNEXT」。




以下は、CALL VNEXTを使って変数定義をデータセットに書き出す例です。


* サンプルデータ作成 ;
data DT1;
input A1 A2 A3;
cards;
1 1 2
2 1 2
3 1 2
;

* 変数定義をデータセットに書き出す ;
data DT2;
   length  _NAME  $40. _TYPE  $1.;   * ① ;
   set DT1;

   do until ( _NAME="" );  * ② ;
       call vnext( _NAME, _TYPE, _LENGTH ); * ③ ;
       if  _NAME ^=""  then  output;
   end;

   stop;  * ④ ;
   keep _NAME _TYPE _LENGTH ;
run;

結果データ「DT2」



解説

①変数定義を放り込む変数に、LENGTHを指定。

②CALL VNEXTは実行する度に、順に変数定義を読みに行きます。
なので読み込む定義がなくなるまでDO UNTILでループします。

③変数名を「_NAME」、型を「_TYPE」、変数長を「_LENGTH」という変数に放り込むよう指定。
もし変数名だけ取得したい場合、「call vnext( _NAME)」としてもOK。

④変数定義を取得した後はDT1を読み込み続ける必要がないので、STOPステートメントでデータステップを停止。


POINT
  • 「_ERROR_」「_N_」などの自動変数の定義も読み込まれます。
  • プログラム中で作成した変数(今回の例では「_NAME」「_TYPE」「_LENGTH」)の定義も読み込まれます。またこれらと同じ名前の変数がSETで読み込んだデータセットにもある場合は正しく動かなくなるので、その時はプログラム中で作成する変数名を変える必要あり。
  • 型は文字変数であれば"C"、数値変数であれば"N"が入ります。
  • 得られる変数名の順番は、バージョンや環境によって異なる可能性があるので当てにしない方が良い。




応用例

全変数一括で何か処理したい場合、「CALL VNEXT」と「CALL EXECUTE」を組み合わせると便利です。

CALL EXECUTEはデータステップ100万回の以下記事にて分かり易く解説されてるので参照下さい。
http://sas-tumesas.blogspot.jp/2013/11/call-execute.html


*** 全変数を一括でRENAME(先頭に"X"を付与)するサンプル ;
data _NULL_;
   length _NAME $40.;
   set DT1 ( keep= drop= ) ;   * ① ***;

   call execute( "DATA DT3; SET DT1; RENAME " );

   do until ( _NAME="" );
      call vnext( _NAME );

      /* ^in の後のカッコの中は大文字で指定 */
      if upcase( _NAME ) ^in ( "_NAME", "_ERROR_", "_N_", "" ) then
         call execute( _NAME || "=X" || _NAME );  * ② ***;
   end;

   call execute( "; RUN;" );
   stop;
run;

「CALL EXECUTE」で生成されるプログラム
 DATA DT3; SET DT1; RENAME
 A1                                      =XA1
 A2                                      =XA2
 A3                                      =XA3
 ; RUN;

結果データセット「DT3」


📝
プログラム中で作成している「_NAME」と同じ名前の変数が①の部分でSETしたデータセットにもある場合は正しく動かなくなるので、その時はプログラム中の「_NAME」を別の名前に変える必要がある。



解説

基本的に最初のサンプルプログラムをもとに発展させたやり方です。

①RENAMEする変数を選択したい場合は、3行目の「SET DT1 (keep= drop=)」のところで、
 set DT1 ( keep=A2 A3  drop= );


としたり、RENAMEしたくない変数がある場合、
 set DT1 ( keep=  drop=A1 );

のようにすればOK。

上2つのように書くと、以下のプログラムが生成されます。
 DATA DT3; SET DT1; RENAME
 A2                                      =XA2
 A3                                      =XA3
 ; RUN;


自動変数等がRENAME対象に入らないよう除外した上で、CALL EXECUTEでRENAME処理の文を生成しています。
SASのバージョンが上がって、もし自動変数が追加されてたら、ここをちょっと変える必要あり。



工夫すればなんでも出来るので非常に使い勝手がいいです。
変数を取得する方法は他にも沢山あり、「変数名を取得する方法 [まとめ]」でまとめてるので参考までに。



2014年3月13日木曜日

1行プログラムその1:「1→2」「2→1」のような値変換


数行を要するようなプログラムを1行におさめる事を意識した小技や基本技を紹介したいと思います。

まずは初歩的なものから。
コードやスコアなどの値を以下のように逆転させたいことがあります。
1 → 3
2 → 2
3 → 1

サンプルデータ

data DT1;
input V1;
cards;
1
3
2
;
run;

データセットDT1
V1
 1
 3
 2

求めたい結果
V1  V2
 1    3
 3    1
 2    2



1行で書く方法。

まじめに書くとこんな感じですが、
data DT2;
   set DT1;
   if  V1 = 1 then  V2 = 3 ;
   if  V1 = 2 then  V2 = 2 ;
   if  V1 = 3 then  V2 = 1 ;
run;


以下の式を当てはめて書くと1文でいけます。
(開始値 + 終了値) - 変数値

サンプルの場合、「1~3」の範囲の値なので、当てはめると「(1 + 3) - V1」となります。
data DT2;
   set DT1;
   V2 = 4 - V1 ;
run;

2014年3月6日木曜日

SORTプロシジャの「NODUP」と「NODUPKEY」の違い。


昔、「NODUP」を「NODUPKEY」の省略形だと勘違いして使ったら、思ったとおりの動きをしてくれなくて困ったことがありました。 

今回は「NODUP」と「NODUPKEY」を混同しないようにするための注意喚起になります。
「NODUP」は、最新のリファレンスから記載が削除されていたので詳しい挙動は不明。利用は非推奨。





* Sample data ;
data DT1;
input A B$;
cards;
1 a
1 b
1 a
;

A B 
  1  
  a  
  1  
b  
  1
a  

* 「NODUP」を使ってみる ;
proc sort data=DT1 out=DT2 nodup;
  by A;
run;

期待した結果
A B 
  1  
  a  



実際の結果
A B 
  1  
  a  
  1  
b  
  1
a  


解説

①「NODUP」は「NODUPRECS」を省略した書き方です。

これは、
BY変数で並べ替えた後、1つ前のレコードが全変数同じ値だったら、現レコードを削除する。
という動きをします。

②ここで注意するのは、
全変数同じ値のレコード(サンプルデータでは1行目と3行目)が存在していても、
BY変数で並べ替えた後、前後にレコードが隣り合っていないと削除していない点です。




似たオプション「NODUPKEY」の詳細は以下のリンクを参照下さい。
SORTプロシジャで重複レコードを削除・抽出する。


SORTプロシジャで重複レコードを削除・抽出する。



SORTプロシジャで重複レコードを削除・抽出するオプション「NODUPKEY」と「NOUNIQUEKEY」を紹介したいと思います。




サンプルデータ

data DT1;
input A B C$ @@;
cards;
1 1 a 1 2 a 2 1 c 2 1 b
;
 A 
 B  
 C  
  1     1     a  
  1    2  a 
  2    1  c   
  2    1  b   



例①

proc sort data=DT1 nodupkey out=DT2 dupout=DT3;
  by A B;
run;

DT2
 A 
 B  
 C  
  1     1     a  
  1    2  a 
  2    1  c 


DT3
 A 
 B  
 C  
  2     1  b   


解説

  • NODUPKEY」を指定すると、BY変数が重複してるレコードがあった場合に、最初のレコードのみ残して、以降の重複レコードを削除してくれる。
  • OUT=」のデータセットには削除後のレコードが入る。
  • DUPOUT=」のデータセットには削除したレコードが入る。
  • OUT=」を省略すると、「DATA=」のデータセットに上書きしてしまうので、注意!




例② (SAS9.3からの機能)

proc sort data=DT1 nouniquekey out=DT4 uniqueout=DT5;
  by A B;
run;

DT4
 A 
  B  
 C  
  2    1   c  
  2  1  b 


DT5
 A 
  B  
 C  
  1    1   a  
  1  2  a


解説

  • NOUNIQUEKEY」を指定すると、BY変数が重複してるレコードと、重複していないレコードを分割することが出来る。
  • OUT=」のデータセットには重複してるレコードが入る。
  • UNIQUEOUT=」のデータセットには重複してないレコードが入る。
  • OUT=」を省略すると、「DATA=」のデータセットに上書きしてしまうので、注意!



「NOUNIQUEKEY」 は結構助かる機能ですね。

2014年3月4日火曜日

半角スペースを取り除いてくれる関数達




余計な半角スペース(空白)を削除する方法をいくつか紹介。





📝今回紹介する関数の中で「STRIP関数」と「COMPRESS関数」は日本語等のマルチバイト文字に対応していないので注意!
(バージョンによってマルチバイトへの対応が変わっているので詳細はリファレンスを参照下さい)

data DT1;
    VAR1 = " a   b";

    * ① 両端の半角スペースを除去 ;
    A = stripVAR1 );

    * ② 連続する半角スペースを1つの半角スペースに置換 ;
    B = compbl( VAR1 );

    * ③ 全ての半角スペースを除去 ;
    C = compress( VAR1 );
run;

結果
VAR1 a   b
Aa   b
B a b
Cab



私自身は、①のSTRIP関数の代わりに「cats(VAR1)」と書いたりしてます。
CATS関数は変数や文字の値を連結してくれる関数です。
連結する際に、各引数の両端の半角スペースを除去してくれます。

cats(" a ",  " b c ",  " d ") → "ab cd"

「cats(VAR1)」と書くと、単純に両端の半角スペースを取り除いてくれるだけなので、使い勝手がよい関数です。


注意点

以下の記事で触れていますが、末尾の半角スペースを取り除いても、その結果を変数に格納した時点で、結局末尾に半角スペースが入ってしまうんで、そこの取り扱いだけ注意。

2014年3月1日土曜日

IN=オプションとWHICHN関数のコンボ技。




ちょっとしたコンボ技を紹介。

複数のデータセットをSETして1つにまとめた時、
どのデータセットから持ってきたレコードなのか識別するために由来コードを振ることがあって、そういった時に使えます。


サンプルデータ作成

data DT1;
  do V=1 to 2;
     output;
  end;
run;

data DT2;
  do V=3 to 4;
     output;
  end;
run;

データセット「DT1」
V
1
2

データセット「DT2」
V
3
4




コンボ技

上記データセットを縦結合して、
・「DT1」由来のレコードであれば「ORIGIN=1」
・「DT2」由来のレコードであれば「ORIGIN=2」
というコードの変数を作りたいとします。

求めたい結果
V   ORIGIN
1      1
2      1
3      2
4      2



最近までこんな感じで書いてましたが、、
data OUT2;
   set DT1 (in=_IN1)
         DT2 (in=_IN2)
   ;
   if  _IN1=1 then ORIGIN = 1;
   if  _IN2=1 then ORIGIN = 2;
run;



WHICHN関数を組み合わせると、以下のように書けます。
data OUT2;
   set DT1 (in=_IN1)
         DT2 (in=_IN2)
   ;
   ORIGIN = whichn(1,_IN1, _IN2) ;
run;

結合するデータセットがいくつあっても1文で書けちゃうので楽。


WHICHN関数の構文は「whichn(Y, X1,X2…)」で、Yと同じ値がXの何番目に初登場するか返してくれます。
・YとXは数値型の変数または値である必要があります
・Yと同じ値がXにない場合、0が返される
・Yが欠損値の場合、欠損値が返される