如何避免使用numpy.round进行不正确的四舍五入?

9

我正在使用浮点数。如果我执行:

import numpy as np
np.round(100.045, 2)

我得到:

Out[15]: 100.04

显然,应该是100.05。我知道IEEE 754的存在以及浮点数存储方式导致了这个舍入误差。
我的问题是:我如何避免这个误差?

也许可以看一下decimal模块 - apteryx
3
这是浮点数表示问题还是四舍五入到最近的偶数的问题? - hpaulj
@hpaulj 有点混合在一起,再加上NumPy的舍入算法在中间步骤中引入了误差。由于这些误差,NumPy错误地检测到了一个中间情况,并最终应用了“四舍五入到偶数”的规则,即使从技术上讲它不应该这样做,因为原始值并不是一个精确的平局(由于通常的二进制浮点表示问题)。 - Mark Dickinson
@hpaulj:令人烦恼的是,与Python的 round 操作在所有情况下都进行正确的舍入相比,NumPy的舍入算法更经常给用户满足其朴素期望的结果(假设他们知道舍入到偶数关系),但更容易让用户感到惊讶。 - Mark Dickinson
100.045 实际上是 100.0450000000000017053025658242404460906982421875,在数学上应该四舍五入到2位小数为 100.05。要解决这个问题,通常需要使用更高精度的 float64。我对 NumPy 不熟悉,无法提供更多帮助。 - chux - Reinstate Monica
3个回答

14
你说的部分正确,通常出现“不正确的四舍五入”的原因是浮点数的存储方式不同。有些浮点字面值可以被准确地表示为浮点数,而另一些则不能。
>>> a = 100.045
>>> a.as_integer_ratio()  # not exact
(7040041011254395, 70368744177664)

>>> a = 0.25
>>> a.as_integer_ratio()  # exact
(1, 4)

同样重要的是要知道,你不能从生成的浮点数中恢复使用的文本(100.045) 。因此,你唯一能做的就是使用任意精度数据类型来代替字面值。例如,你可以使用 FractionDecimal (只是提及两种内置类型之一)。

我提到过,一旦将其解析为浮点数,你无法恢复这个字面值,所以你必须将它输入为字符串或其他能够确切表示该数字并由这些数据类型支持的内容:

>>> 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')

概要

所以为了避免这个"错误",你需要:

  • 防止Python将其解析为浮点值,并且
  • 使用一个可以精确表示它的数据类型
  • 然后你必须手动覆盖默认的舍入模式,以便对于"一半"的情况进行四舍五入。
  • (放弃NumPy,因为它没有任意精度的数据类型)

2

在我看来,这个问题没有通用的解决方案,除非你有适用于所有不同情况的通用规则(请参见浮点算术:问题和限制)。但是,在这种情况下,您可以单独四舍五入小数部分:

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

还有一些模块,例如fractionsdecimal,它们提供了快速的、正确舍入的十进制浮点数和有理数算术支持,您可以在这样的情况下使用。


好的,基本上 fmod 函数可以减少这种错误发生的机会...或者我应该如何解释这个?实际上,我想要避免这些情况的发生,而不仅仅是减少它们发生的机会。 - PDiracDelta
@PDiracDelta 是的,它是可以的。类似 np.modf(100.0333) 这样的东西。但在这些情况下,它仍然取决于您的舍入策略。在我看来,除非您有所有不同情况的通用规则,否则没有解决此问题的通用解决方案。 - Mazdak

1

这不是一个错误,而是一个特性)))

你可以简单地使用以下技巧:

def myround(val):
"Fix pythons round"
d,v = math.modf(val)
if d==0.5:
    val += 0.000000001
return round(val)

1
这个答案没有涉及到Numpy。 - Avi Kaminetzky
它涉及到numpy和本地round函数。 - Victor Kuriashkin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接