ラベル 関数: PUT の投稿を表示しています。 すべての投稿を表示
ラベル 関数: PUT の投稿を表示しています。 すべての投稿を表示

2016年3月22日火曜日

数値を四捨五入して文字変数に格納したい場合、PUT関数を使うと想定外の結果になる。



数値を四捨五入して文字変数に格納したい場合、PUT関数を使ってませんか?
以下の例を見てみましょう。

想定通りの結果
data DT1;
    length B $10.;
    A = 1.25;
    B = left(put(A, 12.1));
    put B=;
run;

ログ
B=1.3

小数第2位を四捨五入して文字化した変数が出来ました。
次はどうでしょう。


想定外の結果
data DT1;
    length B $10.;
    A = 4.1-4.05;
    B = left(put(A, 12.1));
    put B=;
run;

ログ
B=0.0

4.1-4.05 = 「0.05」 なので、小数第2位を四捨五入すると「0.1」となるはずが、「0.0」になりました。

これは浮動小数点誤差ってやつのせいですね。
http://sas-tumesas.blogspot.jp/2014/03/blog-post_14.html (データステップ)100万回)


つまり「4.1-4.05」は「0.05」ではなく「0.049999999・・・」みたいな値で格納されてます。
だからPUT関数で「0.0」という値が帰ってきたわけですね。



で、これをどうするかは各自(各社)いろいろ考え・やり方があると思います。
よくROUND関数をかませたりしますが、、、
data DT1;
    length B $10.;
    A = 4.1-4.05;
    B = left(put( round(A,0.1) ,12.1));
    put B=;
run;

ログ
B=0.1



📝ROUND関数には注意すべき動きがある

ROUND関数は「第1引数を第2引数の最も近い倍数に丸める関数」ですが、
その独特の動きや制限も重要です。以下にSAS社のドキュメントの記述を抜粋します
(※日本語ドキュメントは誤記がある?のでちょっと分かりにくいですが英語の方を示します。。)


The Effects of Rounding
Rounding by definition finds an exact multiple of the rounding unit that is closest to the value to be rounded. For example, 0.33 rounded to the nearest tenth equals 3*0.1 or 0.3 in decimal arithmetic. In binary arithmetic, 0.33 rounded to the nearest tenth equals 3*0.1, and not 0.3, because 0.3 is not an exact multiple of one tenth in binary arithmetic.

The ROUND function returns the value that is based on decimal arithmetic, even though this value is sometimes not the exact, mathematically correct result. In the example ROUND(0.33,0.1), ROUND returns 0.3 and not 3*0.1.

「第1引数を第2引数の最も近い倍数に丸める」処理において、10進数の算術と2進数の算術では「正確な倍数」の扱いの違いがあって、そこをROUND関数では調整してるような事が書かれているのでしょうか(間違っていたらご指摘をお願いします)


Producing Expected Results
In general, ROUND(argument, rounding-unit) produces the result that you expect from decimal arithmetic if the result has no more than nine significant digits and any of the following conditions are true:
  • The rounding unit is an integer.
  • The rounding unit is a power of 10 greater than or equal to 1e-15. (If the rounding unit is less than one, ROUND treats it as a power of 10 if the reciprocal of the rounding unit differs from a power of 10 in at most the three or four least significant bits.)
  • The result that you expect from decimal arithmetic has no more than four decimal places.

期待される結果を生成する条件が示されています。思ったより扱える範囲が狭い?
ちなみに文中の、significant digits と decimal places の意味を混同してるケースをよく見かけるので注意。



その他の挙動についてもドキュメントに書かれているので、要確認!
ROUND関数を単純に四捨五入するやつと思っていると、想定外の結果になるかもしれません。
例えば以下のように最後の桁が「8」か「9」かで結果が異なっています。
data DT1;
 A = round(1.049999999998,0.1);
 B = round(1.049999999999,0.1);
 put A= B=;
run;

ログ
A=1 B=1.1



というわけで、一概にこうすればいいってのはない気がします。
「私はこうしてます」 とか何かアイディアがあったら教えてほしいです。

2013年12月20日金曜日

PUT関数 vs VVALUE関数


以下のように、「PUT関数」を使って文字変換することってよくありますよね。

*** サンプルデータ *******************;
data DT1;
  format   V1 yymmdd10. ;
  V1 = '01jan2000'd; output;
  V1 = '05feb2001'd; output;
  V1 = .; output;
run;

*** PUT関数を使って文字化 ***********;
data DT2;
  length V1 8.  V2 $20.;
  set DT1;
  V2 = put(V1,yymmdd10.) ;
run;


これは「VVALUE関数」でもっと簡単に書けます。

 V2 = VVALUE(V1) ;

「VVALUE関数」は「変数に割り当てられているFORMAT」を使って文字変換してくれる関数です。


注意点


PUT関数と挙動が似ています。
例えば、PUT関数で数値の欠損値を以下のようにFORMAT変換すると、、

 length V3 $20.;
 V3 = put( . , yymmdd10.);

→ 変換後の値は「         .」という感じで先頭に余計な空白が入ります。


また、フォーマットによっては、、

 length V4 $20.;
 V4 = put( 10, best12. );

→ 変換後の値は「          10」という感じでこちらも先頭に余計な空白が入る場合があります。


VVALUE関数も同様に先頭に空白が入る場合があるのでご注意下さい。



2013年11月20日水曜日

文字の先頭をゼロ埋めして桁数を揃える。



他のプログラム言語とかでもよく出てくる「ゼロパディング」といわれる問題。


例えば5桁に揃えたい場合は、
 「123」→「00123」
 「AB」 →「000AB」
みたいな感じに加工する方法。



ゼロ埋め対象が数値変数の場合

data DT1;
   length V1 $5.;
   V0 = 123;
   V1 = put(V0, z5.); 
run;

「Zw.dフォーマット」というのが用意されてて「z桁数」と指定するだけでok。




ゼロ埋め対象が文字変数の場合

data DT2;
   length V0 $3. V1-V3 $5.;
   V0 = "ABC";
   V1 = reverse(substr(reverse(cats("00000",V0)),1,5));  * 方法① ; 
   V2 = cats(repeat("0",4-lengthn(V0)),V0);                    * 方法② ; 
   V3 = tranwrd(put(V0, 5.-R),' ','0');                             * 方法③ ; 
run;


だいたい上記3つのやり方が思い浮かぶ。

方法①
ごり押し感あるやり方。
「cats関数」で「00000ABC」という文字にする。
「reverse関数」で「CBA00000」と逆にしてから、
「substr関数」で先頭5文字を抽出「CBA00」
また「reverse関数」でもとの順に戻せば完成。
非効率だけどきらいじゃない。

追記
上記と同じ原理、かつもっといい方法を教えてもらいました。

  V1 = put(put('00000'||V0 ,$revers8.),$revers5.);

put関数にも文字を逆さまにする「$revers」というformatがある。
うまいのが「$revers5」で、5文字に切りつつ、もとの順に戻してるところ。




方法②
変数「V0」から不足してる文字の長さ分だけ「repeat関数」を使ってゼロを埋めてる。


方法③
個人的には使ってる関数も少なくて一番スマートだと思う。
数値変数の場合でも適用可。

ポイントは「put関数」で5文字の長さにして「-R」を使って文字を右詰して「  ABC」としてるところ
その後、左側に出来た半角スペースを「tranwrd関数」でゼロに置換えてる。

「put関数」の「-R」については PUT関数の小技 を参照。




2013年11月8日金曜日

PUT関数の小技「文字寄せ」


以下のプログラムを実行すると、、

data DT1;
  length VAR1 $20.;
  VAR1 = put(1, 20.);
run;


「         1」みたいな感じで先頭に余計な半角スペースが入ってしまう。
左詰めにするため「left関数」を使う以外に、

 VAR1 = put(1, 20. -L);

と書くこともできてしまう。

ちなみに

 「-L」が左詰め
 「-C」が中央
 「-R」が右詰め

って意味になる。



使うときの注意

① VAR1 = put(1, 8.2 -L);            * OK ;
② VAR1 = put(1, 8.2 -L) || "mg";  * NG ;


上記②の場合、
まず「put(1, 8.2 -L)」で左詰めした分、文字値の右側に半角スペースが移動します。
イメージ:「    1.00」→「1.00    」


この文字と「mg」を結合すると「1.00    mg」といった感じで、不要な半角スペースが混入してしまう。

あくまでもleft関数のような動きをしていることに注意!