2018年3月30日金曜日

文字値の末尾にある空白問題




以下は、長さを「5」に設定した変数Xに「abc」という文字を格納しています。

data DT1;
   length X $5.;
   X = "abc";
run;

  X  
 abc 



次に以下を実行してみます。

data DT2;
   set DT1;
   Y = index( X, " " );
run;

  X  
  Y  
 abc   4


index関数は、変数に対して「特定の文字値」が何バイト目に含まれるか返します。
(シングルバイト専用。日本語等マルチバイト文字には使用NG)


今回は、変数Xに半角スペース(空白)が何バイト目に含まれているかを求めています。
結果は「4」となり、4バイト目に半角スペースが含まれているということになります。

「abc」だから半角スペースなんてないはずですよね。



実は内部では以下のように長さ5のうち、余った末尾の2バイトには半角スペースが入っています。
     
 X 
 a 
 b 
 c 
   
   




なので、

  Y = index( X, " " );

は半角スペースが4バイト目にあると解釈したわけですね。
また以下のように文字値を結合する場合も

  length Y2 $20.;
  Y2 = X || "def";

変数Xの末尾に半角スペースが2つあるので「abc  def」という結果になってしまいます。



そこでよく、trimn関数などで末尾の半角スペースを取り除く処理を入れたりします。

  Y = index( trimn(X), " " );

  length Y2 $20.;
  Y2 = trimn(X) || "def";



ただし、比較演算では末尾の半角スペースを考慮する必要はありません。
例えば「 "abc" = "abc   " 」という比較演算の結果は「等しい」と判断されます。

「比較演算では末尾の半角スペースの有無に意味を持たせていない」ということですね。


今回のデータでも、where条件を書くとき、

   where X = "abc  ";

というように、わざわざ末尾を気にしなくても、以下で問題ないです。

   where X = "abc";


2018年3月27日火曜日

日付・時間・日時の書き方




例えば、2018/03/27という日付値を作りたい時、
以下のように書かれる方も多いと思います。(私もそうですが)

data DT1;
   format x date9.;
   x = input('2018/03/27',yymmdd10.);
run;


ですが、もっと簡単に日付値(と時間値・日時値)を作ることが出来ます。




日付値を作る

「'ddmmmyyyy'd」 という書式で日付値を作ることが出来ます。

data DT1;
    format x date9.;
    x = '27mar2018'd;
run;




時間値を作る

「'hh:mm:ss.s't」 という書式で時間値を作ることが出来ます。

data DT2;
    format x y z time11.2;
    x = '23:05't;
    y = '23:05:10't;
    z = '11:05:10pm't;
run;




日時値を作る

「'ddmmmyyyy:hh:mm:ss.s'dt」 という書式で日時値を作ることが出来ます。

data DT3;
    format x y z datetime21.2;
    x = '27mar2018:23:05'dt;
    y = '27mar2018:23:05:10'dt;
    z = '27mar2018:11:05:10pm'dt;
run;



時間値と日時値で「am/pm」が指定できるのは知らなかったなぁ、常識なのかな。
また一歩SASマスターに近づいた。


2018年3月23日金曜日

PROC FORMAT入門6 : PICTUREステートメント




PICTUREステートメントで日付・時刻・日時値に書式を設定する「DATATYPE=オプション」の説明をしていきます。



📝 前置き
  • PICTUREステートメント1」の前提知識が必須
  • 設定・環境で動きが変わりまくるので挙動確認が必須。一部注意点を最後に記載したのでそちらも要確認!




構文

 PROC FORMAT;

      PICTURE フォーマット名
             =  '書式の定義 (DATATYPE = データのタイプ)
             =  '書式の定義'  (DATATYPE = データのタイプ)
           ・・・
      ;
 RUN;


・フォーマット名の末尾は数字NG(例えば「TEST1」など)

・フォーマットを適用する変数が、日付(DATE)・時刻(TIME)・日時(DATETIME)のどれなのかを「DATATYPE=」に指定

・間違ったDATATYPEを指定しないように!
(例えば対象の変数が日付値なのに、DATATYPE=DATETIMEを指定するなど)






* フォーマット作成 ;
proc format;
   picture mydatetm (default=10)
      low - high = '%Y_%0m_%0d' (datatype=datetime)
   ;
run;

* フォーマット変換 ;
data TEST;
   length X 8. Y $10.;
   X = '14mar2018:21:5:14'dt;
   Y = put(X,mydatetm.);
run;

 Y 
 2018_03_14 


「datatype=datetime」として、日時値の変数に対するフォーマットを作成しています。

書式を定義する部分(青字箇所)に、やたら「%」が書かれてますね。
この「%なんちゃら」の部分を「ディレクティブ」と呼びます。

ディレクティブの組み合わせによって書式を定義します。



ディレクティブ(一部)


※ オプションやディレクティブの組み合わせ、SASの環境・言語設定によって、動作しなかったり動きが変わるため要挙動確認!

ディレクティブ設定内容対応するDATATYPE
%y
年(2桁)
%0yとすると年が1桁の時に先頭に0が埋められる
date,
datetime
%Y年(4桁)date,
datetime
%m
%0mとすると月が1桁の時に先頭に0が埋められる
date,
datetime
%d
%0dとすると日が1桁の時に先頭に0が埋められる
date,
datetime
%H時(24時間表記)
%0Hとすると時間が1桁の時に先頭に0が埋められる
time,
datetime 
%M
%0Mとすると分が1桁の時に先頭に0が埋められる
time,
datetime
%S秒(秒の整数部分のみ対応)
%0Sとすると秒が1桁の時に先頭に0が埋められる
time,
datetime
%%例えば「100%」と表示するには「100%%」と指定date,
time,
datetime
・・・など


例えば、書式を「%Y年%0m月%0d日」と定義したフォーマットを日付値に適用した場合、「2018年03月14日」みたいな感じで適用されます。


・ディレクティブには「%y」と「%Y」のように小文字・大文字の区別があるので注意
・他にも曜日や月名、週数などを設定するディレクティブもあります。あわせてマニュアルもご覧ください。




注意事項


注意①

以下記事の最後らへんで解説している通り、フォーマット適用後の文字の長さを考慮して「DEFAULT=オプション」も併用しないと、文字切れするケースがある。
PICTUREステートメント1

また、PICTUREフォーマットはデフォルト長などの長さに応じて、右詰めで適用されるので、
「    2018年03月14日」みたいに文字が右に詰められて左側に空白(半角スペース)が入る事があるのでそこんところもご留意下さい。



注意②

オプションやディレクティブの組み合わせ、SASの環境・言語設定など、様々な要素によって結果が変わります。
以下、SASの環境によって結果が変わる一例です。

* フォーマット作成 ;
proc format;
   picture mytime (default=8)
      low - high = '%p%I' (datatype=time)
   ;
run;

* フォーマット変換 ;
data TEST;
   length X 8. Y $10.;
   X = '21:05't;
   Y = put(X,mytime.);
run;

結果
SASの環境が「日本語」の場合
 Y 
  午後9  

SASの環境が「英語」の場合
 Y 
      PM9  


ディレクティブ「%p」と「%I」は、以下の意味になります。
・%p: timeやdatetimeの時刻に対してAM/PMを出力
・%I(大文字のアイ): timeやdatetimeの時刻に対して「時(12時間表記)」を出力


上の例ではSASの環境によって結果が変わっています。
(日本語の場合は「午前 or 午後」、英語の場合は「AM or PM」が適用された)


英語の設定にあわせたい場合は、以下のように「LANGUAGE=ENGLISH」と指定します。
(この方法でも環境とか他の要素で結果が変わるかも)

proc format;
   picture mytime (default=8)
      low - high = '%p%I' (datatype=time language=english)
   ;
run;

ちなみに、LANGUAGE=オプションは現状一部の言語にしか対応してません。
(日本語とか中国語とかには設定できない。なんでだ・・・)



注意③

欠損値に「DATATYPE=」を設定すると「ERROR」という文字が返ってきます。

proc format;
   picture mydatetm (default=10)
      . = '%Y_%0m_%0d' (datatype=datetime)
   ;
run;

data TEST;
   length X 8. Y $10.;
   X = .;
   Y = put(X,mydatetm.);
run;

 Y 
      ERROR 




PROC FORMAT入門 : 記事一覧

2. INVALUEステートメント
3. 範囲の指定

2018年3月16日金曜日

固定長のテキストファイルで、先頭スペースを保持して読み込む




以下のテキストファイルを読み込みたいとします。


C:\test\test.txt
0 .
1 a
2  b
3   c


*** 例①:失敗例 *****;
data TEST;
   length X 8. Y $3.;
   infile "C:\test\test.txt" truncover;
   input  X 1-1
             Y 3-5;
run;

  X  
  Y  
 0   
 1  a 
 2  b 
 3  c


文字変数Yには、テキストファイルの中身通り、先頭の半角スペースを保持して以下のように読み込みたかったのですが、ふつうに読み込むとこの半角スペースは消えちゃいます。

  X  
  Y  
 0   
 1  a 
 2   b 
 3    c



先頭の半角スペースを保持するには、以下のような工夫が必要です。
(後述しますが、文字欠損値の取り扱いだけ注意が必要)

*** 例② *****;
data TEST;
   length X 8. Y $3.;
   infile "C:\test\test.txt" truncover;
   input  @X
             @Y  $char3. ;
run;

  X  
  Y  
 0 
 . 
 1 
 a 
 2 
  b 
 3 
   c


INPUT  @読み込み開始位置   変数名   $CHAR読み込む文字の長さ.」という感じで書きます。

  • 「@読み込み開始位置」で変数毎に読み込みを開始する位置を指定し、
  • インフォーマット「$CHAR」を使って先頭の半角スペースを切り捨てずに読み込んでいます
  • 「$CHAR」は文字変数として読み込むときに使います。上の例の変数Xは数値変数として読み込みたいので「$CHAR」は指定していません


注意
  • 文字欠損値の取り扱いだけ注意
    • 最初の例(例①)では、テキストファイルの1行目のドット「.」は欠損値として扱われて文字変数Yに読み込まれましたが、
    • 今回の例(例②)では、$CHARを使ったことで、テキストファイルの1行目のドット「.」がそのまま「.」という文字値として文字変数Yに読み込まれました。


その他の補足

今回の例で使用している「TRUNCOVERオプション」について知りたい方は、以下記事の最後らへんで解説しています。



2018年3月13日火曜日

SASでトランプを作る



SGPLOTでトランプ作ってみました。

以下のような感じで1枚ずつ、マーク4種類 × 数字13枚 = 52枚のトランプがグラフとして出力されます。


出力結果の一部を切り貼りした寄せ集め



プログラム


※SAS9.4で動作します。バージョンや環境などで結果が綺麗に出力されないとかあるかもしれません。

ハッキリいってプログラムとしては汚いです。とりあえず作ったって感じです。
あとジョーカーを含めるとプログラムが複雑になっちゃうので、やめました。やるとしたらsymbolimageステートメントとか使って好きな画像をトランプの絵柄にしてジョーカーにする感じになります。


/*** トランプの数字とマークの表示位置をデータ化 ***/
data CARD;
    input SYMBOL:$7. NUMBER:$5. X Y;
    TOP_X     = 1.5;
    TOP_Y1    = 14;
    TOP_Y2    = 12.5;
    BOTTOM_X  = 3.5;
    BOTTOM_Y1 = 0;
    BOTTOM_Y2 = 1.5;
cards;
symbol1 1  2.5  7
symbol1 2  2.5  12
symbol2 2  2.5  2
symbol1 3  2.5  12
symbol2 3  2.5  2
symbol1 3  2.5  7
symbol1 4  2    12
symbol2 4  2    2
symbol1 4  3    12
symbol2 4  3    2
symbol1 5  2    12
symbol2 5  2    2
symbol1 5  3    12
symbol2 5  3    2
symbol1 5  2.5  7
symbol1 6  2    12
symbol2 6  2    2
symbol1 6  3    12
symbol2 6  3    2
symbol1 6  2    7
symbol1 6  3    7
symbol1 7  2    12
symbol2 7  2    2
symbol1 7  3    12
symbol2 7  3    2
symbol1 7  2    7
symbol1 7  3    7
symbol1 7  2.5  9.5
symbol1 8  2    12
symbol2 8  2    2
symbol1 8  3    12
symbol2 8  3    2
symbol1 8  2    7
symbol2 8  2.5  4.5
symbol1 8  3    7
symbol1 8  2.5  9.5
symbol1 9  2    12
symbol2 9  2    2
symbol1 9  2.5  6.999
symbol1 9  2    8.666
symbol2 9  2    5.333
symbol1 9  3    8.666
symbol2 9  3    5.333
symbol1 9  3    12
symbol2 9  3    2
symbol1 10 2    12
symbol2 10 2    2
symbol1 10 2.5  10.333
symbol2 10 2.5  3.666
symbol1 10 2    8.666
symbol2 10 2    5.333
symbol1 10 3    8.666
symbol2 10 3    5.333
symbol1 10 3    12
symbol2 10 3    2
symbol1 J  2.5  10
symbol1 J  3    10
symbol1 J  2.8  7.5
symbol1 J  2.8  5
symbol1 J  2.4  3.5
symbol1 J  1.9  4.5
symbol1 Q  2    10.5
symbol1 Q  2.5  11
symbol1 Q  3    10.5
symbol1 Q  3.2  8.5
symbol1 Q  3.2  6.5
symbol1 Q  3.05 3.85
symbol1 Q  2.5  4
symbol1 Q  2    4.5
symbol1 Q  1.8  6.5
symbol1 Q  1.8  8.5
symbol1 Q  2.75 5.8
symbol1 K  2    10.5
symbol1 K  2    8
symbol1 K  2    5.5
symbol1 K  2    3
symbol1 K  3    10.5
symbol1 K  2.8  8
symbol1 K  2.6  5.3
symbol1 K  3    3
;


/*** マーク別に1~Kまでのトランプを作成 ***/
%macro trump( unicode=, color= );


/* Attribute Map */
data MYMAP;
     ID="myid";
     VALUE="symbol1"; MARKERCOLOR="&color"; MARKERSYMBOL="symbol1"; output;
     VALUE="symbol2"; MARKERCOLOR="&color"; MARKERSYMBOL="symbol2"; output;
run;


/* Plot */
proc sgplot data=CARD noautolegend dattrmap=MYMAP;
     by NUMBER notsorted;

     xaxis min=-1 max=5   display=none;
     yaxis min=-1 max=15  display=none;

     /* トランプの形に線を引く */
     refline 1.2 3.8  / axis=x;
     refline -1 15 / axis=y;

     /* トランプのマークをUnicodeで取得 */
     symbolchar name=symbol1  char=&unicode;
     symbolchar name=symbol2  char=&unicode / rotate=180;

     /* トランプの左上の数字とマークをPlot */
     text    x=TOP_X    y=TOP_Y1    text=NUMBER  / textattrs=(size=0.6cm color=&color) ;
     scatter x=TOP_X    y=TOP_Y2                 / markerattrs=(symbol=symbol1 size=1.3cm color=&color);

     /* トランプの右下の数字とマークをPlot */
     text    x=BOTTOM_X y=BOTTOM_Y1 text=NUMBER  / textattrs=(size=0.6cm color=&color)  rotate=180 ;
     scatter x=BOTTOM_X y=BOTTOM_Y2              / markerattrs=(symbol=symbol2 size=1.3cm color=&color) ;

     /* トランプの真ん中のマークをPlot */
     scatter x=X        y=Y                      / attrid=myid  markerattrs=(size=2.1cm) group=symbol;

run;

%mend;

%trump( unicode='2660'x, color=black )  /* スペード */
%trump( unicode='2663'x, color=black )  /* クラブ */
%trump( unicode='2665'x, color=red   )  /* ハート */
%trump( unicode='2666'x, color=red   )  /* ダイヤ */




2018年3月8日木曜日

【SYMBOLGENオプション】展開したマクロ変数の値をログに表示する



「なんのオプションか分からないけど、みんな使ってるから自分も使っとくか」みたいなパターンが多いのがこの「symbolgenオプション」





   options symbolgen;


って書くと、プログラム実行時に展開されたマクロ変数の値がログに表示されます。



例①
%let x=123;
%put &x;


ログ
SYMBOLGEN:  マクロ変数 X を 123 に展開します。
123



例②
%macro test( ds=, out=, by=);

    proc sort data=&ds out=&out;
       by &by;
    run;

%mend;

%test( ds=sashelp.class, out=work.out1, by=name );


ログ
SYMBOLGEN: マクロ変数 DS を sashelp.class に展開します。
SYMBOLGEN: マクロ変数 OUT を work.out1 に展開します。
SYMBOLGEN: マクロ変数 BY を name に展開します。




元に戻すには


   options nosymbolgen;



2018年3月5日月曜日

PROC FORMATの「PICTUREステートメント」で気をつけたい内部の動き



PROC FORMATのPICTUREステートメントはちゃんと内部挙動を理解しないと痛い目を見ます。
動きが独特なので、しっかり理解できてる人はSASマイスターとして認定してもいいくらいです。


※今回の内部挙動に関する説明は私の推測も入ってるのであしからず。。間違いがあったらご指摘ください。




PICTUREステートメントの動き

以下のフォーマットを例に内部の動きを説明していきます。

proc format;
   picture TEST1_
       0 - 100 = '009.00%';
run;


① まず、フォーマットをあてる変数に10のn乗を掛けます。(n = 定義した書式の小数点以下桁数)

今回定義した書式は '009.00%' です。小数点以下桁数は2桁で定義されてますよね。
なのでフォーマットをあてる変数に10の2乗を掛けます。


1.23 → 123
3.456 → 345.6
(小数点以下2桁部分を整数部分にシフトさせている。)


②結果から小数部分を切り捨てます。
123 → 123
345.6 → 345


③この値を、定義した書式に右からあてはめていきます。

'009.00%' の数字セレクタ部分に右から数値をあてはめると、
123 → 001.23% →「  1.23%」
345 → 003.45% →「  3.45%」





「ROUND」「MULTIPLIER」オプションの動き


以下のフォーマットを例に内部の動きを説明していきます。

proc format;
   picture TEST2_ (round)
      0 - 100 = '009.00%' (multiplier=10) ;
run;


①まず、フォーマットをあてる変数には10のn乗を掛けずに、multiplier=オプションに設定した数を掛けます。


今回は「multiplier=10」なので
1.23 → 12.3
3.456 → 34.56


②roundオプションを指定している場合、小数部分を切り捨てではなく四捨五入します。

12.3 → 12
34.56 → 35


③この値を、定義した書式に右からあてはめていきます。

'009.00%' の数字セレクタ部分に右から数値をあてはめると、
12 → 000.12% →「  0.12%」
35 → 000.35% →「  0.35%」



実践


例えば、以下の変数Xには何かの割合が格納されているとします。

data DT1;
   input X;
   cards;
0.123
1
0.456
0.6789
;
run;

X
 0.123 
 1 
 0.456 
 0.6789 


このXを「0.123 → '12.3%'」 のように百分率にした上で%をつけたいとします。
今回解説した内容を踏まえると以下のようになるかと思います。

proc format;
   picture TEST3_ (round)
      0 - 1 = "009.0%" (multiplier=1000);
run;

data DT2;
   set DT1;
   length PCT $30.;
   PCT = put(x,TEST3_.);
run;

X 
 PCT 
 0.123
  12.3% 
 1 
 100.0%
 0.456
  45.6%
 0.6789 
  67.9%



厄介なケース


内部で10のn乗とか計算してることによって、小数点誤差が発生する可能性があります。

「小数点誤差?」って人は以下参照
http://sas-tumesas.blogspot.jp/2014/03/blog-post_14.html(データステップ100万回)


小数点誤差が問題になる例
proc format;
    picture TEST4_
       0 - 999 = '009.000000000' ;
run;

data DT3;
    X = 2.01;
    length Y $30.;
    Y = put(X,TEST4_.);
run;

X 
 Y 
 2.01 
   2.009999999 

「2.01」が「  2.00999…」となってしまいます。


以下のようにROUNDオプションで四捨五入しとけば一応は「2.01」と表示されますが、、

proc format;
    picture TEST4_ (round)
       0 - 999 = '009.000000000';
run;

data DT3;
    X = 2.01;
    length Y $30.;
    Y = put(X,TEST4_.);
run;

X 
 Y 
 2.01 
   2.010000000 

以下の最後の方で説明している通り、ROUNDしとけばOKというわけでもないのでご注意下さい。
http://sas-boubi.blogspot.jp/2016/03/put-floating-point-error.html


2018年3月1日木曜日

PROC FORMAT入門5 : PICTUREステートメント



PICTUREステートメント1」の続きです。
(基本的な動きを知らない方はまず上記の記事をご確認下さい)


PICTUREステートメントで小数を扱う時に使用する「ROUNDオプション」の説明をしていきます。





* PICTUREフォーマットの定義 ;
proc format;
   picture TEST1_
      0 - 100 = '009.00%'
   ;
   picture TEST2_ (round)
      0 - 100 = '009.00%'
   ;
run;


・「ROUNDオプション」をつけると四捨五入してくれます(今回は書式を '009.00%' と定義したので、小数点以下第3位を四捨五入し、第2位まで表示するようにしています)


・「ROUNDオプション」をつけないと切り捨てになります(今回は書式を '009.00%' と定義したので、小数点以下第3位を切り捨てし、第2位まで表示するようにしています)


・オプションの組み合わせ・使用方法によっては、動作しなかったり動きが変わるため要挙動確認!
その他、ミスりやすい挙動について、記事の最後のほうに解説記事のリンクを載せているのでご参照下さい。






では、適当なデータを作って、FORMAT変換後の結果を見てみましょう。

* Sample Data ;
data DT1;
   input X;
   cards;
12
12.34
12.345
;

* Format変換してみる ;
data DT2;
   set DT1;
   length Y1 Y2 $20.;
   Y1 = put(X,TEST1_.);
   Y2 = put(X,TEST2_.);
run;

 X  Y1  Y2 
12 
  12.00%   12.00% 
12.34 
  12.34%   12.34%
 12.345 
  12.34%  12.35%


ここまでそんなに難しくないですよね。
でも実は内部ではちょっと複雑な動きをしています。

とくにPICTUREステートメントで小数を扱う場合に、ちゃんと内部の動きを理解してないと想定外な結果になり痛い目を見ます(以下リンク参照)

(http://sas-boubi.blogspot.jp/2018/03/proc-formatpicture.html)






PROC FORMAT入門 : 記事一覧

2. INVALUEステートメント
3. 範囲の指定