いものやま。

雑多な知識の寄せ集め

Pythonの四捨五入で四苦八苦した話。(その2)

前回のあらすじ: Pythonのビルトインのround()は普通の四捨五入とは違う偶数丸めというものだったので、floor()を使って普通の四捨五入を実装したよ。

誤差との戦い

と、まぁ普通はここまでなんだけど、いろいろ試すと「あれ?」ってなるケースが出てくる。

具体的には、1.255を四捨五入して小数第二位までにしようとしたとき。 普通に考えれば小数第三位の数字が5なので切り上げられて1.26になると思うけど、残念ながらそうはならない。

myround2(1.255, 2)  # => 1.25

ちなみに偶数丸めするround()ならこれは1.26になるはずなんだけど、試したらこうだった:

round(1.255, 2)  # => 1.25

Pythonくんさぁ・・・

それはともかく、どうしてこうなるのかというと、誤差の関係。 浮動小数点数だと10進数の数字は正確にもてない場合があって、その場合に計算するといろいろ誤差が出てしまう。

これは実際に1.255 * 100を計算してみるとよく分かる:

1.255 * 100  # => 125.49999999999999

見てのとおり、きっちり125.5になっていなくて125.499999...となってしまっている。 このせいで0.5を足しても126に上がってくれなくて、結果1.26ではなく1.25となっていた。

まぁ、誤差が原因だとしょうがない感じはある。 きっと他のプログラミング言語でも同様の制約は出るはずだし。

と思って試しにRubyでやってみた結果がこれ:

1.255.round(2)  # => 1.26

えっ、ちゃんと四捨五入できてる? なんで???

ちなみに、もちろんRubyでも内部では誤差を持ってて、1.255 * 100を計算してみるとこうなる:

1.255 * 100  # => 125.49999999999999

じゃあ、Rubyは一体どうやって四捨五入を行なっているのか。 それについてはまたあとで言及したい。

PyPIのパッケージ

まぁ何はともあれ、四捨五入が思ったよりも大変そうということが分かったので、だったら誰かがパッケージを作って公開してるだろうとPyPIを検索。 すると、案の定いくつかのパッケージが見つかった:

最初に言っておくと、これらのパッケージはダメダメなので使ったらダメ。 (宣伝になってしまうけど、これらを使うくらいなら拙作のsaneroundを使って・・・)

実際、試してみた結果がこれ:

まずはround2。

from round2 import round2

round2(0.5)  # => 1, OK
round2(1.5)  # => 2, OK
round2(-0.5)  # => -1, OK
round2(-1.5)  # => -2, OK
round2(15, -1)  # => nan, NG
round2(25, -1)  # => nan, NG
round2(1.255, 2)  # => 1.25, NG

見てのとおり、四捨五入の桁で負の数は未サポート。 また、1.255の小数第三位を四捨五入しても1.26にはならずに1.25になってしまってる。

次にmath-round。

from math_round import mround

mround(0.5)  # => 1, OK
mround(1.5)  # => 2, OK
mround(-0.5)  # => 0, NG
mround(-1.5)  # => -1, NG
mround(-2.0)  # => -1, NG(根本的な問題がありそう)
mround(15, -1)  # => 20.0, OK
mround(25, -1)  # => 30.0, OK
mround(1.255, 2)  # => 1.25, NG

こちらは負の数を四捨五入したときの挙動がおかしい。 単に絶対値で扱ってないことによる問題かと思いきや、-2を四捨五入すると-1になってるあたり、根本的な問題がありそう。 そして当然1.255の四捨五入結果は1.25。

最後にmath-round-af。

from math_round_af import get_rounded_number

get_rounded_number(0.5)  # => 1.0, OK
get_rounded_number(1.5)  # => 2.0, OK
get_rounded_number(-0.5)  # => -1.0, OK
get_rounded_number(-1.5)  # => -2.0, OK
get_rounded_number(15, -1)  # => ValueError, NG
get_rounded_number(25, -1)  # => ValueError, NG
get_rounded_number(1.255, 2)  # => 1.25, NG

こちらも四捨五入の桁で負の数は未サポートとなっている。 また、1.255の四捨五入結果はやっぱり1.25。

とまぁこんな感じで、精度の問題どころか、基本的な実装すらできてないお粗末なパッケージしかなくて、本当にダメダメ。 3つも引っ張ってくりゃ普通は1つくらいまともなパッケージありそうなもんだけど。。。

もちろん、ちゃんと探せばもしかしたらまともなパッケージがまだあるかもしれない。 でも、これはもう自分でなんとかした方が早そうだなとなった。

ということで、次はRubyの実装を調査していくことになる。

今日はここまで!