マクロの%doループはループ値にリスト形式の離散値は指定できないので、工夫しようの話

例えば適当なマクロがあったとして

%macro a(no);
data DS&no;
a=&no;
run;
%mend a;

このマクロaのパラメータを1から7で実行したいと思えば

%macro tloop;
%do i = 1 to 7 ;
%a( &i )
%end;
%mend tloop;

として、

%tloop

とすればできます。

ではパラメータが1 3 5 7のように非連続であればどうしましょう?

%macro _tloop;
%do i = 1 , 3 , 5 , 7 ;
%a( &i )
%end;
%mend _tloop;

%_tloop


とかって書きたいところですが、こいつは漏れなくエラーです。

通常のデータステップのdoステートメントでは

do i = 1 , 3, 5, 7;end; のような非連続値をリスト形式で指定して回せますが
マクロの%do はできません。基本的には別物だと考えた方がいいでしょう。


でも、どうしても、非連続のループがやりたいのって場合はどうするか。
ぱっと思いつく範囲で書いてみました。


まずパターン1として、今回はパラメータを変えながらマクロを実行したいだけなので
ループ用マクロを作らずに通常のデータステップの、do ループで値を遷移させて
call executeしちゃうパターン

data _null_;
do i = 1 , 3 , 5 , 7;
call execute( cats( '%a(' , i ,')' ));
end;
run;


つづいてパターン2 として minoperator オプションを使って
マクロ内でのin演算子を有効にし、ループ変数の最小最大値でループして
inでターゲットの非連続値の値が来た時のみ実行するパターン

%macro loop;
options minoperator;
%do i = 1 %to 7;
%if &i in 1 3 5 7 %then %a(&i);
%end;
options nominoperator;
%mend loop;

%loop



パターン3はインデックス値をマクロ変数で与えて%scanで代入していくパターン

%macro loop2 ;
 %let loopval =1 3 5 7; 
   %let i = 1 ;                 
   %do %until(%scan(&loopval,&i) eq) ;
      %let j = %scan(&loopval,&i);
     %a(&j)
      %let i = %eval(&i+1) ;
   %end ;                       
%mend loop2;                

%loop2

パターン4 はパターン3と発想は同じで、よりシンプルな書き方

%macro loop3;
%let loopval=1 3 5 7;
%do i=1 %to %sysfunc(countw(&loopval));
%let j=%scan(&loopval,&i);
   %a(&j)
%end;
%mend;

%loop3;


パターン2以外は、多分ループ用に与えたい値が文字であっても
そのまま応用できると思います。

また、多分、他にもいろいろ書けるとは思います

univariateプロシジャで任意のパーセント点を出力する話

適当なテストデータをつくります

data Q1;
call streaminit(2015);
do i= 1 to 100;
X = int(rand('uniform')*100);
output;
end;
run;

パーセント点を算出してデータセットに出力したい場合、
univariateプロシジャを利用して、以下のようにかけます

proc univariate data = Q1 noprint ;
var X ;
output out = A1
p1    =p1
p5    =p5
p10   =p10
q1    =p25
median=p50
q3    =p75
p90   =p90
p95   =p95
p99   =p99
;
run ;





1%点はp1 5%点はp5 といったようにキーワードが決まっていて
結果を格納したい変数を右辺に書くわけです。
q1は25%点、q3は75%点、medianは50%点のことです。

ただ、例えば35%点を算出したいと思って p35= と書いてもエラーになります。
上のコードで指定したポイント以外は、統計量キーワードとして登録されていません。

しかし、じゃあできないと言うわけではなく、そういった場合は
pctlpts= と pctlpre = をセットで使うことで可能になります。

まずpctlpts=で、出したいパーセント点を指定します。
そしてpctlpre=で格納する変数名の接頭部分を指定します。

pctlpts=35 pctlpre = p とするとできる変数名はp35
pctlpts=55 pctlpre = XX とするとできる変数名はXX55
みたいな感じです。

実際に

proc univariate data = Q1 ;
var X ;
output out = A2
       pctlpts  = 35
       pctlpre = p ;
run ;





という感じです。

pctlpts=の指定は、複数指定もできて、例えば以下のように
かけば、0から100まで5%刻みで出力したりできます。

proc univariate data = Q1 noprint ;
var X ;
output out = A3
       pctlpts  = 0 to 100 by 5
       pctlpre = p ;
run ;

画像は途中までです






複数変数の場合は

proc univariate data = Q1 noprint ;
var X i ;
output out = A4
       pctlpts  = 35
       pctlpre = PX_ Pi_ ;
run ;






のようにも書けます。



nomreplaceオプションでマクロの上書きをブロックする方法

以下の2つのコードを実行します

%macro mm;
%put AAAA ;
%mend mm;

%macro mm;
%put BBBB ;
%mend mm;

そして


%mm

とマクロを実行したとき、ログにでるのはAAAA BBBBどっちでしょうか?
はい、BBBBです。そりゃそうですね。
%macroステートメントで、既にマクロカタログに存在している(コンパイル済)マクロ名を指定した場合
マクロの内容は上書きされます。

さて、それはいいんですが、そのことで怖い思いをした経験はあるでしょうか?
要するに、うっかり別の処理を同じマクロ名で書いてしまって上書きしてしまい、実行時に
意図していない処理が行われたという感じの体験です。
僕はあります。

基本、自分で書いた1つのプログラム内であれば、よほど疲れてない限り、同じマクロ名使っちゃうことは
少ないと思います。

ただ、例えば%include等を使えば、他のプログラムのマクロを実行できるわけですが
大規模な仕事になってくると、それが結構な量になりがちです。

しかも他人の書いたプログラムと共有する場合、なにか管理する術を考えないと、高確率で
バッティングします。

データを読み込むマクロに名前をつけろと言われたら
%makedataとか%datainとか%dataimpとか、似たりよったりな命名になりがちです。

また、1人でやっててもロジカルチェックとかで各チェックをマクロにしてる場合とか、途中の仕様変更で
チェック番号の全振り直しとかふざけてたことが起きたりして、やっちゃたりします。


さて、そういった時、実行したコードでマクロ名がバッティングして上書きが起きていないかを
確かめるにはどうすればいいでしょう。

options nomreplace;

としてから、実行してみましょう。


%macro mm;
%put AAAA ;
%mend mm;

とすると
ERROR: マクロ MM はコンパイルされません。 NOMREPLACE
       オプションが設定されています。
       ソースコードは、 %MEND
       ステートメントが検出されるまで取り除かれます。

とエラーがでてきます。

これがでるということはその時点で、指定したマクロ名は既にコンパイル済の状態ということです。
おしまい。

ちなみに

options mreplace;

で元の状態に戻ります。

SASの「プログラムを開く」をした時の初期フォルダパスを変更する方法

かなりニッチな小技ですが、たまたま困っている方がいたので、その方に捧げます。

SASの画面で、「ファイル」→「プログラムを開く」とするとダイアログが立ち上がりますが
その際の「ファイルの場所」のデフォルトのパスをどうやって変えるかという話です。

ちなみにWindows環境であれば、デフォルトは恐らくSASインスト時に
MyDocument下に作成される「My SAS Files」フォルダ下のSASのバージョン番号フォルダではないかと思います。

方法は以下の通りです

SASのコンフィングファイル「SASV9.CFG」に事前に記述を追加します。
パスは環境や設定によってことなりますがwindows環境で言語日本語にしているならなら
(C:\Program Files\SAS\SASFoundation\9.2\nls\ja)等に存在するのがデフォルトでしょうか?

ちなみにコンフィングファイルいじるのに慣れている変態系の人は別にいいんですけど
まだおっかなびっくりな方は、余計なところを変えないように注意すると共に必ず変更前に
ファイルをコピーしてバックアップしておきましょう。

コンフィングファイルをメモ帳でもなんでもいいのでテキストエディタで開いて、
いちばんお尻とかでいいので

-SASINITIALFOLDER "任意のフォルダパス"

と書き足して保存すればOKです。

今後SASを新しく立ち上げたときから設定が有効化されているはずです。




SQLでフォーマットとかラベルとかを消す場合のちょっと変わった書き方の話とか

脈絡のない話をいくつか。


最近、SAS友のコードレビューしていて、ん??と思ったのが

proc sql noprint;
create table B as
select X + 0 as X
      ,Y||'' as Y
from A;
quit;

っていうコード(ほんとはもっと処理が入りますが、単純化しています)。

意図わかります?一瞬、なにがしたいんだ?ってなりません?

実はこれ、SQLでデータ元の変数をそのまま参照したいけど、それに定義されているフォーマットやラベルは
初期化したいという意図があるんですね。

ためしに

data A;
X=1;Y='A';
label X='ラベルX' Y='ラベルY';
format X yymmdds10.;
run;








としてから、上のコードを実行すると確かにラベルとフォーマットが消えていることが確認できます。







もちろん

proc sql noprint;
create table _B as
select X format=best. label=''
      ,Y label=''
from A;
quit;

こうしてもいいんだけど、そうやって書き連ねるより楽だし、このことを知っていれば、初期化してるんだって
わかりやすいですね。

ちなみにデータステップで

data C;
set A;
X=X+0;
Y=Y||'';
run;

としてもXとYは元の変数を引き継いでしまうんですね。盲点といえば盲点かも。


ちなみにちなみに、もしデータセットを新規に作らずにフォーマットやラベルを変更する場合、よく
proc datasets;でmodifyを使うと思いますが、

SQLならalter文で書けます。(あんまりSASでalter文を書く必然性がなかったから書いたことないけど)

proc sql noprint;
alter table A 
modify X format=best. label='';
quit;


続いての話題、SAS忘備録のSQL入門がシリーズ15回までいって、とりあえず終了予定だそうです
このシリーズは必読物です。勉強になるし、わかりやすい!特にSASで初めてSQLに触れる方は必読です。
で、最後に僕のブログの書籍紹介にリンクを貼っていただいていているのですが、そこに1冊追加です

「プログラマのためのSQL 第4版 すべてを知り尽くしたいあなたに」
Joe Celko (著), ミック (翻訳, 監修)
出版社: 翔泳社; 2013/5/23

タイトルにグッときたら買いでしょう。それが全てを物語っています。

あと、日本のSQLをひっぱるミックさんがまた最近本をだされて
「SQL実践入門──高速でわかりやすいクエリの書き方」
っていうのを、今読もうとしています。

はい、それだけです。


続いての話題、
SASプログラマーなら必ず一度はお世話になっているはずのサイト「SAS統計データ解析入門
ですが、そちらの管理人さんが海外赴任される等の諸事情で、サイトを更新していきたいのに中々できなくてジレンマを感じている。できればSASの普及に意欲のある方でサイトを引き継ぎたいという方がいればお願いしたいというご相談を受けました。
最初、僕が引き継がせていただこうかとも思ったのですが、まあ、僕のブログは知っての通り、一部のマニア向けなもので、僕はそういうのしか書けないことを見抜かれて、やんわり断っていただきました。

なので、「SAS統計データ解析入門」のテイストを愛しつつ、ぜひ後を継ぎたいという方は、サイトの掲載のメールアドレスか、僕の方にご連絡ください。

続いての話題、
今年もSASユーザー総会の季節がやってきます

2日間にわたって行われるSASユーザーのお祭りで、参加費も安い!
一般:5,400円(税込)
教育機関/公的研究機関:2,700円(税込)
学生:1,100円(税込)

しかし、さらにお得に、参加費を無料にして、論文集もタダで貰え
懇親会でタダ飯を食べることができる裏ワザがあります。

そうです、自分で論文かプレゼンテーション、ポスター・セッションを投稿して発表すればいいんです!
僕も恐らくまた投稿すると思います。
毎年常連の方の発表もいいんですが、やっぱり色んな分野の新しい人の発表を聞いてみたいです。

以上、脈絡のない話でした。


%superqとかのあんまり面白くないマクロのクォートの話

マクロ絡みの話です。
&とか%とか;とか、そういうSASにとって意味のある記号が、含まれる際に
そいつらをただの文字として扱いたい場合にマクロ引用符関数といったものを使います。
ほっとくと勝手に展開しようとしたり、変なとこで区切って解釈されてエラーになりますからね。
いわゆるクォート処理です。

(SASmemo - マクロ関数、自動マクロ変数一覧)
http://www50.atwiki.jp/sasmemox/pages/56.html

(Welcome to データ分析・マイニングの世界 by SAS)
http://wikiwiki.jp/cattail/?Base%20SAS%A5%DE%A5%AF%A5%ED

ただ、マクロのクォート周りはあんまり得意じゃなくて
正直きっちりと理解できてません。

なので、あんまり僕の説明はあてにしないでください。

さて、以下のようなデータがあるとします。
どうでもいい話ですが、世の中には結構「&」の入った会社名って多いんですよね
SASプログラマーにはつらい話です。

data Q1;
X='A&A';Y=1;Z=3;output;
X='B&B';Y=2;Z=2;output;
X='C&C';Y=3;Z=1;output;
run;

そこで、マクロ変数に値を入れて、それによって抽出を行うコードを考えてみましょう

%let MA = B&B ;

data E1;
 set Q1;
 where X = "&MA" ;
run;

まあ、できたデータセットE1の中をみると一応「B&B」で抽出できてるんですが
ログに
WARNING: 記号参照 B を展開していません。」というメッセージがでています。
これは%let MA = B&B ;の時点で「&B」の部分を「B」というマクロ変数と解釈して
それを展開しようとしたけどBというマクロ変数がなかったので展開できずに、B&Bという文字列のまま
マクロ変数MAに入れましたという意味です。

今回はBというマクロ変数がなかったので、WARNINGがうざいなってだけで済みますが、もし
あった場合は展開されて、本来予期しない結果になってしまいます。

それを防ぐにはまず。

%let MA = %nrstr(B&B);

data A1;

 set Q1;
 where X = "&MA" ;
run;

こういうアプローチがとれます。%nrstrで「&」をただの文字扱いにしています。


では、マクロ変数の生成が%letではなく、call symput(x)の場合はどうでしょうか?

例えば

data _null_;
call symputx('MA','B&B');
run;

data E2;

set Q1;
where X = "&MA" ;
run;

とすると先程と同じようにWARNINGがでます。


これもまあ、

data _null_;
call symputx('MA','%nrstr(B&B)');
run;

とすれば一応解決です。


ただ、これまで見てきたのはマクロ変数に値を格納する際にクォート関数で
包んで一緒に入れてやることで、展開時に文字とみなしてやろうぜという発想です。

次は、そうではなくて、マクロ変数定義は以下のようにクォート関数なしで
そのまま格納し

data _null_;
call symputx('MA','B&B');
run;

展開時になんとかして、文字として解釈させようぜという発想のコードです。

その場合、以下のように書けます。

data A2;
set Q1;
where X = "%superq(MA)" ;
run;

superq関数はとても説明しにくいので、人の言葉を借ります

引数に指定されたマクロ変数の値に対してマクロプロセッサが展開を行わないようにした上で、その値に含まれる特殊文字をクォート」(SASmemo)
引数に与える文字列をマクロ変数名とみなし、1回だけ展開した値を返す」(Welcome to データ分析・マイニングの世界 by SAS)
The %SUPERQ function locates the macro variable named in its argument and quotes the value of that macro variable without permitting any resolution to occur.(SAS(R) 9.2 Macro Language: Reference)


或いは以前、紹介したsymget関数も、この場合は使えます。こいつはマクロ変数を展開した文字値を返すSAS関数です。

SYMGET関数でマクロ変数の値を取得する
http://sas-tumesas.blogspot.jp/2013/12/symget.html

data A3;
set Q1;
where X = symget('MA') ;
run;

でOKです。

ただし、symget関数はあくまでただのSAS関数として値を返すものですので


例えば以下のようにマクロ変数に抽出式を入れて

data _null_;
call symputx('wh','Y=1&Z=3');
run;

展開する場合

data A4;
set Q1;
where %superq(wh);
run;

だと正しい結果になりますが


data E3;
set Q1;
where symget('wh') ;
run;

だと、3レコードとも抽出されます。
これは結構不思議かもしれませんが

symget('wh')の結果返される値がnullかどうかという真偽値によって
whereが働いてしまうからです。

つまりsymget('wh')が返すのはこの場合「Y=1&Z=3」という文字列なのですが
この文字列はnullじゃないですよね? よって全て真となって
意図した抽出にはならないわけです。

マクロのクォート周りは、ドツボにハマりやすいので、周りにマクロ組むのが上手い人が
いたら直ぐに聞いた方がいいですね!


ハッシュオブジェクトで、definedataメソッドの対象となるのがデータセット内の全変数である場合、全部列挙しなくても、all:'Y'が効くという話と、メソッドのkey指定の変数名とdefinekeyで定義している名前が違ってもいけるよという細かい話

名人戦に電王戦の最終局と目が離せませんね!SASしてる場合じゃないですね!

久しぶりの更新です。

さて、実は最近、ハッシュオブジェクトに関する質問が結構きます。
(コメントや掲示板も使っていただけると嬉しいですが)

日本でも遅ればせながら少しずつ普及してきているんでしょうか?
誰にも頼まれてないのにハッシュオブジェクトを日本に普及させようと目論む狂信者の僕としては結構嬉しいです。(海外のSASプログラマーの興味は既にDS2にいってるのかもしれませんが、、)

今までは割とざっくりと、こんなメソッドがあって、こんなことができますみたいな話が多かったのですが、少しずつ細かい部分についても書いていきたいと思います。

同じ結果を導くのに、結構書き方が何通りもあって、よく言えば柔軟性があり、悪く言えば紛らわしいんですね。

例えば

data Q1;
ID=1;A=1;B=2;C=3;D=4;E=5;F='A';output;
ID=2;A=2;B=3;C=4;D=4;E=5;F='B';output;
ID=4;A=3;B=4;C=5;D=4;E=5;F='C';output;
run;

というデータがあって、

data A1;
if 0 then set Q1;

declare hash h1(dataset:'Q1');
h1.definekey('ID') ;
h1.definedata('ID','A','B','C','D','E','F');
    h1.definedone();

do i = 1 to 5;
ID= i;
rc = h1.find();
if rc ^= 0 then do;
call missing(of ID--F);
end;
output;
end;

drop rc i;
run;

という処理を書くとします。
今回は文法のお話で
データにも、処理の内容にも特に意味はないのでデータセット内のキャプチャは省略です。
ハッシュ学習中の方は結果を予想してから実際に動かしてみてください。

上記のコードを書いていて、まず鬱陶しいのが、
h1.definedata('ID','A','B','C','D','E','F'); の部分ですね。
データステップと違って、クォートしてカンマでつないで指定なのがとても面倒です。

続いて

ID= i;
rc = h1.find();

のようにデータステップ中の変数とハッシュオブジェクトのkeyの変数名を合わせてから
空括弧でメソッドを指定するという書き方ももちろんOKなのですが、無駄に割り当てをしなくても
実は書くことができます。


その2点について改善したのが下のコードになります。

data A2;
if 0 then set Q1;

declare hash h1(dataset:'Q1');
h1.definekey('ID') ;
h1.definedata(all:'Y');
    h1.definedone();

do i = 1 to 5;
rc = h1.find(key : i);
if rc ^= 0 then do;
call missing(of ID--F);
end;
output;
end;

drop rc i;

run;

まず

h1.definedata(all:'Y'); の部分ですね。これによってIDからFまで全ての変数を指定したのと同じことになります。
ただし、注意なのはこの方法は
declare hash h1(dataset:'Q1'); のように、ハッシュオブジェクト定義時にdataset:で定義と同時にデータセットを取り込む書き方の場合しか使えません。
ハッシュオブジェクトはdeclare hash h1()のように空で作ってから、そのあとaddメソッドなどで中身をいれることもできますが、その場合definedataで全変数と言われても何の全変数やねん!となるので無理なわけです。

また、ついでにちょっと関係ない話ですが、上記の2つのコードとも if _N_ = 1 then がないのはなんで?と思われた方いらっしゃいますか?もしそう思われたら、結構ハッシュに慣れてますね。
ハッシュオブジェクトの定義は1ステップ内で1度行えばよく、1obs読み込むごとにやると効率が悪いのでif _N_ = 1 then do; end;の間にdeclareステートメント以下定義部分を入れることが多いのですが、今回はそもそもsetでデータセット読み込んでないので_N_=1しかないから、省略してるんですね。以上。

さて続いて

rc = h1.find(key : i); の部分に注目してみます。これは変数 i の値が動的に与えられ、それによってハッシュオブジェクト内のID変数が検索されるのですね。

ここでやりがちなのが
rc = h1.find(key : 'i'); とコーテーションで包んでしまうことです。ハッシュ定義時の指定がクォート方式なので大変ややこしいのですが、それをすると全く違う意味になってしまいます。

それは "i"という文字列で検索しろという命令になってしまいます。今回IDにiなんて文字は入ってませんし、そもそも数値型なので、エラーになってしまいます。

ハッシュオブジェクトのkeyが文字値の場合はエラーにならない分、余計にタチが悪く、こっちは動的に検索してるつもりが、実は全て固定値で検索してたっていうことになってしまいます。


さて、こう説明すると、だいたい次に聞かれるのが、じゃあ ID A C F とかって全部じゃなくて指定したい時はやっぱり一個一個、クォートしてコロンなの?という質問です。

基本的には、そうなんです!ということなんですが、一応以下のように書いて、all:'Y'に持ってくこともできます。データセットの指定にデータセットオプションが聞くので、オプションで絞ったうえで全部指定にすれば結果的に部分指定していることになるってことですね。


data A3;
if 0 then set Q1(keep=ID A C F);

declare hash h1(dataset:'Q1(keep=ID A C F)');
h1.definekey('ID') ;
h1.definedata(all:'Y');
    h1.definedone();

do i = 1 to 5;
rc = h1.find(key : i);
if rc ^= 0 then do;
call missing(of ID A C F);
end;
output;
end;

drop rc i ;

run; 


おわりです!