ifn(ifc)関数の留意点。偽のルートも全部内部で計算されてるからねって話

うっかりしていると2017年も大晦日!
ちょっとブログのサボり癖がついてしまいそうで、このままじゃいかんので更新します。
来年はもうちょっと頑張ります。

たいしたネタじゃないですが、大分前に下書きしてた以下の記事。

=============================

ifn関数便利です。結構使います。
SQLでも使えるし、なかなか良いやつですね。

ただ、ちょっと注意なのがIfステートメントと内部処理の違いを
ちゃんと知ってはいてねということです。

例えば以下のコード。

data a;
X="A";Y=.;output;
X="B";Y=1.;output;
run;

data b;
set a;
A=sum(Y,1);
Z=ifn(X="A",1,Y+1);
run;















結果は全く問題ありません。
ところがログには「欠損値を含んだ計算により‥」とでます。
Zには欠損なんてないのに。

つまりIFN関数は、先に引数を全部計算してから、条件分岐で
どっちを採用するかを選択しているわけです。
結果的に通らないルートも全部処理しちゃうんですよ。
Ifステートメントは、真の場合のみthen以下の処理がおきるので、そこが大きな
違いです。

例えば、処理時間のめっちゃかかる計算の例として、たらいまわし関数を例にだすと
ifステートメントとifn関数で処理時間が何十倍も変わってきたりするわけですね。
以下がコードですが、ifステートメント0.55秒に対してifnだと54.75秒かかってます。

proc fcmp outlib=WORK.FUNCDT.CARCALC ;
  function takf(x, y, z);
  if x <= y then
    return(z);
  else
    return (takf(takf(x - 1, y, z), takf(y - 1, z, x), takf(z - 1, x, y)));
  endsub;
run;
options cmplib=WORK.FUNCDT;


data q1;
do i = 1 to 100;
output;
end;
run;

data a1;
set q1;
if _N_=50 then  A= takf(18, 9, 0);
run;

data a2;
set q1;
A= ifn(_N_=50,takf(18, 9, 0),.);
run;










ちなみに、この性質を逆手にとって、相変わらずぶっ飛んだアイデアを公開している人がいるので紹介しておきましょう。

「LAG関数とIFN / IFC関数のコンボ技」
http://sas-boubi.blogspot.jp/2015/03/lagifn-ifc.html



SASグローバル認定プログラム SAS Certified Statistical Business Analyst Using SAS 9: Regression and Modelingを受けてみた

ブログのネタは溜まってるんですが、なんだかんだとバタバタしてて更新滞ってます。
またそのうち、投下していきたいと思います。

さて、SAS認定資格の話ですが、BASE,ADVANCE,CLINICALに続いて4つ目の資格に挑戦してみました。
「SAS Certified Statistical Business Analyst Using SAS 9: Regression and Modeling」
http://www.sas.com/offices/asiapacific/japan/training/certify/sba9.html

出題範囲(SAS社ページより)
-----------------------------------------------------------------------------
分散分析
 分散分析の仮定の検証
 GLMおよびTTESTプロシジャを使用した母平均の差の分析
 分散分析におけるポストホック検定の実施による処理効果の評価
 因子間の交互作用の検出と分析
線形回帰
 REGおよびGLMプロシジャを使用した線形重回帰モデルの当てはめ
 線形重回帰モデルに対するREGプロシジャの出力の精査
 REGプロシジャを使用したモデル選択の実施
 診断分析と残差分析の使用による回帰モデルの妥当性の評価
ロジスティック回帰
 LOGISTICプロシジャによるロジスティック回帰の実施
 入力の選択によるモデル性能の最適化
 LOGISTICプロシジャの出力の解釈
 LOGISTICおよびSCOREプロシジャを使用した新規データセットのスコアリング
予測モデルの性能に向けた入力の準備
 入力データにおける潜在的な問題の同定
 DATAステップの利用によるループや配列、条件文、関数を用いたデータ・ハンドリング
 予測モデルにおけるカテゴリ変数の水準数の削減
 CORRプロシジャを使用した不要な変数のスクリーニング
 経験ロジットのプロットを使用した非線形性を持つ変数のスクリーニング
モデル性能の計測
 「正直な評価」原理を適用したモデル性能の計測
 混同行列を使用した分類性能の評価
 学習および評価データを使用したモデル選択と評価
 モデルの比較や選択を行うためのグラフ(ROC、リフト、およびゲインチャート)の作成と解釈
 スコアリングを行ううえでの効果的なカットオフ値の確立
 スコアリングのためのカットオフ値の効果的な意思決定
-----------------------------------------------------------------------------

長いので今後、SBAと書かせてください。

日本での受験が可能になってから、もう2年くらいたちますかね。
しかし、現時点で日本での有資格者はわずか21人!!

なんせ、BASE,ADVANCEと違って、テキストも模擬問題もないし、情報もほとんどないんで手をだしにくいですよね。

というわけで、そんな皆様のために、先陣?きってSBAの首獲ってきました。

あんまり、こういう問題でたよってピンポイントでいうのはまずいと思うので、個人的な感想と独り言だけ、ごく簡単にかいて終わります。

・ややマニアック(高度)&システムよりな部分のあるAdvanceに比べて、統計解析の理論・実装・結果の解釈という三点セットが入って、難易度もほどほどな資格なので、解析担当者のスキルアップや理解度の確認に良い資格だと思いました。新人の方が目標とかに据えるなら、Advanceより先にこっち勉強した方が、データサイエンティストを育てるという観点では、真っ当なんじゃないかなぁって思いましたね。
(僕の個人的な趣味趣向としては是非Advanceを先に勉強して、データステップマニアになろうぜ!って思うけど)
別にSASのセールスマンじゃないけど、偉い立場の方向けにアピールしときます。会社でサポートする価値のある資格だと思いますし、もし面接にこれ持ってる人がいたら、少なくとも「なんちゃって統計プログラマー」ではないと判断していいはずですよ。
資格の名前にBusinessとか入ってますけど、解析の素材が売り上げ予測だったり、倒産有無なだけで、治療効果の予測とか疾患の有無とかに変えれば、そのまま医薬や他業界で使える内容なんで、分野あんま関係ないです。別にビジネスに特化した知識は一切出ませんでした。
・出題範囲に偽りはない。ちゃんとその通りにでたよ。トピックごとの出題比率はさておき。
※ただ、英語試験での記述「Apply the principles of honest assessment to model performance measurement」を「正直な評価」原理を適用したモデル性能の計測って訳すのはどうなんでしょう。そんな言葉あるんでしょうか…。一般的な表現ならごめんなんさい。
普通に、複数のモデルの比較評価の仕方や、モデルのバリデーションについて出題されてた箇所のこと言ってるんだと思いますが。
・どっかの誰かさんみたいに、データステップで点数稼ごうとかって愚かな考えはやめた方がよい。出題比率的に。
・普段からSAS Outputはちゃんとみよう。解析プロシジャまわすと一杯色々でますけど、どういう統計量なのかとか知っておくのは大事。
・ods graphicsでだしてくれてるプロットも大事、
・個人的感触として、統計解析の理論部分についてはそんなに難しくないはず。
・出題範囲にあがっている GLM REG LOGISTICの基本的なアウトプットの解釈と、基本的なステートメント・オプションは押さえておいた方が。
・挙げられている(ROC、リフト、およびゲインチャート)について、ROCはともかく、リフトとかゲインは医薬であんまり書いたりしないかもなので調べておこう。
・広域をカバーする統計の教科書や社内資料があるなら復習しておくとよいかも。


まあ色々、自分で考えて広く勉強すればいいと思います。
結果、テストにでなくて、対テストとしては無駄になることも多いけど、勉強するのは悪いことじゃないですしね。

以上、頑張って!


裏シージャ proc spellでスペルチェックだ

マニュアルに載ってないけど実は存在している「UNDOCUMENTED SAS PROCEDURES」、いわゆる裏シージャや闇シージャと呼ばれるプロシジャの話です。

その中でも、誰か使い道考えてくれないかなぁってやつをご紹介。

まずテキストファイルで

「I am SAS YAMA.」

とだけ書かれたものを用意します。

そして、以下のようにProc Spellにかけると

filename text 'ファイルのフルパス';
proc spell in=text verify suggest;
run;

アウトプットウインドウに

 ファイル: "TEXTDATA"

   認識されないワード               度数     行

   SAS                              1       1
         候補: ASS, AS, GAS, HAS, PAS, WAS, SAD, SAG, SAP, SAT, SAW,
                      SAY, SEAS, SPAS, SAGS, SAPS, SAWS, SAYS, SASH

   YAMA                             1       1
         候補: YAM, MAMA, YAMS



と出力されて、修正候補まで出してくれます。

辞書データを読み込ませて賢くすることもできるみたいなので
誰か面白いことに使ってください。リファレンスないけど。


proc scaprocの世界

最近、面白そうだなと思って注目してるプロシジャを紹介します。
その名もProc SCAPROCです。なんて読むのでしょうね。

9.2からいるので、さして新しくもないですね。知ってました?
こいつの機能はずばりSAS Code Analyzerです。

ちょっと奥深いので、何回かにわけるかも。
とりあえず今回はさわりだけ。

実行されたSASコードが何をしているのかを分析し、その結果を出力してくれます。

「proc scaproc;record "★出力先指定" オプション;run;」
として、そこから
「proc scaproc; write; run;」までの間に実行されるSASコードが分析対象になります。

使い方的にはproc printtoに似てますね。

例えば
proc scaproc;
record "★出力先指定\record.txt"
attr
;
run;

data OUT;
set sashelp.class;
 Z=999;
run;

proc summary data=sashelp.class(keep=age);
var age;
output out=summary mean=mean;
run;


proc scaproc; write; run;

とすると、
どんなデータセットが読み込まれ、どんなデータセットが出力されたのか?
ステップに何秒かかったか?動きのあったマクロ変数は?などの情報を、ちょっと人間には優しくない形式ですが、だしてくれます。オプションの種類と出力の解読方法についてはまた今度くわしく。


以下が出力されたファイルの中身です(一部抜粋)


* JOBSPLIT: DATASET INPUT SEQ #C00003.CLASS.DATA */
/* JOBSPLIT: DATASET OUTPUT SEQ WORK.OUT.DATA */
/* JOBSPLIT: ATTR #C00003.CLASS.DATA INPUT VARIABLE:Name TYPE:CHARACTER LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR #C00003.CLASS.DATA INPUT VARIABLE:Sex TYPE:CHARACTER LENGTH:1 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR #C00003.CLASS.DATA INPUT VARIABLE:Age TYPE:NUMERIC LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR #C00003.CLASS.DATA INPUT VARIABLE:Height TYPE:NUMERIC LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR #C00003.CLASS.DATA INPUT VARIABLE:Weight TYPE:NUMERIC LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR #C00003.CARS.DATA INPUT VARIABLE:Make TYPE:CHARACTER LENGTH:13 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR #C00003.CARS.DATA INPUT VARIABLE:Model TYPE:CHARACTER LENGTH:40 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR #C00003.CARS.DATA INPUT VARIABLE:Type TYPE:CHARACTER LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR #C00003.CARS.DATA INPUT VARIABLE:Origin TYPE:CHARACTER LENGTH:6 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR #C00003.CARS.DATA INPUT VARIABLE:DriveTrain TYPE:CHARACTER LENGTH:5 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR #C00003.CARS.DATA INPUT VARIABLE:MSRP TYPE:NUMERIC LENGTH:8 LABEL: FORMAT:DOLLAR8. INFORMAT: */
/* JOBSPLIT: ATTR #C00003.CARS.DATA INPUT VARIABLE:Invoice TYPE:NUMERIC LENGTH:8 LABEL: FORMAT:DOLLAR8. INFORMAT: */
/* JOBSPLIT: ATTR #C00003.CARS.DATA INPUT VARIABLE:EngineSize TYPE:NUMERIC LENGTH:8 LABEL:Engine Size (L) FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR #C00003.CARS.DATA INPUT VARIABLE:Cylinders TYPE:NUMERIC LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR #C00003.CARS.DATA INPUT VARIABLE:Horsepower TYPE:NUMERIC LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR #C00003.CARS.DATA INPUT VARIABLE:MPG_City TYPE:NUMERIC LENGTH:8 LABEL:MPG (City) FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR #C00003.CARS.DATA INPUT VARIABLE:MPG_Highway TYPE:NUMERIC LENGTH:8 LABEL:MPG (Highway) FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR #C00003.CARS.DATA INPUT VARIABLE:Weight TYPE:NUMERIC LENGTH:8 LABEL:Weight (LBS) FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR #C00003.CARS.DATA INPUT VARIABLE:Wheelbase TYPE:NUMERIC LENGTH:8 LABEL:Wheelbase (IN) FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR #C00003.CARS.DATA INPUT VARIABLE:Length TYPE:NUMERIC LENGTH:8 LABEL:Length (IN) FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA OUTPUT VARIABLE:Name TYPE:CHARACTER LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA OUTPUT VARIABLE:Sex TYPE:CHARACTER LENGTH:1 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA OUTPUT VARIABLE:Age TYPE:NUMERIC LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA OUTPUT VARIABLE:Height TYPE:NUMERIC LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA OUTPUT VARIABLE:Weight TYPE:NUMERIC LENGTH:8 LABEL:Weight (LBS) FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA OUTPUT VARIABLE:Make TYPE:CHARACTER LENGTH:13 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA OUTPUT VARIABLE:Model TYPE:CHARACTER LENGTH:40 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA OUTPUT VARIABLE:Type TYPE:CHARACTER LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA OUTPUT VARIABLE:Origin TYPE:CHARACTER LENGTH:6 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA OUTPUT VARIABLE:DriveTrain TYPE:CHARACTER LENGTH:5 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA OUTPUT VARIABLE:MSRP TYPE:NUMERIC LENGTH:8 LABEL: FORMAT:DOLLAR8. INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA OUTPUT VARIABLE:Invoice TYPE:NUMERIC LENGTH:8 LABEL: FORMAT:DOLLAR8. INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA OUTPUT VARIABLE:EngineSize TYPE:NUMERIC LENGTH:8 LABEL:Engine Size (L) FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA OUTPUT VARIABLE:Cylinders TYPE:NUMERIC LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA OUTPUT VARIABLE:Horsepower TYPE:NUMERIC LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA OUTPUT VARIABLE:MPG_City TYPE:NUMERIC LENGTH:8 LABEL:MPG (City) FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA OUTPUT VARIABLE:MPG_Highway TYPE:NUMERIC LENGTH:8 LABEL:MPG (Highway) FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA OUTPUT VARIABLE:Wheelbase TYPE:NUMERIC LENGTH:8 LABEL:Wheelbase (IN) FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA OUTPUT VARIABLE:Length TYPE:NUMERIC LENGTH:8 LABEL:Length (IN) FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA OUTPUT VARIABLE:Z TYPE:NUMERIC LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ELAPSED 5  */
/* JOBSPLIT: SYSSCP LIN X64 */
/* JOBSPLIT: PROCNAME DATASTEP */
/* JOBSPLIT: STEP SOURCE FOLLOWS */

data OUT;
set sashelp.class
 sashelp.cars;
 Z=999;
run;


/* JOBSPLIT: DATASET INPUT SEQ WORK.OUT.DATA */
/* JOBSPLIT: DATASET OUTPUT SEQ WORK.SUMMARY.DATA */
/* JOBSPLIT: FILE OUTPUT /home/morioka0380/record.txt */
/* JOBSPLIT: ATTR WORK.OUT.DATA INPUT VARIABLE:Name TYPE:CHARACTER LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA INPUT VARIABLE:Sex TYPE:CHARACTER LENGTH:1 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA INPUT VARIABLE:Age TYPE:NUMERIC LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA INPUT VARIABLE:Height TYPE:NUMERIC LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA INPUT VARIABLE:Weight TYPE:NUMERIC LENGTH:8 LABEL:Weight (LBS) FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA INPUT VARIABLE:Make TYPE:CHARACTER LENGTH:13 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA INPUT VARIABLE:Model TYPE:CHARACTER LENGTH:40 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA INPUT VARIABLE:Type TYPE:CHARACTER LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA INPUT VARIABLE:Origin TYPE:CHARACTER LENGTH:6 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA INPUT VARIABLE:DriveTrain TYPE:CHARACTER LENGTH:5 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA INPUT VARIABLE:MSRP TYPE:NUMERIC LENGTH:8 LABEL: FORMAT:DOLLAR8. INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA INPUT VARIABLE:Invoice TYPE:NUMERIC LENGTH:8 LABEL: FORMAT:DOLLAR8. INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA INPUT VARIABLE:EngineSize TYPE:NUMERIC LENGTH:8 LABEL:Engine Size (L) FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA INPUT VARIABLE:Cylinders TYPE:NUMERIC LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA INPUT VARIABLE:Horsepower TYPE:NUMERIC LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA INPUT VARIABLE:MPG_City TYPE:NUMERIC LENGTH:8 LABEL:MPG (City) FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA INPUT VARIABLE:MPG_Highway TYPE:NUMERIC LENGTH:8 LABEL:MPG (Highway) FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA INPUT VARIABLE:Wheelbase TYPE:NUMERIC LENGTH:8 LABEL:Wheelbase (IN) FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA INPUT VARIABLE:Length TYPE:NUMERIC LENGTH:8 LABEL:Length (IN) FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.OUT.DATA INPUT VARIABLE:Z TYPE:NUMERIC LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.SUMMARY.DATA OUTPUT VARIABLE:_TYPE_ TYPE:NUMERIC LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.SUMMARY.DATA OUTPUT VARIABLE:_FREQ_ TYPE:NUMERIC LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: ATTR WORK.SUMMARY.DATA OUTPUT VARIABLE:mean TYPE:NUMERIC LENGTH:8 LABEL: FORMAT: INFORMAT: */
/* JOBSPLIT: SYMBOL GET SYSSUMSTACKODS */
/* JOBSPLIT: SYMBOL GET SYSSUMTRACE */
/* JOBSPLIT: ELAPSED 8  */
/* JOBSPLIT: PROCNAME SUMMARY */
/* JOBSPLIT: STEP SOURCE FOLLOWS */

proc summary data=OUT;
var age;
output out=summary mean=mean;
run;

inputされたデータセットの変数情報とoutputの変数情報をだすためにattrオプションをつけてます。
これを使って差分を考えれば、一応、そのステップで新規に作成されたであろう変数を、ある程度特定できますね

X軸の値ラベルに改行を含む場合、axistableで実装するのは邪道っすか?って話

sgの機能は凄い充実してるし、未だ凄い勢いで増え続けてるけど、たまに、えっ!ていうのがなかったりしますよね。
或いは、あるのかもしれないけど、もはや指定できるオプションが多すぎて、目的のものをうまく探せないっていう。

例えば、聞きたいんですけど、以下のような軸ラベルのグラフをSGで描く場合って皆さん一体どうしてるんです?




























軸の値ラベルに改行ってどうしてます?って話です。
そもそもそんなデザインのグラフにしないっていうのはなしで。

離散値であれば、軸のtype=discreteしてsplitcharで指定して区切ればいいですけど、
未だ最新verでも連続量に対応してないですよね?
unicodeで改行コード打ち込んでもなんかうまくいかないです。多分fontを対応しているものに変えればできるんだろうけど、それはしたくない。

やっぱannotateデータセット作ってsgannoで描いてるんでしょうか?
こんなちょっとしたことのために、annotate作るのは正直しゃくですよね。十分慣れてるならいいですけど

ふと思ったんですけど、annotate作るぐらいなら、xaxistableで軸附属表を軸の値ラベルに見せかけてみたらどうです?
本来の軸値ラベルと、軸ラベルは消してしまって、表で表現しちゃうっていう。

実は先の図は実際にその方法(以下のコード)で書いたやつです。

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

data xa;
do X=1 to 10;
xvalue1=cats(X);
xvalue2="時間";
xvalue3="経過後";
if X=5 then xvalue4="経過時間";
else xvalue4 ="";
output;
end;
run;

data A;
set xa Q1;
run;

proc sgplot data=A;
scatter x=X y=Y;
xaxis display=(nolabel novalues) values=(1 to 10 );
xaxistable xvalue1 xvalue2 xvalue3/x=X nolabel;
xaxistable xvalue4/x=X nolabel valueattrs=(size=11) pad=(top=10);
run;


axistableの本来の使用目的から外れている気がしますが、annotateの構造より正直
万人に受け入れやすいと思うんですけど、どうなんでしょう




小技というか裏技? subpad関数は現実に存在しない場所から文字をもってこれる

すっかり久しぶりの更新になってしまいました

この1ヶ月ほど色々ありました

藤井聡太が勝ったりとか、負けたりとか、また勝ったりとか。
あとは特に何もないですね。

リハビリにしょうもない記事をひとつ

subpad関数のリファレンスを読むと

・SUBPAD(string, position <, length>)

第2引数のpositionに関しては正の整数です。
と書いてある。

そんな風に書かれると、0とか負の整数指定したらどうなるのかって
試してみたくなるのが人の性ってもんですね。

というわけでやってみます。

data A;
X="ABCDE";
do i = 5 to -5 by -1;
Y=subpad(X,i);
output;
end;
run;

どうせエラーになるんだろうなぁと思っていたら

おおぅ、ログにはなにもでず、正常に実行完了。

そして中身をみると






















0番目の文字位置とか-1番目の文字位置とか存在しない場所から半角スペースとってきてる。亜空間でも見に行ったんですかね。

任意の数だけデータを半角スペース下げするのに使えるかなぁ。
でも正規なリファレンスに反した書き方だから、将来的にエラーにされても文句いえんなぁ


RWIはデータステップの美しさを目に見えるようにするために生まれたんだねって話

SASのRWIについて、RTF出力への対応がされたら、解析帳票作るのに使うから
勉強するよって話をよく聞きます。

医薬系の解析帳票についてRWIがProc Report等と比べて、第一選択になりえる
機能を持っているということはSASNAMIさんが解説されてます

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

はやく次期バージョンとかで対応されないかなぁ。


それはさておき、解析帳票作るのに使えんのか、使えないのかってとこだけに目がいきがちですがRWIの本質はそこではないと思うのです。

データステップ内で動的にレポート生成できるということが本質で、
データステップ内で何が起きているのかをビジュアル化できるという魅力があると思います。

ということで、今回は、ソートアルゴリズムの授業を想定し、バブルソートをSASの配列で表現した時、配列要素がどのように更新されていくかをRWIでビジュアル化してみましょう。

バブルソートは隣り合う二つの要素を大小比較でひたすら並び替えていく方法です。
並び替えが起きなくなるまでループします。もっとも原始的なアルゴリズムですね。

いまの時代、call sornで一撃やないのとか言わないでね。

普通にSASで書くとこんな感じかな

data _null_;
array ar{10} ( 3 5 8 2 1 9 4 7 6 0);
do until(finish=1);
  finish=1;
    do i=1 to dim(ar) - 1;
        tmp=.;
        if ar{i} > ar{i+1} then do;
            tmp = ar{i};
            ar{i} = ar{i+1};
            ar{i+1}=tmp;
            finish=0;
            put ar{*}=;
        end;
    end;
end;
run;

結果は





















う~ん、地味。じっくりみないとピンとこない。

そこで、上記のコードにRWIをたしちゃいます

data _null_;
array ar{10} ( 3 5 8 2 1 9 4 7 6 0);
array num _numeric_;

/*試行回数*/
total=0;

dcl odsout ob();
ob.layout_gridded(columns: 2,column_gutter: '1mm');

/*初期状態描画*/
 ob.region();
 ob.table_start();
  ob.row_start();
ob.format_cell(data: "回数",  style_attr: "background=green color=white");
ob.row_end();
ob.row_start();
        ob.format_cell(data: "初期状態");
    ob.row_end();
  ob.table_end();

 ob.region();
 ob.table_start();
ob.row_start();
  do i=1 to dim(ar);
          ob.format_cell(data: vname(ar{i}),  style_attr: "background=blue color=white");
end;
ob.row_end();
  ob.row_start();
  do i=1 to dim(ar);
        ob.format_cell(data: ar{i});
 end;
   ob.row_end();
    ob.table_end();

/*バブルソート開始*/
do until(finish=1);
  finish=1;/*終了フラグ*/
    do i=1 to dim(ar) - 1;
        tmp=.;
        if ar{i} > ar{i+1} then do;
            tmp = ar{i};
            ar{i} = ar{i+1};
            ar{i+1}=tmp;
            finish=0;
total=total+1;

/*描画*/
ob.region();
ob.table_start();
ob.row_start();
ob.format_cell(data: "回数",  style_attr: "background=green color=white");
ob.row_end();
ob.row_start();
      ob.format_cell(data:cats(total, "回目"));
   ob.row_end();
 ob.table_end();

  ob.region();
    ob.table_start();
ob.row_start();
  do j=1 to dim(ar);
          if j = i  or j = i+1then ob.format_cell(data: vname(ar{j}),  style_attr: "background=red color=white");
else ob.format_cell(data: vname(ar{j}),  style_attr: "background=blue color=white");
end;
ob.row_end();
  ob.row_start();
  do over num;
        ob.format_cell(data: num);
end;
   ob.row_end();
    ob.table_end();

        end;
    end;

end;

run;

結果は、ちょっと長くて画像分割しちゃってますが、以下の感じです










































やっぱRWI、面白い。これに最初に目をつけて体系的に説明して、世に広めた忘備録のa.matsuさんは本当に偉大だと思います。

「レポート作成インターフェイス(RWI)入門1」
http://sas-boubi.blogspot.jp/2014/10/rwi1.html

複数回transposeしてからmergeする前にちょっと考えてみてって話

例えば以下のデータセットから

data Q1;
ID="001";VISITN=1;A=1;B=2;C=3;output;
ID="001";VISITN=2;A=3;B=4;C=5;output;
ID="002";VISITN=1;A=6;B=7;C=8;output;
ID="002";VISITN=2;A=9;B=0;C=1;output;
run;











以下の全転置データセットを作りたい場合










proc transpose data=Q1 out=_A prefix=A_;
  var A;
  id VISITN;
  by ID;
run;
proc transpose data=Q1 out=_B prefix=B_;
  var B;
  id VISITN;
  by ID;
run;
proc transpose data=Q1 out=_C prefix=C_;
  var C;
  id VISITN;
  by ID;
run;
data RES1;
merge _A _B _C;
by ID;
drop _NAME_;
run;

と書いたりします。

もちろん、これで正解です。
変数が10個なら10回transposeかければいいんですが、ちょっと工夫を考えてみます

例えば、今回の場合だと「ID」 と「パラメータ」と「値」で構成される縦積み構造にしてから転置すれば1回の転置でスムーズに結果がだせます。

つまり

data _Q1;
set Q1;
array ch A--C;
do over ch;
VNAME =vname(ch);
VAL=ch;
output;
end;
keep ID VNAME VISITN VAL;
run;
proc transpose data=_Q1 out=RES2(drop= _NAME_) delimiter=_;
  var VAL;
  id VNAME VISITN;
  by ID;
run;








というわけ(IDの複数指定ができるようになったのは9.3から)。

1回途中で作ってる縦積み構造(_Q1)の中身をみてみると




















となるわけです。


別にこっちの方法が正解だというわけではなく、状況に応じて考えましょうという話ですね。対象が文字列変数の場合、縦積み構造にするときに文字切れしないように最大lengthにしなければならないので注意が必要です。

SASの場合、BYやCLASSステートメントが強力なので、インプットやアウトプットの形にちょっと頭を使えば、同じ機能のステップを反復する必要がない状況が多いです。

まあ、言いたいことを一言で言うと、データステップも楽しい世界だよってことです。


影の薄い%syscallの話

例えば以下のようにマクロ変数が3つあったとします。

%let a = 3;
%let b = 1;
%let c = 2;

あんまりやらない処理ですが、a,b,cの中身の値を昇順ソートして
a=1,b=2,c=3と再代入したいとします。

実は1行で書けますが、ぱっとコードが思いつきますか?

知ってるか知らないかですが、以下のように書けます

%syscall sortn(a,b,c)

で終わりです。
確認してみると

%put &=a  &=b  &=c;





というわけ。
%sysfuncはよく知られてるけど、それのコールルーチン版の%syscallはあんまり知られてない。

まあ、確かに使わざるをえない機会はそれほど多くない気がする。



libname excelで参照しているデータセット(エクセルSheet)がダブルクリックで開けない

地味に困ってる人が多いみたいなのでメモ。

libname excelして、ライブラリ参照しているエクセルシートを、ダブルクリックで
データビューから直接見ようとすると開けずにエラーになる現象。

もしSASが9.4で64bitで動いてるなら

options validmemname=extend;

した後にもう一回挑戦してみましょう。たぶん開けるのでは?

以下のProblem Noteで言及されてました

「Problem Note 51691: SAS® VIEWTABLE does not display an Excel sheet that is accessed using a LIBNAME statement」
http://support.sas.com/kb/51/691.html


ODS EPUBで電子書籍を作って遊ぶ話

この季節、新しい人と触れ合ったりすると、これからSASを始める日本全国のSASユーザーに、もっと役に立つ基礎的かつ実用的な記事を書くぞ!って気持ちが湧いたりします。

湧いたりはするんだけど、実際書いてみるとこのザマですよ。
駄目なんですよ、こういう内容しか書けないんです。SAS勉強するならSAS忘備録みましょう、書籍の「統計解析ソフトSAS」読みましょうね。
ここはSASのジョークサイトだと思ってください。

というわけで、SAS9.4から追加されたods epubでepub形式、ようするに電子書籍リーダー用のファイルを作れるようになりました。

自分はタブレット端末持ってないし、本も紙でしか見たことないような遅れ人なんですが
後ろの席に座ってる人がやたらとEPUB形式の魅力を語ってくれるので興味をもっちゃいました。

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

ods escapechar="^";
ods graphics on;

ods epub file="XXXXXXX/sassyama.epub"
title="SASはたのしい"
options(creator="SASYAMA" cover_image="XXXX/美濃囲い画像.jpg");
proc odstext;
p "実行コード"/style=[font_size=8];
p "proc freq data=sashelp.class;^{newline 1} tables age*sex/plot=freq;^{newline 1} run;"
/style=[font_size=8 font_style=Italic];
run;

proc freq data=sashelp.class;
tables age*sex/plot=freq;
run;
ods epub close;

結果、出力されたファイルを何かしらのソフトで開きます。
画像はChromeのアドオンのReadiumってやつで開いてます。

まずは表紙、特に意味もなく美濃囲いの美しい画像を使ってみました

















んで、中身


















たぶん、こだわりだしたらキリがないくらい色々できるみたい。

コード書いてるときは、100%おふざけのつもりだったんだけど、テキストとか画像とか、SAS出力を簡単にまとめれるし、リンクみたいなのも作れそうだから、何か仕事にも使えるかも。

例えばね、実行プログラム自身をproc streamでincludeして、どうにかodstextで表示したりして、実行ログとアウトプット、主要なデータセットのビューを埋め込んだりして
、もしかしたら仕様書の記述も加えたりしたら、なかなか面白い解析メタデータブックみたいなのができそうですね。
それを例えば新人教育の教材として、epub形式で配布するとかさ。

ああ、でもそれならYoshihiro Fukiyaさんが提案されてる「情報共有としてのSAS & Jupyter Notebookの使用」の方がよいのかな

まあ、あと例えば営業の人が全員ipad支給されてたりすることあるじゃないですか、そういう部隊にSASアウトプットを即時的に資料提供するのに使ったりとかね。
ないしは日報とかでその日の売り上げレポートをバッチでばらまくとか。

おお、なんかそれらしくまとまった。
やっぱり新しいアイデアは、遊びから始まるということで。こじつけだけど。







3回話しかけると襲い掛かってくる村人をコルーチンで表現する話

最初にちょっとしたSAS絡みのニュース
①SASユーザー総会のサイトがオープンした。聴講するより発表した方が楽しいので、アブストラクトだしましょうという話
http://www.sascom.jp/sug17/

②生物統計ハンドブックが12年ぶりに新版で内容変わるみたいなので、医薬系ユーザーは会社にねだって買って貰っとこうという話
http://www.scientist-press.com/11_339.html

さて、本題、例えばSASでロールプレイングゲームを作ろうと思います。
ある村人に話しかけると、村の名前を言ってくれるのですが、三回同じように話しかけると会話内容が変わって戦闘になるというイベントを作りたいと思います。
この3回話しかけると処理が変わるという機能をSASで実装するにはどうしますか?
今までは村人の会話挙動をSASマクロで実装して、村人に話しかけた回数をグローバルマクロ変数かSASデータセットに保存する必要がありました。
しかし、たかだか村人一人の挙動制御のためにグローバルマクロ変数を一つ作っているとキリがないです。
何がいいたいかというと、SASマクロの弱点は、ある機能に紐づく値をその機能自信に保持させれないということです。だってローカルマクロ変数は実行終わると消えちゃうんだもん。

そこでProc Luaの出番です。以下のコードで村人の挙動を作成します

proc lua;
submit;
function murabito()
    print("ここはSASむらです。")
    coroutine.yield()
    print("ここはSASむらです。")
    coroutine.yield()
    print("しつこいな、ころしてやる")
    return "----戦闘開始-----"
end
-- コルーチンを作成
co=coroutine.wrap(murabito)
endsubmit;
quit;

で、実際に3回話しかけてみると

proc lua;
 submit;
 -- 1回目話しかける
 print(co())
 -- 2回目話しかける
 print(co())
 -- 3回目話しかける
 print(co())
 endsubmit;
quit;












これがLuaのコルーチンです。色々あって奥が深いんですけど、とりあえず簡単な例です。
ようはcoroutine.yield()と書いたポイントで、一旦処理を中断して、戻すことができるわけです。次に呼び出された際はcoroutine.yield()の次の行から実行されます。
ちなみに戻り値も設定できます。
関数をコルーチンにするにはcoroutine.wrapを使えばOKです。

実は、ある機能自体に状態を記憶させるのは、コルーチン以外にもクロージャという機能で実装できます。
詳しくは過去記事「クロージャってなんじゃろ?」
http://sas-tumesas.blogspot.jp/2016/11/blog-post.html
をどうぞ。

どっちを使えばよいかは、好みとケースバイケースですが、今回みたいな単純な処理の場合、コルーチンの方がわかりやすいかもですね。カウント変数いらないし。


sgplotで日本地図を書いて新入社員の気をひこうって話

春ですね。
新SASプログラマーが増えるこの季節。

ベテランのSAS使いである皆様方は基本的には無口か、話が面白くない人が多いと思うので(偏見)、新人が気を使って適当な話題をふろうとして「SAS社ってどこにあるんですか?」とか当たり障りのないことを訊いたりするわけですよ。

そこで「六本木にあるよ」とか答えてるようじゃ、クソですよ。面白くもなんともない。

そういうときは敢えて何も答えずに、高速タイピングを駆使して
以下のプログラムを打ち込んで(5秒以内が望ましい)、F3かF8をターン!!とおっさんらしく、うるさく弾いて実行!!

data my_map;
set mapsgfk.japan end=eof;
ID=cats(ID,SEGMENT);
output;
/*緯度経度を追加*/
if eof then do;
 XX=139.729075;
 YY=35.660409;
 text="このへんにあるよ";
 output;
end;
run;

proc sgplot data=my_map noborder noautolegend;
/*地図*/
polygon x=LONG y=LAT id=id /
 fill outline lineattrs=(color=lightGray thickness=1)
 fillattrs=(color=Azure);
/*散布図とテキストを重ねてる*/
scatter x=XX y=YY;
text x=xx y=YY text=text/position=BOTTOMRIGHT ;
xaxis display=none;
yaxis display=none;
run;

アウトプットにどかんと





















となるわけです。(散布図は六本木ヒルズの緯度経度を指定してます)
なんて、ユーモアがあってデキる人だ!一生ついていきます!となること請け合いです。

まあ、真に受けて実践したら、キモがられること請け合いですけどね。

でも、たまにはSASのお勉強の間に、こういうの挟むのもいいんじゃないですかね。
(僕のようなふざけたSAS使いになってしまっても責任はとれない)

さて、本題。
sgplotのpolygonステートメントは凄い便利。
あとtextプロットが地味だけどスゲー使える。
gmapとちがってSAS GRAPHのライセンスもいらないし、緯度経度データがあればすぐかける。しかも他のグラフと簡単に重ねれるので、エリアごとの人口とか売上とかを図示するのも比較的かんたんにかける。

解析対象集団の構成フロー図とか、従来なら図としても表としてもプログラミングしにくい系のものも簡単に作れるし、こりゃあ役に立ちますぜ、奥さん。



Proc Luaのsas.write_dsに流す際にvarsテーブルで定義情報を制御してやろうじゃないって話

忘備録の「PROC LUAによる変数作成と行追加のはなし」を読み返してふと思ったこと
(http://sas-boubi.blogspot.jp/2016/09/proc-lua_26.html#comment-form)

既存データセットを上書き更新せずに変数追加はできない。
それはよくわかる。仕様ならしようがない。

代わりにsas.submitでコード流してくれとSAS社からのお達し。
SASデータセットの操作はSASコード実行するのが一番速いので、SAS社の言ってることは正しい。わかる。

けど、それじゃ、Luaで操作してるって感じがないから残念っていうmatsuさんの気持ちも凄いわかる。

じゃあさ、どうせ、setでデータセット読み込んで、データセットを上書き更新しちゃうならsas.load_dsでデータセットをLuaのテーブルにしちゃってさ、
ディスクリプタ部に該当するvars子テーブルの中身をいじって再度write_dsしちゃうのはどう?
処理時間的には非効率だけど、Lua書いてるって感じがして面白いのと、
Lua固有の操作で属性に関与できるから可能性のある考えじゃない?っていう提案。

要するに以下みたいなこと


data Q1;
VAR1=1;VAR2="A";output;
VAR1=2;VAR2="B";output;
run;









proc lua ;
submit;
local TB=sas.load_ds("Q1")
TB.vars.var3={type="C",length=20,label="ラベル"}
sas.write_ds(TB,Q1)
endsubmit;
quit;











write_dsはload_dsで作成される構造と同じテーブルを渡すことで
LuaテーブルからSASデータセットを作ってくれる。

おさらいしておくと、load_dsで作成される構造とは

proc lua;
submit;
local TB=sas.load_ds("Q1")
print(table.tostring(TB))
endsubmit;
quit;

で、自分で確認してほしんですけど(画像でかくなるから)、
テーブル直下に[1]とか[2]とかってobs番号がキーになって子テーブルができて、その中に
各変数名がキーになって値がはいる。
また、それとともにキー[vars]として子テーブルができて,その中に変数名がキーになって子テーブルができて、その中にさらに属性値名がキーになって値が入るわけです。
テーブルの入れ子構造で、本当面白い。

ちなみに、当然の疑問として、varsテーブルを作らずにwrite_dsするとどうなるかという話ですが、以下を実行してみてください。

proc lua ;
submit;
local TB={[1]={X=1,Y="A"}}
sas.write_ds(TB,"Q2")
endsubmit;
quit;

文字値のレングスのデフォが何故か500になったりするから要注意ね。

あと、Luaのテーブルに変換して、またSASデータセットに変換するという迂回をしてるんで速度はsas.submitで捌くのに比べると当然、ゲロ遅くなります。

あと当たり前だけど、ハッシュ型において、絶対的な格納位置みたいな概念ないから
SASデータセットにした際の変数位置って、どうやって制御したもんかなっていうのも課題としてあります。
正直、データハンドリングの本質的には、変数の格納位置なんて本来はどうでもいい。
そこを処理のとっかかりにしたり、やたら縛られるSASが独特なわけで。
本当は、んなもん飾りですよ。偉い人にはそれが、、、。





変数の値の中で、データを分解してソートしたり重複を除いたりしてみる

自分で問題を作って、自分で頑張って解いて
自分で自分を褒めるという悲しい遊びを繰り返す可哀想な子です。

例えば、以下のようなデータがあって、

data Q1;
length x $1000.;
x="3,1,2,5,3";output;
x="1,2,3";output;
x="1,1,1,3,2,2,2,4";output;
x="2";output;
x="";output;
x="999,15,3";output;
run;














xの中身をカンマで分解して、取り出した数字をソートして、重複をとって、再びカンマでつなぎ合わせて戻す。
ただし、入る数字は飛び番もあり、カンマの数も上限がないとする。
上限値を設定することなく、最速最短ステップで解け。

そういう問題に対して、以下のように書いてみました。

data A1;
set Q1;
if _N_=1 then do;
declare hash h1(ordered:"A");
declare hiter hi1("h1");
h1.definekey("val","valc");
h1.definedone();
end;

do i=1 to count(x,",") +1 ;
valc=scan(x,i,",");
val=input(valc,best.);
h1.ref();
end;

   rc = hi1.first();
   _x=valc;
   do while (rc = 0);
  if _x ne valc then _x=catx(",", _x,  valc );
      rc = hi1.next();
   end;

h1.clear();

keep x _x;
run;















パチパチパチ、できたできた。あんたはエライ!

あってるかどうか、最善手かどうかはわからない。

面白ポイントとしては
ハッシュオブジェクトでキー重複除去と並び替えを行って
それをハッシュ反復子オブジェクトで取得してるとこ。
反復子だと、キーがどう設定されてるかを無視するわけで、今回は並び替えが終わった時点で、キーの役目は終わってるわけですからね。
反復子がキーの呪縛から逃れて順次取得できるところにカタルシスがあるんです

そして、1obsごとにハッシュの中身をクリアしてるところも見どころ。
テンポラリーの作業場としてのハッシュオブジェクトという考え方は面白い。

一生懸命定義して、工夫してハッシュに格納したデータを、1obsごとにゴミの様に捨てることの背徳感。ゾクゾクってしません?



Luaの標準ライブラリ関数とかSASライブラリ関数とか

Luaにおいては、関数はfunctionで定義するわけですが、
よっぽど基本的なものは、標準ライブラリとしてproc lua内で使用できます。

たとえばprint()とかipairs()とかtype()とか、今まで普通に説明してきましたが
これらは基本ライブラリの関数です。

標準ライブラリには、基本ライブラリの他にも色々あって
数学関数ライブラリのmathや、文字関数ライブラリのstring、
テーブル操作ライブラリのtableなどです。ライブラリっつうか、テーブルの中に格納された関数を使うイメージで、ライブラリ名.関数名で使います。

例えば以下のイメージ。

proc lua restart;
submit;

--mathのmax関数で最大値
x=math.max(2,5,3,1)
print("max関数:",x)

--stringのsub関数で部分文字列
y=string.sub("ABCDEF",2,3)
print("sub関数:",y)

--tableのsort関数でソートして、unpackでリスト展開
t={5,3,1,2}
table.sort(t)
print("sort関数:",table.unpack(t))

endsubmit;
quit;







どんなライブラリがあって、どういう関数があるかはググッてもらえばすぐに
でてくるのですが、注意があります。

proc luaでは対応していないライブラリや関数があるということです。

例えば外部ファイル操作のioライブラリやOS絡みのosライブラリなどは
対応していないようです。
ようですと言うのは、使ってみるとめっちゃエラー吐いたり
固まったりするので、どうやら無理っぽいという経験でしかないからです。
将来的には動いたりするのかも。
まあ、でもSASの中で動かしてるので、外部とのやりとり系がうまく動かないのは当然なのかもしれません。

でね、これまたproc lua特有なのですが、sasライブラリというのがあってですね。(言葉がややこしい!SAS用語とかぶるから!!)
sas.submit()とかもそうなんですが、

基本的に[sas.SAS関数名]でSAS関数を使うことができます。
つまり、以下のような感じ。

proc lua restart;
 submit;

 t={1,2,3}
 x=sas.catx("," ,table.unpack(t) )
 print(x);

 print(sas.max(3,4,2))

 y=sas.substr("ABCDEF",2,3)
print("substr関数:",y)

 print(sas.constant('pi'))

 endsubmit;
quit;







Luaの標準ライブラリとsasライブラリで、機能が重複しているものは多々あります。
string.subとsas.substrとか、minとかmaxとか。
どっち使ってもいいんですが、基本的に勝手知ったるSAS関数の方がみんなわかりやすいかも。


でも、これまた注意点ですが、全部はサポートされてないんですね。
このへん、ちゃんと独立したproc luaのリファレンス作ってくれないとつらいですね。




MAXをとるためにORDINALを使う屁理屈

実戦用というよりかは
頭を柔くするパズル感覚、昔やってた「詰めSAS」的な話として見てください。

さて、以下のデータセットがあって、

data A;
X1=1; X2=3 ;X3=2;output;
X1=.;  X2=.  ;X3=2;output;
X1=3; X2=.  ;X3=3;output;
X1=.;  X2=.  ;X3=.;output;
run;









MAXをとれと言われれば一目、こう書きます。

data B;
set A;
 M=max( of X: );
run;









しかし、NOTEがでます。










NOTEを回避するには

data C;
set A;
 if n(of X:) ne 0 then M=max( of X: );
run;

と書くわけですが、結局、引数が全欠損の場合、いずれにせよ
結果は欠損で変わらない。

そうすると、NOTEの有無を除けば「 if n(of X:) ne 0 then 」の部分は完全な無駄手、
結果は同じなのに一手損しているようで気持ち悪いなぁと昔から思っていました。

個人的な感性の話を抜きにしても、
min maxが入れ子になっているとき、かつ各ブロックで全欠損が生じる可能性が許容されている場合、別に出てもいいNOTEを回避するためだけに、いちいち、分解して書かないといけなかったりして面倒なこともあります。

これ、実は以下のように書けば、ifを書かずにNOTEもださずに、同じ結果に
なると思うんですね。

data D;
set A;
 M=ordinal( 3 , of X: );
run;

ordinal関数は、欠損値を含めて、n番目に小さい数をとるという関数です。
nを引数の数に合わせることで、最終番目に小さい数、すなわちそれが最大値という理屈です。

はなから欠損値を含めることを想定した関数なので、引数が全欠損でも仕様範囲内なのでNOTEはでない。
同値があっても、引数の数を指定していれば結果は揺らがない。

最大をとるために、あえて逆に小さい方からみるっていう発想が頭の体操になるなぁと。

対象引数リストが配列なら、ordinalの第一引数にはdim関数を入れちゃえば、より安心なコードになるね。



毎度毎度「SAS 日付 フォーマット 」とかでググるのはもうウンザリなんだ!!って話

日付を「January 1, 1960」(WORDDATE)でだせとか、「昭和35年 1月 1日」(JNENGO)でだせとかってときに毎回、なんのフォーマットあてればいんだっけってなって、だいだい検索してる。

検索すればすぐ出てくるからいいんですけど、何回も何回もデジャブってると、ちょっと面白くない。

以前の記事「カラーリストの出力」
http://sas-tumesas.blogspot.jp/2017/01/blog-post.html
と似た話だけど、SAS使いなら、わかんないことはSASにコードで教えてもらおうぜってことで

以下のコードなんかどうでしょう?


proc sql;
select fmtname
 ,fmtinfo(fmtname,"desc") as 説明
 ,putn(0,cats(fmtname,maxw,".")) as 例
from dictionary.formats
where fmttype eq "F" and first(fmtname) ne "$"
;
quit;

実行すると、その環境で使用可能な全数値フォーマットと
fmtinfo関数で取得したフォーマットの説明、0にそのフォーマットを
あててみた結果がずらっとでます。(画像は一部を適当に切ってます)












































ショートカットキーとか省略形に追加して、すぐ呼び出せるようにしとくと便利

でもね、

アフリカーンス語では金曜日のこと「Vrydag」って言うんだ~。
てかアフリカーンス語ってなんだ?
ググッてみよ~、へ~、とかって遊んでしまって、本末転倒なんですけどね。

中途半端な優しさはいらない。いっそ激しくNOAUTOCORRECT

さて問題。
以下のコードを実行するとどうなるでしょうか?

date a;
sent sashelp.class;
fi sex="女子" the doo;
putit "A";
outputsomething;
endit;
ru;


なんじゃこれ?こんなもん通るかっ!って思ったあなたはまだ甘い!!

WARNINGはでるけど、処理は実行されます。
つまり







































ということ。

スペルミスってレベルを超えてる気がするけど…。

まあ、SASの優しさではあるんだけど、スペルミス混じったコードがそのままになっているのは恥ずかしいことですよね
いっそ一思いにエラーにして俺を叱ってくれ!って思うこともありますよね。


そんなドMには

options noautocorrect;

を送りましょう。


これを使って、さっきのコードを流すと










































気持ちいいくらいに全否定

今更ながら9.4でPROC SQLにMEDIANが集約関数として実装されたよ。

ワシはPROC MEANSつかわんと、要約統計量も全部PROC SQLで書くんじゃい!!
というタイプの人にとって、MEDIANがSQL関数として実装されていないのは大きな障壁でした。

「WARNING: MEDIAN関数が1引数のみを使用して呼び出されました。しかし、SQL集計関数ではありません。SQL集計は行われません。」
に落胆した人も多いことでしょう。

いや、SASなんだから、普通にUNIVARIATEとかMEANS使おうよって気もしますが、一応9.4から対応されましたよ。

proc means data=sashelp.fish median;
var weight;
run;

proc sql ;
select median(weight) as 中央値
from sashelp.fish ;
quit;


















ちなみにUsage Noteでたまたま見つけて、知ったんですが、SQLリファレンスのWhat's Newに記載がないのはなんでなの??


「Usage Note 12133: Prior to SAS® 9.4, the Base SAS® MEDIAN() function has limitations when used in PROC SQL」
http://support.sas.com/kb/12/133.html

outputメソッドって引数に複数指定できたんだねって話

最近、初心に戻ってハッシュオブジェクトのリファレンスを読み返していて初めて知ったこと。

outputメソッドにdataset:を複数指定できる!おぉ、知らなかった。

例えば

data Q1;
X=3;Y="A";output;
X=2;Y="B";output;
X=1;Y="A";output;
X=1;Y="A";output;
X=2;Y="B";output;
X=3;Y="B";output;
run;













といったデータがあって、
data _null_;
set Q1 end=eof;
if _N_=1 then do;
declare hash h1(dataset:"Q1",multidata:"Y",ordered:"Y");
h1.definekey("X");
h1.definedata(all:"Y");
h1.definedone();
end;
h1.ref();
if eof then h1.output(dataset:"OUT1(where=(Y='A'))"
                                       ,dataset:"OUT2(where=(Y='B'))");
run;

とすると

【OUT1】


【OUT2】







となる


お~、そうなんだ~。
複数指定したいと思ったことはあまりないけど、知っておいて損はないですね






データをざっと見る話

プログラムをつくる際、ステップのたびにデータセットの中を確認するのはさすがに大変だけど、ちょいちょい覗いてチェックしながら書いた方が、結果的にミスなく速く終わるっていうのはよくある話です。

で、そういう話からデータをざっと見るのに、どんなことされてますか?って聞かれました。

はい、残念ながら特にこれといった凄技は使ってないです。

例えば僕の場合、補助的なお手軽チェック法としては
上位10カテゴリをmaxlevels=10ですくって全変数見る以下のようなコード

proc freq data=sashelp.cars order=freq;
 tables _all_ / maxlevels=10;
run;



























gsubmit "proc freq data=%8b.'%32b'N  order=freq;tables _all_ / missing maxlevels=10;run;"

と改造して、SASの右クリックカスタムメニューに入れているので、気になるデータセットを右クリックすればすぐにfreqの結果を見れるようにしてます。
(やり方は以下の記事参照)

gsubmitでSASメニューをカスタマイズしたおす話
http://sas-tumesas.blogspot.jp/2015/12/gsubmitsas.html

_all_で指定してるから、いちいち変数指定する必要や漏れもなく手軽。ID番号や登録日もどざっとでちゃうけど、10個なんで、スクロールですぐすっ飛ばせる。

最後はもちろんmaxlevelつけずに見た方がいいけど、途中確認は簡易で高速な方が、小まめにやる気が起きやすいかも。

あとはクロス表でチェックしたい際に
listオプションつけた方が目視チェックしやすいっていう話は、よく見ますよね
9.4からはcrosslistオプションが追加されました。listの場合は合計行の情報が落ちるわけですがcrosslistだと合計に関してもlist化されます。全体集計に意味がある場合はこっちの方がいいかなぁ

何もつけない場合も含めて3種の出力を比べてみる

title"通常";
proc freq data=sashelp.class;
tables sex*age;
run;

title"list";
proc freq data=sashelp.class;
tables sex*age/list;
run;

title"crosslist";
proc freq data=sashelp.class;
tables sex*age/crosslist;
run;






























あと個人的に好きなのが、クロスでチェックしたい際に、リストと合わせて
ods graphics on;
proc freq data=sashelp.cars order=freq;
 tables ORIGIN*TYPE/ plot=mosaic;
run;
ods graphics off;



とfreqのついでにモザイクプロットがplot=mosaicとするだけで出て来るのでつけてます。見た目が素敵で和むっていうのが主な使用理由ですが、リストでは気づきにくいけどプロットすると意外に気づくこともあるものです。

数値変数についてはやっぱり

ods graphics on;
ods select BasicMeasures plots;
proc univariate data=sashelp.class plot;
var _numeric_; 
run;
ods graphics off;






























って感じかなぁ。
要約統計量だけ見て、すぐに状況把握できるといいんでしょうけど、僕はグラフ欲しい派です。
ヒストグラムと箱髭は、考えた人偉いなぁって思います。
やっぱり、ID番号とか、見なくていいものもでちゃうし、グラフ書くと時間かかっちゃうのがネックですけどね

ああ、あと、実際見たことあるのは、変数をビン化するhpbinプロシジャ(9.4)をデータチェックに使っている人がいましたね。ヒストグラム描けばって思うけど、データ量が凄くてできるだけ高速に省エネでっていう状況ならありですかもね。デフォルトだと
データ範囲(最大値-最小値)をビン数で割った数の区切りでビン化されます

proc hpbin data=sashelp.class numbin=10;
 input _numeric_;
run;



subpad関数はもう少し使われてもよさそうだけどもって話

substr関数を使って、特定の文字数で変数を分割したいなってときがあったとします。
3文字ごとに3つの変数にわけようと思って、以下のコードを書いたとします。

data b;
set a;
x1=substr(x,1,3);
x2=substr(x,4,3);
x3=substr(x,7,3);
run;

ところが流し込むデータを受け取ってみると、こんな感じでした。

data a;
x="1234";output;
run;

先のコードをデータセットaに対して実行すると





となり、結果は正しいのですが






ログにノートがでます。
xのlengthが4のため、4文字目から3文字とれと言われても
はみ出てるやんっていうメッセージです。
同様の理由で7文字目はそもそもないのでメッセージがでます。

まあ、理由も明確に説明できるので
いいちゃいいかもしれませんが、理想としてはログにこういうのは極力だしたくないし
_ERROR_=1たつのはやはり健全ではない。

この場合においては、subpad関数が有効で、はみ出た分は自動的に空白で埋めて
ログには何もださない。SASによる説明に少し補足すると
「必要に応じて空白埋め込みを使用し、(第3引数で)指定した長さの部分文字列を返します。」

書き方はsubstrと同じで以下になる。

data c;
set a;
x1=subpad(x,1,3);
x2=subpad(x,4,3);
x3=subpad(x,7,3);
run;

ただし、要注意なのがsubstr変数が作るx1 x2 x3のlengthは
もとの変数xのlengthが引き継がれますが
subpad関数で作成される変数は文字変数戻りのデフォである
$200.になるのでそこは要注意です。

substrなら、性質上、元の変数のlengthを引き継いでおけば生成される変数で内部の値が
それを超えることはありえないので文字切れはないですが、subpadの場合、
 x1=subpad(x,1,300);などとした場合201-300文字は切れることになります。

ただ、その場合は親切設計で



とわかりやすくですので、ログを全く見ないアンポンタン以外は、まず気づけるはず。

割といい関数だけど、存在をよく忘れてしまう






小ネタ:%putに_readonly_と_writable_が追加されてるなぁ

本来、以前の記事といっしょに挙げようと思っていて忘れていた話。

9.4M3でreadonlyが追加されたことを受けて、%putの指定で、
読み取り専用のマクロ変数を出力する_readonly_と、逆に
上書き可能なマクロ変数を出力する_writable_が追加されている。

_global_ _local_ _user_と_all_ _automatic_も復習しておくと

%global A;
%global B;
%global  /readonly C=1 ;

%macro m1;
%local A;
%local D;
%local /readonly E=1 ;

%put *===_global_===*;
%put _global_;

%put *===_local_===*;
%put _local_;

%put *===_user_===*;
%put _user_;

%put *★===_readonly_===*;
%put _readonly_;

%put *★===_writable_===*;
%put _writable_;

/*===_all_はたくさん出るから割愛===*/
/*%put _all_;*/

/*===_automatic_はたくさん出るから割愛===*/
/*%put _automatic_;*/

%mend;

%m1

結果は

*===_global_===*
GLOBAL A
GLOBAL B
GLOBAL C 1
*===_local_===*
M1 A
M1 D
M1 E 1
*===_user_===*
M1 A
M1 D
M1 E 1
GLOBAL A
GLOBAL B
GLOBAL C 1
*★===_readonly_===*
M1 E 1
GLOBAL C 1
*★===_writable_===*
M1 A
M1 D
GLOBAL A
GLOBAL B



みたいな感じ

小ネタ:なぜこの男らしいコードが通らないのか

人のコード見てて、すごい面白かったのが
以下のコードです。

options nodsnferr;
data all;
set wk1-wk9999999999;
run;

なるほどね。この人は set wk: ;の書き方を知らなかったわけです。
しかしnodsnferrを調べて頑張った。勢いを感じるコードですね

ちなみにnodsnferrについては

http://sas-tumesas.blogspot.jp/2014/03/error-workxxdata-0.html

を参照してください。

その発想は面白いのですが、残念ながらこのコードは実行すると失敗します。

なぜ失敗するでしょうか?また具体的にどう直せばコードとして成立するでしょうか?

正解をいうとですね、
一度実行してみて、エラーメッセージをみるとはっきりします

ERROR: 数字付きデータセットリスト(WORK.wk1-WORK.wk9999999999)の数字が大きすぎます。最大値は2147483647です。

ね。そういうわけなんですよ。

一般的な言語の整数型の範囲は -2,147,483,648 ~ 2,147,483,647( ± 2**31 )ですがその値が、データセットの接尾数値検索の上限値になってるんですね。
(2の31乗なのは2進法で32bitのうち、符号に使う1を引いた数ですね)

なので
set wk1-wk2147483647;とすれば、コードとしては成立します。
しかし注意!!!!!! コロンと違って、ハイフンでつないだ場合、SASがデータセットを順に見ていくのにそれなりに時間かかります。億超えなんて指定した場合は、洒落にならんぐらい時間かかるか、場合によっては落ちます。

なんにでも限界値というものがあるという教訓ですね。

以前このブログで警鐘をならした
「西暦2万1年問題」に通じるものがありますね。
ちなみにこの問題はまだ未解決です。SAS社には早急に対応してほしいですね。
残された時間は、あと1万8千年もないですからね

西暦2万1年問題
http://sas-tumesas.blogspot.jp/2014/11/1.html

if 0 then setにunion corrしたviewを突っ込むことで、2つのデータセットのLengthを大きい方に自動的に寄せることができるんじゃないかというアイデア

データステップの、誰もやらない、絶対役に立たないマニアックな書き方を紹介して
一部の特殊な嗜好を持った悲しい人達が、仕事中に見てニヤニヤできればいいなぁっていう思いでやってる部分が大きいのである意味、ブログの主旨にあった記事かも。

今日、仕事中に、setステートメントの前に割り当てステートメントで、setで指定しているデータセット中に含まれる変数を指定するとlengthはどうなるかという話で恥を書く。

ようするに以下のコードを実行した際にデータセットがどうなるかって話。

data XXXX;
 NAME="A";
 set sashelp.class;
run;

まあ、実際やってみて貰えればわかるんですけど、僕はちょっと勘違いしてたんですよ。
そしたら、えぇ、こんなのBASEレベルの基本知識っすよとドヤ顔される。
誰とはいいませんけど僕の席の後ろの席に

要するに、割り当てだろうが、setだろうが、先に入った値、変数でlengthが決まるという基本知識ですよ、ええ、SASの基本ですね、はいはい。うっかりしてましたってば。

例えば以下の2つのデータセットがあったとして

data X;
length A $1. B $5.;
A="1";B="1234";C=1;
run;





data Y;
length B $1. A $5.;
B="1";A="1234";D=2;
run;






Xを先にsetすると

data OUT1;
set X Y;
run;







よくみると、Aの2obs目が一文字目で切れてる。
確認してみると










データセットXの変数AのlengthがデータセットY由来にも適用されちゃってるわけですね。

逆に今度はYを先にsetすると


data OUT2;
set Y X;
run;







Bが切れるわけですよ。

どうすればいいのか?

いや、素直にlengthステートメントで指定すればいいんですよ。
切り捨てが起きうる状況の場合、必ずログに






こういうのがでるんで、そしたらlengthを調べて、十分な値にしてやればいいんです。

はい、でもそれだと面白くもなんともないんで、lengthステートメントを使わない方法を考えてみます。
たとえば変数が凄い多くあって、lengthが仕様で定義されてない場合とか面倒でしょ。

これも、指摘してきた人に指摘されたことですが、SQLのunionを使うと、大きい方にlengthを寄せてくれます。
つまり

proc sql;
create table OUT3 as
select * from X
union all corr
select * from Y
;
quit;

とすれば













おぉ!!
しかし、Xにしかない変数C、Yにしかない変数Dが落ちてやがる。

じゃあ、corrとればいいんでない?と思っちゃうかもしれないけど、変数情報が完全に同一でない
状況でcorrをつけないと、まあ、ひどいことになるから、興味のある人はやってみてください。

/*---------追記-----------------*/
matsuさんに突っ込まれました。「outer union corr」使えばいいやんと。
あ~、しまった、その通りです。ボケてましたよ…。駄目だなぁ。
というわけで、以下あんまり意味のない記事になってしまいましたが、
連結後の処理をSQLじゃなくて、データステップでやりたい場合?になるのかなぁ。
/*-----------------------------*/


じゃあどうするかということで、例えば2ステップですが、こんなんどうでしょう?

proc sql;
create view vw1 as
select * from X
union corr
select * from Y
;
quit;

data OUT4;
if 0 then set vw1;
set X Y;
run;
















おぉ!ばっちし!!
多分そんなに遅くないし、どんなデータが来てもコードを変えずに実行できるし、なかなか面白いんじゃないですか?

多分、他にもやりようあるので、是非考えてみてください。

以上。