2024年6月2日日曜日

【PROC FORMAT】数値フォーマットの落とし穴



FORMATプロシジャの「FUZZ=オプション」のデフォルト設定が悪さをすることがあります。
まずは「FUZZ=オプション」の説明を交えて紹介していきます。



落とし穴①

proc format;
value x (fuzz=1)
3 - 5  = "aaa"
;
run;

data test;
 format x x.;
 do x=1 to 7;
  output;
 end;
run;


「3~5」の値を  "aaa" と表示するフォーマットですが、「fuzz=1」というオプションも指定しています。
この「FUZZ=」に指定した値を誤差として「- ~ +」して範囲を拡張することが出来ます。
今回だと「3~5」に「-1 ~ +1」した範囲なので、「2~6」の範囲を "aaa" と表示するフォーマットになります。


では次。ヘンテコな結果になります。

proc format;
value x (fuzz=1)
3 <-< 5  = "aaa"
;
run;

data test;
 format x x.;
 do x=1 to 7;
  output;
 end;
run;


「3より大きく、5より小さい」値に対して "aaa" と表示するフォーマットですが「fuzz=1」によっておかしな結果になります。
FORMATプロシジャで範囲の指定に使う「<」は「大なり、小なり」の意味ではなく「除外」の意味合いがあるようですね。
つまり今回の例では「3<-<5」と「fuzz=1」の組み合わせによって、「2~6」の範囲で「3」と「5」を除く値に "aaa" と表示するフォーマットになっているようです。



📝 まとめ
「FUZZ=」と「<」の組み合わせは危険。




落とし穴②


まず、前提知識として「浮動小数点誤差」の理解から。
以下、2オブザベーションとも値が「0.1」になるはずなのに、、

data test;
 x=0.1; output;
 x=0.3-0.2; output;
run;

data test2;
 set test;
 if x=0.1 then y=1;
run;


「if x=0.1 then y=1」の結果、2オブザベーション目の値が何故か「0.1」ではない事が分かります。
これが有名な浮動小数点誤差ってやつで、2オブザベーション目は「0.09999999...」みたいな値が格納されてしまっているわけですね。


データステップ100万回や私の過去記事でも紹介済です。


そしてここから本題。
FORMATを定義した時の浮動小数点誤差の取り扱いを見てみます。

proc format;
 value x
 0.1 = "0.1です"
 0.2 = "0.2です"
 0.3 = "0.3です"
 ;
run;

data test;
 format x x.;
 x=0.1; output;
 x=0.3-0.2; output;
run;


あれ?浮動小数点誤差のデータも「0.1」としてフォーマットをあてられてる??
実は、VALUEまたはPICTUREフォーマット、かつ数値フォーマット(1 = "aaa"のような左辺が数値のフォーマット)では「fuzz=1E-12」という非常に小さな値がデフォルトで設定されています。
浮動小数点誤差で「0.09999999...」みたいに格納されてる値が「0.1」のプラスマイナス「1E-12」の中に入っているため「0.1です」と表示することが出来たわけです。

いい感じじゃん!と思いきや、非常にヘンテコな挙動をする場合があります。



例1

「0.1以上、0.2未満」の範囲のFORMATを作って、それを今回の浮動小数点誤差が起きているデータに割り当てると、、

proc format;
 value x
 0.1 -< 0.2 = "0.1以上、0.2未満"
 ;
run;

data test;
 format x x.;
 x=0.1; output;
 x=0.3-0.2; output;
run;

これは想定通りですね。フォーマットの範囲「0.1 -< 0.2」と「fuzz=1E-12」の組み合わせによって浮動小数点誤差で「0.09999999...」みたいに格納されてる値も「0.1以上、0.2未満」として表示することが出来ました。



例2

例1のフォーマットに「0 -< 0.1」という範囲を追加してみます。

proc format;
 value x
 0 -< 0.1 = "0以上、0.1未満"
 0.1 -< 0.2 = "0.1以上、0.2未満"
 ;
run;

data test;
 format x x.;
 x=0.1; output;
 x=0.3-0.2; output;
run;

あれ?今度は「0以上、0.1未満」の値としてフォーマットがあてられてる??
これは今回フォーマットに追加した「0 -< 0.1」と「fuzz=1E-12」の組み合わせによる範囲に浮動小数点誤差が起きている「0.09999999...」が入ってしまうため。先にこのフォーマットが適用されてしまったというわけですね。



例3

今度は、例2のフォーマットに「0.2 -< 0.3」という範囲を追加してみます。

proc format;
 value x
 0 -< 0.1 = "0以上、0.1未満"
 0.1 -< 0.2 = "0.1以上、0.2未満"
 0.2 -< 0.3 = "0.2以上、0.3未満"
 ;
run;

data test;
 format x x.;
 x=0.1; output;
 x=0.3-0.2; output;
run;

あれ?また「0.1以上、0.2未満」の値としてフォーマットあてられてる??
実は変数値がどのフォーマットの範囲に入っているか内部で検索する際、特殊な順番で検索が行われています。
今回は1個目の「0 -< 0.1」ではなく、2個目の「0.1 -< 0.2」の方が先に検索されたためです。



📝 まとめ
範囲を指定するフォーマットは「FUZZ=」の挙動に注意!
「fuzz=0」とすれば範囲の拡張がされなくなりますが、浮動小数点誤差が考慮されなくなってしまうので、そこのケアが必要になります。
解決策は時と場合によって異なるので、適宜ご留意下さい。