SASマクロについてメモ

Ways to Store Macro Source Codes and How to Retrieve Themよりメモ。
作成したSASマクロを、%INCLUDE、AUTOCALL LIBRARY、STORED MACROの4つの方法について書かれています。
ざっと読んだ限り、やはりSTORED MACROが最強のようです。
それに反して、AUTOCALL LIBRARYの方法は、デメリットしか書かれていないみたいです。

Disadvantage:
• The macro is compiled only the first time it is used in a SAS® session (any further calls will work with the
compiled code). If any changes are made, it must be stored again before the changes take effect and a new
SAS® session begins. Thus, if you are working on macro modifications, this is a hindrance. The complied
original macro code still exists in the catalog WORK.SASMACR until the end of the SAS® session.
• Any macro language open code outside of the macro definition in the saved macro file will not be reexecuted
with additional macro calls made within a SAS® session.

あまり使い道がないんでしょうかね。
STORED MACROでDESオプションを使うと、そのマクロの説明が入れられるんですね。
知りませんでした。

%macro macro_name (parameters) / STORE SOURCE DES=”description”;
 macro program code
%mend macro_name;

各オプションの説明

・STORE option = stores the compiled macro in a SAS® catalog in a permanent SAS® data library.
・SOURCE option = stores the macro source code along with the compiled code (new in SAS® version 9.1). The
SOURCE option requires that the STORE option and MSTORED option be set. The code saved by SOURCE
option begins with the %MACRO and ends with %MEND statement.
・DES option = specifies a description for the macro entry in the macro catalog. It must be in quotation marks. The
description appears in the CATALOG window when you display the contents of the catalog containing the stored
compiled macro facility.

マクロカタログを実行せずに参照する方法

Technical Support:マクロカタログからマクロのソースコードを参照するよりメモ
マクロカタログを実行せずに参照する方法。
SAS Ver9以降なら下記の方法でできるそうです。

ストアドコンパイルマクロのサンプル
SASのサンプルでは「C:\temp」になっていたのを「C:\test」に変更しました。

LIBNAME maclib 'C:\test';
OPTIONS MSTORED SASMSTORE=maclib;

%MACRO sample /STORE SOURCE;
DATA class;
SET sashelp.class;
RUN;
%MEND;

%COPYステートメントでSOURCEオプションを指定すれば参照できるようです。

LIBNAME maclib 'C:\test';
OPTIONS MSTORED SASMSTORE=maclib;
/* ソースコードSASログに表示 */
%COPY sample /SOURCE;
/* ソースコードをテキストファイルに保存 */
%COPY sample /SOURCE OUT='C:\temp\sample.sas';

個人的には、テキストファイルに保存する必要がないので

%COPY sample /SOURCE OUT='C:\temp\sample.sas';

この一行を削除して実行しました。

  LIBNAME maclib 'C:\test';
    OPTIONS MSTORED SASMSTORE=maclib;
                        /* ソースコードをSASログに表示 */
    %COPY sample /SOURCE;

実際に実行してみると、確かにログに出力されました。

1    LIBNAME maclib 'C:\test';
NOTE: ライブラリ参照名 MACLIB を次のように割り当てました。
       エンジン : V9
       物理名 : C:\test
2        OPTIONS MSTORED SASMSTORE=maclib;
3                            /* ソースコードをSASログに表示 */
4        %COPY sample /SOURCE;
%MACRO sample /STORE SOURCE;
      DATA class;
        SET sashelp.class;
      RUN;
    %MEND;

MERGE:8つの方法

Merging Data Eight Different Waysより

データセットをマージする8つの方法が紹介されています。
以下のPATDATAとADVERSEの2つのデータセットをSUBJECTでマージし、
かつADVERSEのみ存在するデータを残したものをALLDATA0に格納するやり方を
例としています。

Dataset: PATDATA
	SUBJECT TRT_CODE
	124263 A
	124264 A
	124265 B
	124266 B
	
Dataset: ADVERSE
	SUBJECT EVENT
	124263 HEADACHE
	124266 FEVER
	124266 NAUSEA
	124267 FRACTURE

方法(1)DATAステップ内でMEREGEを使う方法。
一番ポピュラーなやりかたですね。

DATA alldata0;
	MERGE adverse (in=a)
	patdata (in=b);
	BY subject;
	IF a;
RUN;

(2)PROC SQLでJOIN句で連結する方法です。

PROC SQL;
	CREATE TABLE alldata0 AS
		SELECT a.*, b.trt_code
		FROM adverse a
		LEFT JOIN
		patdata b
		ON a.subject=b.subject;
	QUIT;
RUN;

(3)SETステートメントでKEYオプションを使う方法。
こんなやり方もあったんですね。知りませんでした。

DATA alldata0;
	SET adverse;
	SET patdata KEY=subject /UNIQUE;
	DO;
		IF _IORC_ THEN DO;
			_ERROR_=0;
			trt_code='';
		END;
	END;
RUN;

(4)PATDATAから文字フォーマットを作り出して、それをADVERSEデータセットに当てる方法。

DATA fmt;
	RETAIN fmtname 'TRT_FMT' type 'C';
	SET patdata;
	RENAME subject=start trt_code=label;
RUN;

PROC FORMAT CNTLIN=fmt;
RUN;

DATA alldata0;
	SET adverse;
	ATTRIB trt_code LENGTH=$1 LABEL='Treatment Code';
	trt_code=PUT(subject,$trt_fmt.);
RUN;

(5)PATDATAからハッシュテーブルを作って、ADVERSEデータセットにヒット差せる方法。
ただしSAS9.1以降でないとできないそうです。

DATA alldata0;
	IF _n_=0 THEN SET patdata;
	IF _n_=1 THEN DO;
	DECLARE HASH _h1
		(dataset: "PATDATA");
		rc=_h1.definekey("SUBJECT");
		rc=_h1.definedata("TRT_CODE");
		rc=_h1.definedone();
		call missing(SUBJECT,TRT_CODE);
	END;
	SET adverse;
	rc=_h1.find();
	IF rc^=0 THEN trt_code=" ";
	DROP rc;;
RUN;

(6)配列を使う方法。
ここまでするんだったら、個人的には普通にMERGEかPROC SQLを使った方が良い気がしますけど・・・

DATA _null_;
	SET sashelp.vtable;
	WHERE libname='WORK';
	WHERE ALSO memname in('PATDATA','ADVERSE');
	CALL SYMPUT('X'||memname,put(nobs,8.));
RUN;

DATA alldata0;
	LENGTH trt_code $1;
	ARRAY f{&xpatdata.,2} $6 _TEMPORARY_;
	DO i=1 TO &xpatdata.;
		SET patdata (RENAME=(trt_code=trt_code_dict));
		f{i,1}=PUT(subject,6.);
		f{i,2}=trt_code_dict;
	END;
	DO i=1 TO &xadverse.;
		SET adverse;
		trt_code='';
		DO j=1 TO &xpatdata.;
			IF subject=INPUT(f(j,1),best.) THEN DO;
				trt_code=f{j,2};
				OUTPUT;
			END;
			IF ^MISSING(trt_code) THEN LEAVE;
		END;
		IF MISSING(trt_code) THEN OUTPUT;
	END;
	DROP i j trt_code_dict;
RUN;

(7)MODIFYステートメントを使う方法
なんだかかえって何をしているのかわかりづらくなってしまう気がします。

DATA adverse;
	DO p = 1 TO totobs;
		_IORC_ = 0;
		SET patdata POINT=p NOBS=totobs;
		DO WHILE(_IORC_=%SYSRC(_SOK));
			MODIFY adverse KEY=subject;
			SELECT (_IORC_);
				WHEN (%SYSRC(_SOK)) DO; /*MATCH FOUND*/
					SET patdata POINT=p; trtc=trtcode; REPLACE;
				END;
				WHEN (%SYSRC(_DSENOM)) _ERROR_ = 0; /*NO MATCH FOUND*/
				END;
				OTHERWISE DO; /*A MAJOR PROBLEM SOMEWHERE*/
					PUT 'ERROR: _IORC_ = ' _IORC_ / 'PROGRAM HALTED.';
					_ERROR_ = 0; STOP;
				END;
			END;
		END;
	END;
	STOP;
RUN;

(8)最後はCALL EXECUTEを使う方法

DATA _null_;
	SET patdata;
	CALL EXECUTE("DATA alldat;"||
	" SET adverse;"||
	" WHERE subject='"||STRIP(subject)||"';"||
	" trt_code='"||STRIP(trt_code)||"';"||
	"PROC APPEND BASE=alldata0 DATA=dat0 FORCE;"||
	"RUN;");;
RUN;

PROC PRINTで一定データ数毎に空行を挿入する

http://support.sas.com/kb/31/366.html/よりメモ。
PROC PRINTで空行を挿入するサンプルが紹介されています。
この例では5オブザベーション毎に空行を挿入しています。

/* Insert blank observation after every 5 observations */
data class_blanks;
set sashelp.class;
output; /* Output real observation */
if mod(_n_,5)=0;
array allnums {*} _numeric_ ;
array allchar {*} _character_ ;
drop i;
do i=1 to dim(allnums); allnums{i}=.; end;
do i=1 to dim(allchar); allchar{i}=' '; end;
output; /* Output blank observation */
run;
options missing=' '; /* Display numeric missing as blank */
proc print data=class_blanks noobs;
title 'SASHELP.CLASS with Blank Line After Every 5 Obs';
run;

SAS Ver9.2では、上記のようなことをしなくてもblanklineオプションを使えばすむそうです。

/* The BLANKLINE option accomplishes the same result */
proc print data=sashelp.class blankline=5;
title 'SASHELP.CLASS with Blank Line After Every 5 Obs';
run;

ひとつのデータセットからグループ毎にデータセットを新たに生成する

http://support.sas.com/kb/26/140.html/よりメモ。
ひとつのデータセットの変数の値を見て、変数のグループごとにデータセットを作る方法のようです。
サンプルでは変数colorの値を参照して、その値をグルーピングし、
かつそのグループごと(blue,green,red)にデータセット作ります。

/* Example 1 - Use macro logic to create a new data set for each BY-Group in */
/*             an existing data set.                                         */
/*             an existing data set.                                         */

/* Create sample data */

data test;
  input color $ num;
datalines;
blue 1
blue 2
blue 3
green 4
green 5
red 6
red 7
red 8
;

/* Create a new macro variable, VARn, for each BY-Group and a */
/* counter of the number of new macro variables created.      */

data _null_;
  set test end=eof;
  by color;
  /* On the first member of the BY-Group, create a new macro variable VARn */ 
  /* and increment the counter FLAG.                                       */
  if first.color then do;
   flag+1;
   call symput('var'||put(flag,8. -L),color);
  end;
  /* On the last observation of the data set, create a macro variable to */
  /* contain the final value of FLAG.                                    */
  if eof then call symput('tot',put(flag,8. -L));
run;

/* Create a macro to generate the new data sets. Dynamically produce data set names */
/* on the DATA statement, using subsetting criteria to create the new data sets     */
/* based upon the value of the BY variable.                                         */                                          

%macro groups(dsn,byvar);
  data %do i=1 %to &tot;
         &&var&i
       %end;;
    set &dsn;
      %do i=1 %to &tot;
        if &byvar="&&var&i" then output &&var&i;
      %end;
  run;
%mend groups;

/* Call the macro GROUPS.  Specify the name of the data set to be split */
/* in the first macro parameter and the name of the BY variable in the  */
/* second parameter.                                                    */

%groups(test,color)


proc print data=blue;
  title 'Blue';
run;

proc print data=green;
  title 'Green';
run;

proc print data=red;
  title 'Red';
run;

もうひとつの方法。
こちらはデータステップ内にCALL EXECUTEをかませたパターン。
結果は、上のExample1と同じです。

/* Example 2 - Use CALL EXECUTE to pass a parameter to a macro in order to */
/*             create a new data set for each BY-Group in an existing data */
/*             set.  The output is identical to the output created by      */
/*             Example 1 above.                                            */

/* Compile the macro BREAK.  The parameter BYVAL will be generated below in */
/* the CALL EXECUTE.                                                        */

%macro break(byval);                                                                                                                                  
   data &byval;                                                             
      set test(where=(color="&byval"));                              
   run;                                                                                                                                                 
%mend;                                                                      
                                                                   
/* Use the same TEST data set created for Example 1. */ 
                                                                                                                                                     
data _null_;                                                                
  set test;                                                               
  by color;                                                                  
  if first.color then 
    call execute('%break('||trim(color)||')');                
           
run;