我正在使用浮点数。如果我执行:
import numpy as np
np.round(100.045, 2)
我得到:
Out[15]: 100.04
显然,应该是
100.05
。我知道IEEE 754的存在以及浮点数存储方式导致了这个舍入误差。我的问题是:我如何避免这个误差?
我正在使用浮点数。如果我执行:
import numpy as np
np.round(100.045, 2)
我得到:
Out[15]: 100.04
100.05
。我知道IEEE 754的存在以及浮点数存储方式导致了这个舍入误差。>>> a = 100.045
>>> a.as_integer_ratio() # not exact
(7040041011254395, 70368744177664)
>>> a = 0.25
>>> a.as_integer_ratio() # exact
(1, 4)
同样重要的是要知道,你不能从生成的浮点数中恢复使用的文本(100.045
) 。因此,你唯一能做的就是使用任意精度数据类型来代替字面值。例如,你可以使用 Fraction
或 Decimal
(只是提及两种内置类型之一)。
我提到过,一旦将其解析为浮点数,你无法恢复这个字面值,所以你必须将它输入为字符串或其他能够确切表示该数字并由这些数据类型支持的内容:
>>> from fractions import Fraction
>>> f = Fraction(100045, 100)
>>> f
Fraction(20009, 20)
>>> f = Fraction("100.045")
>>> f
Fraction(20009, 20)
>>> from decimal import Decimal
>>> Decimal("100.045")
Decimal('100.045')
然而,这些在NumPy中的效果不佳,即使您设法让它工作,与基本浮点运算相比,它几乎肯定会非常慢。
>>> import numpy as np
>>> a = np.array([Decimal("100.045") for _ in range(1000)])
>>> np.round(a)
AttributeError: 'decimal.Decimal' object has no attribute 'rint'
一开始我说你只说对了一部分,还有一个细节!
你提到将100.045四舍五入得到100.05是显而易见的。但这一点根本不明显,在编程中使用浮点数时甚至是错误的(对于“普通计算”来说则正确)。在许多编程语言中,“半”值(小数点后面的数字为5)并不总是向上取整——例如Python(和NumPy)采用“银行家舍入法”,因为它更少受偏差影响。例如,0.5
会被四舍五入为0
,而1.5
会被四舍五入为2
。
因此,即使100.045
可以准确表示为浮点数,由于这种取整规则,它仍然会四舍五入为100.04
!
>>> round(Fraction("100.045"), 1)
Fraction(5002, 5)
>>> 5002 / 5
1000.4
>>> d = Decimal("100.045")
>>> round(d, 2)
Decimal('100.04')
这甚至在NumPy文档中的numpy.around
中被提到:
注释
对于刚好处于舍入小数值之间的值,NumPy会将其舍入为最近的偶数值。因此,1.5和2.5舍入为2.0,-0.5和0.5舍入为0.0,等等。由于IEEE浮点标准[R1011]中十进制分数的不精确表示以及通过十的幂进行缩放时引入的误差,结果可能令人惊讶。
(强调是我的。)
Python中唯一(至少我知道的)允许手动设置舍入规则的数字类型是Decimal
- 通过ROUND_HALF_UP
:
>>> from decimal import Decimal, getcontext, ROUND_HALF_UP
>>> dc = getcontext()
>>> dc.rounding = ROUND_HALF_UP
>>> d = Decimal("100.045")
>>> round(d, 2)
Decimal('100.05')
所以为了避免这个"错误",你需要:
在我看来,这个问题没有通用的解决方案,除非你有适用于所有不同情况的通用规则(请参见浮点算术:问题和限制)。但是,在这种情况下,您可以单独四舍五入小数部分:
In [24]: dec, integ = np.modf(100.045)
In [25]: integ + np.round(dec, 2)
Out[25]: 100.05
这种行为的原因不是因为将整数与小数部分分开会对round()
的逻辑产生任何影响。而是因为当你使用fmod
时,它会给你一个更真实的数字的小数部分,实际上是一个四舍五入的表示。
在这种情况下,dec
是什么:
In [30]: dec
Out[30]: 0.045000000000001705
您可以检查使用0.045
得到相同结果的圆形:
In [31]: round(0.045, 2)
Out[31]: 0.04
100.0333
,小数部分会是一个稍微更小的版本,正如我之前提到的,你想要的结果取决于你的舍入策略。In [37]: dec, i = np.modf(100.0333)
In [38]: dec
Out[38]: 0.033299999999997
还有一些模块,例如fractions
和decimal
,它们提供了快速的、正确舍入的十进制浮点数和有理数算术支持,您可以在这样的情况下使用。
fmod
函数可以减少这种错误发生的机会...或者我应该如何解释这个?实际上,我想要避免这些情况的发生,而不仅仅是减少它们发生的机会。 - PDiracDeltanp.modf(100.0333)
这样的东西。但在这些情况下,它仍然取决于您的舍入策略。在我看来,除非您有所有不同情况的通用规则,否则没有解决此问题的通用解决方案。 - Mazdak这不是一个错误,而是一个特性)))
你可以简单地使用以下技巧:
def myround(val):
"Fix pythons round"
d,v = math.modf(val)
if d==0.5:
val += 0.000000001
return round(val)
round
操作在所有情况下都进行正确的舍入相比,NumPy的舍入算法更经常给用户满足其朴素期望的结果(假设他们知道舍入到偶数关系),但更容易让用户感到惊讶。 - Mark Dickinson100.045
实际上是100.0450000000000017053025658242404460906982421875
,在数学上应该四舍五入到2位小数为100.05
。要解决这个问题,通常需要使用更高精度的 float64。我对 NumPy 不熟悉,无法提供更多帮助。 - chux - Reinstate Monica