はじめに
先日、端数の処理方法について考える機会がありまして、割と調べたのでまとめてみようかなと思います。
端数処理(丸め)の規則
よく知られている端数処理は
・切り上げ
・切り下げ
・四捨五入
ですが、これ以外にも
・偶数丸め
・奇数丸め
・五捨六入
などいろいろあるようです。参考はWikipedia。
その中でも、偶数丸めは結構使われてます。
偶数丸めは、「端数が0.5より小さいなら切り捨て、端数が0.5より大きいならは切り上げ、端数がちょうど0.5なら切り捨てと切り上げのうち結果が偶数となる方へ丸める(つまり偶数+0.5なら切り捨て、奇数+0.5なら切り上げとなる)方法」。偶数丸め <=> 奇数丸め。
round()
端数処理の代表的な関数だと思います。
自分は「四捨五入」といえばこの関数、だと思っていました。多くのサイトでもそのように紹介されています。ですが偶数丸めです。
print(round(1.5))
# 四捨五入であれば3になるはずだが
print(round(2.5))
2
2
float
round()が偶数丸めであることがわかりましたが、float型を丸める多くの場合注意が必要です。
次の例を見てみます。
# 小数第1位までの偶数丸め(小数第2位を丸める)
print(round(2.55, 1))
2.5
小数第2位が5なので偶数丸めだと2.6になりそうですが、なぜか2.5になっています。
これはround()の不具合とかではなく、2.55が原因です。
試しに2.55を小数第20位まで出力してみます。
print(f'{2.55:.20f}')
2.54999999999999982236
2.55000000000000000000ではない??
これだと小数第2位が5未満なので偶数丸めをしても切り下げられて2.5になることに納得できます。
ですが、そもそもなぜ2.55が2.549999...になっているのか。これは2.55に限らず、多くの小数でも同じようなことがあるようです。
コンピュータは入力された数字をそのまま出力しているわけではなく、一度コンピュータが解釈できる2進数に変換して、再度10進数に変換して出力しています。
多くの小数で上記のようなことが起こるのは、10進数の小数を正確に2進数変換ができない(循環小数でしか表現できない)ことが多く、コンピュータが有限桁までの近似値に丸め、その近似値を10進数に戻しているためです。
例えば10進数の0.1を2進数に変換すると、0.000110011… のように無限に続きます。これを丸め10進数に変換すると、以下のような近似値になります。
# 0.1を小数第20位まで出力
x = 0.1
print(f'{x:.20f}')
0.10000000000000000555
これを知っていないと、丸めや計算処理による予期しない結果がバグの温床になりそうです。
Decimal
丸めや計算での想定外の結果を回避するためにDecimal型を使うと良いようです。Decimalは浮動小数点数だけでなく、1固定小数点数も扱うことができます。
from decimal import *
# 固定小数点数を扱う場合は数字を文字列で渡す
x = Decimal('0.1')
print(x)
# 浮動小数点数
y = Decimal(0.1)
print(y)
0.1
0.1000000000000000055511151231257827021181583404541015625
固定小数点数のほうは想定した数字を返してくれます。
先の2.55を偶数丸めすると想定した結果になります。
x = Decimal('2.55')
print(round(x, 1))
2.6
また、Decimalは独自に丸めメソッドを持っており、以下のように多くの丸め規則を設定できます。
・ROUND_CEILING
・ROUND_FLOOR
・ROUND_DOWN
・ROUND_UP
・ROUND_05UP
・ROUND_HALF_DOWN(五捨六入)
・ROUND_HALF_EVEN(偶数丸め、デフォルト)
・ROUND_HALF_UP(四捨五入)
ROUND_CEILING
Infinity 方向に丸めます。
n = Decimal('0.11').quantize(Decimal('0.1'), rounding=ROUND_CEILING)
print(n)
n = Decimal('-0.11').quantize(Decimal('0.1'), rounding=ROUND_CEILING)
print(n)
0.2
-0.1
ROUND_FLOOR
-Infinity 方向に丸めます。
n = Decimal('0.11').quantize(Decimal('0.1'), rounding=ROUND_FLOOR)
print(n)
n = Decimal('-0.11').quantize(Decimal('0.1'), rounding=ROUND_FLOOR)
print(n)
0.1
-0.2
ROUND_DOWN
ゼロ方向に丸めます。
n = Decimal('0.11').quantize(Decimal('0.1'), rounding=ROUND_DOWN)
print(n)
n = Decimal('-0.11').quantize(Decimal('0.1'), rounding=ROUND_DOWN)
print(n)
0.1
-0.1
ROUND_UP
ゼロから遠い方向に丸めます。
n = Decimal('0.11').quantize(Decimal('0.1'), rounding=ROUND_UP)
print(n)
n = Decimal('-0.11').quantize(Decimal('0.1'), rounding=ROUND_UP)
print(n)
0.2
-0.2
ROUND_05UP
ゼロ方向に丸めた後の最後の桁が 0 または 5 ならばゼロから遠い方向に、そうでなければゼロ方向に丸めます。
# ゼロ方向に丸めると0.0 => 小数第1位が0なのでゼロから遠い方向に丸めなおす
n = Decimal('0.01').quantize(Decimal('0.1'), rounding=ROUND_05UP)
print(n)
# ゼロ方向に丸めると0.1 => 小数第1位が1なのでこのまま
n = Decimal('0.11').quantize(Decimal('0.1'), rounding=ROUND_05UP)
print(n)
# ゼロ方向に丸めると0.5 => 小数第1位が5なのでゼロから遠い方向に丸めなおす
n = Decimal('0.51').quantize(Decimal('0.1'), rounding=ROUND_05UP)
print(n)
0.1
0.1
0.6
ROUND_HALF_DOWN
近い方に、引き分けはゼロ方向に向けて丸めます
n = Decimal('0.24').quantize(Decimal('0.1'), rounding=ROUND_HALF_DOWN)
print(n)
n = Decimal('0.25').quantize(Decimal('0.1'), rounding=ROUND_HALF_DOWN)
print(n)
n = Decimal('0.26').quantize(Decimal('0.1'), rounding=ROUND_HALF_DOWN)
print(n)
0.2
0.2
0.3
ROUND_HALF_EVEN
近い方に、引き分けは偶数整数方向に向けて丸めます。(偶数丸め)
n = Decimal('0.24').quantize(Decimal('0.1'), rounding=ROUND_HALF_EVEN)
print(n)
n = Decimal('0.25').quantize(Decimal('0.1'), rounding=ROUND_HALF_EVEN)
print(n)
n = Decimal('0.26').quantize(Decimal('0.1'), rounding=ROUND_HALF_EVEN)
print(n)
0.2
0.2
0.3
ROUND_HALF_UP
近い方に、引き分けはゼロから遠い方向に向けて丸めます。(四捨五入)
n = Decimal('0.24').quantize(Decimal('0.1'), rounding=ROUND_HALF_UP)
print(n)
n = Decimal('0.25').quantize(Decimal('0.1'), rounding=ROUND_HALF_UP)
print(n)
n = Decimal('0.26').quantize(Decimal('0.1'), rounding=ROUND_HALF_UP)
print(n)
0.2
0.3
0.3
丸め時に毎回規則を定義するのが面倒なときもありそうです。デフォルトの設定を変更することができます。
# デフォルト
getcontext()
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])
roundingプロパティを書き換えるだけです。
getcontext().rounding = ROUND_HALF_UP
getcontext()
Context(prec=28, rounding=ROUND_HALF_UP, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])
参考
・公式ドキュメント
・Pythonの丸め処理について調べてみた
まとめ
知らないことを調べてまとめるのは大変でしたが、少しレベルアップしたのでよかったです。また不正確なところや間違っていることがあればコメントいただけると幸いです。