smoothconnectでSG系グラフの線を滑らかにする話

最近、エクセルで滑らかな曲線の折れ線グラフを書いてる人がいて、「へ~、そんなんできるんだ」と言うと、「は?そんなんも知らないんですか?」と馬鹿にされました。

だって、最近のエクセルあんまりよく知らないしと言うと、「スムージングにチェック入れるだけだし、2003とかでもありましたけど?」とさらに馬鹿にされました。

挙句に「SASではどう書くんですか?」と聞かれ「え、そんなんできたかな?ちょっとわかんない」と答えると、「SASが書けるだけが取り柄なのに全然駄目じゃないですか」と軽蔑の眼差しを向けられてしまいました。

家に帰って、泣きながら調べてみると、あったよ、smoothconnectオプション。
でも9.4からか、結構最近だなぁ。

適当にデータ作って

data Q1;
    call streaminit(777);
    do X=1 to 20;
        Y=rand('uniform');
        output;
    end;
run;

まずは普通にプロット

proc sgplot data=Q1;
    series x=X y=Y/ lineattrs=(thickness=2);
run;






















で、次に

proc sgplot data=Q1;
    series x=X y=Y/smoothconnect lineattrs=(thickness=2);
run;





















おぉ!できた!できたよ!

ちなみにGTLで表現するなら

proc template;
  define statgraph temp1;
     begingraph; 
     layout overlay;
        seriesplot x=X y=Y / lineattrs=(tickness=2)
         smoothconnect=true;
     endlayout;  
   endgraph;
 end;
run;

proc sgrender data=Q1 template=temp1;
run;























で同じだと思う。


ちなみに、また別記事にするかもですが、データ数が多い場合、細かい部分で線が表現しきれず
ドットぽくなってしまうことがあります。
またwaterfallpltのようなぴったりくっついた棒グラフを書きたいのになんか一部に変な隙間ができたりすることもあります。
そういう、細かい部分で生じるアラ?を補正にするにはsubpixelオプションを使います。
今回のグラフでも、画質マニアなら


proc sgplot data=Q1 subpixel;
    series x=X y=Y/smoothconnect lineattrs=(thickness=2);
run;

または

proc template;
  define statgraph temp1;
     begingraph/subpixel=on
     layout overlay;
        seriesplot x=X y=Y / lineattrs=(tickness=2)
         smoothconnect=true;
     endlayout;  
   endgraph;
 end;

proc sgrender data=Q1 template=temp1;
run;

とすると






















うん、やっぱこの程度のグラフだと素人には違いがさっぱりわかりませんね


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



書籍紹介⑪ 改訂版 統計解析ソフト「SAS」

【統計解析ソフト「SAS」】 という書名のSASプログラマーの名著があります。
以前、このブログでも紹介したことがあり、amazonについてるベタ褒めのレビューも僕です。

前回記事
http://sas-tumesas.blogspot.jp/2013/10/sassas.html

SASやってる人(特に医薬)には必ず薦めるぐらい良い本で、SAS本にしては結構売れてるんじゃないのかなぁと
思っていたのですが(僕も何冊かは貢献したはず)、
急に絶版になるというニュースを聞き、とても落ち込んでいました。

しかし!!別の出版社から再販が決まり、改訂版として復活するそうです!!

出版社のページ
http://www.cutt.co.jp/book/978-4-87783-503-3.html

著者の御一人である方のページをみると、まあ、某出版社といろいろあって大変だったんだなぁってことがわかります。

続・わしの頁
http://www.cwk.zaq.ne.jp/fkhud708/index2.html

レイアウトが見づらかったのはそんな事情があったんですね。


まだ中身を見てないのですが、ページ数をみると旧版の倍近くになっているので
きっと改善されているのでしょう。

ん?そういえば、お値段もなんだか倍になっているような……

まあ、でもね、SAS社が出してる「Base Programming for SAS® 9 完全ガイド 第2版 」は
783ページで19,440円(税込)ですしね。

SASの基礎と実務で使うテクニックを知るために公式のトレーニングに
十何万もかけていくならこれ一冊で充分では?と、個人的には思いますが。


proc stremの世界

テキストファイルに任意の文字列、それもデータや出力結果じゃなくて
もっと文章的なものやプログラムコード、あるいはhtmlのようなマークアップファイル言語のファイル
を出力したい時ってありますよね。

基本今までは、fileステートメントと、putステートメントとかでやりくりする感じだったのですが
9.4から面白いプロシジャが追加されてました。
その名もstreamプロシジャです!!
(一応9.3から試用版で使えるみたいですが)

まずは何より、使用例を見ましょう!!

%let person1 = おじいさん;
%let person2  =おばあさん;
%let place1 = 小さな村;

filename a "C:\Users\SASYAMA\Desktop\test.txt";
proc stream outfile=a resetdelim="goto";
begin
むかしむかし &person1.と&person2.が&place1 に住んでおったそうだ goto NEWLINE;
&person1.は山に芝刈りにいき goto NEWLINE;
&person1.は川に洗濯にいった。
;;;;

まず、ちょっと変わってるのが、このプロシジャはrun;もquit;もつけません。
プロシジャの終わりをセミコロン4つのステートメントで示します。

で、上記を実行すると、test.txtが作成され、その中身は以下の感じです。







コードを解説すると、まずfilenameで名前付けしたものをoutfile=で指定します。

そしてbeginステートメント以下、書いた文字列がそのまま出力されます。
ただし改行コードの出力についてはそのままだと反映されません。
まずresetdelim=で、通常の文字列ではないことをSASに分からせるためのデリミタキーワードを
設定します。
今回はgotoがそれです。そして設定したキーワード + NEWLINEとすることでそれが改行となります。

さて、しょっぱなの例が意味不明でしたが、海外論文などをみるとweb系で、データに応じて複雑な
htmlを作らなければならないような方々にかなり歓迎されているようです。
単純なのを作ってみると

filename c "C:\Users\SASYAMA\Desktop\test2.html";
proc stream outfile=c;
begin
<html>
<head><title>stremプロシジャの世界へようこそ</title></head>
<body>
<h1>あなたのアカウント名は &sysuserid.. だ!</h1>
<h2>
今日の日付は
%sysfunc(today(),yymmdds10.)です。
</h2>
<h3>
こんな感じでマクロを絡めてのテキスト生成が楽なのです
</h3>
</body></html>
;;;;
run;

みたいな?










ただ、これだけだと、そこまで便利か?って感じですが、実はまだまだ奥深くて
たとえば以下のように外部ファイル b.txtが既にある場合







filename file_a "C:\Users\SASYAMA\Desktop\a.txt";
filename file_b "C:\Users\SASYAMA\Desktop\b.txt";

proc stream outfile=file_a resetdelim="goto";
begin
data A goto NEWLINE;
%include file_b; goto NEWLINE;
run;
;;;;

とすると、a.txtの中身は









このように、%includeによって外部ファイルの内容を展開して埋め込むことが
できます。
この機能が、パッチワーク的に複雑なテキストを構築しやすくて良いようです。

どうです??
単純だけど、結構可能性ありそうじゃないです?

ちなみに僕はかなりハマッてます!


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

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

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

 

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

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


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

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

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



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

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

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

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

















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

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

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
















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

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

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

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

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

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

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

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

















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

以下の通りです

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













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

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

以上、おそまつ。


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

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

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

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










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


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

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

run;

ログには







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

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

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

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

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

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

run;

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

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

run;

結果は同じ








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

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

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

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

run;

















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

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

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

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

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


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

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

結果は








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

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

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

run;

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

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

data A;
X=1;
run;

data A;
X='1';
run;


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

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


まあ

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


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

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

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

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

ので、


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

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

%m

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

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


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

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

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

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

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