首页 > 解决方案 > 对使用 python Decimal 包的金融应用程序中的浮点精度损失感到困惑

问题描述

70000.0*5.65500*18.0/36000.0在计算并将结果与​​另一个数字进行比较时,我在财务应用程序中遇到问题。

准确的结果是 197.925

使用 Decimal 时,结果取决于运算顺序:

from decimal import Decimal
from fractions import Fraction
Decimal('70000.0')*Decimal('5.65500')*Decimal('18.0')/Decimal('36000.0')

The result is Decimal('197.925000')

Decimal('70000.0')*Decimal('5.65500')/Decimal('36000.0')*Decimal('18.0')

The result is Decimal('197.9249999999999999999999999')

使用 Decimal + Fraction 时,结果仍然不准确:

Decimal('70000.0')*Decimal('5.65500')*Decimal(float(Fraction(18, 36000)))

The result is Decimal('197.9250000000000041201417278')

使用原生浮点数时,操作顺序不影响结果,但结果仍然不准确:

Decimal(70000.0*5.65500*18.0/36000.0)

The result is Decimal('197.92500000000001136868377216160297393798828125')

Decimal(70000.0/36000.0*5.65500*18.0)

The result is Decimal('197.92500000000001136868377216160297393798828125')

并且通过对待Decimal(1.0/36000.0)Decimal(5.655/36000.0)作为乘数,顺序几乎不会影响结果,而结果仍然不准确:

Decimal('70000.0')*Decimal('5.65500')*Decimal('18.0')*Decimal(1.0/36000.0)

The result is Decimal('197.9250000000000094849096025')

Decimal('70000.0')*Decimal('5.65500')*Decimal(1.0/36000.0)*Decimal('18.0')

The result is Decimal('197.9250000000000094849096026')

Decimal('70000.0')*Decimal(5.655/36000.0)*Decimal('18.0')

The result is Decimal('197.9250000000000182364540136')

Decimal('70000.0')*Decimal('18.0')*Decimal(5.655/36000.0)

The result is Decimal('197.9250000000000182364540136')

如果没有办法做到绝对准确,容错或许是一条出路:在容错范围内比较两个数。

本机浮点数的精度为 1E-14

Decimal(70000.0/36000.0*5.65500*18.0) - Decimal('197.925000')

The result is Decimal('1.136868377216160297393798828E-14')

默认设置的小数精度为 1E-25

Decimal('70000.0')*Decimal('5.65500')/Decimal('36000.0')*Decimal('18.0') - Decimal('197.925000')

The result is Decimal('-1E-25')

小数的精度可由用户设置

import decimal as decimal
from decimal import Decimal, Context
decimal.setcontext(Context(prec=60))
Decimal('70000.0')*Decimal('5.65500')/Decimal('36000.0')*Decimal('18.0')

The result is Decimal('197.924999999999999999999999999999999999999999999999999999999')


Decimal('70000.0')*Decimal('5.65500')*Decimal('18.0')/Decimal('36000.0')

The result is Decimal('197.925000')


Decimal(70000.0/36000.0*5.65500*18.0) - Decimal('197.925000')

The result is Decimal('1.136868377216160297393798828125E-14')


Decimal('70000.0')*Decimal('5.65500')/Decimal('36000.0')*Decimal('18.0') - Decimal('197.925000')

The result is  Decimal('-1E-57')

在金融应用中,为了保证绝对安全,有没有推荐的容错?容错为 1E-20 的默认小数精度是否足够?

标签: pythonfloating-pointdecimalfinance

解决方案


就像浮点算术一般,Python 的 Decimal并不精确,并且会出现舍入误差。与 相比float,与 Decimal 的主要区别在于它使用以 10 为底的表示,因此像“0.1”这样的值可以精确表示。

在你的第一个例子中,

Decimal('70000.0')*Decimal('5.65500')/Decimal('36000.0')*Decimal('18.0')

The result is Decimal('197.9249999999999999999999999')

在此计算中,Decimal 使用其默认的 28 位精度。请注意,结果以 28 位有效数字打印,并且在最低位相差一个时是正确的。换言之,计算的舍入误差为 1 “最后一个单位”(ULP)。通常,涉及少量浮点运算的表达式会出现一些 ULP 舍入误差。所以这是按预期工作的。

我看到您已经尝试提高 Decimal 的精度。这降低了舍入误差的幅度,但是是的,仍然存在舍入误差。

如果您不希望在打印结果的末尾看到尾随的“9”,请使用例如round(result, 12)舍入底部的几位数字。

或者,如果必须进行精确计算,我可以理解金融应用程序,那么不要使用 Decimal 或其他浮点表示。用分数将计算重新表述为算术:

Fraction(70000) * Fraction(5655, 1000) * Fraction(18) / Fraction(36000)

这恰好产生了正确的答案,7917/40 = 197.925。

另请参阅 Python 文档浮点算术:问题和限制


推荐阅读