外部ファイルをいっぺんに読み込んで連結した時に、各オブザベーションの由来ファイルパスを取得する方法:infileステートメント+filename=

SAS忘備録の記事に乗っかったテクニックを一つ。

記事「外部ファイルをいっぺんに読み込んで連結する。」
http://sas-boubi.blogspot.jp/2014/06/blog-post.html

で複数のファイルを一括りにして、一気に読み込む方法が紹介されています。
これ、凄い便利です。

ただ、ちょっと困ったのが、データセットができあがった時に、このオブザベーションはどのファイルから読み込まれたものなんだろう?っていうのがわからないことでした。

そこで、Cドライブ下のsampleというフォルダ内にある全てのcsvファイルを読み込んで1つのデータセットにするコードを考えます。

今、TEST1.csvとTEST2.csvがフォルダ内にあって、以下の内容の場合










filename  FL  "C:\sample\*.csv" ;

data OUT1;
length FPATH $100.;
 infile  FL  dsd filename=FPATH;
 input A$ B$ C;
  X=FPATH;
run;

とすると、結果は







となって変数Xに、由来元のファイルのフルパスが表示されます。

気づかれた方も多いかもしれませんが、このfilename=の挙動は
indsname=オプションの挙動に酷似しています。

indsname=については
「indsname オプションで、由来元のデータセット名を取得する」
http://sas-tumesas.blogspot.jp/2013/10/indsname.html

「indsname定跡」
http://sas-tumesas.blogspot.jp/2014/02/indsname.html

などを参照ください


runステートメントにcancelオプションをつけることでステップを実行させなくする話

多分、runステートメントについて、ヘルプを見ることってあまりないので、気づきにくいのですが、
runステートメントにはcancel オプションというものがあって

例えば以下のプログラムを実行すると

data Q1;
 X=1;
run cancel;

proc univariate data=Q1;
 var X;
run cancel;

















となって、要は構文チェックがかかって、実行直前までいくけど、実際実行はキャンセルされる。
みたいな働きをします。

runの後につけるので、データステップでもプロシジャステップでも使えます。

デバック用のオプションですが、このcancelの部分をマクロ変数にしておいて、実行するしないを切り替えみたいな使い方をしている人も見たことあります。

また、runステートメントを書かないsqlプロシジャの場合、どうするかという話ですが
(ちなみにproc sqlに無理やり、runを書くと「 PROC SQL ステートメントはすぐに実行されるため、 RUN ステートメントは必要ありません」という割とレアなNOTEがでます)

その場合はsqlプロシジャにはvalidateオプションがあるので、それを使います。
validateについてはSAS忘備録で、少し触れられています
http://sas-boubi.blogspot.jp/2014/04/sql_11.html


ハッシュ反復子オブジェクトとcall compcostルーチンで、マジカルチェンジのデータを順番通りに連結する

マジカルチェンジって知っていますか?

「チェ~ンジチェンジ、マジカルチェンジ、○○○という字を一文字変えて、×○○」って言って、繋げていくゲームです。

僕は今ちょうど30歳で、マジカル頭脳パワーっていうテレビ番組で、このゲームがやられていたころには、結構大きくなっていた(中学生ぐらいか?)ので、実際人とやったことないのですが、見た記憶はあります。

以前、マジカルバナナのデータを加工する記事
「ハッシュオブジェクトでマジカルバナナのデータを順番通りに連結する」
http://sas-tumesas.blogspot.jp/2014/09/blog-post_16.html

をやったので、同じノリで、マジカルチェンジもやってみようとしたら、これが思った以上に
難易度が激辛で困りました。

一応書くには書きましたが、間違いがあるかもしれず、また、より効率的な方法があると思うので
皆さんも挑戦してみて、教えてください。

データは

data Q1;
X='きいろ';output;
X='こいる';output;
X='こいん';output;
X='まいこ';output;
X='まいる';output;
X='かいろ';output;
X='かいこ';output;
run;











で、前回同様、先頭の1ワード目は固定ですが、以降は順番がぐちゃぐちゃで、ゲームが成立していないので、上記のデータセットから





のような結果を作るのが今回のお仕事です。


僕のコードは以下の感じです

data A1;
length Y RESULT $100;
 if _n_=1 then do;
  call missing(Y);
  call compcost('REPLACE=',99);
  declare hash hq(dataset: 'Q1(rename=(X=Y))');
  declare hiter hi('hq');
   hq.definekey('Y');
   hq.definedata('Y');
   hq.definedone();
 end;
set Q1(obs=1);
 rc=hi.first();
 RESULT=X;
  do until(hq.num_items=1);
   COST=compged(X,Y);
   if COST=99 then do;
    RESULT=catx('→',RESULT,Y);
rc=hq.remove(key:X);
    X=Y;
   end;
   rc=hi.next();
 end;
keep RESULT;
run;

はい、まず以前、ハッシュ反復子オブジェクトはいずれ、基礎から説明しますといってましたが、今回それを使ってます。
説明すると長くなるので、今回は概要だけ掴んでいただけたらと思います。

そもそも、今回の問題がバナナより難しいのは、キーマッチングじゃないからなんですね!1文字違いかどうかを判定するまで、くっつけれないんですね!で結局、1個1個、判定していくしかないんです。
だからしてfindメソッドを始めとした、ハッシュオブジェクトから、キー情報を元にデータをルックアップする仕組みが使えないんです。

だから、一端、ハッシュオブジェクトを作った後に、
declare hiter hi('hq');
として、そのハッシュオブジェクト「hq」をハッシュ反復子オブジェクト「hi」に進化させてます。

ハッシュ反復子オブジェクトは、キーの縛りから開放されます。findメソッド等、キーを手がかりにするメソッドが使えなくなる代わりに、firstメソッド(ハッシュ反復子オブジェクト内の最初のデータをとってくる)やnextメソッド(次のデータをとってくる)のように、単純に格納順でデータを見れるようになるんですね。


で、今回のもう一つの目玉が、1文字だけ変わっているという状態をどのように判定するかですね。
これは例えば1文字目が違って後は一緒、2文字目が違って後は一緒・・というように素直に条件分岐してもいいです。
ただ、例えば、今回は3文字のマジカルチェンジですが、文字数が増えると少し面倒になります。

そこで以前、文字列の類似度をスコア化する話題をした際に
「データステップ100満開 文字列がどれくらい類似しているかを定量化する」

でcompget関数を説明しました。

1文字消して一致したら何ポイント、1文字変えたら何ポイントといったように、一致するまでにかかるコストをスコア化する関数です。

このスコアルールを、自分で変更できるのが call compcostです。

これを使って、1文字違いのケースを、他のコストスコアをどう組み合わせても出現しないような数字にしてやります(他のケースのコストを全部0にしてもいいけど、列挙するのが面倒なので)

 call compcost('REPLACE=',99);は、1文字の置換のコストを99に設定しています。

他にどんな操作があるかはリファレンスを見てください。


以上が今回のコードの種明かしです。

もっと良いコードをどなたかお願いします。

transposeして、横にしてからarrayで、ループで、スコア比較して横同士入れ替えていくような方がいいのかなぁ。それはそれで面倒そうだけど。


で、どうでもいい話なんですが、僕、ハッシュオブジェクトからハッシュ反復子オブジェクトを作る
declare hiter書くとき、瞬間的に凄いテンションがあがるんですよね。

ハッシュオブジェクトが超サイヤ人2だとしたら、反復子オブジェクトは超サイヤ人3、ハッシュオブジェクトが始解なら反復子は卍解みたいな。
いや、別にハッシュ反復子オブジェクトになったから強いとかじゃないですし、反復子定義後も、ハッシュオブジェクトが消えるわけではなく、そっちに対するメソッドも有効なので、性質の違いがあるだけで、ちょっと例えおかしいんですが。

ただ、いっつも凄い気持ちいいので、やっぱり、男って、子供から大人まで、変形とか変身とか、進化とか、そういうのに弱いのかなぁって思いました。

Ζガンダムが一番好きですし、将棋ウォーズで、片美濃囲い→美濃囲い→高美濃囲い→銀冠のエフェクトの声聞くと、昂ぶりますし。

これ以上書くと、病院勧められそうなんでこのへんで終わります。

intnx関数とintervaldsオプションである日付の前後に発生するイベント日を取得する

例えば、何かの祝日で、普段であれば平日の曜日に休みがあったりすると、この休みが終わったら次の祝日はなんだっけ?来月は祝日あったっけ?とか考えたりすることってあると思います。

data Q1;
X=input('2014/03/05',yymmdd10.);output;
X=input('2014/04/29',yymmdd10.);output;
X=input('2014/07/14',yymmdd10.);output;
format X: NLDATEW30.;
run;







のように、ある日付が与えられた時に、その日から見て、直前にあった祝日と、直後にある祝日の日付を取得せよという問題があったとします。

まず、米国の祝日であればholiday関数とかでeaster = holiday('easter', 2014);とかすれば、その年の祝日の日付を取得できますが、日本の祝日には対応してません。

fcmpプロシジャやマクロで日本用のholiday関数を誰かが作ってくれれば有り難いのですが、今回は手抜きで

data EVENT;
begin=input('2014年01月13日 月曜日',NLDATEW30.);output;
begin=input('2014年02月11日 火曜日',NLDATEW30.);output;
begin=input('2014年03月21日 金曜日',NLDATEW30.);output;
begin=input('2014年04月29日 火曜日',NLDATEW30.);output;
begin=input('2014年05月05日 月曜日',NLDATEW30.);output;
begin=input('2014年07月21日 月曜日',NLDATEW30.);output;
begin=input('2014年09月15日 月曜日',NLDATEW30.);output;
begin=input('2014年09月23日 火曜日',NLDATEW30.);output;
begin=input('2014年10月13日 月曜日',NLDATEW30.);output;
begin=input('2014年11月03日 月曜日',NLDATEW30.);output;
begin=input('2014年11月24日 月曜日',NLDATEW30.);output;
begin=input('2014年12月23日 火曜日',NLDATEW30.);output;
format begin NLDATEW30.;
run;
















のように2014年において、月曜日から金曜日が祝日または振替で休みとなる日付がデータとして与えられたとしましょう。


そうすると以下の様なプログラムが成立します。(後でコメントみてください!)

options intervalds=(event=EVENT);
data A1;
 set Q1;
 NEXT_HOLI=intnx('event',X,1);
 PRE_HOLI=intnx('event',X-1,-1,'end')+1;
 format NEXT_HOLI PRE_HOLI NLDATEW30.;
run;






おそらくintervaldsオプションについて書いた以前の記事と

intnx関数の記事をみていただければ


理屈がわかるかと思います。

ただ、直前の祝日といったように逆向きに負の値でまたぎをとるのはちょっと頭を悩まします。
上記の書き方で本当に正解なのか?あるいは最善の書き方なのかはちょっと自信ないです。
どなたかご教示ください。

なんで上記のような書き方にしているかについては、'end'を外したらり、-1をとったり、+1をとったりして試して、実感してください。

追記>>コメントに正解の書き方をいただいたので、見てください!!



(以下、いつもの、誰も喜ばないおまけ)

さて、電王戦タッグマッチはSAS忘備録のa matsuさんと見に行くことになりましたが、他にも会場にいらっしゃるSASユーザーの方がいればぜひ教えてください。

あぁ、それにしても昨日の王位戦は、、。木村一基八段を応援していたので、辛かったです。
羽生さんはもう、ほんと強いですね、人間とは思えない。

木村一基を知らないという方は「木村一基 ぼやき」とかで、動画を見ていただければ、その人間元的魅力の一端がわかるんじゃないかと思います。

木村先生は名言が多いのですが
「負けと知りつつ、目を覆うような手を指して頑張ることは結構辛く、抵抗がある。でも、その気持ちをなくしてしまったら、きっと坂道を転げ落ちるかのように、転落していくんだろう。」(将棋世界2007年5月号)
なんかは、自分的に不本意な、美しくないと自覚しているプログラムを書いている時に、気持ちを奮いたたせるためによく思い浮かんだりします。


intnx関数について基本の話

前回、intck関数とintervaldsのコンボを紹介したので、次はintnx関数とintervaldsのコンボをやろうと思うのですが、まずintnx関数を使ったことない方も多いと思うので、基本を紹介します。

以下のデータセットがあったとします。

data Q1;
X=input('2013/04/05',yymmdd10.);output;
X=input('2013/01/31',yymmdd10.);output;
X=input('2014/06/14',yymmdd10.);output;
format X: NLDATEW30.;
run;







さて、次のコードを実行すると

data A1;
set Q1;
X_1=intnx('year',X,1);
X_2=intnx('month',X,1);
X_3=intnx('week',X,1);
format X: NLDATEW30.;
run;






はい、もうピンときたと思いますが、これintck関数の逆の働きをするんです。
intck関数が時点Aから時点Bまでチェックポイント単位Cで見た時、ちょうどD単位分跨いだ。のDを求めるのに対して

intnx関数は時点Aからチェックポイント単位CでD単位分跨いだら時点Bになった。のBを求める関数です。

なのでX_1=intnx('year',X,1);は年単位で、1単位分跨いだ時点です。年のチェックポイントは1月1日なので、全部、翌年の1月1日になっています。

同様にX_2=intnx('month',X,1);は、翌月の1日になってます。

X_3=intnx('week',X,1);はちょっとわかりにくいですが、日曜日がチェックポイントなので、直後の日曜日となります。

続いて

data A2;
set Q1;
X_1=intnx('year',X,1,'sameday');
X_2=intnx('month',X,1,'sameday');
X_3=intnx('week',X,1,'sameday');
format X: NLDATEW30.;
run;






samedayオプションをつけたことで、1単位分跨いだ後の同日時点という意味になります。
すなわち
X_1=intnx('year',X,1,'sameday');は1年後の同月同日となり
X_2=intnx('month',X,1,'sameday');は1月後の同日(1月31日の1月後の同日が2/28になっていることに留意)
X_3=intnx('week',X,1,'sameday');は、次週の同曜日です。

また、endを使うと

data A3;
set Q1;
X_1=intnx('year',X,1,'end');
X_2=intnx('month',X,1,'end');
X_3=intnx('week',X,1,'end');
format X: NLDATEW30.;
run;






もうこれは説明いらないですね。1単位分跨いだ後の末時点。或いは2単位分跨いじゃう直前部分とも言い換えれるでしょう。


次は、よりマニアックに

data A4;
set Q1;
X_1=intnx('year1.6',X,1);
X_2=intnx('week1.2',X,1);
format X: NLDATEW30.;
run;






これはちょっと誤解しやすいところですが、1単位分跨いだ後の、小数点分下位単位をまたいだ日付です。

X_1=intnx('year1.6',X,1);は1年後の6月です(1年6ヶ月後じゃないことに要注意!!)
全部6月になりますからね!!year1.5は1年後の5月であって、0.5を半年とかって風に比で考えてはだめです!
X_2=intnx('week1.2',X,1);は1週後の月曜日です。日曜日が0.1だから月曜は0.2ね!

はい、負の値をとって、逆上った日付もとれます

data A5;
set Q1;
X_1=intnx('year',X,-1,'sameday');
X_2=intnx('month',X,-1,'sameday');
X_3=intnx('week',X,-1,'sameday');
format X: NLDATEW30.;
run;






samedayやendの他にmiddleやbeginもあるのですが、読んで字のごとくなので割愛します。









電王戦タッグマッチ

このブログは、SASプログラミングと将棋は非常に似ているという説を(勝手に一人で)唱えている人が書いているので、急に将棋の話が入ります。

電王戦タッグマッチのファイナルラウンドの観覧券が抽選であたったので
http://ex.nicovideo.jp/denou/tag2014/

一緒に行ける方がいらっしゃったら、sasyupi@gmail.comまで連絡ください。

チェスの世界にはアドバンス・チェスっていうのがあって、プレイヤーがコンピュータの提示する手を参考にしながらプレイするんですね。

すると例えば、人間より強いコンピューターがいたとして、そのコンピュータより、人間+コンピューターのペアの方が強いということが、不思議なことにどうやら実際あるそうなんです。
まあ、この辺はちょっと議論が熱くなる箇所なので、深くは立ち入りませんが、そうだとしたら面白いですよね。

コンピューターの演算は凄まじく、時間をかければ天文学的な局面を読めるのですが、
あくまで、1つのインプットに対して1つのアウトプット、要は局面を点で捉えるんですね、人間のようにこの勝負は、だいたいこういう風な流れにして、こういう局面に収束させて勝ちに持って行こうといった、いわゆる構想力というか方針決定能力っていうか、大局観というのがないと言われているんですね。

いくら凄まじく読めても、将棋の指し手の選択肢の分岐数はもう、果てしない数らしいので、ある程度大局観がないと、もしかしたらあんまり有益じゃないところ、或いは一見有益なんだけど、さらに深く進むと実は逆に凄い有害な変化を読みきれずに指しちゃうってことなんです。

人間は、そんな何億も読んでないですが、重要なところを直感で察知して、そこを重点的に深く深く読んだりする能力や、この手順はなんだか最終的にこっちが悪くなる気がするといった、気配や感覚を感じる能力があるみたいなんですって。

そこで人間が、局面によってはコンピューターの指し手をガン無視して、構想を描くような手を指して、方針を決めてやってから、演算させると、単体でやるより実は有効な局面に効率的に誘導できるということなんでしょう。単体より強くなるというのは。多分。

統計解析プログラミングって、ちょっとアドバンスチェスに似てると思うんですよね。
結局は、人間の解釈が必要っていうか、実際計算するのはプログラムでもそもそも人間の着想力や構想力に依存するっていうか。まあ人間世界に何かを還元するためにやっている部分が大きいのでそりゃ人間がいるわっていう。

てかSASが自分で意思を持って、計画立てたり、提案してみたり、勝手に仕事しだしたら、僕ら全員失業ですね。


与えられた区間中に、イベントが何回発生しているか。イベントのデータセットをintervaldsオプションで読み込んでユーザー定義間隔を作成してintck関数で使用する方法

例えば以下のようなデータがあるとします。

data EVENT;
begin=input('2014/01/05',yymmdd10.);output;
begin=input('2014/01/31',yymmdd10.);output;
begin=input('2014/02/01',yymmdd10.);output;
begin=input('2014/02/08',yymmdd10.);output;
begin=input('2014/02/25',yymmdd10.);output;
begin=input('2014/03/01',yymmdd10.);output;
begin=input('2014/03/05',yymmdd10.);output;
begin=input('2014/03/10',yymmdd10.);output;
begin=input('2014/03/14',yymmdd10.);output;
begin=input('2014/03/18',yymmdd10.);output;
format begin yymmdds10.;
run;














これはそうですね、例えば、ある会社と取引のあった日の履歴のデータだとします。


そして次に以下のデータセットを見てください。

data Q1;
ST=input('2014/01/01',yymmdd10.);EN=input('2014/01/15',yymmdd10.);output;
ST=input('2014/01/16',yymmdd10.);EN=input('2014/01/31',yymmdd10.);output;
ST=input('2014/02/01',yymmdd10.);EN=input('2014/02/15',yymmdd10.);output;
ST=input('2014/02/16',yymmdd10.);EN=input('2014/02/28',yymmdd10.);output;
ST=input('2014/03/01',yymmdd10.);EN=input('2014/03/15',yymmdd10.);output;
ST=input('2014/03/16',yymmdd10.);EN=input('2014/04/01',yymmdd10.);output;
format ST EN yymmdds10.;
run;










これは集計する対象区間を表しています。
この企業では、毎月、1日から15日まで、15日から月末までで区切って取引件数を集計している
としましょう。

さてQ1の各期間(STからEN)までの間に、データセットEVENTの日付がそれぞれ何回あるかをカウントするという問題です。

先に、求めるべきデータセットは










のような感じです。


さてさて、皆さんなら、どのようにして切り込みますか?

merge? transpose? sql? hash? array? formatとmeans?

多分、方法はたくさんあると思いますが、今回はintervaldsオプションとintck関数の
コラボで頑張ってみます。

で、早速コードなのですが、驚かないでください。


options intervalds=(event=work.EVENT);
data A1;
 set Q1;
 COUNT=intck('event',ST-1,EN);
 format ST EN yymmdds10.;
run;

はい、なんとこれだけです。

これでさっきの求めるべきデータセットになります。

結構凄くないですか!

でも実はちょっとやりやすいように問題を作ってますので、そこも含めて説明します。


まずはintck関数を知らない場合、まずググってください。ちょっと説明が難しい関数です。

2つの日付(日時)データから、間隔を出すのですが、それが単純に日数差からの計算ではなく、例えば月の初日の開始時点を超えた回数、1月1日の開始時点を超えた回数で、間隔が何ヶ月だとか何年だとかって出します。

つまり、2007年12月31日と2008年1月1日をintck関数でyearを指定してだすと、1って出てきます。
2008年1月1日と2008年12月31日なら0です。
日数差で考えるとかたや1日、かたや364日ですが、重要なのはチェックポイントをまたいだかどうかなのです。

でoptions intervalds=(event=work.EVENT);はそのintck関数に使用するチェックポイントを自分で作成することができる方法なのです。

eventがユーザーがつける任意の名前で、あとでintck関数で指定します。
=でそのチェックポイント日時の入ったデータセットを指定します。

重要なのはここで起点日の変数名を必ず「begin」にしておくことです。なってないならrenameしましょう。
これが定義に必要な決まり文句、予約語になっています。
(ちなみにendという変数もつけれて、チェックポイントに幅をもたせれるのですが、またこんど)

もう一つ超重要なのが、beginの日付は必ず昇順になってないといけません!重複や、endを使う場合、区間の重なりも絶対ダメです。
エラーはでないけど、カウントがめちゃくちゃになるので、マジで危険です。
今回はソートしてませんが、必ずソートしておくことをお勧めします。

でCOUNT=intck('event',ST-1,EN); の部分です。
'event'の部分で、ユーザーが作成した任意の定義名を指定します。

ST-1としているのは、こうしとかないと、同日の場合にまたいだと見なされず、カウントされないからです。

どうでしょうか?このブログにしては割りと実用的な話だっと思います。

ただ、グループごとに処理する場合は使いにくいです。

例えば、臨床試験データなどで、被験者ごとにある注目している区間内に発生した、注目事象のカウントといったケースに適応しようとすると、被験者ごとにユーザー定義チェックポイントを作って、
被験者ごとに関数で指定しなきゃいけないわけです。
少数ならいいですが、データ数が多い場合は厳しいでしょう。処理時間も結構かかります。

またもう一つ、重要なのが、今回は強烈な前提条件として同日に複数レコード発生がないというものを置いています。通常の集計と違って、この方法は、チェックポイントのまたぎをカウントするので、同日に複数たっているとお手上げです。1件としてカウントします。それでいいなら逆に使えますが。

freqプロシジャのnlevelsオプション

データに含まれる変数ごとの水準数(度数じゃなくて、カテゴリ数・パターン数)を取得したい場合
freqプロシジャの、nlevelsが便利です。


例えば以下の様な適当なデータセットがあったとして

data Q1;
X=1;Y='A';output;
X=1;Y='B';output;
X=.;Y='A';output;
X=2;Y='D';output;
X=3;Y='C';output;
X=2;Y='E';output;
run;



ods output Nlevels=A1;
proc freq data=Q1 nlevels;
   tables _all_;
run;

とすると、アウトプットの方は






















こんな感じです。赤く括った箇所が、nlevelsオプションとつけたことで現れた部分です。
水準数と、その中で欠損水準・非欠損水準の数を出してくれます。

で、ods outputで、これをデータセット化すると





となります。

こっから、マクロ変数に突っ込みたい時なんかに便利ですが、
一つ要注意なのが、欠損水準が指定した全変数中一つもない場合、変数「NMissLevels」「NNonMisslevels」の変数そのものが出現しないという問題です。

要するに

ods output Nlevels=A2;
proc freq data=Q1 nlevels;
   where X^=.;
   tables _all_;
run;

とすると

データセットは






の2変数だけです。

プログラマーの気持ちとしては、こういう、データ依存で出力デザインが可変になるのは、ちょっと困りますよね。
欠損水準数とか、値0でいいから、列持たせといてよ!こっちはコードで変数指定してるのに、無かったら、こけちゃうやん!って感じです。

まあ、そこだけ注意してコードを書いてください。





ハッシュオブジェクトでマジカルバナナのデータを順番通りに連結する

ハッシュオブジェクトが効果的な状況として、自己参照してループしながらキールックアップしていくようなのものが有名です。

例えば今、マジカルバナナをした結果のデータが以下のようにあるとします。
(マジカルバナナって何?って言う世代がSASを触っていることはまだ無いと信じたい、、、)

data Q1;
length X Y $100.;
X='バナナ';Y='滑る';output;
X='冬';Y='雪';output;
X='スキー';Y='冬';output;
X='雪';Y='冷たい';output;
X='滑る';Y='スキー';output;
run;










ただし何故か、1オブザベーション目以降については、並び順が崩れてしまいました。


このデータから、





のようなデータセットを作成せよというのが今回のお仕事です。


で、それをハッシュオブジェクトで実現する場合、以下のようにかけます。

data A1;
length RESULT $100;
 if _n_=1 then do;
  declare hash hq();
   hq.definekey('X');
   hq.definedata('X','Y','FL');
   hq.definedone();
 end;
 do until(END1);
  set Q1 end=END1;
   FL=0;
   rc1=hq.add();
 end;

 do until(END2);
  set Q1 end=END2;
   rc2=hq.find();
   if rc2=0 and FL=1 then continue;
    RESULT=catx('→',X,Y);
    X=Y;
   do until(rc3^=0);
      rc3=hq.find();
      if rc3=0 then do;
       FL=1;
       hq.replace();
       RESULT=catx('→',RESULT,Y);
       X=Y;
      end;
     else output;
    end;
  end;
keep RESULT;
run;


まあ、長いですけど、大したことはしていなくて、setするデータセット自体をハッシュオブジェクトに入れておきます。
それで、Xでキーマッチングして、マッチしたら、RESULTに連結して、次はYの値をXに持ってきて、マッチしてってことです。
ポイントはハッシュオブジェクト作成の際にFLという新変数を作成していることで、こいつは、既に使用済みになった行に対してのフラグの役割を果たすわけですね。if continueのところでマッチされたデータが使用済みかを判定しているわけです。
それで、また、キーがマッチした場合はreplaceメソッドで、ハッシュオブジェクトに使用済み更新をかけています。

配列とかSQLでも同じ処理をかけるとは思いますが(ぜひ別の解法コードを教えてください)、個人的に一番しっくりくるのがハッシュオブジェクトかなぁって感じします。

ちなみにもし、バナナが先頭固定じゃない場合は、生成されたデータから文字列が最長のものを選ぶためのもう1ステップがどうしても必要になると思います。

また複数のマジカルバナナデータが混合した場合、問題はかなり複雑になります。同じワードが他のバナナで発生した場合、そっちに移ってしまう可能性があるからです。そうなると、全オブザーベーション分、作るだけ作ってから正解を選ぶ必要がありそうです。
まあ、そこまで突き詰めるような問題ではないか





SQL専用演算子 eqt演算子について(その他のtruncated演算子についても)

SASのテクニック、というか実務者にとっての小技として、わざとエラーを出して、ログのエラーメッセージからSASに指定できるパラメータを教えて貰うというものがあります。

確か、書籍「統計解析ソフトSAS(工学社)」のどこかにも、そのことが書いてあって、わ~現場の泥臭い感じがいいな~って思いました。

さて、脱線しましたが、SQLプロシジャで色んな書き方を試して、遊んで?いた時のこと








「ERROR 22-322: 構文エラーです。次の 1 つを指定してください : !, !!, &, *, **, +, ',', -, /, <, <=, <>, =, >, >=, ?, AND, BETWEEN, CONTAINS, EQ, EQT, GE, GET, GT, GTT, LE, LET, LIKE, LT, LTT, NE, NET, OR, ^=, |, ||, ~=.」

今まで何回も出したことあるエラーですが、ふと目がとまりました。

え?EQTってなによ? あれ、GTT? は?LTT?
いや、よく見るとさらに、GETLTENET

EQはequal(=)のこと、GEはgreater than or equal to(大なりイコール ≧)ってわかるけど、
こいつらはなんなんだ!!

え~、結構SASオタのつもりだったのに、まさか、いまさら知らない演算子とか!!ショックだわ!
てか、SQL専用演算子なんかあったのか、勉強不足でした。

で、早速調べてみると、eqtは「equal to truncated strings 」とのこと、
http://support.sas.com/documentation/cdl/en/proc/61895/HTML/default/viewer.htm#a002473695.htm

で、その他のGTTとかLTTとかもどうやら同じような系統のようです。


で、一体、こいつはどんな働きをする子かな、と試してみました。

一つ理解すれば、後はまあ同じ拡張なので、今回はeqt演算子で。

data Q1;
X='ABCDE';output;
X='BCDEA';output;
X='DCDEB';output;
run;







というデータセットがあったとして


proc sql noprint;
create table A1 as
 select X
 from Q1
 where X eqt 'AB'
;
 quit;

とすると





あ~、なるほど、指定した抽出文字列の長さ以降を切り捨てて比較ということか、
だからtruncatedね、
つまりこの場合、平たく言うと Xが'AB'から始まる文字列ってことか。

そうするとike演算子で

proc sql noprint;
create table A0 as
 select X
 from Q1
 where X like 'AB%'
;
 quit;

と書くのと同じか。
like演算子については記事「whereステートメントでのみ使用できる演算子_contains,between,like」

を参照してください(ただし、記事に誤解を招く表現があります。
この記事はデータステップのwhereステートメントでの話であって、like演算子はsqlでは、別にwhere句に限定せずに記述できますからね)


なるほど~、じゃあ例えばXが「AB」から始まる場合にフラグ変数をたてたければ

proc sql noprint;
create table A2 as
 select X
       ,X eqt 'AB' as FL
 from Q1
;
 quit;










あるいは

data Q2;
Y='BC';output;
Y='DC';output;
Y='AB';output;
run;







みたいなデータセットと以下のような条件で結合する

proc sql noprint;
create table A3 as
 select X,Y
 from Q1 inner join Q2 on
 x eqt Y 
;
 quit;












って感じになる。


あ~、これ結構いいですね!守備範囲せまいけど、like演算子で記述するより見た目がすっきりしてますね!
今後、gtt lttとかの使い道についても考えてみます。

でも、これって、知らない人がコードレビューしたら、絶対ミスタイピングだと思われそう、、、。

univariateプロシジャのヒストグラムに要約統計量のテーブルを突っ込む話

univariateプロシジャは多機能の極みなので、できることを1つずつ紹介してたら、本できちゃいそうなので、結構基本的で結構使えるのに知らない人が多いと、僕が勝手に思ったものを適当に紹介します。

データはなんでもいいです。

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




ods graphics on;
proc univariate data=Q1 noprint;
 histogram X;
run;

とすれば





















こんな感じのヒストグラムができると思います。
histogramには、盛りだくさんのオプションが用意されていて、見栄えや幅や密度曲線はめたり、もうやりたい放題なので、詳しくはヘルプを見て下さい。

で、今回紹介したいのは上のグラフに、要約統計量の情報を突っ込むinsetです。

なにも難しいことはなくて、以下で終わりです。

proc univariate data=Q1 noprint;
 histogram X;
 inset n="N" (8.0)
 mean="平均" (8.2)
 std="標準偏差" (8.3)
 / header='要約統計量' pos=NE;
run;





















なんか右上にでてる!って感じでOKです。

inset 以下、出力したいキーワード='ラベル名'(フォーマット指定)で基本OKです。

スラッシュ以下にレイアウト絡みのオプションが、これまたいっぱいあります。
header=はそのテーブルのヘッダー、
そしてpos=はposition、つまり表示場所なのですが、これが凄い独特で、何故か
方位磁石方式なんですね。

つまり、いまpos=NEとしている NEとはNorthEast、つまり北東、すなわち右上に表示しろいう命令なんです。
なのでpos=SWとすれば、南西、つまり左下に表示されます。

なんじゃそりゃ。


SQL:主にexistsについて

以下の2つのデータセットについて、Q2に同じあるオブザベーションの存在するQ1のオブザベーションを抽出するという問題をSQLで考えてみます。(単純な問題なので画像は省略です)

data Q1;
X='い';output;
X='ろ';output;
X='は';output;
run;

data Q2;
X='は';output;
X='へ';output;
X='ほ';output;
run;

答えはX='は'の1オブザベーションです。

まず、内部結合です。

proc sql noprint;
 create table A1 as
  select Q1.X
  from Q1 inner join Q2
  on Q1.X=Q2.X;
quit;

続いて inとサブクエリを使ってみます。

proc sql noprint;
 create table A2 as
  select X
  from Q1
  where X in (select X from Q2);
quit;

続いて、ややSQL慣れしていないと意味がとりにくいですが一般的にinより処理効率がよい
existと使ってみます

proc sql noprint;
 create table A3 as
  select X
  from Q1
  where exists (select 1 from Q2 where Q1.X=Q2.X);
quit;

です。
exists後のサブクエリのselect句に1とか適当に入れてますが、実はここのselect句はなんでもいいんです!*だろうが'あああ'とかでも結果に影響しません。
なんでかって言うと、existは、サブクエリの抽出条件により結果行が1行でも返ってくるかどうかを真偽として抽出をかけるからで、select句はSQLの構文上いるだけで意味ないんです。
ただ、データベースの製品によっては1のように定数にした方がパフォーマンス速いことがあるので、プログラマーによっては1と書きます。最近は*にしとく方が一般的らしいですが、どうなんでしょう。

最後に、今回はQ1で抽出したい変数と、Q2の変数が全く同じなので
intersectが使えます。

proc sql noprint;
 create table A4 as
  select X
  from Q1
  intersect
  select X
  from Q2;
quit;

intersectについては
「SQLで他のデータセットと共通、非共通のデータをとる(差集合 積集合) EXCEPT  INTERSECT」
を参照してください。

いや、SQLはほんとう、面白いし、奥深いし、頭使いますね。







三角関係_途中.sas

過去のプログラムを整理していた際、「三角関係_途中.sas」というプログラムがでてきた。

?なんだっけ??

開けてみると

data Q1;
NAME='Aさん';X=1;Y=1;XEND=1.5;YEND=2;output;
NAME='Bさん';X=1.5;Y=2;XEND=2;YEND=1;output;
NAME='Cさん';X=2;Y=1;XEND=1;YEND=1;output;
run;
proc sgplot data=Q1; 
        scatter y=y x=x/datalabel=NAME;
        vector x=xEnd y=yEnd /xorigin=x yOrigin=y;
run; 


実行すると





















見にくいけど、線の先っぽは矢印になっていて、ラベルで「Aさん」とか「Bさん」とか入っている

あ~思い出しました。
SASを初めて1月くらいの時に、初めてsgplotのvectorを知って、使ってみたくなって作ったんでした。

テレビジョン(古い)とかによく載ってる人物相関図をSASで作ろうとして、途中で飽きて放置してたやつだ。

僕はどうも昔から、SASの使用用途をちょっと間違えちゃってる子だったんですね。


でも、この人物相関図、AさんがBさんを好きで、BさんがCさんを好きで、CさんがAさんを好きだとすると、必然的に誰かが同性愛者になってしまうような、、、。まあどうでもいいや。

smallest関数とordinal関数

かなり、ちょっとした話です。

smallest関数はn番目に小さい値を返す関数です。
ordinal関数はn番目に小さい値を返す関数です。

何が違うか?nullに対する扱いが違います。

data Q1;
 array X{4}(.,0,-1,2);
run;





に対して、ついでにmin関数も入れて、以下のコードを実行します

data A1;
 set Q1;
  min=min(of X:);
  smallest=smallest(1,of X:);
  ordinal=ordinal(1,of X:);
 keep min--ordinal;
run;

結果は





となります。

つまりminやsmallestはnull値を除外してn番目に小さい値をとり、ordinalはnullも含めてということですね。

おわり

SQL:ALL述語と極値関数を用いた比較の挙動差異について

これも本を読みかえしていて、再発見した話です。

以下のように

data Q1;
do X=8,2,4,7,1;
 output;
end;
run;








data Q2;
do X=9,3,5,5,6;
 Y='A';
 output;
end;
run;









という二つのデータセットがあったとします。

そこで
Q2の全ての値より小さいQ1のオブザベーションを抽出せよ。という問題が出たとします。

その場合、

proc sql noprint;
create table A1 as
 select X
 from Q1
 where X<ALL(select X from Q2);
quit;

と書くことができます。
結果は






です。
ALLはこのように全ての値との比較を行うことができるキーワードです。

でも、恐らく多くの方がこう思われているはずです、最小値と比較すりゃええやん、と。

つまり
proc sql noprint;
create table A2 as
 select X
 from Q1
 where X<(select min(X) from Q2);
quit;

ということで、結果は先のコードと全く同じです。

さて、ではALLと極値関数比較に違いはないのでしょうか?
あるんです。

先ほどのQ1と、新しく以下のデータセットQ3に対して同じ処理を考えます

data Q3;
do X=.,3,5,5,6;
 Y='A';
 output;
end;
run;









に対して

proc sql noprint;
create table A3 as
 select X
 from Q1
 where X<ALL(select X from Q3);

create table A4 as
 select X
 from Q1
 where X<(select min(X) from Q3);
quit;

とすると

ALLを使って作成したデータセットA3は







minを使って作成したA4は





です。

つまりALLでは、nullが入ったサブクエリ結果をとると、正しくできないので注意ということです。
whereでnullを省けば、作成可能です。

続いて、以下のコードではどうでしょうか

proc sql noprint;
create table A5 as
 select X
 from Q1
 where X<ALL(select X from Q3 where Y='B');

create table A6 as
 select X
 from Q1
 where X<(select min(X) from Q3 where Y='B');
quit;

Q3に対してwhere Y='B'としていますが、Q3のYの値は全て'A'なので、このサブクエリは空集合です。
さてこのように空集合が引数となった場合のALLの結果は









つまり、条件がかからずQ1がそのまま抽出されます。

対してmin関数で作成したA6については








です。

つまりALLと極値関数比較の違いは

★サブクエリ結果にnullが含まれる場合
  ALL→×
  極値→○
★サブクエリ結果そのものが空集合である場合
  ALL→全オブザベーションがそのままでる
  極値→×

といった感じなので、そこを踏まえて使いましょうということでした。


SQL:自己結合、自己非等値結合で直積から組み合わせまで

以前、
「n個の変数からm個の変数を選択する、組み合わせのデータセットを作成する方法」
http://sas-tumesas.blogspot.jp/2014/01/m.html

では、データステップで4C2の組み合わせデータセットを作りましたが、
今回はSQLで同じことをやっていきます。

自己結合は1つのデータセットを、まるで同じものが複数あるかのように見立てて結合する
やり方で、生粋のSAS使いには苦手な人が多いような気がします。

data Q1;
X='い';output;
X='ろ';output;
X='は';output;
X='に';output;
run;








今回はまず、上図のような形で縦に値を持たせたデータセットを作ります。


そして、まず直積の説明(cross joinでも可)

proc sql noprint;
 create table A1 as
  select T1.X as X1
        ,T2.X as X2
  from Q1 T1,Q1 T2;
quit;

結果は




















となって、4掛ける4で16オブザベーションになります。
自己結合の場合、変数がどっちのテーブル由来(同じテーブルなのでどっちとかってのも変ですが)
か記述できなくなるので
Q1 T1のように「テーブル名 半角スペース 別名」として、Q1テーブルにT1という仮名を与えます。
Q1 as T1のようにas使ってもOKです



これだと、「い」「い」のように同じ値を2回使ったりします。

次に、同じものが2度現れない順列で
4P2を表現するなら、同じ値の出現を消せばいいわけなので

proc sql noprint;
 create table A2 as
  select T1.X as X1
        ,T2.X as X2
  from Q1 T1,Q1 T2
  where T1.X^=T2.X;
quit;

とすれば
















のように12オブザベーションになります。

ただ、これだと「い」「ろ」と「ろ」「い」のように、順番を並び変えただけの組み合わせ的には
同じものが含まれます。


そこで、 where T1.X^=T2.X;としているところをwhere T1.X<T2.X;としてしまいます。
文字列に不等号使うのに違和感を感じるかもしれませんが、文字の出現順で大小決まるだけなので数字と変わりません。
要するに、2パターンでてこられると困るので、不等号にしとけば絶対1パターンしかでないでしょって感覚なわけです。

proc sql noprint;
 create table A3 as
  select T1.X as X1
        ,T2.X as X2
  from Q1 T1,Q1 T2
  where T1.X<T2.X;
quit;


結果は










6オブザベーションで、正解です。

このあたりの話は以前SQL関連の書籍を紹介した中の
http://sas-tumesas.blogspot.jp/2013/10/sassql.html


達人に学ぶ SQL徹底指南書
 著者:ミック
 出版社:翔泳社 (2008/2/7)

を参考にしています。




ちょっとした話 配列要素にコロンとループ変数の初期値

もしかしたら既に別の投稿で触れていたかもしれませんが、最近はっとした、小話です。

data Q1;
 array X{5}(1:5);
run;

とすると




こうなります。

おぉ~、in演算子のコロン範囲指定と同じ理屈か~。

コロン範囲指定については
SAS忘備録の「条件式(IF/WHERE)におけるINオペレータの小技」
http://sas-boubi.blogspot.jp/2014/01/in.html


ちなみに全部に1を入れたければ
data _Q1;
 array X{5}(5*1);
run;

と書けば、1を5要素に入れるという指定になります。


もう一つは、もしかしたら常識なのかも

data Q2;
 do until(X=2);
  output;
  X+1;
 end;
run;

の結果は






です。

あ~、いっつもループ変数に初期値割り当ててから回してたから、意識したことなかったけど
なにも指定しないと0から始まるんだ、そうだっけ?知ってたような、初めて知ったような。


SASプログラマーはどこにいるのか

SASのコードをガリガリ書いている人って、日本にどれだけいて
みんなどこで働いてんのかなぁって仕事中にぼんやり考えたりしませんか?

SAS人口はさておき、生息地域について、このブログのアクセスログから考えてみました。

おかげさまで1周年なので、昨年の9月から今年の9月までのセッション数(1回サイトを訪れたら1カウントみたいな感じ)を地域で分けて、降順に並べてみました。括弧内の割合は、期間中の全セッション数に占める割合です。

おそらく、このブログを見る人の大半は普段SASコードを書いておられる人のはずです。

当然、たまたま検索で迷い込んできたりする人もいますし、アクセス解析の地域っていうのは非常にざっくりしたものだそうです(モバイルでアクセスしてる場合も特に)。そもそも分け方がよくわからないし。また同一ユーザーの複数アクセスを重複カウントしているんで、熱心にたくさん見て下さる方がいる地域は上位に来ちゃいます。あと、ちゃんと細かい設定していなかったせいで、僕の居住地域(埼玉)の順位がかなり上位になったりして、ノイズが激しいのですが、まあお遊び程度に見て下さい。

まずTOP10から






















千代田区、中央区、港区、新宿区だけで、全体の4割ぐらいになっちゃってますね。
まあ、会社自体多いですし、医薬系のメーカーやCROも多いし、どんな業界も、基本的に分析とかデータを集める機能は本社の部署に持たすことが多くて、その本社が東京って場合が多いですもんね。

以下











































これを見ると、かならずしも関東だけではないですね。大学とか研究機関でもSASコードは
思いっきり書くだろうし。

無償版のSAS University Editionがリリースされたことで、アクセス地域にどのような変化が
起こるかとか解析したら面白いのかもしれませんね。