2015年1月30日金曜日

TRANSPOSEのIDステートメント入門




TRANSPOSEはデータを転置するプロシジャです。
今回は質問をよく頂く、IDステートメントの使い方を中心にまとめてみました。


注意:
TRANSPOSEの「IDステートメント」は、 システムオプション「VALIDVARNAME」の設定により挙動が変わります。今回は「VALIDVARNAME=V7」としている場合の例になります。



基本構文:

PROC TRANSPOSE   DATA=対象データセット   OUT=出力データセット ;
      VAR  転置する変数  ;
      BY   BY変数 ;
      ID    ID変数 ;
RUN;



まずは、例を見ていきましょう。

*** サンプルデータ ;
data DT1;
input NO$ MONTH YEN;
cards;
A 1 5000
A 2 500
A 3 1000
B 1 2000
B 3 1500
;

NOMONTHYEN
A15000
A2500
A31000
B12000
B31500

サンプルは、各預金者(NO)に対する、月別(MONTH)の預金額(YEN)だとします。


*** プログラム①  ;
proc transpose data=DT1 out=OUT1;
   var YEN;
   by NO;
run;

NO_NAME_COL1COL2COL3
AYEN50005001000
BYEN20001500


*** プログラム②  ;
proc transpose data=DT1 out=OUT2;
   var YEN;
   by NO;
   id MONTH;
run;

NO_NAME__1_2_3
AYEN50005001000
BYEN20001500


解説

プログラム①
NO毎に、YENを横に転置してます。


プログラム②
プログラム①に似た結果ですが、異なるのは、転置後の変数名と1500円の位置。
これはIDステートメントの機能で、ID変数の値を変数名に割り当てているからです。


今回の例では、
MONTH=1 の時、変数名「_1」、
MONTH=2 の時、変数名「_2」、
MONTH=3 の時、変数名「_3」に、転置するYENの値をはめ込んでいってます。



ポイントと注意点


今回の例のポイントとして、MONTHの値をそのまま変数名にすると、「1」「2」「3」となって、変数名の命名規則に反してるので、先頭にアンダーバーがついた「_1」「_2」「_3」という変数名になります。
(命名規則に反している箇所は「_」「P」「N」「D」などのSASが決めた文字で埋められます)

命名規則に反しているかどうかの判断はシステムオプション「VALIDVARNAME」の設定によるので、この設定次第で挙動が変わります。



以下のように、MONTHの値がもし、「M1」「M2」「M3」とかだと、命名規則に則してるので、転置してもそのまま「M1」「M2」「M3」という変数が出来ます。↓↓

*** サンプルデータ ;
data DT2;
input NO$ MONTH$ YEN;
cards;
A M1 5000
M2 500
M3 1000
M1 2000
M3 1500
;

NOMONTHYEN
AM15000
AM2500
AM31000
BM12000
BM31500

proc transpose data=DT2 out=OUT3;
   var YEN;
   by NO;
   id MONTH;
run;

NO_NAME_M1M2M3
AYEN50005001000
BYEN20001500



1つ気をつけるのが、IDステートメントの変数にformatが割り当てられてる場合です。
なぜなら、formatをあてたときの値が、転置後の変数名になってしまうからです。↓↓

*** サンプルデータ ;
proc format;
   value MONTH_
       1 = "JAN"
       2 = "FEB"
       3 = "MAR"
   ;
run;

data DT3;
input NO$ MONTH YEN;
format MONTH MONTH_.;
cards;
A 1 5000
A 2 500
A 3 1000
B 1 2000
B 3 1500
;

NOMONTHYEN
AJAN5000
AFEB500
AMAR1000
BJAN2000
BMAR1500

proc transpose data=DT3 out=OUT4;
   var YEN;
   by NO;
   id MONTH;
run;

NO_NAME_JANFEBMAR
AYEN50005001000
BYEN20001500


上の例では、formatをあてたID値「JAN」「FEB」「MAR」を転置後の変数名にしようとします。

format値ではなく、変数値をID値としたい場合、以下のようにformatを除いてあげればOK。

proc transpose data=DT3 out=OUT4;
   var YEN;
   by NO;
   id MONTH;
   format MONTH ;
run;


最後にもうひとつ注意点。
転置後の変数名に割り当てる「ID値」が長すぎる場合、ID値を途中で切った値を転置後の変数名として割り当てます。

ID値を何バイトで切ってしまうのかは、環境やバージョンによって異なると思うので、詳細はリファレンスを参照ください。



2015年1月26日月曜日

ODS PDF等の出力で、ページの向きを途中から縦や横に変える方法


データステップ100万回 「外部ファイルへのODS出力のページ設定、縦横」 で、ページの向きを縦や横に設定する方法が解説されてます。


今回は、出力の途中でページの向きを変える方法を紹介します。

(「ODS PDF」で今回のテクニックが使えるのは、SAS9.3以降です。)



*** Sample Data ;
data DT1;
    do i=1 to 10;
        output;
    end;
run;


*** ページ向きを縦にして、PDFで出力 ;
options orientation=landscape;
ods pdf file="PDFファイルを出力するパス\TEST.pdf";

   proc print data=DT1;
   run;

*** ページ向きを横に変更 ;
options orientation=portrait;
ods pdf;

   proc print data=DT1;
   run;

ods pdf close;


TEST.pdf


上のプログラムで赤字にした箇所がポイントです。

options orientation=portrait;

でページの向きを横向きに変更しています。
これだけでは、変更が反映されないですが、

ods pdf;

を入れると、変更した設定を反映して出力してくれます。


2015年1月22日木曜日

ライブラリ内のSASデータセット名を取得する



方法は色々ありますが、今回はDATASETSプロシジャを使った方法を紹介します。


とりあえず、WORKに入ってるデータセット名を取得してみましょう。
まずサンプルデータから。

*** Sample Datasets ;
data DT1; run;
data DT2; run;
data DT3 / view=DT3; run;


DT1とDT2はデータセット、DT3はSASビューです。WORKにはこの3つのみが存在しているとします。


「データセット名」を取得(取得対象: DT1, DT2)

ods output  members = OUT1;
     proc datasets  lib=WORK  memtype=data;
     quit;
ods output close;



「データセット名」と「SASビュー名」を取得(取得対象: DT1, DT2, DT3

ods output  members = OUT2;
     proc datasets  lib=WORK  memtype=(data view);
     quit;
ods output close;



解説

ods output members = 結果のデータセット ;
     proc datasets  lib=ライブラリ  memtype=取得するデータのタイプ ;
     quit;
ods output close;


データのタイプは、「data」でデータセット、「view」でSASビューを表します。


2015年1月21日水曜日

データセットを縦結合する際、変数名が同じでlengthが異なってたら、最大のlengthに合わせる便利技


長いタイトルですが、SQLのいいとこ取りした便利技の紹介です。


Sample Data

data DT1;
length  A $2.  B $1.;
input A$ B$ ;
cards;
01 a
02 b
02 b
;
run;

data DT2;
length  A $1.  B $2.;
input A$ B$;
cards;
3 cc
4 dd
;
run;

DT1
 B 
  01 
   a    
  02  
   b  
  02
   b  

DT2
 A  
B
   3   
  cc   
   4
  dd  


サンプルデータをまずSETで結合してみます。
data OUT1;
   set DT1 DT2;
run;

ログ
WARNING: 入力データセットによって、変数Bに複数の長さが指定されました。データの切り捨てが発生します。

 B 
  01 
   a   
  02  
   b  
  02
   b  
  3
   c  
  4
   d  

結果を見ると、変数Bの値が、一部文字切れしちゃってますね。


これは、
「set DT1 DT2」 で最初に指定したデータセットDT1にある変数のlengthが、結合後のデータセットに割り当てられるからです。


つまり、変数Bのlengthが、DT1は「$1」、DT2は「$2」と異なっていますが、
上記青字の性質によって、結合時に「$1」が割り当てられます。

なので、DT2から持ってきた変数Bは、length「$1」に合わせて文字を切ってしまいます。


解決法

正攻法として、SETする時にlengthを設定しなおせばokですが、、、

data OUT2;
   length A B $3.;
   set DT1 DT2;
run;



SQLプロシジャ入門14:データセットを縦結合する【UNION】」で紹介した「OUTER UNION CORR」を使えば、
なんと結合するデータセットの中で最大のlengthを勝手に設定してくれます。

proc sql;
   create table OUT3 as
   select * from DT1
      outer union corr
   select * from DT2;
quit;

 B 
  01 
   a   
  02  
   b  
  02
   b  
  3
   cc  
  4
   dd  



ただし、SETステートメントと異なる動きあり。


まず、以下のプログラムと結果を見てみましょう。
*** Sample Data ;
data DT1;
   A = .;
run;

data DT2;
   format A yymmdd10. ;
   A = '01jan2015'd ;
run;

DT1
      A        
         .  

DT2
 A  
 2015-01-01 


*** SETステートメントによる結合 ;
data OUT1;
   set DT1 DT2;
run;

 A 
          .
  2015-01-01 


*** 「OUTER UNION CORR」による結合 ;
proc sql;
   create table OUT2 as
   select * from DT1
      outer union corr
   select * from DT2;
quit;

       A        
           .
      20089 

2つの結合結果を比較してみると、2行目の値が異なってますね。

これは「SETステートメント」の方は、format 「yymmdd10」 が割り当てられていて、
「OUTER UNION CORR」の方は、formatが割り当てられてないというだけで、
値自体は同じものです。


リファレンスから探せなくて確かではないですが、結合したデータセットの format, informat は、
  • 「SETステートメント」: 最初のデータセットDT1にformat, informatが割り当てられてなければ、次のデータセットDT2のformat, informatを割り当てている模様。
  • 「OUTER UNION CORR」: 最初のデータセットDT1のものを割り当てている模様。

他にも差異があるかもしれません。
利用する際はいろいろテストしたり、期待通りの結果になるかご確認ください。


この違いを気にしないシチュエーションであれば、超便利なテクニックですね。

2015年1月19日月曜日

SQLプロシジャ入門14:データセットを縦結合する【UNION】




SQLプロシジャで複数のデータセットを縦結合する方法の紹介です。




サンプルデータ
data DT1;
input A$ B$ ;
cards;
001 aa
002 bb
002 bb
;

data DT2;
input A$ C$;
cards;
002 bb
003 cc
;

DT1
 B 
  001 
  aa   
  002  
  bb  
  002
  bb  

DT2
 A 
C
  002  
  bb   
  003
  cc  




① 「UNION」 と 「UNUION ALL」
proc sql;
   create table DT3 as
   select * from DT1
      union
   select * from DT2;
quit;

DT3
 B 
  001 
  aa   
  002  
  bb  
  003
  cc  

基本構文
SELECT文  union  SELECT文

解説
・SELECT文の結果を縦に結合します。

変数名ではなく変数の順番で結合されます。
(今回の例では、DT1の変数BとDT2の変数Cは同じ2列目にある変数なので、無理矢理BとCを縦にくっつけちゃいます。)

出力データで値が重複してるレコードは、重複分が削除されます
(たとえば、
DT1の 「A="002" and B="bb"」 と、
DT2の 「A="002" and C="bb"」 の3レコードは重複してるので、1レコードだけ残してあとは削除されます。)

重複を削除したくない場合は、「union all」と指定すればok。





② 「UNION CORR」 と 「UNUION CORR ALL」
proc sql;
   create table DT4 as
   select * from DT1
      union corr
   select * from DT2;
quit;

DT4
  001 
  002  
  003

基本構文
SELECT文  union corr  SELECT文

解説
・SELECT文の結果を縦に結合します。

結合時に共通する変数名のみを残します。
(今回の例では、DT1とDT2で共通する変数名はAのみなので、これだけ残る。)

出力データで値が重複してるレコードは、重複分が削除されます
(たとえば、
DT1とDT2の 「A="002"」 の3レコードは重複なので、1レコードだけ残してあとは削除されます。)

重複を削除したくない場合は、「union corr all」と指定すればok。





③ 「OUTER UNION CORR」
proc sql;
   create table DT5 as
   select * from DT1
      outer union corr
   select * from DT2;
quit;

DT5
  A  
  B 
  C  
  001 
  aa  
  002
  bb 

  002
  bb

  002
  
  bb  
  003

  cc



基本構文
SELECT文  outer union corr  SELECT文

解説
・SELECT文の結果を縦に結合します。
・データセット間で共通する変数名同士を結合し、片方にしかない変数も残してくれてます。
重複レコードも削除されません




14.データセットを縦結合する【UNION】
15.NULLの扱いに関する注意点

2015年1月15日木曜日

SASにおける日付,時間,日時の概念について。


まずは以下のプログラムと実行結果を見てみましょう。

data DT1;
  format A yymmdd10.  B time8.  C e8601dt19. ;

  A = 0;
  B = 0;
  C = 0;
  output;

  A = 1;
  B = 1;
  C = 1;
  output;
run;


 A
B
 1960-01-01  
  0:00:00  
 1960-01-01T00:00:00  
 1960-01-02  
  0:00:01  
 1960-01-01T00:00:01  


数値「0」と「1」に対して、日付フォーマット、時刻フォーマット、日時フォーマットを割り当てた結果です。


ここから以下の事が分かります。


「日付値」は 1960年1月1日 を起算として、+1すると1日増える。
「時間値」は 0時間0分0秒 を起算として、+1すると1秒増える。
「日時値」は 1960年1月1日 0時間0分0秒 を起算として、+1すると1秒増える。



それで?って感じですが、これを知ってると、日付計算が楽になります。
例えば、ある日時変数があって、その日時の10分後を計算したいとかって場合に、

   「日時変数 + (10分×60秒)」

と計算すれば良いことがわかります。


関数で日時操作する方法もあるので、参考まで。
日付値と時間値から、日時値を作る小技。
日時値に、時間や日を足す簡単な方法。

2015年1月12日月曜日

ODS PDF等の出力で、ページ内折り返しをする節約テクニック


ODS PDF や ODS RTFでファイル出力する際、個人的に使ってる節約テクニックを紹介。

まずは以下のとおり、PROC PRINTの結果をPDFファイルに出力してみます。

* Sample Data ;
data DT1;
  do i=1 to 50;
     output;
  end;
run;


* PDFファイルに出力 ;
ods pdf  file="出力するパス\TEST.pdf";

   proc print data=DT1;
   run;

ods pdf close;


TEST.pdf

結果を見ると、2ページにまたがって出力されてますが、余白がいっぱいあってページの使い方が勿体ないなぁって感じです。
そこで、、


* PDFファイルに出力 ;
ods pdf  file="出力するパス\TEST2.pdf" columns=2;

   proc print data=DT1;
   run;

ods pdf close;


TEST2.pdf


1ページに収まってスッキリしました!
columns=」で、ページ内を折り返す数を指定してます。
出力先によっては挙動の違いや制限などもあるようなのでリファレンスも参照ください。


これは、見栄えだけでなく、
印刷時に、紙や印刷時間の節約にもなりますね。


2015年1月9日金曜日

日時値に、時間や日を足す簡単な方法。




ある日時の、2時間後、1日後、5秒前とかをDHMS関数で簡単に求める方法を発見したので紹介します。
日付値と時間値から、日時値を作る小技。」を発展させたやり方なので、まずはそちらをご覧ください。




書き方


第4引数に元となる日時をいれて、あとは足したい日数,時間,分を第1引数から順番に指定すればok。
DHMS( 日数 , 時間 ,  , 日時  )


秒も足したい場合は、第4引数に「日時+秒」みたいな感じで書けばいけます。
DHMS( 日数 ,  ,  , 日時 + 秒  )






data DT1;
  format A B C D E e8601dt19.;

  A = input( '2001-02-10T10:20:30', e8601dt19. );

  * 2時間後 ;
  B = dhms( 0, 2, 0, A );

  * 2時間前 ;
  C = dhms( 0, -2, 0, A );

  * 1日と2時間後 ;
  D = dhms( 1, 2, 0, A );

  * 2時間5秒後 ;
  E = dhms( 0, 2, 0, A+5 );
run;


結果
A = 2001-02-10T10:20:30 元の日時値

B = 2001-02-10T12:20:30 2時間後
C = 2001-02-10T08:20:30 2時間前
D = 2001-02-11T12:20:30 1日と2時間後
E = 2001-02-10T12:20:35 2時間5秒後





2015年1月5日月曜日

1年分のカレンダーを作るSASプログラム


本年も、どうぞよろしくお願い致します!!


年の始まりということで、以前紹介した「RWIでカレンダーを作ってみる。」を改良して、1年分のカレンダーを作るプログラムを2パターン作りました。
祝日を色分けする処理も加えてます。


(実行する場合は、最後のほうに書いた補足の通り、プログラムを直してください)


(2020/12/26 出力結果のタイトル文字を大きくするなど、プログラムを改良)


まずは、以下の通り、その年の祝日をデータセット化しておきます。
「祝日一覧」とか適当にググると調べられます。

*** 2015年の祝日をデータセット化 ;
data HOLIDAY;
input  i :yymmdd10. HOLIDAY :$30.;
cards;
2015/1/1 元日
2015/1/12 成人の日
2015/2/11 建国記念の日
2015/3/21 春分の日
2015/4/29 昭和の日
2015/5/3 憲法記念日
2015/5/4 みどりの日
2015/5/5 こどもの日
2015/5/6 振替休日
2015/7/20 海の日
2015/9/21 敬老の日
2015/9/22 国民の休日
2015/9/23 秋分の日
2015/10/12 体育の日
2015/11/3 文化の日
2015/11/23 勤労感謝の日
2015/12/23 天皇誕生日
;


カレンダーのパターン①



*** 年を指定 ;
%let YEAR = 2015;


*** 用紙の向きを横に ;
options orientation = landscape;


title height=20pt "SAS Calendar ( &YEAR )";
ods pdf file="出力するパス\calendar1.pdf" notoc;


*** RWIでカレンダーを作る ;
data _NULL_;

  * 祝日一覧をハッシュオブジェクトに格納 ;
  length HOLIDAY $20.;
  dcl hash hs( dataset:"HOLIDAY" );
  hs.definekey( "i" );
  hs.definedata( "HOLIDAY" );
  hs.definedone();


  * 月ごとにループ ;
  do MONTH = 1 to 12;

     * 月の開始日と終了日を取得 ;
     STDT = mdy(MONTH, 1, &YEAR);
     ENDT = intnx("month", STDT, 0, "end");

     * カレンダー作成開始 ;
     dcl odsout ob();
     ob.table_start();

         * 月を出力 ;
         ob.head_start();
         ob.row_start();

              ob.format_cell( data: put(STDT,monname20.), column_span:7,
                                     style_attr: "background=midnightblue  color=white
                                                      fontsize=10  fontweight=bold  just=left");
         ob.row_end();

         * 曜日を出力 ;
         ob.row_start();
              do i = 1 to 7;
                  ob.format_cell( data: char("SMTWTFS",i) ,
                                        style_attr: "background=skyblue color=white fontsize=5") ;
              end;
         ob.row_end();
         ob.head_end();

         * 日付を出力 ;
         ob.row_start();
              do i = STDT to ENDT;

                  * 初日が日曜以外であれば空白を埋める ;
                  if i=STDT then do i2=1 to weekday(STDT)-1;
                      ob.format_cell();
                  end;

                  * 日付を出力 (文字色も設定) ;
                  HOLIDAY="";

                  *--- 平日は黒 ;
                  COLOR="BLACK";

                  *--- 日曜と祝日は赤 ;
                  if weekday( i )=1 or hs.find()=0 then COLOR="RED";

                  *--- 土曜は青 ;
                  else if weekday( i )=7 then COLOR="BLUE";

                  ob.format_cell( data:cats( day( i ), "#", HOLIDAY ) ,split:"#",
                                         style_attr:"height=2.7cm width=3.5cm fontsize=5 color="||COLOR);


                  * 土曜日であれば折り返す ;
                  if weekday( i )=7 and i^=ENDT then do;
                       ob.row_end();
                       ob.row_start();
                  end;

              end;
         ob.row_end();

      ob.table_end();

      * 月ごとに改ページ ;
      if MONTH^=12 then ob.page();

  end;
run;


ods pdf close;




カレンダーのパターン②




*** 年を指定 ;
%let YEAR = 2015;


*** 用紙の向きを縦に ;
options orientation = portrait;


title height=20pt "SAS Calendar ( &YEAR )";
ods pdf file="出力するパス\calendar2.pdf" notoc;


*** RWIでカレンダーを作る ;
data _NULL_;

  * 祝日一覧をハッシュオブジェクトに格納 ;
  length HOLIDAY $20.;
  dcl hash hs( dataset:"HOLIDAY" );
  hs.definekey( "i" );
  hs.definedata( "HOLIDAY" );
  hs.definedone();

  * カレンダー作成開始 ;
  dcl odsout ob();
  ob.layout_gridded(columns:3,rows:4);

  * 月ごとにループ ;
  do MONTH = 1 to 12;

      * 月の開始日と終了日を取得 ;
      STDT = mdy(MONTH, 1, &YEAR);
      ENDT = intnx("month", STDT, 0, "end");

      ob.region();
      ob.table_start();

         * 月を出力 ;
         ob.head_start();
         ob.row_start();

              ob.format_cell( data: put(STDT,monname3.), column_span:7,
                                     style_attr:"background=midnightblue  color=white 
                                                     fontsize=3  fontweight=bold  just=left");
         ob.row_end();

         * 曜日を出力 ;
         ob.row_start();
              do i = 1 to 7;
                  ob.format_cell( data: char("SMTWTFS",i),
                                        style_attr: "background=lightsteelblue color=white");
              end;
         ob.row_end();
         ob.head_end();

         * 日付を出力 ;
         ob.row_start();
              do i = STDT to ENDT;

                  * 初日が日曜以外であれば空白を埋める ;
                  if i=STDT then do i2=1 to weekday(STDT)-1;
                      ob.format_cell();
                  end;


                  * 日付を出力 (文字色も設定) ;
                  HOLIDAY="";

                  *--- 平日は黒 ;
                  COLOR="BLACK";

                  *--- 日曜と祝日は赤 ;
                  if weekday( i )=1 or hs.find()=0 then COLOR="RED";

                  *--- 土曜は青 ;
                  else if weekday( i )=7 then COLOR="BLUE";

                  ob.format_cell( data: day( i ), style_attr:"color="||COLOR );


                  * 土曜日であれば折り返す ;
                  if weekday( i )=7 and i^=ENDT then do;
                       ob.row_end();
                       ob.row_start();
                  end;

             end;
         ob.row_end();
      ob.table_end();

  end;
  ob.layout_end();
run;


ods pdf close;




補足


・SAS9.4より前のバージョンでは、format_cellメソッドの「style_attr」を「overrides」に置き換える。


・プログラムの最初らへんの「ods pdf」部分を、各自保存したいパスに変えてください。
ods pdf file="結果を保存するパス\ファイル名.pdf";


・ 出力する年を変えたい場合は、プログラム先頭の以下の箇所を変えてあげる。

*** 年月を指定 ;
%let YEAR = 2015;

また、その年の祝日にあわせてデータセットHOLIDAYを更新してください。