アクチュアリーのためのPython入門(第5回)
なぜPythonのroundではなくdecimalを使うのか
はじめに
前回までで、生命表・基数表・給付現価・保険料・責任準備金と、
一通りの計算をPythonで行いました。
今回は少し毛色を変えて、**計算結果の「丸め」**について扱います。
これは練習レベルではあまり問題になりませんが、
- 実務での営業保険料、責任準備金
- 解約返戻金
では、無視できない重要な論点です。
Pythonのroundは「普通の四捨五入」ではない
まず、多くの人が直感的に「四捨五入」だと思っているround()ですが、
Pythonのround()は偶数丸めという方式を使っています。
例を見てみます。
print(round(1.5))
print(round(2.5))
結果は、
- round(1.5) → 2
- round(2.5) → 2
となります。
2
2
「0.5は切り上げる」と思っていると、
2.5が2になるのは違和感がありますよね。
これは、
ちょうど0.5のときは、偶数になる方に丸める
というルールとなっているからです。
なぜ偶数丸めが使われるのか
偶数丸めは、計算を大量に繰り返したときに
切り上げ・切り下げの偏りを減らすという利点があります。
統計や科学の計算では、とても合理的な方法です。
ただし、生命保険の実務では話が変わります。
生命保険の計算では、保険料や解約返戻金など、
お金そのものを扱います。
このとき重要なのは、
- 計算方法が説明できること
- Excelなど他のソフトウェアでも計算結果が一致すること
です。0.5ならば必ず切り上げなければなりません。
もう一つの問題:浮動小数点の誤差
さらに、もう一つ大事な問題があります。
Pythonの数値の形式float(Excelではdoubleに相当)は、
内部では数値を2進数で扱っています。
そのため、10進数では「きれいに割り切れる数」でも、
内部では近似になります。
例えば、
print(round(102.03-100.18,1))
とすると、102.03-100.18=1.85なので、
小数第2位を四捨五入して、1.9になるはずですが、結果は、
1.8
になります。
この現象はPython特有ではありません。
ExcelやAccessでも同様のことが起こりえます。
実務でよくある対処法
実務ではこの問題を避けるために、
最終結果に影響を与えないようなごく小さい数(例:1E-10)を
足してから丸める独自の四捨五入関数を作る
といった対応をします。
Excel VBAでも、Round関数をそのまま使わず、
自作関数を使うケースは珍しくありません。
Pythonでも同様に、decimalモジュールを使う方法があります。
decimal を使った四捨五入
以下が、今回使った四捨五入関数です。
# 端数処理 四捨五入
# decimalのインポート
from decimal import Decimal, ROUND_HALF_UP, getcontext
# 四捨五入の関数
def roundhu(value, ndigits):
getcontext().prec = 28
d = Decimal(str(value))
quant = Decimal("1").scaleb(-ndigits)
return d.quantize(quant, rounding=ROUND_HALF_UP)
分割して説明を付け加えていきます。
from decimal import Decimal, ROUND_HALF_UP, getcontext
- Decimal
→ 10進数をそのまま扱う数値型 - ROUND_HALF_UP
→ 一般的な四捨五入 - getcontext
→ decimal計算全体の設定(精度など)を管理する
getcontext().prec = 28
- decimald計算で使う有効桁数を指定
- 28桁はdecimalのデフォルトで、実務では十分な精度
- ここでは「精度不足による誤差」を避けるため明示的に設定
d = Decimal(str(value))
ここが非常に重要です。
- Decimal(value)とすると、floatの誤差をそのまま引き継ぐ
- str(value)にしてからDecimalに渡すことで
見た目通りの10進数として扱える
quant = Decimal("1").scaleb(-ndigits)
- scaleb(n)は10のn乗を掛ける操作
- 例として、ndigits=2の場合は、
→ Decimal("1").scaleb(-2)=Decimal("0.01")
つまり、「小数点第何位で丸めるか」を指定しています。
return d.quantize(quant, rounding=ROUND_HALF_UP)
- quantize
→ 指定した桁数で丸める - ROUND_HALF_UP
→ 5は必ず切り上げ(実務では一般的)
この結果として、
ExcelやPython標準のroundとは異なり、
意図通りの四捨五入ができます。
まとめ
今回は、最終的な端数処理に着目して
Decimal型を使った四捨五入の方法を紹介しました。
実務では、途中計算からDecimal型を使うケースもありますが、
本連載では分かりやすさを優先し、
これまではfloat型のまま計算しています。
次回は、これまで作ってきた計算を
Decimal型に置き換えた実装例を紹介します。