Numpy以不同于python的方式进行回合

Numpy rounds in a different way than python

代码

1
2
3
4
import numpy as np
a = 5.92270987499999979065
print(round(a, 8))
print(round(np.float64(a), 8))

1
2
5.92270987
5.92270988

知道为什么吗?

在numpy来源中找不到任何相关内容。

更新:
我知道处理这个问题的正确方法是以这种差异无关紧要的方式构建程序。我做的。我在回归测试中偶然发现了它。

UPDATE2:
关于@VikasDamodar评论。一个人不应该相信repr()函数:

1
2
3
4
>>> np.float64(5.92270987499999979065)
5.922709875
>>> '%.20f' % np.float64(5.92270987499999979065)
'5.92270987499999979065'

UPDATE3:
测试了python3.6.0 x32,numpy 1.14.0,win64。另外在python3.6.4 x64,numpy 1.14.0,debian。

UPDATE4:
只是要确定:

1
2
3
4
5
6
7
import numpy as np
a = 5.92270987499999979065
print('%.20f' % round(a, 8))
print('%.20f' % round(np.float64(a), 8))

5.92270987000000026512
5.92270988000000020435

Update5:
以下代码演示了在不使用str的情况下在哪个阶段发生差异:

1
2
3
4
5
6
7
8
>>> np.float64(a) - 5.922709874
1.000000082740371e-09
>>> a - 5.922709874
1.000000082740371e-09
>>> round(np.float64(a), 8) - 5.922709874
6.000000496442226e-09
>>> round(a, 8) - 5.922709874
-3.999999442783064e-09

很明显,在应用'round'之前,它们是相同的数字。

Update6:
与@ user2357112的答案相反,np.round大约比圆形慢4倍:

1
2
3
4
5
6
7
8
9
%%timeit a = 5.92270987499999979065
round(a, 8)

1.18 μs ± 26.5 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)  

%%timeit a = np.float64(5.92270987499999979065)
round(a, 8)

4.05 μs ± 43.9 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

另外在我看来np.round做得更好,四舍五入到最近的甚至比内置round:最初我得到这个5.92270987499999979065数字,除以11.84541975两个。


float.__round__需要特别注意使用正确舍入的双字符串算法生成正确的舍入结果。

NumPy没有。 NumPy文档提到了这一点

Results may also be surprising due to the inexact representation of decimal fractions in the IEEE floating point standard [R9] and errors introduced when scaling by powers of ten.

这样更快,但会产生更多的舍入误差。它会导致您所观察到的错误,以及错误,其中数字甚至更明确地低于截止值仍会被四舍五入:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> x = 0.33499999999999996
>>> x
0.33499999999999996
>>> x < 0.335
True
>>> x < Decimal('0.335')
True
>>> x < 0.67/2
True
>>> round(x, 2)
0.33
>>> numpy.round(x, 2)
0.34000000000000002

NumPy舍入的时间越来越慢,但这与舍入算法的速度没有任何关系。 NumPy和常规Python数学之间的任何时间比较都归结为NumPy针对整个阵列操作进行了优化。在单个NumPy标量上进行数学计算有很多开销,但使用numpy.round对整个数组进行舍入可以轻松地使用round舍入一系列浮点数:

1
2
3
4
5
6
7
8
9
10
11
In [6]: import numpy

In [7]: l = [i/7 for i in range(100)]

In [8]: a = numpy.array(l)

In [9]: %timeit [round(x, 1) for x in l]
59.6 μs ± 408 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

In [10]: %timeit numpy.round(a, 1)
5.27 μs ± 145 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

至于哪一个更准确,那肯定是float.__round__。你的数字更接近于5.92270987而不是5.92270988,而且它是圆形的,甚至是圆形 - 一切都是均匀的。这里没有关系。


是的,另一种处理这类事情的方法是使用在python3中不那么慢的Decimal:

1
2
3
4
%%timeit d = D('11.84541975'); q = D('0.00000001')
(d/2).quantize(q)

485 ns ± 10.2 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)


tldr

Builtin roundnumpy.round使用不同的舍入算法。对于大多数数字,它们的结果是相同的,但对于某些角落情况则大不相同

两者都适用于某些用途。

对于标量,round更快,对于数组,np.round更快。

说明

&NBSP; ?&NBSP; Builtin round使用直接检查所请求的十进制数字的方法。无论发生什么事情(即使它是...... 499999),它向下四舍五入,并且将5舍入到最近的偶数,除非之后有一个1(例如...... 500001),在这种情况下它会向上舍入。

&NBSP; ?&NBSP; np.round将数字乘以所请求的10的幂,通过普通规则舍入到最接近的int,然后再除以10的相同幂。

它为0.33 / 2等案例提供了更可预测的结果:

1
2
3
4
5
6
>>> 0.33/2
0.165
>>> round(0.33/2, 2)
0.17
>>> np.round(0.33/2, 2)
0.16

这里0.165应该四舍五入到最近的偶数,即0.16。

更新:
然而,对于像1.09 / 2这样的案件,它会遭遇四舍五入的错误(正如Mark Dickinson在评论中指出的那样):

1
2
3
4
5
6
>>> 1.09/2
0.545
>>> round(1.09/2, 2)
0.55
>>> np.round(1.09/2, 2)
0.55

我能想到的唯一解决方法是

1
2
>>> round(round(1.09*100)/2)/100
0.54

哪个有效,但远非普遍。


在我的特殊情况下,一种非常直接的方法来解决这两个函数之间的差异以获得一致的结果是通过乘法和除法。

对于我的应用程序,它似乎比native round更好地工作,给出与np.round相同的结果:

1
2
3
4
'%.20f' % (round(a*1e8)/1e8)
'5.92270988000000020435'
'%.20f' % (round(np.float64(a)*1e8)/1e8)
'5.92270988000000020435'

更新
感谢@ user2357112我发现它正是在np.round内部发生的事情(multiarray / calculation.c#L665),所以除非你在numpy和native python之间交叉测试你的结果,否则使用numpy是安全的圆形版本没有那些额外的分区和python级别的乘法。

UPDATE2
当处理标量时,这种在python级别上的除法和乘法方法比原始round慢一些(~30%)但比np.round快得多(~3次)(给出与np.round相同的结果):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
%%timeit c = 11.84541975
round(c/2)
349 ns ± 10.2 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%%timeit c = 11.84541975
round(c*1e8/2)/1e8
519 ns ± 13 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%%timeit c = np.float64(11.84541975)
round(c/2)
1.67 μs ± 20.2 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%%timeit c = np.float64(11.84541975)
round(c*1e8/2)/1e8
2.01 μs ± 37.9 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

UPDATE3
Python的内置round使用了一种直接的方法,它只对完全可表示的二元有理数进行"四舍五入"规则,如0.375(这是一个整数除以2的精确幂),从而有效地将此规则替换为所有其他带有'围绕领带号码的数字,恰好有49999表示向下并且恰好以50001向上结束'。我不确定这个算法是好还是坏,但绝对不太适合手动检查。