裏シージャ 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はあんまり知られてない。

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