mathpython

広告

Pythonで整数や小数を四捨五入する:roundよりもDecimalを使う(銀行家の丸めに注意しよう)

Python で四捨五入は decimal パッケージの Decimal オブジェクトを使います。

from decimal import Decimal

x = 1234.5678

print(type(x))
# <class 'float'>

a = Decimal(x)

print(a)
# 1234.567800000000033833202905952930450439453125

print(type(a))
# <class 'decimal.Decimal'>

1234.5678 は float 型ですが、Decimal 型になると不正確な小数になっています。これを正確に 1234.5678 とするには、下のように文字列型にします。

from decimal import Decimal

x = 1234.5678

a = Decimal(str(x))

print(a)
# 1234.5678

print(type(a))
# <class 'decimal.Decimal'>

Decimal に文字列の 1234.5678 を入れています。まずはここを注意しましょう。

続いて Decimal の quantize で四捨五入します。

from decimal import Decimal, ROUND_HALF_UP

x = 1234.5678

a = Decimal(str(x))

b = a.quantize(Decimal('0'), rounding=ROUND_HALF_UP)

print(b)  # 1235
print(type(b))  # <class 'decimal.Decimal'>

quantize の第一引数に四捨五入する桁数、第二引数にオプションを入れます。rounding に指定する値は import してください。桁数は上のように Decimal オブジェクトを指定し、中に文字列の 0 を入れます。

小数点以下の桁数で四捨五入する

小数点以下の桁数で四捨五入するときは 0.1 や 0.01 といった文字列を使います。

from decimal import Decimal, ROUND_HALF_UP

x = 1234.5678

y = Decimal(str(x))

a = y.quantize(Decimal('0'), rounding=ROUND_HALF_UP)
b = y.quantize(Decimal('0.1'), rounding=ROUND_HALF_UP)
c = y.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
d = y.quantize(Decimal('0.001'), rounding=ROUND_HALF_UP)

print(a)  # 1235
print(b)  # 1234.6
print(c)  # 1234.57
print(d)  # 1234.568

整数部分で四捨五入する

整数部分で四捨五入するときは注意が必要です。結論から言うと下が正解です。

from decimal import Decimal, ROUND_HALF_UP

x = 1234.5678

y = Decimal(str(x))

a = y.quantize(Decimal('0'), rounding=ROUND_HALF_UP)
b = y.quantize(Decimal('1E1'), rounding=ROUND_HALF_UP)
c = y.quantize(Decimal('1E2'), rounding=ROUND_HALF_UP)
d = y.quantize(Decimal('1E3'), rounding=ROUND_HALF_UP)

print(a)  # 1235
print(b)  # 1.23E+3
print(c)  # 1.2E+3
print(d)  # 1E+3

桁数の指定で E を使います。この書き方を知らないと、つい下のように書いてしまうでしょう。

from decimal import Decimal, ROUND_HALF_UP

x = 1234.5678

y = Decimal(str(x))

a = y.quantize(Decimal('0'), rounding=ROUND_HALF_UP)
b = y.quantize(Decimal('10'), rounding=ROUND_HALF_UP)
c = y.quantize(Decimal('100'), rounding=ROUND_HALF_UP)
d = y.quantize(Decimal('1000'), rounding=ROUND_HALF_UP)

print(a)  # 1235
print(b)  # 1235
print(c)  # 1235
print(d)  # 1235

10 や 100 は意味のない値です。Decimal に入れるには E を使った書式にする必要があります。四捨五入の結果は 1.23E+3 のようになるため、下のように整数値に変換すると見やすくなります。

from decimal import Decimal, ROUND_HALF_UP

x = 1234.5678

y = Decimal(str(x))

a = y.quantize(Decimal('0'), rounding=ROUND_HALF_UP)
b = int(y.quantize(Decimal('1E1'), rounding=ROUND_HALF_UP))
c = int(y.quantize(Decimal('1E2'), rounding=ROUND_HALF_UP))
d = int(y.quantize(Decimal('1E3'), rounding=ROUND_HALF_UP))

print(a)  # 1235
print(b)  # 1230
print(c)  # 1200
print(d)  # 1000

rounding オプション を ROUND_HALF_DOWN にする

ROUND_HALF_UP は一般的な四捨五入ですが、これを ROUND_HALF_DOWN にすると 5 の扱いが変わります。

from decimal import Decimal, ROUND_HALF_DOWN

a = Decimal(str(1.4)).quantize(Decimal('0'), rounding=ROUND_HALF_DOWN)
b = Decimal(str(1.5)).quantize(Decimal('0'), rounding=ROUND_HALF_DOWN)
c = Decimal(str(1.6)).quantize(Decimal('0'), rounding=ROUND_HALF_DOWN)

print(a)  # 1
print(b)  # 1
print(c)  # 2

1.5 が 2 でなく 1 に丸まっています。これは ROUND_HALF_DOWN が 5 を切り捨てるためです。ROUND_HALF_UP と比較してください。

from decimal import Decimal, ROUND_HALF_UP

a = Decimal(str(1.4)).quantize(Decimal('0'), rounding=ROUND_HALF_UP)
b = Decimal(str(1.5)).quantize(Decimal('0'), rounding=ROUND_HALF_UP)
c = Decimal(str(1.6)).quantize(Decimal('0'), rounding=ROUND_HALF_UP)

print(a)  # 1
print(b)  # 2
print(c)  # 2

1.5 がきちんと 2 に丸まっています。

rounding オプション を ROUND_HALF_EVEN にする

ROUND_HALF_EVEN の丸め方は複雑です。しかしこれを理解すると Python の四捨五入で Decimal を使うべき理由がわかります。

from decimal import Decimal, ROUND_HALF_EVEN

a = Decimal(str(0.5)).quantize(Decimal('0'), rounding=ROUND_HALF_EVEN)
b = Decimal(str(1.5)).quantize(Decimal('0'), rounding=ROUND_HALF_EVEN)
c = Decimal(str(2.5)).quantize(Decimal('0'), rounding=ROUND_HALF_EVEN)
d = Decimal(str(3.5)).quantize(Decimal('0'), rounding=ROUND_HALF_EVEN)
e = Decimal(str(4.5)).quantize(Decimal('0'), rounding=ROUND_HALF_EVEN)
f = Decimal(str(5.5)).quantize(Decimal('0'), rounding=ROUND_HALF_EVEN)

print(a)  # 0
print(b)  # 2
print(c)  # 2
print(d)  # 4
print(e)  # 4
print(f)  # 6

不思議なことが起きています。0.5 は 0 になっているため 5 は切り捨てると思いきや、1.5 はきちんと 2 になっています。そして 2.5 は 2 になっています。

0.5 → 0

2.5 → 2

4.5 → 4

これは bankers' rounding (銀行家の丸め)という特殊な四捨五入で、「四捨五入したい桁の 1 つ上の桁が偶数のときは、5 を切り捨てる」というやり方です。このやっかいな四捨五入は、なんと後述する round に採用されています。他のプログラミング言語からきた人は round で四捨五入をするかもしれませんが、「銀行家の丸め」で意図しない結果が出てくるでしょう。

Python の round で丸める

Python の round は「銀行家の丸め」(以下「銀行丸め」と呼ぶ)です。一般的な四捨五入と 5 の扱いが違います。round の第二引数は桁数を意味します。

第二引数を指定しない場合、小数点以下第一位で銀行丸めとなります。これは 0 を指定したときと同じふるまいです。1 を指定すると小数点以下第二位…となります。

a = 452719.3806

print(round(a))  # 452719
print(round(a, 0))  # 452719.0
print(round(a, 1))  # 452719.4
print(round(a, 2))  # 452719.38
print(round(a, 3))  # 452719.381
print(round(a, 4))  # 452719.3806
print(round(a, 5))  # 452719.3806
print(round(a, 6))  # 452719.3806

round の第二引数にマイナスの値を指定すると整数部分の銀行丸めになります。

a = 452719.3806

print(round(a, -1))  # 452720.0
print(round(a, -2))  # 452700.0
print(round(a, -3))  # 453000.0
print(round(a, -4))  # 450000.0
print(round(a, -5))  # 500000.0
print(round(a, -6))  # 0.0
print(round(a, -7))  # 0.0
print(round(a, -8))  # 0.0

6 桁の場合、マイナス 6 以下の整数を指定すると 0 になります。

round で「銀行家の丸め」を確認する

round が銀行丸めになっていることを確認しましょう。

a = round(0.5, 0)
b = round(1.5, 0)
c = round(2.5, 0)
d = round(3.5, 0)
e = round(4.5, 0)
f = round(5.5, 0)

print(a)  # 0.0
print(b)  # 2.0
print(c)  # 2.0
print(d)  # 4.0
print(e)  # 4.0
print(f)  # 6.0

0.5 と 2.5 が 0 と 2 になっています。この挙動を見ると、なにも考えずに round を使うことは少し危険かもしれないと思うでしょう。Python の四捨五入は Decimal が無難です。

広告