2021.07.13

Pythonのround()が単純な四捨五入じゃないので調べてみた

Python3系で四捨五入をしたい場面があったので、round()を使っていたのですが、よく見てみたら必ずしも5を繰り上げているわけではなさそうでした。気になって調べてみた結果をまとめました。

組み込み関数round()とは

小数を整数に丸められる関数で、第一引数に元の数値を指定することで使えます。また、第二引数に桁数を渡すことで何桁に丸めるかを指定することもできます。<br> 出典: Python3.9.4のドキュメント<br> 例えば

round(123.456)
# 123

round(123.456, 1)
# 123.5

round(123.456, -1)
# 120

となります。

気になったポイント

冒頭の「必ずしも5を繰り上げているわけではなさそう」というのを詳しく見てみると、

print('1.4 =>', round(1.4))
print('1.5 =>', round(1.5))
print('1.6 =>', round(1.6))
# 1.4 => 1
# 1.5 => 2
# 1.6 => 2

print('2.4 =>', round(2.4))
print('2.5 =>', round(2.5))
print('2.6 =>', round(2.6))
# 2.4 => 2
# 2.5 => 2
# 2.6 => 3

となっていました。<br> 簡単に書くと、
・1.5 → 2
・2.5 → 2
・3.5 → 4
・4.5 → 4
になります<br>

このような丸め方は「偶数への丸め(round to even)」と呼ばれる、端数処理の種類の1つのようです。<br>

余談: Python2系と3系でround()の挙動が違う

Python2系だとround()は単純な四捨五入のようです。 詳しくは Language differences and workarounds — Supporting Python 3 - The Book Site に書かれています。

偶数丸めのメリット

上でも貼った偶数への丸め(round to even)によると、偶数丸めは四捨五入よりも「望ましい」とされ、その理由として

端数0.5のデータが有限割合で存在する場合、四捨五入ではバイアスが発生するが、偶数への丸めではバイアスが無い。つまり、多数足し合わせても、丸め誤差が特定の側に偏って累積することがない。ただし、偶数+0.5は現れるが奇数+0.5は現れないデータのように分布に特殊な特徴がある場合は、バイアスが発生することがある。

と書かれています。<br>

具体例を考えてみる

これだけだとよくわからなかったので、例を挙げながら考えてみました。<br> 1, 2, ..., 19, 20という数列について考えてみます。
まず、この数列の和をとると210になります。<br> これらの数字を1の位で四捨五入した数列の和をとると、
1~4は0, 5~14は10, 15~20は20になるため、

0 × 4 + 10 × 10 + 20 × 6 = 220

になります。<br> これらの数字を1の位で偶数丸めしたした数列の和をとると、
1~5は0, 6~14は10, 15~20は20になるため、

0 × 5 + 10 × 9 + 20 × 6 = 210

となります。<br> この違いが四捨五入のバイアスの話で、こう見るとたしかに偶数丸めの方が望ましそうです。

丸め方による違いの原因

なぜこの違いが出てくるかというと、等差数列の和の公式 (初項+末項)×数列の長さ/2 の考え方をしてみるとわかりやすいです。<br> 1, 2, 3, 4, 5, 6, 7, 8, 9
という数列(和は45)を1の位で四捨五入して和をとるときに、

   1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9  
=  (1+9) +  (2+8) +  (3+7) +  (4+6) + 5
= (0+10) + (0+10) + (0+10) + (0+10) + 10
= 50

となります。
このようにペアをとっていくときに5が余り、この余った5を0と10のどちらに丸めるかで誤差が出てきます。11から20の数列、21から30の数列でもペアをとっていくと同様に15, 25が余ります。
そこで、

 5 →  0
15 → 20
25 → 20
35 → 40
...

というように、丸める際に五捨(?)と五入(?)を交互に行うことで、バイアスを無くすことができます。
これが偶数丸めの方が四捨五入よりも望ましいと言われる理由でした。

まとめ

Python3系のround()は偶数丸めをしていて、四捨五入に比べて、丸める前との誤差が少ないのがメリットのようです。
紛らわしいことを避けたいならmath.floor()math.ceil()を使って切り捨て/切り上げにしてしまったり、Decimal.quantizeで四捨五入した方がよさそうです。