filenameで複数ファイルを一括指定して、読み込む場合に各ファイル1行目がラベル行だと、難しいことになるがeovをうまく使えば解決できる話

まずはSAS忘備録の記事「外部ファイルをいっぺんに読み込んで連結する。」を読んで
ください。
http://sas-boubi.blogspot.jp/2014/06/blog-post.html

そこで、以下のようなファイルが、1つのフォルダの中に入ってるとします。

 

(カーソルで潰れてますが 1行目はX Y Zって入ってます)

なんだ、忘備録の方法で一発じゃんと思い


とりあえず、1つにfilenameします。

filename in "C:\temp\TEST\*.txt";

なおSAS雲丹の場合は、既定のフォルダ内に作成してから、
filename in "/folders/myfolders/TEST/*.txt"; とかですね。



data A1;
infile in;
input X Y Z $;
run;

とします。するとなんだかログに

NOTE: X に対して、無効なデータが行 1 カラム 1-1 にあります。
 NOTE: Y に対して、無効なデータが行 1 カラム 3-3 にあります。
 NOTE: X に対して、無効なデータが行 4 カラム 1-1 にあります。
 NOTE: Y に対して、無効なデータが行 4 カラム 3-3 にあります。
 NOTE: X に対して、無効なデータが行 7 カラム 1-1 にあります。
 NOTE: Y に対して、無効なデータが行 7 カラム 3-3 にあります。」

いっぱいでます。
は?NOTEなんて出てても問題ないでしょ。しらねぇよ。と思ってデータセットを開けると

















おぉ?なんだこれ。ってなります。

ああ、そうか忘備録の例ではテキストの1行目からデータだったけど、
今回は1行目はラベルがはいってんのか、これはデータじゃないからな。
数値型の変数にラベルの文字列を入れようとしたからNOTEがでて、欠損値になってんのか。
はいはいはい、よし!

data A2;
infile in firstobs=2;
input X Y Z $;
run;
















firstobs=2として、2行目から読み込めば問題なし!
楽勝!
…なんかやっぱりログにNOTEでてるけど無視無視!
と思って、データセットを開けると

おぉ。そうじゃないよって感じですね。
しかし、filenameの一括指定は、複数のファイルを1つのファイルとして擬似的に
みなして処理するのでfirstobs=2などとしても、1番最初のファイルの1レコード目以外
は読み込まれてしまいます。

さて、この各ファイルの先頭行を読まずに、ファイル一括指定で
データセットをどう作るかという問題、どう攻めますか?

まあ、個別に1ファイルずつ読み込んで、データセットを後で縦結合でもいいですけど
そんなことする必要はありません。

盤上この一手は「eov」オプションの活用です。
eovとは何か、それは、複数ファイルからの読み込み時に
2ファイル目以降の先頭に1をたてるオプションです。
ただ、いきなり何もせずに指定すると、2ファイル目の先頭で
1が立って以降、1がretainされる性質を持っているので、
各レコードの読み込み前に0にもどす必要があります。

以下のコードはeovの働きをみるサンプルです。

data A3;
infile in eov=eov;
eov = 0 ;
input X Y Z $;
FL = eov;
N =_N_;
run;

結果は以下のようになります。

















さて、そのeovの働きを活かして、今回やりたいことを実現するコードは

以下の通りです

data A4;
infile in eov=eov;
eov = 0 ;
input @;
if _N_ ^= 1 and eov ^= 1 then do;
input X Y Z $;
output;
end;
run;













なるほど、_N_ = 1は1ファイル目の1レコード目、eov=1は2ファイル目
以降の1レコード目だから、それ以外の時にinputするわけか。

けど、「input @;」は何?
これは、実はeovに1が入るのはinput文が実行された時なんですね、
しかし今回は余計なNOTEを出さないためにも
input文を実行するかどうかを条件分岐したいわけで、読み込んで
からじゃ遅いわけですね。
ので inputの後に変数名を指定せずに、空撃ちしているわけです。
@はつけないと、読み込みが次の行にいってしまいます(ポインタが移動。@をつけるとその行に留まる)。
空撃ちした後、読み込むかどうかの結果がでるまでその場で足踏みしとけってことですね。

以上、おそまつ。


自動変数_i_って何?って話と、2つの配列に共通の値があるかでフラグたてる話

ちょっと最近、知ったばっかりのことで、
間違ってたら教えて欲しいのですが。

以下のようなデータがあって

data Q1;
A=1;B=2;C=3;X=4;Y=5;Z=6;output;
A=4;B=7;C=5;X=2;Y=6;Z=4;output;
A=8;B=6;C=9;X=2;Y=4;Z=6;output;
A=1;B=2;C=8;X=3;Y=9;Z=5;output;
run;










A B Cで配列AR1、  X Y Zで配列AR2を作って、
それぞれの要素番号2についてログに出したければ


data A1;
 set Q1;
 array AR1{*} A B C;
 array AR2{*} X Y Z;

 put AR1{2}= AR2{2}=;

run;

ログには







となります。
ここまではOKですね。

次に非明示添字配列を考えてみます。
明示・非明示とは、AR1{*}やAR1{3}などのように配列名の後に
要素数を添字で明示するのが明示配列、しないのが非明示配列です。

【参考記事】非明示添字配列と do over LOOPの利用
http://sas-tumesas.blogspot.jp/2014/01/do-over-loop.html

非明示配列は要素数を定義しませんが、実は内部的には
要素番号が振られているため、以下のように書けます。

data A2;
set Q1;
array AR1 A B C;
array AR2 X Y Z;

put AR1{2}= AR2{2}=;

run;

しかし、実はなんと以下のようにも書けるらしんです

data A3;
set Q1;
array AR1 A B C;
array AR2 X Y Z;
_i_=2;
put AR1= AR2=;

run;

結果は同じ








_i_って!そんなのがあるんですね~。
こいつ、実は_N_と同じで、出力データセットから自動にドロップ
される、見えない系の自動変数です。
配列のインデックスをつかさどる自動変数のようですね。
つまり、普段の明示配列で、iとかでループして分岐するような
処理について非明示配列では_i_を使って条件分岐できるというわけ
ですね。

2番目の要素が7の時に★つけてだせと言われれば

data _NULL_;
set Q1;
array AR1 A B C;

do over AR1 ;
if _i_ = 2 and AR1= 7 then put "★" AR1=;
else put  AR1=;
end;

run;

















みたいな。いや、最初に言ったように非明示でも
普通にAR1{2}=7と書きゃいいんですけどね。

_i_の活用法でいいの知っている方がいたら是非教えてください。

さて、今までの話と特に関係ないんですが、配列関係でたまに聞かれる
処理があるので少し紹介。

それは配列AR1にある値が配列AR2に存在するかを調べて
フラグを立てるみたいな処理です。

多分、以下のようにかけると思います


data A4;
set Q1;
array AR1{*} A B C;
array AR2{*} X Y Z;

do i = 1 to dim(AR1);
if AR1{i} in AR2 then FL=1;
end;
run;

結果は








非明示なら以下のように書けます。

data A4;
set Q1;
array AR1 A B C;
array AR2 X Y Z;

do over AR1;
if AR1 in AR2 then FL=1;
end;

run;

cat系関数に数値型を指定の話

あんまり綺麗じゃない、どちかというと外法っぽい小技の紹介です。
以下の2つのデータセットAを作成するコードを見てください。

data A;
X=1;
run;

data A;
X='1';
run;


変数Xが数値型か文字型か、実行時まで確定しないという状況があるとします。
(proc importやlibname excelとかでexcelを読む場合、型が自動判定なので、
まあよくある話です)

そこで、Xが文字型だったらそのままYに代入し、数値型ならbestフォーマットで
文字型に直してYに代入したいという要望があったとします。


まあ

data B;
length Y $200. ;
set A ;
Y = X;
run;


としちゃえば、Xが文字型だろうと数値型だろうといけますが、当然、数値の場合
「NOTE: 以下の箇所で数値を文字値に変換しました。」がでます。
このNOTEをださないようにするにはどうするか。

やってしまいがちなのが以下のような書き方

data B;
set A;
if vtype(X) = 'C' then Y = X ;
else if vtype(X) = 'N' then Y = put(X,best. -L) ;
run;

しかし、これをXが文字型の場合のデータセットAに対して実行すると
ERROR : 出力形式 $BEST が見つからないか、またはロードできません。
となります。else ifステートメントが実行されることはないのですが、
コンパイルの時点で、文字型の変数に数値用のフォーマット充てようとしてるぞバカって
早とちりされてエラーになっちゃいます。

ので、


proc sql noprint;
   select TYPE into: MTYPE
   from DICTIONARY.COLUMNS
   where LIBNAME="WORK" and MEMNAME="A";
quit;

%macro m;
data B;
set A;
%if       &MTYPE = char %then Y = X ;
%else %if &MTYPE = num  %then Y = put(X,best. -L) ;;
run;
%mend m;

%m

のように、一旦Xの型をマクロ変数に格納して(例ではSQL使いましたが方法は何でもいいです)、それによって%ifで
マクロ内で実行されるコードを分岐させるといった方法をとれます。

ですが!!はっきりいって面倒くさい!
型によって実行されるコードが複雑な処理で、厳密に分けたい場合は、上記の感じでいいのですが単純にbestあてて文字にしたいだけなら、以下のような外法もあります


options missing ='';
data B ;
set A;
Y = cats(X,'');
run;
options missing ='.';

cat系関数は、基本文字変数に対する関数ですが、実は数値型の変数を指定すると
自動でbestフォーマットをあてて処理してくれます。暗黙の型変換ではなく関数の
働きによるものなので、結果は同じでもこの場合NOTEがでないんですね。
文字変数だった場合は、ブランクをくっつけているだけなので何もおきません。
なんか、ログを綺麗にするためだけの詐欺みたいな感じですけど。

options missing ='';は、Xが数値変数且つnullだった場合に「.」が文字化されないようにするための一手ですね。

何かほかにいいやり方をお持ちの方は是非教えてください。

まあ、そもそも型がぶれるっていう状況がよくないんですが。



マクロの%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を新しく立ち上げたときから設定が有効化されているはずです。