python - 解释区间 [0, 1] 中明显联系的舍入方向上的惊人奇偶性

标签 python floating-point rounding ieee-754

考虑形式为 0.xx5 的浮点数集合。之间0.01.0 :[0.005, 0.015, 0.025, 0.035, ..., 0.985, 0.995]我可以在 Python 中轻松列出所有 100 个这样的数字:

>>> values = [n/1000 for n in range(5, 1000, 10)]
让我们看看前几个和最后几个值来检查我们没有犯任何错误:
>>> values[:8]
[0.005, 0.015, 0.025, 0.035, 0.045, 0.055, 0.065, 0.075]
>>> values[-8:]
[0.925, 0.935, 0.945, 0.955, 0.965, 0.975, 0.985, 0.995]
现在我想将这些数字中的每一个四舍五入到点后两位小数。一些数字将被四舍五入;有些会被四舍五入。我有兴趣准确计算有多少舍入。我也可以在 Python 中轻松计算:
>>> sum(round(value, 2) > value for value in values)
50
所以事实证明,这 100 个数字中正好有一半被四舍五入了。
如果您不知道 Python 在底层使用二进制浮点,那么这个结果就不足为奇了。毕竟,Python 的 documentation明确指出 round函数使用舍入到偶数(又名银行家的舍入)作为其舍入模式,因此您希望这些值交替向上舍入和向下舍入。
但是 Python 确实在底层使用了二进制浮点数,这意味着除了少数异常(exception)(即 0.1250.3750.6250.875 ),这些值不是精确的关系,而只是非常这些关系的良好二进制近似。毫不奇怪,对舍入结果的仔细检查表明这些值不会交替上下舍入。相反,每个值向上或向下舍入取决于二进制近似值发生在十进制值的哪一侧。所以没有先验的理由期望值的一半向上取整,一半取下。这让我们得到的结果正好是 50 有点令人惊讶。
但也许我们只是走运了?毕竟,如果你掷一枚公平的硬币 100 次,恰好得到 50 次正面的结果并不罕见:它发生的概率约为 8%。但事实证明,这种模式在小数位数较多的情况下仍然存在。以下是四舍五入到小数点后 6 位的类似示例:
>>> values = [n/10**7 for n in range(5, 10**7, 10)]
>>> sum(round(value, 6) > value for value in values)
500000
这里再次将明显的联系四舍五入到点后的小数点后 8 位:
>>> values = [n/10**9 for n in range(5, 10**9, 10)]
>>> sum(round(value, 8) > value for value in values)
50000000
所以问题是:为什么正好有一半的案例四舍五入?或者换句话说,为什么在这些小数关系的所有二进制近似值中,大于真实值的近似值数量与较小的近似值数量完全匹配? (可以很容易地证明,对于精确的情况,我们将有相同数量的向上舍入和向下舍入,因此我们可以忽略这些情况。)
笔记
  • 我假设 Python 3。
  • 在典型的台式机或笔记本电脑上,Python 的浮点数将使用 IEEE 754 binary64(“ double ”)浮点格式,以及整数的真正除法和 round函数将使用舍入到偶数舍入模式正确舍入操作。虽然语言本身不能保证这一切,但这种行为非常普遍,我们假设在这个问题中使用了这样一台典型的机器。
  • 这个问题的灵感来自 Python 错误报告:https://bugs.python.org/issue41198
  • 最佳答案

    不是答案,只是想充实一下令人费解的地方。这当然不是“随机”,但请注意这还不够 ;-) 看看具体的两位数案例:

    >>> from decimal import Decimal as D
    >>> for i in range(5, 100, 10):
    ...     print('%2d' % i, D(i / 100))
        
     5 0.05000000000000000277555756156289135105907917022705078125
    15 0.1499999999999999944488848768742172978818416595458984375
    25 0.25
    35 0.34999999999999997779553950749686919152736663818359375
    45 0.450000000000000011102230246251565404236316680908203125
    55 0.5500000000000000444089209850062616169452667236328125
    65 0.65000000000000002220446049250313080847263336181640625
    75 0.75
    85 0.84999999999999997779553950749686919152736663818359375
    95 0.9499999999999999555910790149937383830547332763671875
    
    现在您可以配对 i/100(100-i)/100并且它们的数学总和正好是 1。所以这对,在上面,5 与 95,15 与 85,等等。 5 次向上舍入的确切机器值,而 95 次向下舍入的机器值,这是“预期的”:如果真和为 1,并且一个加数“向上舍入”,那么肯定另一个“向下舍入”。
    但并非总是如此。 15 和 85 都向下取整,25 和 75 是混合,35 和 65 是混合,但 45 和 55 都向上取整。
    是什么在起作用,使总的“上升”和“下降”案例完全平衡?马克表明他们为 10**3 做了, 10**7 , 和 10**9 ,并且我也验证了指数 2、4、5、6、8、10 和 11 的精确平衡成立。
    令人费解的线索
    这是非常微妙的。而不是除以 10**n ,如果我们乘以它的倒数呢?与上面的对比:
    >>> for i in range(5, 100, 10):
    ...     print('%2d' % i, D(i * (1 / 100)))
    
     5 0.05000000000000000277555756156289135105907917022705078125
    15 0.1499999999999999944488848768742172978818416595458984375
    25 0.25
    35 0.350000000000000033306690738754696212708950042724609375
    45 0.450000000000000011102230246251565404236316680908203125
    55 0.5500000000000000444089209850062616169452667236328125
    65 0.65000000000000002220446049250313080847263336181640625
    75 0.75
    85 0.84999999999999997779553950749686919152736663818359375
    95 0.95000000000000006661338147750939242541790008544921875
    
    现在有 7 个(而不是 5 个)案例。
    对于 10**3 , 64 (而不是 50) 向上取整;为 10**4 , 828(而不是 500),用于 10**5 , 9763(而不是 5000);等等。因此,在计算中不超过一个舍入误差是非常重要的 i/10**n .

    关于python - 解释区间 [0, 1] 中明显联系的舍入方向上的惊人奇偶性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62721186/

    相关文章:

    转换为 int 和浮点错误?

    c - 这个数学舍入函数是如何工作的?

    python - 使用 dask 时如何避免 `Bag.take(n)` 的空结果?

    python - 安装脚本退出并出现错误 : Unable to find vcvarsall. bat

    python - 使用Python脚本更新MySQL表

    c - 如何判断用户输入的小数位有多少位?

    ruby - 四舍五入后总和不为100怎么办?

    python - DRF 将 ArrayField 序列化为字符串

    c - 可移植一致浮标

    c - 字符串不允许 float /十进制数