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がリリースされたことで、アクセス地域にどのような変化が
起こるかとか解析したら面白いのかもしれませんね。



カテゴリ数が少ない場合の頻出アイテムセットの単純集計をする話

僕は医薬系でSASを使ってきましたが、そんな僕でも流石にビールと紙おむつの話ぐらいは聞いたことがあります。

スーパーの買い物データを、Aの商品とBの商品を一緒に購入している客数という見方で分析したところ、ビールと紙おむつを一緒に買っている人の数が多かったので、売り場の位置を隣にしたら売上が上がって良かったね!バスケット分析だね、データマイニングの始まりだねって例の話です。
なんか事実かどうかは諸説あったりするらしいので興味のある方は調べてみて下さい。

今回はそういう同時に購入されている商品ペア(アイテムセット)の単純カウントの話です。

以前、メールで質問があって、

「いつも見てます。すみませんけど、売上データから、2つの商品を同時購入している件数を出したいです。支持度とか信頼度とかの計算はいいので、とりあえず件数を降順に出せればいいです。対象商品の種類は多くて15種類程度です。当方、SQLはわからないので使わないでください。IMLもありません。商品マスタは使わずに、売上データだけで、かつデータが更新されてもコードを変える必要がないものにしてください」(割と原文ママ)

という内容でした。

ちょっとその時期、時間が無かったので、一応ぱっと思いついたものを書きました。

まずは以下の感じで、売上データを作りました。

data Q1;
call streaminit(1234);
do NAME='A','B','C','D','E','F';
 do i=1 to rand('table',0.1,0.1,0.3,0.3);
   ITEM=choosec(rand('table',0.2,0.2,0.2,0.2),'オムツ','ビール','パン','牛乳','りんご');
   output;
 end;
end;
drop i;
run;
proc sort data=Q1 nodupkey;
 by NAME ITEM;
run; 






















NAMEは顧客IDでITEMが購入商品です。
Aさんが買ったのは、[りんご]と[ビール]と[牛乳]です。
2つで1カウントのアイテムセットで考えると
[りんご,ビール] [りんご,牛乳] [ビール,牛乳]の3つになります。

さてこの顧客データから、一番多いアイテムセットは何でしょう?という問題です。

proc transpose data=Q1 out=Q2 prefix=I_;
 var ITEM;
 by NAME;
run;

data Q3;
 set Q2;
   array AI{*} $ I_:;
   do i=1 to comb(dim(AI),2);
      call allcomb(i, 2, of AI(*));
 if I_1^='' and I_2^='' then output;
   end;
 keep NAME I_1 I_2;
run;

proc freq data=Q3 noprint;
 tables I_1*I_2/out=A1(drop=PERCENT);
run;

proc sort data=A1;
 by descending COUNT;
run;


で結果は

















となって、良し、ビールと牛乳並べて売っとくか!ってことです。(データも結果の解釈も適当なのでそこは当てにしないでください)

この方法はとりあえず転置して、購入商品の情報を横持ちにしてから
call allcombで組み合わせを作成しています。

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

を参考にしてください。

しかし、このコードの最大の欠陥はcall allcombが引数を20個までしか持てないということです。
つまり売り上げデータにおいて商品の種類が20種類以内じゃないとダメなわけです。
ちょっと実践的じゃないですね。

で後日、調べてみると色んなプログラムが既に発表されていました。

2011年のSASユーザー総会で「SASによる頻出アイテムセットの抽出」とタイトルものが発表されていて、まさしくばっちりな内容でした。
著者の谷岡田出男さんが代表取締役をされているデータマインテック株式会社のWebページに
プログラム全てが公開されていました。
http://www.dataminetech.co.jp/soft/disallow/SAS_PGM20110613.sas

自分で書く前に調べとけばよかったです。

ちなみについ最近から、1982年から2013年までの全ての論文がPDF形式で閲覧可能になりました。
SASのプレミアムラウンジにユーザー登録して、「ダウンロード」タブから「SASユーザー総会資料」を選択すれば、なんと全部無料で見放題です。
素晴らしいですね。








GKPIプロシジャは見た目が楽しくて良いという話

例えば、何かプロジェクトの進捗を報告する際に

「先月末までに進捗率80%を目標としていましたが、実際は65%でした。
目標未達ですが、50%以上であれば許容範囲と想定していたので、まあ、勘弁してください。」

的なことを文章でつらつら書いてもなんか冗長でイマイチです。
65%(目標値85%)とだけ書いてもなんか味気ないです。

そういう時は、GKPIプロシジャで話の種になりそうな、それっぽいキャッチーな図を描いて
誤魔化して?しまいましょう。

ods listing close;
goptions reset=all device=javaimg;
ods html;

proc gkpi mode=raised;
speedometer actual=0.65 bounds=(0 0.25 0.5 0.75 1) 
            /target=0.8
format="percent8."
             colors=(red yellow green blue);
run; 
quit;

先に結果から















うん、おもしろいから良し!


まず、device=javaimgは、このgkpiの出力はjavaimageとやらじゃないといけないらしいので
決まり文句です。

mode=raisedは、外して実行してみればわかりますが、なんというか立体感?てかり?みたいな
ものをグラフに持たせるかどうかのオプションです。

speedometerはそのまんま、速度計型に出力してくださいという命令

actual=0.65は実測値ですね
bounds=(0 0.25 0.5 0.75 1)はメータの色区切りですね。
target=0.8は目標値
format="percent8."はフォーマット
colors=(red yellow green blue)は分割された色区切りの色を順番に指定してます。

ちなみに
speedometerの部分は変えることができます。
順番にコードと結果を示します

proc gkpi mode=raised;
trafficlight actual=0.65 bounds=(0 0.25 0.5 0.75 1) 
            /target=0.8
format="percent8."
             colors=(red yellow green blue);
run; 
quit;
























proc gkpi mode=raised;
hslider actual=0.65 bounds=(0 0.25 0.5 0.75 1) 
            /target=0.8
format="percent8."
             colors=(red yellow green blue);
run; 
quit;









proc gkpi mode=raised;
vbullet actual=0.65 bounds=(0 0.25 0.5 0.75 1) 
            /target=0.8
label='目標'
format="percent8."
             colors=(red yellow green blue);
run; 

quit;

























ちなみに、以前紹介した
「SASでGIFアニメを作る」
http://sas-tumesas.blogspot.jp/2014/07/sasgif.html

では、パラパラ漫画の遷移速度を指定できるので、
経過時間と速度のデータから、リアルな速度計の加速アニメーションがつくれるはずです。
多分。



ループ処理のネスト階層をログにだしてデバックしやすくする方法

ループは深く掘りすぎると、どこで何してんだか分かりにくくなります。
さらにインデントもちゃんとされていないようなコードの検証作業などになると、ちょっとつらいです。

例えば以下のようなコードの場合、どの階層で割り当てステートメントが実行されているか
わかりにくいです。

data A1;
do X=1 to 2;
A=1;
do Y=1 to 2;
B=2;
do Z=1 to 2;
C=3;
output;
end;
D=4;
end;
E=5;
end;
F=6;
run;


そういった時は

data A1/nesting;
do X=1 to 2;
A=1;
do Y=1 to 2;
B=2;
do Z=1 to 2;
C=3;
output;
end;
D=4;
end;
E=5;
end;
F=6;
run;

と/nesting;をつけて実行してみましょう。

ログに




























ちょっと画像だと見にくいかもですが、
ループのカウンター変数(或いはuntilやwhileの場合はその部分)に赤下線が入ります。
endステートメントにも入ります。
赤下線でも別にエラーというわけではないです。

そしてその赤下線の下に
「 *** DO begin level 1 ***」や「 *** DO end level 1 ***」といったように、ループの開始位置と終了位置はここですよと教えてくれます。

ささやかなで単純な助けなので、つけなくてもループ文くらい読み解けるわって思うのですが、
案外馬鹿にできないもので、うまくいかずに泥沼化した時は素直につけるようにしています。



文字列の部分一致による特殊な結合

今、以下のデータセットがあるとします。

data Q1;
X='ABCD';output;
X='BCBD';output;
X='ACAC';output;
X='ABAB';output;
X='ADCB';output;
run;









次に以下のデータセットをみます

data Q2;
Y='AB';Z=1;output;
Y='BC';Z=2;output;
Y='BD';Z=3;output;
Y='CA';Z=4;output;
run;








データセットQ1の1オブザベーション目のXは'ABCD'ですが、この文字列は
Q2のY='AB'とY='BC'を部分的に含んでいます。

このように片方のデータセットの文字列がもう一方に含まれている場合に結び付ける結合を考えてみます。

こういった結合になんか決まった名前あるんですかね?

SQLで、僕のレベルでぱっと思いつくのは、

proc sql noprint;
 create table A1 as 
  select X,Y,Z
  from Q1,Q2
  where X contains Y;
quit;

って感じです。
結果は










パフォーマンスは置いといて、まあとりあえず直積作ってから
一方が一方に含まれているって抽出式です。単純明快。

この方法の結果について、少し気になるのが

Q1のX='ABAB'に対してQ2のY='AB'を1オブザベーションで出力しているところです。
出現回数を考慮するのであれば、ABが2回出現しているので、2オブザベーション出力ということになります。

さて、出現回数分オブザベーションを起こすとなると、SQLでやる場合、かなり難易度があがりそうです(すみません、ノ―アイデアでした。分かる方、コメントお願いします)

まあ、SQLで難しいならデータステップでやりましょう。

data A2;
set Q1;
 do i=1 to Q2OBS;
  set Q2 nobs=Q2OBS point=i;
    do j=1 to count(X,Y);
     output;
end;
  end;
 drop j;
run;

で結果は











です。狙い通りABが2obs起きてますね

直積については
「詰めSAS4回目_直積(デカルト積、単純結合)を作成する」
http://sas-tumesas.blogspot.jp/2013/09/sas4.html


基本的に、なんかわけわからん条件がついた結合は、直積作ってからの条件式や、ループで、
だいたいケリがつきます。データが巨大になってくると、考え物ですが。

よくあるのが、文字列の類似度をスコア化して、一定水準以上の類似が見られる場合は
キーが一致したとみなして結合するような、キー一致条件緩和タイプのマージ処理です。
実践例としては、臨床開発ならフリーテキストで収集された薬剤名に薬剤辞書のコードをふる際の
候補レコードを出すような、まあ名寄せ処理的なケースですね。

基本、今回のコードに

「データステップ100満開 文字列がどれくらい類似しているかを定量化する」
http://sas-tumesas.blogspot.jp/2014/03/100.html

の内容を合わせてやれば簡単です。

以上です。


ちなみにもう9月ですが、ちょうどこのブログを始めたのが昨年の9月だったので
あっという間に1年です。
1-2カ月でネタ切れ閉店の予定が思いのほか長く続いてます