はじめに
結論: Decimalの ROUND_HALF_UP 一強です.
比較対象: Python標準,NumPy,Pandas,Decimal,SciPy,SymPy
悪名高いPythonの四捨五入ですが,様々なライブラリで丸めのためのメソッドが実装されています.
Python標準のround()
で2.5を四捨五入すれば分かりますが,Pythonのroundは四捨五入ではなく銀行家の丸めです.
銀行家の丸め
偶数丸めとも.
普通の四捨五入と違うところは,丸める対象が5の場合,偶数に丸められるという特徴があります.
標準関数でそんなことしちゃダメでしょ.
ライブラリごとの比較
検証用コード
Python 3.10で検証.
評価対象
0.4, 0.5, 0.6, 1.4, 1.5, 1.6, 2.4, 2.5, 2.6, 3.4, 3.5, 3.6, -0.4, -0.5, -0.6, -1.4, -1.5, -1.6
0, 1, 2, 3, -1, -2 に0.5 ± 0.1 を足したものをそれぞれのライブラリで四捨五入します.
各ライブラリによる四捨五入の実装
py_result = round(value)
np_result = np.round(value)
pd_result = pd.Series([value]).round().iloc[0]
decimal_result = Decimal(value).quantize(Decimal('1')) # rounding=ROUND_HALF_EVEN
decimal_result_u = Decimal(value).quantize(Decimal('1'), rounding=ROUND_HALF_UP)
scipy_result = scipy.special.round(value)
sympy_result = float(sympy.N(value).round())
評価用コード全文
import numpy as np
from decimal import Decimal, ROUND_HALF_UP
import math
import pandas as pd
import scipy
import scipy.special
import sympy
values_list = [[0.4, 0.5, 0.6],
[1.4, 1.5, 1.6],
[2.4, 2.5, 2.6],
[3.4, 3.5, 3.6],
[-0.4, -0.5, -0.6],
[-1.4, -1.5, -1.6]]
methods = ['Python', 'NumPy', 'Pandas', 'Decimal', 'Decimal-UP', 'SciPy', 'SymPy']
print(f"{'Value':<10}" + ' '.join([f'{methods[i]:<10}' for i in range(len(methods))]))
print("="*40)
isOK_result = np.array([True]*len(methods))
for i, values in enumerate(values_list):
isOK = np.array([True]*len(methods))
for j, value in enumerate(values):
py_result = round(value)
np_result = np.round(value)
pd_result = pd.Series([value]).round().iloc[0]
decimal_result = Decimal(value).quantize(Decimal('1')) # rounding=ROUND_HALF_EVEN
decimal_result_u = Decimal(value).quantize(Decimal('1'), rounding=ROUND_HALF_UP)
scipy_result = scipy.special.round(value)
sympy_result = float(sympy.N(value).round())
isOK_tmp = [result == ((i if j==0 else i+1) if i<=3 else (-abs(i-4) if j==0 else -abs(i-4+1))) for result in [py_result, np_result, pd_result, decimal_result, decimal_result_u, scipy_result, sympy_result]]
isOK = np.logical_and(isOK, isOK_tmp)
print(f"{value:<10} {py_result:<10} {np_result:<10} {pd_result:<10} {decimal_result:<10} {decimal_result_u:<10} {scipy_result:<10} {sympy_result:<10}")
print(f"OK: {' '.join(methods[k] for k in range(len(methods)) if isOK[k])}")
isOK_result = np.logical_and(isOK_result, isOK)
print("="*40)
print('Result')
print(f"OK: {' '.join(methods[k] for k in range(len(methods)) if isOK_result[k])}")
結果
Valueの値をそれぞれのライブラリで丸めています.
Decimal
はDecimal標準の丸め (初期設定 rounding=ROUND_HALF_EVEN
)
Decimal-UP
は丸め対象が5のとき大きい方に丸める丸め rounding=ROUND_HALF_UP
です.
OK: には,ちゃんとした四捨五入 (銀行家の丸めでない,0.5を大きい整数に丸めるもの) が実装されているライブラリを表示しています.
Value Python NumPy Pandas Decimal Decimal-UP SciPy SymPy
========================================
0.4 0 0.0 0.0 0 0 0.0 0.0
0.5 0 0.0 0.0 0 1 0.0 0.0
0.6 1 1.0 1.0 1 1 1.0 1.0
OK: Decimal-UP
========================================
1.4 1 1.0 1.0 1 1 1.0 1.0
1.5 2 2.0 2.0 2 2 2.0 2.0
1.6 2 2.0 2.0 2 2 2.0 2.0
OK: Python NumPy Pandas Decimal Decimal-UP SciPy SymPy
========================================
2.4 2 2.0 2.0 2 2 2.0 2.0
2.5 2 2.0 2.0 2 3 2.0 2.0
2.6 3 3.0 3.0 3 3 3.0 3.0
OK: Decimal-UP
========================================
3.4 3 3.0 3.0 3 3 3.0 3.0
3.5 4 4.0 4.0 4 4 4.0 4.0
3.6 4 4.0 4.0 4 4 4.0 4.0
OK: Python NumPy Pandas Decimal Decimal-UP SciPy SymPy
========================================
-0.4 0 -0.0 -0.0 -0 -0 0.0 0.0
-0.5 0 -0.0 -0.0 -0 -1 0.0 0.0
-0.6 -1 -1.0 -1.0 -1 -1 -1.0 -1.0
OK: Decimal-UP
========================================
-1.4 -1 -1.0 -1.0 -1 -1 -1.0 -1.0
-1.5 -2 -2.0 -2.0 -2 -2 -2.0 -2.0
-1.6 -2 -2.0 -2.0 -2 -2 -2.0 -2.0
OK: Python NumPy Pandas Decimal Decimal-UP SciPy SymPy
========================================
Result
OK: Decimal-UP
結果: Decimal で ROUND_HALF_UP を用いた場合のみ正しい
Decimalを初期設定で扱うなら,なんと全滅.すべてのライブラリで銀行家の丸めが採用されています.
結果,DecimalでROUND_HALF_UPを使用する場合のみ,正しい四捨五入ができることが分かりました.
Decimal(value).quantize(Decimal('1'), rounding=ROUND_HALF_UP)
Decimalの初期設定も銀行家の丸めなのは,他の関数と併用するときにバグらないようにするための配慮なのかもしれませんね.