ラベル ハッシュオブジェクト の投稿を表示しています。 すべての投稿を表示
ラベル ハッシュオブジェクト の投稿を表示しています。 すべての投稿を表示

強さとはなにか。イロレーティングの話

最近教えて貰ったんですが、ドラゴンボールのフリーザって、最近の映画で復活して、体が金色になって戦闘力が1垓(京の上、100000000000000000000)になったそうです。
僕が知っているのは「私の戦闘力は53万です」ですげーっ!!て時代だったんで、そんなジンバブエドルみたいな単位になってるとは知りませんでした

しかし、スカウターみたいな便利なものがあればいいですが(よく爆発するけど)、強さといった複合的要因でなりたっているものをスコア化するのはなかなか難しいです。
また、普段からスカウター慣れしていないと算出された戦闘力の差が、どの程度ならどのくらい勝てるかいうことが実感できないです。
(正確な値だしているのに、ちっ壊れてやがる!グシャっていうシーンあった気もするけど、慣れすぎているが故の弊害でしょうね)


そういう実際の能力を数値化する方法と違うアプローチとして、測定対象の対戦結果データがある程度存在する場合、強さ(勝率と捉える)が正規分布に従うと仮定し、数値化した値の差から勝率が逆算できるように作られたイロレーティングというものがあります。

原理など詳しくはwikiや参考ページなど検索してみてください。

【wikipedia】
https://ja.wikipedia.org/wiki/%E3%82%A4%E3%83%AD%E3%83%AC%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0

【ヘキサドライブ(ゲーム会社)公式ブログ】
http://hexadrive.sblo.jp/article/67433861.html


イロレーティングの肝となるのは以下三点。

・ゲームの結果は一方の勝ち、一方の負けのみとし、引き分けは考慮しない(0.5勝0.5敗と扱うものとする)。
・200点のレート差がある対局者間では、レートの高い側が約76パーセントの確率で勝利する。
・平均的な対局者のレートを1500とする。

特に2点目、レーティングの差から、仮に相手を見たこともなくても勝率が推定できるという
のがいいんです。

もともとチェスで運用されていたもので、今はネット将棋、囲碁など主たるサイトはこれを基本にそれぞれカスタマイズして使ってユーザーの実力を測り、対戦マッチング等に利用しています。

僕もネット将棋やるのですが、これが結構よくできてるなぁって実感します。
実際、後で自分の対戦成績と対戦相手のレーティングで集計してみると200差の相手には
3割勝ててないですし、レーティングで100あいてると、やっててもちょっと自分より強いなって感覚を持ちます

算出法はいたって簡単、何局か後でまとめて計算するのと都度計算するので多少ロジック代わりますが、

プレイヤー1のレーティングがrate1、プレーヤー2がrate2とすると
プレイヤー1が勝利する確率e1は以下で算出できます
e1 = 1/( 1 + 10 ** ((rate2-rate1)/400) );

そして、対局後のレーティングは,
勝利の場合S=1 敗北はS=0 引き分けはS=0.5として
対局後のレーティング = 現在のレーティング + 16 * ( S  - e1);

16は定数で、プロなら16、アマなら32にすると安定するといわれています

例えば以下のように現在のレーティングデータと
data Rating;
length name $10.; 
input name rate;
cards;
A 1500
B 1500
C 1500
D 1500
;
run;












時系列に並んだ対局結果のデータセットがあった場合
(1はプレイヤー1が勝者、2は2が勝者、3は引き分け)
data hosi;
length player1 player2 $10.;
input player1 player2 win;
winner=choosec(win,player1,player2,'引き分け');
cards;
A B 1
B A 2
D A 2
B C 1
C B 2
B D 1
D B 1
A C 1
A D 1
C D 3
D C 1
C A 2
D C 1
C D 2
B C 1
;
run;




























コードは

data cal;
if _N_=0 then set rating;
if _N_=1 then do;
declare hash h1(dataset:'rating',ordered:'Y');
h1.definekey('name');
h1.definedata('name','rate');
h1.definedone();
end;
set hosi end=eof;

if h1.check(key:player1) ne 0 then do;
h1.add(key:player1,data:player1,data:1500);
end;
if h1.check(key:player2) ne 0 then do;
h1.add(key:player2,data:player2,data:1500);
end;

h1.find(key:player1);
rate1=rate;
h1.find(key:player2);
rate2=rate;

e1 = 1/( 1 + 10 ** ((rate2-rate1)/400) );
e2 = 1/( 1 + 10 ** ((rate1-rate2)/400) );
rate1 = rate1 + 16*(ifn(win=1,1,ifn(win=3,0.5,0)) - e1);
rate2 = rate2 + 16*(ifn(win=2,1,ifn(win=3,0.5,0)) - e2);

h1.replace(key:player1,data:player1,data:rate1);
h1.replace(key:player2,data:player2,data:rate2);

h1.output(dataset:cats('rating',_N_));

run;

とすれば、1対局ごとのレーティングの状態をデータセット化できます
(わかりやすくするために1局ごとに作ってますが、普通はif eofで最後の
時だけratingのデータセットを吐けばいいです)

1局目、A対BでAが勝利した後のデータセットをみてみると、
このようにAが少しあがり、Bが少し下がります。












最終的に全対局後をみると以下のようになり











全勝のAと全敗のCでは100ほど開いています。
100だと勝率64%ぐらいです

ある程度データがたまらないと安定したレーティングは得られませんが
まあ、AとCが戦ったら6割5分Aが勝ちそうだというのは感覚的に納得できます

ちなみに式をみればわかりやすいですが、レーティング差が大きい相手との
対局ほど、移動するレーティングが大きくなります。
雑魚に負けた方はレーティングが大きく下がり、下克上した方は大きく上がります。
わかりやすいですね。

イロレーティングの改良法については色々研究されているみたいなので興味のある方はぜひ
勉強してみてください

2地点間の緯度経度から直線距離でもっとも近いデータを取得する話 geodist関数

親戚の子供の宿題を手伝っていたんですが、以下の問題に頭抱えました。

「県庁所在地で、直線距離がもっとも近いのは何県と何県?
地図帳とものさしを使って考えてみよう」

うわぁ~、凄いめんどい。

「名古屋と岐阜であってるよね」って言われても、そんなのわかんない。
大阪-神戸だと思ったけど自信なし

あたりをつけて、さし当ててみるけど、結構難しいぞこれ。
わかります??

困った時のSAS On Demand
ネットさえつながればどこでも自由にSAS使えるので、ちょっとPC借りて
以下のコードをパチパチ。

data q1;
length no 8. shi $10. ido keido 8.;
input no shi ido keido;
cards;
1 札幌市 43.063968 141.347899
2 青森市 40.824623 140.740593
3 盛岡市 39.703531 141.152667
4 仙台市 38.268839 140.872103
5 秋田市 39.7186 140.102334
6 山形市 38.240437 140.363634
7 福島市 37.750299 140.467521
8 水戸市 36.341813 140.446793
9 宇都宮市 36.565725 139.883565
10 前橋市 36.391208 139.060156
11 さいたま市 35.857428 139.648933
12 千葉市 35.605058 140.123308
13 新宿区 35.689521 139.691704
14 横浜市 35.447753 139.642514
15 新潟市 37.902418 139.023221
16 富山市 36.69529 137.211338
17 金沢市 36.594682 136.625573
18 福井市 36.065219 136.221642
19 甲府市 35.664158 138.568449
20 長野市 36.651289 138.181224
21 岐阜市 35.391227 136.722291
22 静岡市 34.976978 138.383054
23 名古屋市 35.180188 136.906565
24 津市 34.730283 136.508591
25 大津市 35.004531 135.86859
26 京都市 35.021004 135.755608
27 大阪市 34.686316 135.519711
28 神戸市 34.691279 135.183025
29 奈良市 34.685333 135.832744
30 和歌山市 34.226034 135.167506
31 鳥取市 35.503869 134.237672
32 松江市 35.472297 133.050499
33 岡山市 34.661772 133.934675
34 広島市 34.39656 132.459622
35 山口市 34.186121 131.4705
36 徳島市 34.06577 134.559303
37 高松市 34.340149 134.043444
38 松山市 33.84166 132.765362
39 高知市 33.559705 133.53108
40 福岡市 33.606785 130.418314
41 佐賀市 33.249367 130.298822
42 長崎市 32.744839 129.873756
43 熊本市 32.789828 130.741667
44 大分市 33.238194 131.612591
45 宮崎市 31.91109 131.423855
46 鹿児島市 31.560148 130.557981
47 那覇市 26.212401 127.680932
;
run;

県庁所在地と緯度経度をネットから適当に拾ってきて
geodist関数にあてます。
geodist関数は(地点1の緯度,地点1の経度,地点2の緯度,地点2の経度)で、距離を返します(デフォルトなら単位はキロメートル、オプションでマイルにもできます)

ちなみに緯度経度には日本測地系と世界測地系があって、
微妙に違うので、できれば世界に変換して同じ測地系で比べましょう。

変換法はPHPだけど式は同じなので以下のページで紹介されているコードなんかがいいと
思います
http://d.hatena.ne.jp/nakamura001/20080501/1209660263


で、書いたコードは以下のとおり、これぐらいのオブザベーション数なら
直積作ってもよかったですけどハッシュでもわりとあっさり書けました。

data a1;
length _shi $10. _ido _keido 8.;
if _N_=1 then do;
call missing(_shi,_ido,_keido);
declare hash h1(dataset:'q1(rename=(shi=_shi ido=_ido keido=_keido))');
h1.definekey('no');
h1.definedata('_shi','_ido','_keido');
h1.definedone();
end;
set q1;
do no= 1 to 47;
rc=h1.find();
dist=geodist(ido,keido,_ido,_keido);
if dist^= 0 and (dist<mindist or mindist=.) then do;
mindist=dist;
moyori=_shi;
end;
end;
keep shi moyori mindist;
run;

proc sort data=A1;
by mindist;
run;
proc print;
run;

結果は



















































































滋賀の大津市と京都の京都市か~、確かに近いな!
10キロちょっとか

神戸にとっての最寄は大阪だけど大阪の最寄は奈良か。
へ~、そういう感じだったんですね。
勉強になりました。

詰めSAS データステップで新しく作成された変数か、元からデータセットに あった変数かを判定する方法を考えてみる。vinarray関数とか

結構、よく質問されるのが、データステップ中で新しく追加された変数か、
読み込んだデータセットに元からあった変数どうかを判別したいみたいな話です。

いちいち、データセットの仕様書つくるの面倒くさいから
先にプログラム書いちゃって、その実行ついでに新規変数がどうやって生成されているかを
取得して、仕様書を自動生成しちゃえば楽だよなぁみたいな、おバカなこと考えたことありません?
え、いや、僕はないですけどね。

ところが、SASの場合、これが結構ハードで厳しい。
手抜きするつもりが、真面目にやる1000倍くらい時間がかかったりするのもよくある話です

しかも、いまだに完全に納得できる方法が思いついてません
誰か教えてください。

とりあえず、前後のデータセットをコンペアしてその結果を読み込むという
面倒すぎる方法は除外して、1ステップ内で、本処理のついでにできる方法に限定します。

僕が考えた案を2つほど。

以下のデータセットがあります。



変数 a b c dがあります。

data Q1;
a=1;b=2;c=1;d='A';
run;

まず一つ目の方法は以下です。

data A1;
set Q1;
array an _numeric_;
array cn _character_;

/*新しい変数 e f g 作る*/
e=1;
f='b';
g=a+b;

/*チェックしてみる*/
flg_a=ifc(vinarray(a),'元','新');
flg_b=ifc(vinarray(b),'元','新');
flg_c=ifc(vinarray(c),'元','新');
flg_d=ifc(vinarray(d),'元','新');
flg_e=ifc(vinarray(e),'元','新');
flg_f=ifc(vinarray(f),'元','新');
flg_g=ifc(vinarray(g),'元','新');

put (flg_a--flg_g) (/=);

run;

判定結果をputしているので、それを見てみると











追加した3つの変数が正しく判定されているのがわかります。

なにをしているかというと、最初に数値型、文字型それぞれ全変数を
配列に定義しちゃいます。

で、その後、割り当てステートメントがはいるわけですが、
ここで新規に割り付けられた変数は、最初に定義している配列には含まれていない
わけです。

そこで、判定の箇所でvinarray関数という、引数の変数が何かしらの配列に属しているか
どうかを1 , 0で返すニッチな関数で判定します。
配列に入ってないということは、新しい変数だろってノリで。

しかしまあ、お気づきの方もいるでしょうが、これは新しく作る変数を配列に定義しているような
プログラムでは正しく判定できない限定的な方法ですね。
e f gが何の配列にもたまたま定義されていないからそれでいけてるわけです。
同じく、新規変数をarrayで生成したりしても判別不能になります。
(他にも弱点はありますがそれは後で)

次に思いついたのはハッシュオブジェクトです。

data A2;
set Q1;

/*この時点での全変数の情報を一旦ハッシュオブジェクトにいれる*/
if _N_=1 then do;
 declare hash h1();
 h1.definekey('vname');
 h1.definedone();
 array an _numeric_;
 array cn _character_;
 do over an;
  vname=vname(an);
h1.add();
 end;
 do over cn;
  vname=vname(cn);
h1.add();
 end;
 drop vname;
end;

/*新しい変数 e f g 作る*/
e=1;
f='b';
g=a+b;

/*チェックしてみる*/
flg_a=ifc(h1.check(key:vname(a)),'新','元');
flg_b=ifc(h1.check(key:vname(b)),'新','元');
flg_c=ifc(h1.check(key:vname(c)),'新','元');
flg_d=ifc(h1.check(key:vname(d)),'新','元');
flg_e=ifc(h1.check(key:vname(e)),'新','元');
flg_f=ifc(h1.check(key:vname(f)),'新','元');
flg_g=ifc(h1.check(key:vname(g)),'新','元');

put (flg_a--flg_g) (/=);

run;

結果は同じなので割愛。

最初のレコードの時点のPDVの変数名情報をハッシュオブジェクトに格納して
ステップの終わりで、checkメソッドで、存在するかどうかを判定する方法
(さっきと逆に、あれば0,なければエラーコードとして0以外の数字が戻ります)
これならば、ステップの途中で新規変数に配列つかったりしてても問題なしです。


ただ、2つの方法とも、致命的に弱点なのがあくまでステップ開始時の
PDVにある変数を、元からある変数だと考えている点です。

つまり length f $100.;といったようにlengthやformatステートメントなど
データを読み込む前に箱を定義しちゃうようなものがはいると、fは実際は元データセットに無いけど
元からある変数ってことになるんですね。
ここが僕の限界。

う~ん、どうするのが正解なんでしょう。
contentsプロシジャとかsqlとかで、ガチで大元のデータセット情報読み込むコードを
作ってステップ内でdosubl関数で回すとか?

あるいは新規データセットのPDVに影響受けずに変数名をとれるのかな?
if _N_=0の状態でvnextルーチン回したりできんのかな?

誰か教えてください

ユニークなIDとか作れって言われたらの話

多分需要ないし、方法に工夫もないけど、与えらえた文字列から指定の長さで
ユニークなIDを作成するマクロです。

すでに発行済のIDが入ったデータセットを指定することで
過去に発行したものとも重複しない作成が可能です。

使用可能文字の数と、作成IDの長さの設定によって、生成できるIDの限界数が
決まります。
例えば100万の生成限界なのに90万のIDを発行したりすると実行効率遅いです。
生成限界はできるだけ余裕を持たせてください。

アルゴリズムは単純ですが
生成限界がぶっとんだ値であれば、何十万発行しようが結構速いと思います。

すでに存在するデータセットに付与して作りたい場合は
マクロをちょっと書き換えてsetをいれて、do untilのループをとっちゃえばいいです。


%macro unique_make(outds=,obs=,idlength=,seed=,moji=,ban=);
/*--------------------------------------------------------
outds=作成されるデータセット
obs =作成するオブザベーション数
idlength=作成される文字列の長さ
seed =作成のための乱数シード
moji =使用する文字列(半角英数字)
ban  =データセット指定(ここにあるidは作成されない)
----------------------------------------------------------*/
data &outds(keep = id);
length id $&idlength..;
if _N_ = 1 then do;
declare hash h1
%if %length(&ban) =0 %then %do;();%end;
%else %do;(dataset:"&ban");%end;
h1.definekey('id');
h1.definedone();
end;
x="&moji";
n=length(x);
kumi=n**&idlength;

put 'NOTE:与えられた文字の数は ' n;

put 'NOTE:生成限界は ' kumi;

call streaminit(&seed);

do until(obs=&obs);
do until(okfl=1);
do i = 1 to &idlength;
r=int(rand('uniform') * n +1);
id = cats(id , char(x,r));
if i = 5 then do;
if h1.check() ne 0 then do;
okfl=1;
h1.add();
output;
obs+1;
end;
else id='';
end;
end;
end;
end;
run;
%mend;

以下実行例です。


/*英数字で5桁のIDを10000発行*/

%unique_make(outds=A1
 ,obs=10000
 ,idlength=5
 ,seed=2345
 ,moji=abcdefghijklmnopqrstuvwxyz0123456789
);

/*先に発行したA1とかぶらないようにさらに10000発行*/

%unique_make(outds=A2
 ,obs=10000
 ,idlength=5
 ,seed=6789/*シード変えた方がよい*/
 ,moji=abcdefghijklmnopqrstuvwxyz0123456789
 ,ban=A1
);

SASで作った迷路をSASで解いてやったぞ!誰もそんなこと求めてないのは知ってるけどね!な話

ブログ「僕の頁 <SASと臨床試験と雑談と>」のSASNAMIさんも最近RWIに手をだされて、のっけから素晴らしい記事を書かれてます。

Report Writing Interface (RWI)を試してみる
http://sasboku.blog.fc2.com/blog-entry-54.html

そして、そのSASNAMIさんに火をつけた張本人である忘備録のa.matsuさんはというと、以下のような記事を書いて、もう何か色々イっちゃってます。

RWIで迷路を作る
http://sas-boubi.blogspot.jp/2015/08/rwi.html


迷路作成プログラムできましたか、matsuさんが迷路をSASで生成するというのなら
それならば僕はその迷路を解くプログラムを書きましょう!

というわけで、どうやって解くかなのです。迷路解法アルゴリズムは色々あるみたいなのですが
今回はセルオートマトンというものを使って解いてみましょう。

迷路生成部分と、それをRWIで描画する部分は忘備録の丸コピでいかせていただきます。
※ちょっとだけ迷路のサイズを小さくしているのは、解法過程をGIFアニメにするのにデフォルトの設定でちょうど1枚に入りきる、いいサイズにしたかったからです。アルゴリズム的にはどんな巨大な迷路でも解けます。


%let n    = 31;
%let init = 1234;

data DT1;
  *** 全マス目分の配列を定義 ;
  array AR(&n, &n) ;

  *** 最初から壁にする部分を2に設定 ;
  do y=1 to &n;
      do x=1 to &n;
         if x in (1,&n) or y in (1,&n) or mod(x*y,2)=1 then AR(x,y)=2;
         else AR(x,y)=1;
      end;
  end;

  *** 乱数を生成して壁の位置を決める ;
  *** 壁の位置 1:上、2:下、3:右、4:左 ;
  call streaminit(&init);
  do y=3 to &n-2 by 2;
  do x=3 to &n-2 by 2;

     *** 1段目は上下左右の壁の乱数を生成 ;
     if y=3 then do;
         * 左のマスが壁だったら上下右のみの壁の乱数を生成 ;
         if AR(x-1,y)=2 then  RAND = ceil(rand('uniform')*3);
         else RAND = ceil(rand('uniform')*4);
     end;

     *** 2段目以降は下左右の壁の乱数を生成 ;
     if y^=3 then do;
         * 左のマスが壁だったら下右のみの壁の乱数を生成 ;
         if AR(x-1,y)=2 then  RAND = ceil(rand('uniform')*2)+1;
         else RAND = ceil(rand('uniform')*3)+1;
     end;

     if RAND=1 then AR(x,y-1)=2; * 上 ;
     if RAND=2 then AR(x,y+1)=2; * 下 ;
     if RAND=3 then AR(x+1,y)=2; * 右 ;
     if RAND=4 then AR(x-1,y)=2; * 左 ;
  end;
  end;
run;

描画描画部分については、マクロ化しています。

%macro rwi(data=);
/*描画マクロ*/
data _NULL_;
  length VAR1 $5.;
  set &data;
  array AR(&n, &n);
  dcl odsout ob();
  ob.table_start();

  do y=1 to &n;
      ob.row_start();

      do x=1 to &n;

         *** 壁と床を描画 ;
          VAR1="";
          if (x=2 and y=2) or (x=&n-1 and y=&n-1) then VAR1="☆";
          ob.format_cell(data:VAR1,  style_attr: "background=" || choosec(AR(x,y),"white","black") ||
                                                                  " height=0.4cm  width=0.3cm");
      end;

      ob.row_end();
  end;

  ob.table_end();
run;
%mend;

そして、ここからが解法部分のコードになります。

%macro automaton;
/*GIFアニメの開始設定*/
options  ANIMATION=START  ANIMDURATION=0.5   PRINTERPATH=GIF ;
ods printer file="/home/sasyamasasyama0/sasuser.v94/meiro.gif"; ;

/*初期局面の描画*/
%rwi(data=DT1)

/*世代データセットの作成*/
data _DT1(genmax=2);
set DT1;
run;

/*ループ脱出条件*/
%let stop = ;

%do %until(&stop=1);
data _DT1;
array AR(&n, &n);
set _DT1;

do i=2 to &n-1;
do j=2 to &n-1;
if ^(i=2 and j=2) & ^(i=&n-1 and j=&n-1) then do;
if sum(AR{i-1, j}=2 , AR{i+1, j}=2 , AR{i, j-1}=2 , AR{i, j+1}=2)=3 then AR{i, j}=2;
end;
end;
end;
run;

/*ハッシュオブジェクトのequalsメソッドで1世代前のデータセットと差があるかを0 1で判定する*/
data _null_;
if 0 then set _DT1;
declare hash hq1(dataset:'_DT1');
hq1.defineKey(all:'yes');
hq1.defineDone();
declare hash hq2(dataset:'_DT1(gennum=-1)');
hq2.defineKey(all:'yes');
hq2.defineDone();
hq1.equals(hash: 'hq2', result: FL);
call symputx('stop',FL);
run;

/*描画*/
%rwi(data=_DT1)

%end;

/*アニメ終了*/
ods printer close ;
options  ANIMATION=STOP ;
%mend automaton;

/*マクロ実行*/
%automaton

結果は









静止画で初期局面と最終局面を見てみると




となって正解のルートだけが残ります。
本当はこの正解の結果を初期局面にもっていって、そのルートにだけ色つけようかと思いましたが
ちょっと面倒になったのでここまでで。

で、実は解法の肝は
if sum(AR{i-1, j}=2 , AR{i+1, j}=2 , AR{i, j-1}=2 , AR{i, j+1}=2)=3 then AR{i, j}=2;
の一行だけなんです。

要するに、あるセルがあって、そのセルの周囲3方向が壁である場合、そのセルは行き止まりであって正解の通路ではないことが確定するといえますよね?
じゃあ、そういうセルは壁にしてしまおうという発想です。それを何回も何回も繰り返します。
そうやって、行き止まりを次々壁に塗り替えていくことで、周囲3方向が壁ではないセルだけが残ります。つまり、正解のルートが浮かびかがってくるわけです。

ただ、このアルゴリズムは迷路に、複数の空白セルが広場のようになっていたり、本ルート以外の意味のないループ道があった場合、それを削ることができないものになっています。
迷路の世界も奥が深いんですね。

SAS的には何回もデータステップを繰り返して、同名のデータセットを上書きしまくってます。
ただし、(genmax=2) の設定により、更新の1つ前のデータセットをDT1(gennum=-1) と書くことで
取得できるようになっています。

更新しても、更新前と内容が変わらない イコール 正解の通路だけが残っているということなので
そうなったらループを抜けます。

今回は更新前後の比較にハッシュオブジェクトのequalsメソッドを使っています。これは僕が単にハッシュ好きということもありますが、今回はどこに差があったかはどうてもよくて、同じか否かだけを01でみたいので、compareプロシジャよりやりやすいと考えたからです

【参考】
ハッシュオブジェクトの世界⑦ コンペア革命 equalsメソッド

で、GIFファイルの作成部分については忘備録の記事をそのまま使わせていただきました。

SASでアニメーションを作る方法(SAS9.4以降)

う~ん、結局、人の褌で…みたいになってしまいましたが、まあ勘弁してください。

若干、GIIFだとセルの大きさが乱れたりしてますが、何ででしょうかね。PDFじゃないから??
まあ、遊びの出力なので大目にみてくださいな。


SASでクイズ作って自分で遊ぶ虚しい話

以下のコードを丸ごとコピペして実行してみてください
※SAS雲丹では無理です。

data u_data;
   length name $20;
   window start
      #3   '★思いつく限りSAS関数名を入力欄に入れてEnterを押せ'
      #5   '※Enterを押すとクリアされるが内部に蓄積してるから安心しろ!'
      #8 '入力エリア:' +1 name attr=underline
      #12 'もう思いつかない!となったらウインドウを閉じるか、コマンド=>にendと打ってEnterを押せ!'
      #13 '';
   display start;
run;
data kaitou;
length level $100.;
if _N_=0 then set sashelp.vfunc(keep=fncname);
if _N_ = 1 then do;
declare hash h(dataset:"sashelp.vfunc(keep=fncname)");
h.definekey('fncname');
h.definedone();
end;
set u_data end = eof;
where name ^= '';
name=upcase(name);
rc=h.check(key:name);
if rc = 0 then do;
hantei='○';
score +1;
end;
if rc ne 0 then hantei ='×';
if eof then do;
if score <= 2 then level ="レベル1 エセSAS使い";
else if score <= 5 then level ="レベル2 駆け出しSAS使い";
else if score <= 10 then level ="レベル3 標準SAS使い";
else if score <= 15 then level ="レベル4 中々なSAS使い";
else if score <= 20 then level ="レベル5 頼りになるSAS使い";
else if score <= 30 then level ="レベル6 ちょっとキモがられてるSAS使い";
else if score <= 40 then level ="レベル7 知りすぎたSAS使い";
else if score <= 50 then level ="レベル8 極めたSAS使い";
else if score <= 60 then level ="レベル9 狂ったSAS使い";
else if score > 60 then level ="レベル10 ジム・グッドナイト SASインスティチュートCEO";
end;
keep name hantei score level;
run;

dm "viewtable kaitou";


多分、以下のような画面がでてきます。




さあ!!クイズの始まりです!!

入力エリアにカーソルを合わせて、知っているSAS関数名を入力し
Enterを押します。













すると文字が消えます。

これを、思いつく関数が無くなるまで繰り返します。

もう無理!となったら、ウインドウを閉じるか、コマンド=> の後にendと打ってEnterします。

※SAS自体を閉じちゃダメですよ。

すると、今まで入力したデータに対しての採点結果が表示され、
最終オブザベーションのlevelの箇所に、あなたのSASプログラマーとしての評価が表示されます。











はい、以上、悪ふざけでした!

※当然レベルの判定は冗談です

ふざけた例ですが一応、windowステートメントによって生成したウンインドウで
ユーザーの入力をうけとりデータセットを生成するテクニックとか、ハッシュオブジェクトの
checkメソッドとか使ってます。

%winodwによるマクロウインドウ生成は既に記事にしております

http://sas-tumesas.blogspot.jp/2013/10/window.html



ハッシュオブジェクトで、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; 


おわりです!

データセット内のすべての文字変数に対して、それぞれに入っているテキストの長さの最大値を取得するハッシュオブジェクト

SAS忘備録の記事「Hashオブジェクトを使おう。」http://sas-boubi.blogspot.jp/2015/02/hash.html

を読んで感動しました。
わかりやすい。僕の記事の数兆倍わかりやすいので、是非読んでください。

そして思ったのが、やっぱり普及させたい技術についてはそのまま実務で使えたり、
使い道が容易に想像できるコードを例にしないと、僕だけが楽しいばかりで、駄目だなということです。

僕だけが楽しい悪い例の一部↓
「ハッシュオブジェクトでマジカルバナナのデータを順番通りに連結する」
http://sas-tumesas.blogspot.jp/2014/09/blog-post_16.html
「ハッシュ反復子オブジェクトとcall compcostルーチンで、マジカルチェンジのデータを順番通りに連結する」
http://sas-tumesas.blogspot.jp/2014/09/call-compcost.html

実は最近 sumメソッド周りを集中的に研究していて、割と実務的かつ面白なサンプルを生産してるのですが、それは一応ユーザー総会用にしようかと思って、ストックしているので今暫くお待ちください。いずれ全てUPします。

で、前置きが長くなりましたが、ここから本題。



さて、文字型の変数の場合、適切なlengthの設定というのは結構、頭の痛い問題です。
文字切れはやばいし、大は小を兼ねるの発想で、せいぜい$100.くらいしか入らないだろうなってとこも、データ入力に制限がない場合、長文入れる人もいて怖いから取りあえず$1000.とかにしがちじゃないでしょうか。
で、一個$1000.にすると、もうめんどいから文字変数は全部1000でいいやみたいな。

オブザベーション数が少ない、或いはそういう文字変数が少ない場合は別にかまわないです。
しかし、経験のある人も多いと思いますが、文字変数のlengthは実行時間に対して、割とダイレクトに影響します。

なので、データが固まった後でlengthをもっかい決めたいなって時、実際、入ってる値のlengthの最大値が欲しくなります。
或いは、SASデータセットを他のシステムやDBに渡す時とかも実際どの程度とればいいのかを調べることあります。

まあ、length関数と文字変数に対するループで、各変数の長さだして、それをmeansとかにかけて最大値とるとか、或いはretainで最大値更新していってeofで出すとかでいいんですけど、その場合、データセット名だけ指定すれば、常に動くマクロ、できれば処理時間も最速でっていうものを作りにくいんですよね(まあ、僕がマクロ苦手なので)。
あと、できれば、ほかのシステムにペタッと貼り付けたり、そこからコード自動生成するのに
縦積みになってた方がうれしいので敢えてハッシュ使ったという個人的事情もあります。
とりあえずハッシュ使いたいだけという趣味的な部分もありますが。

で、今

data A;
length A B C $1000.;
do i= 1 to 5;
 A=repeat('A',int(ranuni(777)*20));
 B=repeat('B',int(ranuni(777)*20));
 C=repeat('C',int(ranuni(777)*20));
output;
end;
drop i;
run;










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

data _null_;
length var $100. maxlength 8.;
set A end=eof;
array cha _character_;

if _N_ = 1 then do;
call missing(var,maxlength);
declare hash h1();
h1.definekey('var');
h1.definedata('var','maxlength');
h1.definedone();
do over cha;
var = vname(cha);
maxlength = 0;
h1.add();
end;
end;

do over cha;
var = vname(cha);
if h1.find() = 0 & length(cha) > maxlength then do;
maxlength=length(cha);
h1.replace();
end;
end;

if eof then do;
h1.remove(key:'var');
h1.output(dataset:'LEN');
end;
run;


こうすれば、結果は








となります。

今 set A; としているところのデータセット名だけ変えれば常に動くはずです。

実際、自分用に使っているマクロは、その最大文字数の実際のデータと、IDとかもとるようにしていますが、
中身を理解できれば、割と簡単に自分好みにカスタマイズできるはずです。

で、中身の説明ですが
ハッシュオブジェクト定義と、そこへのデータ追加の部分

array cha _character_;

if _N_ = 1 then do;
call missing(var,maxlength);
declare hash h1();
h1.definekey('var');
h1.definedata('var','maxlength');
h1.definedone();
do over cha;
var = vname(cha);
maxlength = 0;
h1.add();
end;
end;

が割と工夫されていて、面白いですね(自画自賛)。
まずからっぽで定義してから、全文字変数の変数名をキーにしてハッシュに格納します。
現状の最大lengthは0にしておきます。

この時点での、このハッシュオブジェクトh1の中身は










となっているはずです。
(慣れるまではデバックのためハッシュオブジェクトの中身をこまめにouputメソッドでだしておくのもいいかもです。)

そのあと
do over cha;
var = vname(cha);
if h1.find() = 0 & length(cha) > maxlength then do;
maxlength=length(cha);
h1.replace();
end;
end;
は、そのまんまですね。findメソッドで、その時点で処理している変数名の現状の最大lengthをハッシュオブジェクトから
引っ張ってきて、比較して、自分の方が大きければreplaceメソッドで更新すると。

なんか今回はハッシュオブジェクトをretainステートメントみたいな感じで使ってるわけです。



最後
if eof then do;
h1.remove(key:'var');
h1.output(dataset:'LEN');
end;

は、varは元のデータセットにない、作った変数なのでremoveメソッドで消します
(※対象のデータセットにvarという名前の変数があったら、コード変えてくださいね)

h1.output(dataset:'LEN')でデータセットを出力します。

if eof then do;にしているのは最後の1回出力すればいいからですね。


以上です。

この変数名をキーにして、ターゲットになる値を更新していく方法は結構応用がききます。



ハッシュオブジェクトで、出力データセット名を動的に制御する。しかもdataステートメントでの指定が不要という話

今年もよろしくお願いいたします。

僕はSASハッシュオブジェクトが大好きです。処理効率がいいとか、柔軟性が高いとか、コードの見通しがよくなるとか、マクロと相性いいとか、お勧めする理屈は僕なりに多々あります。
ただ個人的心情として、ぶっちゃけハッシュオブジェクトはコード書いてて面白いんですよね。

なんで面白いんかな?って考えると、既存のデータステップの枠を超えた処理がかけるので、想像力が試されてる気がして、それが刺激的なんですよね。

例えば、サンプルとして類型の多い有名なコードですが

data Q1;
X=1;Y=1;output;
X=1;Y=2;output;
X=1;Y=3;output;
X=2;Y=4;output;
X=2;Y=5;output;
X=3;Y=6;output;
X=3;Y=7;output;
run;














例えば、上記のようなデータセットを、Xの値で3つのデータセットに分ける場合、

data OUT1 OUT2 OUT3;
 set Q1;
 if X=1 then output OUT1;
 if X=2 then output OUT2;
 if X=3 then output OUT3;
run;

とかけます。
ルールとして、データステップ内でoutputされるデータセットはdataステートメントで指定されている
必要があります。
またOUT1 OUT2 OUT3としていますが、ここをXの値から取得して可変的に増減させることは
通常のステップでは難しいです。
恐らくマクロを使って、まずXの値のパターンをとって、データセット名を生成して、みたいな流れにせざるをえないはずです。

ところが、ハッシュオブジェクトだと

data _null_;
    if _n_=1 then
        do;
            dcl hash hid (ordered:'Y', multidata:'Y');
            hid.definekey ('X');
            hid.definedata ('X','Y');
            hid.definedone ();
        end;
    hid.clear();

    do until (last.X);
        set Q1;
        by X;
        hid.add();
    end;
    hid.output (dataset:cats('OUT',put(X, best.-L)));
run;

で結果は
【OUT1】







【OUT2】






【OUT3】






となります。

かなり変態コードなので少し補足します。
まずDOWループの理屈を使って、by値のXごとにhid.clearとhid.outputが起きるようにしてます。
つまり、まずハッシュオブジェクトの中身をclearメソッドで空にして、by値のグループに対して処理をします。処理は単純にハッシュオブジェクトにそのグループのデータを追加していくだけの処理です。

その処理がおわったらoutputメソッドでデータセット化するわけですが、その時点のXの値を使って合成した文字列をデータセット名にしている。つまり動的に生成できてるわけです。

う~ん、心がいきいきしますね!楽しい!!

ただ、今回はわざと趣味でアクロバティックな例を出しましたが、本当はもっとわかりやすいものなので、わけわからんから業務に導入しない!という結論にならないようにお願いします!

同IDグループの中で、n個前のオブザベーションを参照したり、n個先のオブザベーションを参照したりする処理がハッシュオブジェクトなら割と簡単にできる話

※ちょっとコード修正しました。1obsごとにハッシュオブジェクト定義するようになっていて、効率悪い書き方してました。

SASで、1つ前のオブザベーションが変数の値が欲しい!とか、1つ先が欲しいとかってよくある話です。n個前のということであれば、retainステートメントや、或いはlag関数などで実現できます。

また、n個先であれば、例えばSAS公式の
「次オブザベーションの値を参照する方法」
http://www.sas.com/offices/asiapacific/japan/service/technical/faq/list/body/ba212.html

や、他にpoint=オプションを使った方法(いずれ紹介するかも)などが利用できます。

ただ、例えば、同じIDグループの中で次のオブザベーションといった制限がつくと、少し面倒になります。
(retainであればbyの部分と合わせて、first.で値をリセットしていけばいいし、先をとるためのmergeなら、予めマージキーとしてID内連番を作成して、それに細工をすればできます)

しかし、扱うデータセットのサイズが何万obsとかの巨大なものでないのであれば、ハッシュオブジェクトを使うとかなり楽にでき、さらにより複雑な条件であっても容易に組み込むことができるので、個人的にはお勧めです。

ただし、できれば、ハッシュオブジェクトを勉強して、理屈をある程度理解した上で利用するようにした方がいいと思います。
何をしているのか全くわからずに、コピペからカスタマイズすると、少し危険に思います。

過去のハッシュオブジェクト関連の記事
http://sas-tumesas.blogspot.jp/search/label/ハッシュオブジェクト


それではやってみましょう。

data Q1;
ID='001';X=1;output;
ID='001';X=3;output;
ID='001';X=5;output;
ID='001';X=7;output;
ID='001';X=9;output;
ID='001';X=11;output;
ID='002';X=2;output;
ID='002';X=4;output;
ID='002';X=6;output;
ID='002';X=8;output;
run;


















というデータがあって、同じIDの中で、1obs前のXの値をX_m1に、1obs先のXを_p1、2つ先をX_p2という変数にいれるとします。

data A1;
 if _n_ = 1 then do;
   declare hash hx();
    hx.defineKey('ID','NO');
    hx.definedata('_X');
    hx.definedone();
  do while (^FL);
   set Q1 end=FL;
   by ID;
   if first.ID then NO=0;
   NO+1;
   _X=X;
   hx.add();
  end;
 end;
   set Q1;
   by ID;
   if first.ID then RNO=0;
   RNO+1;
   
   /*同ID内の一つ前のデータを参照*/
   NO=RNO-1;
   rc=hx.find();
   X_m1=ifn(rc=0,_X,.);

   /*同ID内の一つ先のデータを参照*/
   NO=RNO+1;
   rc=hx.find();
   X_p1=ifn(rc=0,_X,.);

   /*同ID内の二つ先のデータを参照*/
   NO=RNO+2;
   rc=hx.find();
   X_p2=ifn(rc=0,_X,.);

 keep ID X X_m1 X_p1 X_p2;
run;


結果は


















このように、ハッシュオブジェクトは、データステップ中の処理と独立したルックアップテーブルを作成して、データステップ中に値のやりくりができるので、使いこなせば、プログラミングの自由度がかなりアップします。



更新回数をカウントしながらデータをupdateする機能を、ハッシュオブジェクトで実現する話

UPDATEステートメントはかなり応用の効くステートメントです。
UPDATEステートメントの基本についてはSAS忘備録の
「UPDATEステートメント入門」
http://sas-boubi.blogspot.jp/2014/06/update.html
が素晴らしいので、まず読んでください。

その上で、

マスターとなるデータセット

data MAS;
 X=1;Y='あ';output;
 X=2;Y='ろ';output;
 X=3;Y='ほ';output;
 X=4;Y='に';output;
run;









トランザクションデータセット

data TRA;
 X=1;Y='ほ';output;
 X=1;Y='ろ';output;
 X=1;Y='い';output;
 X=2;Y='ろ';output;
 X=3;Y='ひ';output;
 X=3;Y='は';output;
 X=5;Y='と';output;
 X=5;Y='ほ';output;
run;












を用意して、

data A0;
update MAS TRA;
by X;
run;

とすると結果は









となります。

トランザクションデータセットに同一キーのものが複数存在する場合、
結果は最終的に出現したデータになります。

例えばX=1でいうと、Yは「ほ」、「ろ」、「い」という順にトランザクションから読み込まれるので、
最終的にマスターデータセットのX=1に対応するYの値は「い」です。

ただ、これを、「ほ」で更新され、「ろ」で更新され、最後に「い」で更新され、
結局値が確定するまで3回上書き更新されたと捉え、この3回というカウントを変数として持たせたいとします。

昔、仕事で、最終的な更新結果を出すと同時に各オブザベーションが何回更新されたか。
つまりトランザクションデータに同一キーが結局いくつ出現したかを出さなければいけない状況がありました。

その時はまだ初心者でハッシュオブジェクト??だったので、普通にupdateしたあとで、別ステップでトランザクションデータのキーをfreqかなんかで集計したデータセットを作って、update後のデータセットとマージして切り抜けたと思いますが。

ひねくれた現在の僕なら以下のようにかけます。

あ、ちなみに今回は、問題を単純化にするためにトランザクションデータにnullは含まれないという前提条件をつけましょう。
updateステートメントにはnullの場合、更新を行わないという機能がありますが、ハッシュオブジェクトでやる場合、それで一つ条件分岐を増やさなければいけないので。
あとマスターの値とトラの値が同一であったとしても、同値で更新を行ったと考えます。

data _NULL_;
  if _n_ = 1 then do;
   declare hash hx(suminc:'UP_COUNT', ordered: 'yes');
    hx.defineKey('X');
    hx.definedata('X','Y','UP_COUNT');
    hx.definedone();
     end;
 do while (^FL);
   set MAS end=FL;
   UP_COUNT=0;
   hx.add();
  end;

 set TRA end=eof;
 UP_COUNT=1;
 rc=hx.check();
 if rc=0 then do;
  hx.sum(sum:up_count);
  hx.replace();
 end;
 if rc^=0 then do;
  UP_COUNT=0;
  hx.add();
 end;
 if eof then hx.output(dataset:'A1');
run;

で、結果は









です。

マスターに存在しなかった値が追加された時にカウントを1とするかどうかについて、今回は、追加は更新カウントに含めないとしています。

以前、紹介したsumincを使用して、checkメソッドによるキー参照をカウントしています。
checkメソッドにより、キーがハッシュオブジェクトに見つかればreplaceで更新、ないならaddメソッドで追加という、極めて単純な構造です。

このコードは、思いつきで、えいやっと書いたやつなんですが、なかなか美しくないでしょうか?
反復子オブジェクトを使わずに表現できていますし、自分的に結構好きですね。