2013年12月27日金曜日

グループ内連番をふる方法。


グループ内連番をふる機会はすごく多いと思うので、まとめてみました。


サンプルデータ

data DT1;
input SUBJID$ V$;
cards;
001 a
001 b
001 c
002 x
002 y
;


求めたい結果
「SUBJID」毎に「V」の順番で連番をふる。





データステップで連番をふる

*** 方法1 **********;
proc sort data=DT1; by SUBJID V; run;

data OUT1;
  set DT1;
  by SUBJID V;

  if first.SUBJID then SEQ=0;
  SEQ+1;
run;

*** 方法2 **********;
proc sort data=DT1; by SUBJID V; run;

data OUT1;
  set DT1;
  by SUBJID V;

  SEQ+1-first.SUBJID*SEQ;
run;


・方法1が一般的なやり方だと思う。
・方法2は仕組みを理解すれば1行で書けるのでおススメ。


📝注意点

今回のテクニックの中で使用している「FIRST.BY変数」は「サブセット化IF」と一緒に使用すると正しく動かない事があります。
(解説記事:「サブセット化IFでありがちな落とし穴」)


2013年12月26日木曜日

SQL「再マージ」入門


「再マージ」は、SAS特有の機能っぽい。
AccessやMySQLで試してみたけど、ダメだった。

以下まとめてみてみました。


①サンプルデータ作成

data DT1;
  input A B;
cards;
1 10
1 20
1 30
2 40
2 50
;
run;




②まずは通常のSQL文

* Aのグループ毎に、Bの最大値を求める ;
proc sql;
  create table  DT2 as
  select  A, max(B) as B2
  from    DT1
  group by  A;
quit;




③「再マージ」を利用したSQL文

proc sql;
  create table DT3 as
  select  A, max(B) as B2, B
  from    DT1
  group by  A
  order by  A, B;
quit;

※ 赤字が②から追記した文。



解説
(1) まず②と同様、Aのグループ毎に、Bの最大値を求めにいきます。





(2) がしかし、select文にはBも指定されています。
そのため以下の元データ「DT1」からBを取ってきて、上記(1)とマージします。








(3)Aのグループ毎に(1)での計算が(2)にマージされます。
出来上がりは、DATAステップの「merge (1)(2); by A」とした時みたいな感じ。(内部処理の動きは違うと思うけど。)








ちなみに、この「再マージ」が行われると、並び順が狂うので、「order by A、B」と並び替えをしています。



ちょっと説明が分かりづらいかもしれないけど、
この仕組みは便利なので、今後活用法を小出ししていこうと思います。

2013年12月24日火曜日

ログに独自のエラーメッセージを表示する。


ログのメッセージは、
「NOTE」「WARNING」「ERROR」の3種類あり、

以下サンプルのように上記キーワードとコロン(:)をつけてPUTすると
ログ上に色つきの独自メッセージを表示できます(キーワードは大文字で記述すること)


サンプルプログラム
data _null_;
  put "NOTE:    独自メッセージ1";
  put "WARNING: 独自メッセージ2";
  put "ERROR:   独自メッセージ3";
run;


ログ



ただし、あくまでNOTE・WARNING・ERRORメッセージっぽい表示をしているだけです。
例えば、この方法でERRORメッセージを出したからといって、データステップを中止させるとか、処理に影響を与える事が出来るわけではありません。



使用例として、以下のように条件によって注意喚起を促したい場合に使える。

*** サンプルデータ ;
data DT1;
input SUBJID$ AGE;
cards;
001 17
002 21
003 16
;
run;

*** 年齢が18歳未満だったら、WARNINGメッセージを表示 ;
data DT2;
 set DT1;
 if AGE<18 then put "WARNING: 18歳未満です。" SUBJID= AGE=;
run;


ログ



自分の書いたメッセージがログに色つきで出るのがなんか楽しい。

2013年12月20日金曜日

PUT関数 vs VVALUE関数


以下のように、「PUT関数」を使って文字変換することってよくありますよね。

*** サンプルデータ *******************;
data DT1;
  format   V1 yymmdd10. ;
  V1 = '01jan2000'd; output;
  V1 = '05feb2001'd; output;
  V1 = .; output;
run;

*** PUT関数を使って文字化 ***********;
data DT2;
  length V1 8.  V2 $20.;
  set DT1;
  V2 = put(V1,yymmdd10.) ;
run;


これは「VVALUE関数」でもっと簡単に書けます。

 V2 = VVALUE(V1) ;

「VVALUE関数」は「変数に割り当てられているFORMAT」を使って文字変換してくれる関数です。


注意点


PUT関数と挙動が似ています。
例えば、PUT関数で数値の欠損値を以下のようにFORMAT変換すると、、

 length V3 $20.;
 V3 = put( . , yymmdd10.);

→ 変換後の値は「         .」という感じで先頭に余計な空白が入ります。


また、フォーマットによっては、、

 length V4 $20.;
 V4 = put( 10, best12. );

→ 変換後の値は「          10」という感じでこちらも先頭に余計な空白が入る場合があります。


VVALUE関数も同様に先頭に空白が入る場合があるのでご注意下さい



2013年12月16日月曜日

全角文字を半角に変換する。




全角から半角へ、半角から全角へと変換する「KPROPCASE関数」についてまとめてみました。



構文

KPROPCASE( 対象変数名,  ' 変換パターン指定 ' )

変換パターン 指定方法
① 全角カタカナ ⇒ 半角カタカナ'full-katakanahalf-katakana'
② カタカナ ⇒ ローマ字'katakana, romaji'
③ 全角アルファベット ⇒ 半角アルファベット

(英数記号も対象。
 ただし全角中点「・」は半角にならなかった。
 ①の指定方法では半角になった)
'full-alphabet, half-alphabet'




④ 小文字 ⇒ 大文字'lowercase, uppercase'


変換パターンは、以下のように逆指定も可。

例:
kpropcase( 対象変数名,  'lowercase, uppercase' )
kpropcase( 対象変数名,  'uppercase, lowercase' )




変換例

data DT1;
  length VAR1 A B C D $30.;
  VAR1 = "あカキ(1abC";
  A = kpropcase( VAR1, 'full-alphabet, half-alphabet' ) ;
  B = kpropcase( VAR1, 'lowercase, uppercase' ) ;
  C = kpropcase( VAR1, 'half-katakana, full-katakana' ) ;
  D = kpropcase( VAR1, 'katakana, romaji' ) ;
run;

結果
VAR1あカキ(1abC
A あカキ(1abC
B あカキ(1AB
C あカ(1abC
D KAKI(1abC



注意

①ふたつめの引数は 「'lowercase', 'uppercase'」ではなく「'lowercaseuppercase'」です。
間違えやすいので注意が必要です。


②半角を全角に変換したい場合、
たとえば以下のプログラムを実行すると、VAR1の値は全角”A”となる。

data DT3;
  length VAR1 $2.;
  VAR1 = "AB";
  VAR1 = kpropcase( VAR1, 'half-alphabet, full-alphabet' );
run;


これは全角変換したことによって、LENGTHが4バイト必要なのに、2バイトしかないため、文字切れを起こしているから。そのため、変換前にLENGTHを2倍に指定し直す必要がある。

(※上記はWindowsのsjis環境での話で、他の環境ならもっとLENGTHが必要)

2013年12月13日金曜日

SQLの小技「CALCULATEDキーワード」


SASのSQL独自の便利機能「CALCULATED」キーワード
MySQLとかでは動かなかった。

このキーワードを使うと、SELECT句で作成した変数を、同じSQL内で参照することが出来る。


*** サンプルデータ ;
data DT1;
   do V1=1 to 3 ;
      output;
   end;
run;


*** 普通は以下のエラーが出る ;
proc sql;
  select sum(V1) as V2 ,
            V2+1 as V3
  from DT1;
quit;

ERROR: 次の列はテーブル内に存在しません: V2.


*** 「CALCULATED」を使った場合 ;
proc sql;
  select sum(V1) as V2,
           calculated V2+1 as V3
  from DT1;
quit;



正直この例だと、「sum(V1)+1 as V3」と書いてもいいけど、、
・「V2」の式が長文の場合、「V3」作る時にもう1回長文かく手間が省けていい。
・内部の処理効率的に「calculated」を使った方がいいのかも。

という理由で、個人的には「calculated」を愛用してる。

2013年12月10日火曜日

欠損値のチェックを簡単に行う。


CMISS関数」は、欠損値の数をカウントしてくれます。
数値変数・文字変数、どちらも指定可能です。

ロジカルチェックやデータクリーニングの際に重宝します。
以下に使用例をまとめてみた。


サンプルデータ

data DT1;
input X1 X2 X3$;
cards;
1 2 aa
3 . bb
5 . .
. . .
;
run;


使用例

/* 欠損値の数を取得 */
data DT2;
  set DT1;
  Y = cmiss(X1,X2,X3);
run;


/* いずれかが欠損値のOBSを特定 */
data DT3;
  set DT1;
  if cmiss(X1,X2,X3) > 0 then FLG=1;
run;


/* すべて欠損値のOBSを特定 */
data DT3;
  set DT1;
  if cmiss(X1,X2,X3) = 3 then FLG=1;
run;


2013年12月6日金曜日

INPUT関数の小技


「INPUT関数」で文字を数値に変換する時、
"100未満"や"2013/10/初旬"とかだと、ログに無効なデータがある旨のメッセージが表示され欠損値となる。

サンプルプログラム

data DT1;
   length V1 $20.;

   V1="2013/10/初旬";
   V2=input(V1,yymmdd10.) ;
run;


ログ






そこで以下のように「??」を入れると、「問題ない」という扱いになりログから上記メッセージを消せる。
「V2」の値は上記の結果と変わらない。

   V2=input(V1,?? yymmdd10.)  ;

ログ









本来、おかしなデータがないか把握する上で、
無効なデータがある旨のログメッセージは絶対出しておくべき。


しかし、
ロジカルチェックなどで「生年月日に日付として不正な記載がある」
のようなチェックをかける場合、不正な記載そのものを抽出するので、わざわざそこをログに注意されなくてもいいと思う時は、「??」を入れたほうが、後のログチェックもスムーズにいく。

2013年12月2日月曜日

色々な例数を簡単に出す。



「SQLプロシジャでフラグ変数を簡単に作成。」を発展させたやり方。


A薬投与例数、B薬投与例数、中止例数、、etc
色々な例数をカウントしたい場合、SQLなら1STEPで簡単に出来てしまう。

サンプルデータ
data DT1;
  attrib
     SUBJID   label="症例番号"       length=$3.
     ARM       label="群"                 length=$10.
     SEX        label="性別"              length=$1.
     AEYN      label="有害事象有無" length=$1.
  ;
  input SUBJID$ ARM$ SEX$ AEYN$ ;
cards;
001 A薬 M  Y
002 B薬 F  Y
003 A薬 F  N
004 A薬 M  Y
005 B薬 F  N
;
run;


求めたい結果



SQLプロシジャで各例数を計算
proc sql;
   create table DT2 as
   select sum(ARM="A薬")  as N_ARM1, 
             sum(ARM="B薬")  as N_ARM2
   from DT1;
quit;


解説
関数の中に条件式を入れてるとこがポイント。

仕組みはまず、条件式に合致する場合「true=1」
合致しない場合「false=0」が返される。
その「0」か「1」になった値をsum関数で合計することで、条件に合致する例数をカウントしたことになる。

ちなみによくやる間違えとして、sum関数じゃなくcount関数を使ってしまいがち。
count関数は非欠損値の数をカウントするので、「0」も「1」もカウントしちゃって意味のない処理になっちゃうので注意。



応用
グループ毎に色々な有無を出すこともできる。

たとえば、上記で作成したサンプルデータで以下のように「群毎の有無」を出したいとする。
ちなみに「0=無」「1=有」を表す。






SQLプロシジャで群毎の各有無を出す。
proc sql;
   create table DT3 as
   select ARM,
          max(AEYN="Y")  as AEYN,
          max(SEX="M" and AEYN="Y")  as MALEYN
   from DT1
   group by ARM;
quit;



解説
条件式が「1=true」「0=false」となることを利用し、
「max(条件式)」とすることで「0=無」「1=有」という感じで、有無を得ることが出来てしまう。


SQLの素晴らしい柔軟さが伺える。

SQLプロシジャでフラグ変数を簡単に作成。


SQLプロシジャで、意外に使われてないと思ったやり方。
CASE式を使用しなくても、簡単にフラグ変数を作成することが出来る。

*** サンプルデータ作成 ;
data DT1;
  do V1=1 to 3;
     output;
  end;
run;

*** SQLプロシジャでフラグ変数を作成 ;
proc sql;
 create table DT2 as
 select  (V1>1) as FLG1,
           (1<V1 and V1<3) as FLG2
 from DT1;
quit;



結果






解説
SELECT 条件式  as  新規変数名」で、
「条件式」に合致する場合「1:true」、合致しない場合「0:false」が新規変数に返される。

これを知っていると色々な応用がきく。

2013年11月27日水曜日

年齢計算の落とし穴。



SASで年齢を計算する方法を紹介します。



※閏年生まれの人の加齢のタイミングをいつにするかによって、書き方が異なります。



以下、各年齢の出し方と結果の違い。

** サンプルデータ ;
data AGE;
 input BIRTH EVENT ;
 informat BIRTH EVENT yymmdd10.;
 format BIRTH EVENT yymmdd10.;
cards;
2000/03/02 2001/03/01
2000/03/02 2001/03/02
2000/03/02 2001/03/03
1992/02/29 1996/02/28
1992/02/29 1997/02/28
;

** 【年齢計算】 2月29日生まれの人の加齢タイミング別 ;
data AGE2;
 set AGE;

 /*- ① -------------------------------------
  閏年 の場合 → 2月29日 に加齢
  平年 の場合 → 3月1日 に加齢
 -----------------------------------------*/
 AGE1 = int( (put(EVENT,yymmddn8.)-put(BIRTH,yymmddn8.))/10000 ) ;


 /*- ② -------------------------------------
  閏年・平年ともに → 2月28日 に加齢
 -----------------------------------------*/
 AGE2 = int(yrdif(BIRTH, EVENT, 'AGE'));


 /*- ③ -------------------------------------
  閏年 の場合 → 2月29日 に加齢
  平年 の場合 → 2月28日 に加齢
 -----------------------------------------*/
 AGE3 = int( (intck('month', BIRTH,EVENT) -
         ( day(EVENT) < min(day(BIRTH),day(intnx('month',EVENT,1)-1)) )
        )/12);
run;





結果データセット「AGE2」









解説

①結果自体は、SASテクニカルサポート「年齢の計算方法」にあるやつと同じになる。
他のプログラミング言語で見かけた方法で、
ポイントは、2つの日付を、日付としてではなく8ケタの数字として見て計算してるところ。

ちなみに同じ結果を返す他の計算式として、以下2つの方法などもあり、どれもうまい。
 AGE4 = intck('YEAR',BIRTH,EVENT)-(put(BIRTH,mmddyy4.)>put(EVENT,mmddyy4.)) ;
 AGE5 = int((intck('MONTH',BIRTH,EVENT)-(day(EVENT)<day(BIRTH)))/12);


②「YRDIF関数」の「AGE」オプションを使ったやり方。
「AGE」オプションは人の年齢を計算できるオプションで、SAS9.3から追加された。


③海外のSASユーザー会で発表された、
「Calculating Age in One Line of Code」(Wei Wang, Highmark Blue Cross Blue Shield, Pittsburgh, PA) にあるやり方。
式は複雑だけど、うまい具合に計算してくれる。


まとめ

「うるう年」の取扱いは、各自異なると思うので、その辺の仕様をはっきりさせ最適な方法を使用するようにしましょう。


2013年11月20日水曜日

文字の先頭をゼロ埋めして桁数を揃える。



他のプログラム言語とかでもよく出てくる「ゼロパディング」といわれる問題。


例えば5桁に揃えたい場合は、
 「123」→「00123」
 「AB」 →「000AB」
みたいな感じに加工する方法。



ゼロ埋め対象が数値変数の場合

data DT1;
   length V1 $5.;
   V0 = 123;
   V1 = put(V0, z5.); 
run;

「Zw.dフォーマット」というのが用意されてて「z桁数」と指定するだけでok。




ゼロ埋め対象が文字変数の場合

data DT2;
   length V0 $3. V1-V3 $5.;
   V0 = "ABC";
   V1 = reverse(substr(reverse(cats("00000",V0)),1,5));  * 方法① ; 
   V2 = cats(repeat("0",4-lengthn(V0)),V0);                    * 方法② ; 
   V3 = tranwrd(put(V0, 5.-R),' ','0');                             * 方法③ ; 
run;


だいたい上記3つのやり方が思い浮かぶ。

方法①
ごり押し感あるやり方。
「cats関数」で「00000ABC」という文字にする。
「reverse関数」で「CBA00000」と逆にしてから、
「substr関数」で先頭5文字を抽出「CBA00」
また「reverse関数」でもとの順に戻せば完成。
非効率だけどきらいじゃない。

追記
上記と同じ原理、かつもっといい方法を教えてもらいました。

  V1 = put(put('00000'||V0 ,$revers8.),$revers5.);

put関数にも文字を逆さまにする「$revers」というformatがある。
うまいのが「$revers5」で、5文字に切りつつ、もとの順に戻してるところ。




方法②
変数「V0」から不足してる文字の長さ分だけ「repeat関数」を使ってゼロを埋めてる。


方法③
個人的には使ってる関数も少なくて一番スマートだと思う。
数値変数の場合でも適用可。

ポイントは「put関数」で5文字の長さにして「-R」を使って文字を右詰して「  ABC」としてるところ
その後、左側に出来た半角スペースを「tranwrd関数」でゼロに置換えてる。

「put関数」の「-R」については PUT関数の小技 を参照。




2013年11月15日金曜日

有害事象の発現例数と件数を簡単に出す。


安全性の解析で出てくる有害事象の集計。
例数と件数をなるべく簡単に少ないステップで出したい。
SQLなら簡単にできてしまう。


* サンプルデータ ;
data AE;
  length SUBJID $3. AETERM $20. AESTDT 8.;
  input SUBJID$ AETERM$ AESTDT;
  informat AESTDT yymmdd10.;
  format AESTDT yymmdds10.;
cards;
001 RBC上昇 2012/01/02
001 WBC減少 2212/02/05
002 RBC上昇 2012/01/05
002 WBC減少 2012/01/05
003 WBC減少 2012/02/10
;
run;












このデータから以下のように有害事象の発現例数・件数を出したい。
(症例内で同じ事象が複数回発現してる場合の扱いとかは、今回の例では特に考えてないです)





* SQLで発現例数と件数を1STEPで出す ;
proc sql;
  create table AE_N as
  select count( distinct SUBJID ) as N1,
             count( SUBJID ) as N2
  from AE;
quit;


N1が例数、N2が件数を求めた結果。



簡単に解説
  • SQLのCOUNT関数: 引数が非欠損値のレコード数を求める
  • DISTINCT: 関数の中に指定すると、引数の値から重複を除いたレコードを計算対象とする

今回の例だと「count( distinct SUBJID )」で、SUBJIDの値から重複を除いた非欠損値のレコード数を求めています(=例数が求まる)

2013年11月14日木曜日

「%IF条件」で「IN」を使う。




%IF  &マクロ変数名  IN  ( 値1, 値2 )


みたいな書き方は出来ない。。。と思ったら実はできる。



以下のプログラムは、マクロ変数 「MVAR1」 に 「1」 か 「2」 が設定されていたら、
ログにメッセージを表示するサンプル。

options minoperator mindelimiter=',' ;

%let MVAR1=1;

%macro TEST;
      %if  &MVAR1 in (1,2)  %then %put マクロ変数 MVAR1には 1 か 2 が設定されています;
%mend;

%TEST;

  • minoperator」 でINが使えるようになります
    • 「IN」の代わりに「#」でもOK( %if &MVAR1 # (1,2) %then ... )
  • mindelimiter」 には値を区切る時の区切り文字(シングルバイト1文字)を指定
    • % & ' " ( ) ;」といったマクロ等で意味を持ちそうな区切り文字は設定できません


今回の例では条件値を「,」で区切っています。
%IF条件でのINを利用不可に戻したいときは

options nominoperator;

delimiterの設定を戻すオプションはヘルプとかみても見つからず。



📝注意



続き・・・
「%IF条件」で「IN」を使う。(その2)


2013年11月8日金曜日

PUT関数の小技「文字寄せ」


以下のプログラムを実行すると、、

data DT1;
  length VAR1 $20.;
  VAR1 = put(1, 20.);
run;


「         1」みたいな感じで先頭に余計な半角スペースが入ってしまう。
左詰めにするため「left関数」を使う以外に、

 VAR1 = put(1, 20. -L);

と書くこともできてしまう。

ちなみに

 「-L」が左詰め
 「-C」が中央
 「-R」が右詰め

って意味になる。



使うときの注意

① VAR1 = put(1, 8.2 -L);            * OK ;
② VAR1 = put(1, 8.2 -L) || "mg";  * NG ;


上記②の場合、
まず「put(1, 8.2 -L)」で左詰めした分、文字値の右側に半角スペースが移動します。
イメージ:「    1.00」→「1.00    」


この文字と「mg」を結合すると「1.00    mg」といった感じで、不要な半角スペースが混入してしまう。

あくまでもleft関数のような動きをしていることに注意!

SUBSTR関数の小技




以下のデータセットがあったとして、、

data DT1;
    length VAR1 $20.;
    VAR1="abcde";
run;

変数「VAR1」の2~3バイト目を「XX」に置換したいとする。
だしたい結果は「aXXde」



「translate」とか「tranwrd」を思い浮かべるけど、以下みたいにも書ける。

data DT2;
    set DT1;
    substr(VAR1,2,2)="XX";
run;


substr関数の意外に知られてない書き方だと思う。ただし、

  • 2バイトを3バイトの文字に置換、みたいな異なる長さへの置換は出来ない。
    • 失敗例1: substr(VAR1,2,2)="XXX" → "aXXde"
    • 失敗例2: substr(VAR1,2,2)="X" → "aX de"
  • シングルバイト専用の関数のため、日本語などのマルチバイトは未対応。
  • 変数を上書きする(今回の例ではVAR1の変数値を置換した結果がVAR1に格納される)のでご注意下さい。