在Python中比较几乎相等的浮点数的最佳方法是什么?

众所周知,由于舍入和精度问题,比较浮点数是否相等有点棘手。

例如:浮点数比较,2012版 .

在Python中处理这个问题的推荐方法是什么?

有标准的库函数吗?

377365 次浏览

做一些像下面这样简单的事情就足够了:

return abs(f1 - f2) <= allowed_error

使用Python的decimal模块,该模块提供了Decimal类。

评论如下:

值得注意的是,如果你是 做繁重的数学工作,而你没有 绝对需要精准的 小数,这很麻烦 下来。浮点数要快得多 处理,但不精确。小数是 非常精确,但速度较慢

我不知道Python标准库(或其他地方)中有任何实现Dawson的AlmostEqual2sComplement函数的东西。如果这是你想要的行为类型,你必须自己实现它。(在这种情况下,与其使用Dawson的聪明的位hack,你可能会更好地使用if abs(a-b) <= eps1*(abs(a)+abs(b)) + eps2或类似形式的更传统的测试。为了获得类似道森的行为,你可能会说一些类似if abs(a-b) <= eps*max(EPS,abs(a),abs(b))的小固定EPS;这与《道森》并不完全相同,但在精神上是相似的。

浮点数不能相等比较的常识是不准确的。浮点数与整数没有什么不同:如果你计算“a == b”,如果它们是相同的数字,你就会得到真值,否则就会得到假值(要理解两个nan当然不是相同的数字)。

实际的问题是:如果我做了一些计算,但不确定我要比较的两个数字是否完全正确,那么该怎么办?这个问题对于浮点数和整数是一样的。如果计算整数表达式“7/3*3”,它将不等于“7*3/3”。

假设我们问"如何比较整数是否相等"在这种情况下。没有唯一的答案;你应该做什么取决于具体的情况,尤其是你有什么样的错误和你想要达到什么。

这里有一些可能的选择。

如果你想要得到一个“正确”的结果,如果数学上精确的数字是相等的,那么你可以尝试使用你所执行的计算的属性来证明你在两个数字中得到相同的错误。如果这是可行的,并且您比较由表达式得出的两个数字,如果计算准确,则会给出相等的数字,那么您将从比较中得到“true”。另一种方法是分析计算的性质,并证明误差永远不会超过一定的量,可能是绝对的量,也可能是相对于一个输入或一个输出的量。在这种情况下,您可以询问两个计算出的数字是否最多相差那么多,如果它们在区间内,则返回“true”。如果你不能证明一个错误界限,你可以猜测并抱最好的希望。猜测的一种方法是评估许多随机样本,看看你在结果中得到什么样的分布。

当然,因为我们只设置了在数学上精确的结果相等的情况下获得“真”的要求,所以即使它们不相等,我们也保留了获得“真”的可能性。(事实上,我们可以通过总是返回“true”来满足这个要求。这使得计算简单,但通常是不可取的,所以我将在下面讨论如何改善这种情况。)

如果您希望在数学上精确的数字不相等时得到“错误”结果,则需要证明在数学上精确的数字不相等时,对数字的计算会产生不同的数字。在许多常见情况下,从实际的角度来看,这可能是不可能的。所以让我们考虑另一种选择。

一个有用的要求可能是,如果数学上精确的数字相差超过一定数量,我们就会得到一个“假”结果。例如,也许我们要计算电脑游戏中扔出去的球飞到哪里,我们想知道它是否击中了蝙蝠。在这种情况下,如果球击中了球棒,我们当然希望得到“真”,如果球离球棒很远,我们希望得到“假”,如果在精确的数学模拟中,球没有击中球棒,但距离击中球棒只有一毫米,我们可以接受不正确的“真”答案。在这种情况下,我们需要证明(或猜测/估计)我们对球的位置和球棒的位置的计算有最多一毫米的综合误差(对于所有感兴趣的位置)。这将允许我们总是在球和球棒相距超过1毫米时返回“false”,如果它们接触则返回“true”,如果它们足够接近可接受则返回“true”。

所以,在比较浮点数时,如何决定返回什么很大程度上取决于你的具体情况。

至于如何证明计算的误差范围,这可能是一个复杂的问题。使用IEEE 754标准的任何浮点实现在四舍五入模式下都会返回最接近任何基本运算(尤其是乘法、除法、加法、减法、平方根)准确结果的浮点数。(在打平的情况下,圆,这样低的位是偶数)(特别注意平方根和除法;你的语言实现可能会使用不符合IEEE 754的方法。)由于这个要求,我们知道单个结果的误差最多为最低有效位值的1/2。(如果它更大,四舍五入就会得到一个在1/2以内的不同数字。)

从这里开始就变得更加复杂;下一步是执行一个操作,其中一个输入已经有一些错误。对于简单的表达式,这些误差可以通过计算得到最终误差的界限。在实践中,这只在少数情况下完成,例如在高质量的数学库中工作。当然,您需要精确地控制执行哪些操作。高级语言通常会给编译器很多空闲时间,所以您可能不知道操作是按什么顺序执行的。

关于这个主题还有很多可以(也已经)写的东西,但我必须在这里停下来。总之,答案是:没有用于这种比较的库例程,因为没有适合大多数需求的单一解决方案值得放入库例程中。(如果比较相对或绝对误差间隔对您来说足够了,您可以不使用库例程简单地进行比较。)

我同意Gareth的答案可能是最合适的轻量级函数/解决方案。

但我认为,如果您正在使用NumPy或正在考虑使用NumPy,那么有一个打包的函数用于此,这将是有帮助的。

numpy.isclose(a, b, rtol=1e-05, atol=1e-08, equal_nan=False)

不过有一点免责声明:根据您的平台,安装NumPy可能是一种非常重要的体验。

我发现下面的比较很有帮助:

str(f1) == str(f2)

对于一些可以影响源数表示的情况,可以使用整数分子和整数分母将它们表示为分数而不是浮点数。这样你就可以进行准确的比较。

详见fractions模块中的分数

Python 3.5添加了__ABC0和cmath.isclose函数,如PEP 485中所述。

如果您使用的是较早版本的Python,则等效函数在文档中给出。

def isclose(a, b, rel_tol=1e-09, abs_tol=0.0):
return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)

rel_tol是一个相对容差,它乘以两个参数的大小中较大的一个;当值变大时,它们之间允许的差异也会变大,但仍然认为它们相等。

abs_tol是一个绝对容差,在所有情况下都按原样应用。如果差值小于这些公差中的任何一个,则认为值相等。

如果你想在测试/TDD上下文中使用它,我会说这是一种标准方法:

from nose.tools import assert_almost_equals


assert_almost_equals(x, y, places=7) # The default is 7

math.isclose ()已经被添加转换为Python 3.5 (源代码)。这里是它到Python 2的一个端口。它与Mark Ransom的one-line的不同之处在于它可以处理“inf"和“;-inf"正常。

def isclose(a, b, rel_tol=1e-09, abs_tol=0.0):
'''
Python 2 implementation of Python 3.5 math.isclose()
https://github.com/python/cpython/blob/v3.5.10/Modules/mathmodule.c#L1993
'''
# sanity check on the inputs
if rel_tol < 0 or abs_tol < 0:
raise ValueError("tolerances must be non-negative")


# short circuit exact equality -- needed to catch two infinities of
# the same sign. And perhaps speeds things up a bit sometimes.
if a == b:
return True


# This catches the case of two infinities of opposite sign, or
# one infinity and one finite number. Two infinities of opposite
# sign would otherwise have an infinite relative tolerance.
# Two infinities of the same sign are caught by the equality check
# above.
if math.isinf(a) or math.isinf(b):
return False


# now do the regular computation
# this is essentially the "weak" test from the Boost library
diff = math.fabs(b - a)
result = (((diff <= math.fabs(rel_tol * b)) or
(diff <= math.fabs(rel_tol * a))) or
(diff <= abs_tol))
return result

这可能是一个有点丑陋的hack,但当你不需要超过默认的浮点精度(大约11个小数)时,它工作得很好。

< em > round_to < / em >函数使用内置str类中的格式的方法将浮点数四舍五入为一个字符串,该字符串表示具有所需小数数的浮点数,然后将eval内置函数应用于四舍五入的浮点数字符串,以返回浮点数字类型。

< em > is_close < / em >函数只是对四舍五入的浮点数应用了一个简单的条件。

def round_to(float_num, prec):
return eval("'{:." + str(int(prec)) + "f}'.format(" + str(float_num) + ")")


def is_close(float_a, float_b, prec):
if round_to(float_a, prec) == round_to(float_b, prec):
return True
return False


>>>a = 10.0
10.0
>>>b = 10.0001
10.0001
>>>print is_close(a, b, prec=3)
True
>>>print is_close(a, b, prec=4)
False

更新:

正如@stepehjfox所建议的,构建rount_to函数避免使用“eval”的更干净的方法是使用嵌套的格式:

def round_to(float_num, prec):
return '{:.{precision}f}'.format(float_num, precision=prec)

遵循同样的思想,使用伟大的新f-strings (Python 3.6+)可以使代码更加简单:

def round_to(float_num, prec):
return f'{float_num:.{prec}f}'

因此,我们甚至可以将其全部包装在一个简单而干净的“is_close”函数中:

def is_close(a, b, prec):
return f'{a:.{prec}f}' == f'{b:.{prec}f}'

我喜欢Sesquipedal的建议,但有修改(一个特殊的用例时,两个值都是0返回False)。在我的例子中,我使用的是Python 2.7,只使用了一个简单的函数:

if f1 ==0 and f2 == 0:
return True
else:
return abs(f1-f2) < tol*max(abs(f1),abs(f2))

这对于你想要确保两个数字是相同的“达到精度”的情况很有用,并且不需要指定公差:

  • 找到两个数字的最小精度

  • 将两者舍入到最小精度并进行比较

def isclose(a, b):
astr = str(a)
aprec = len(astr.split('.')[1]) if '.' in astr else 0
bstr = str(b)
bprec = len(bstr.split('.')[1]) if '.' in bstr else 0
prec = min(aprec, bprec)
return round(a, prec) == round(b, prec)

如上所述,它只适用于字符串表示中没有'e'的数字(意思是0.9999999999995e-4 <编号<= 0.9999999999995e11)

例子:

>>> isclose(10.0, 10.049)
True
>>> isclose(10.0, 10.05)
False

比较不带atol/rtol的给定小数:

def almost_equal(a, b, decimal=6):
return '{0:.{1}f}'.format(a, decimal) == '{0:.{1}f}'.format(b, decimal)


print(almost_equal(0.0, 0.0001, decimal=5)) # False
print(almost_equal(0.0, 0.0001, decimal=4)) # True

至于绝对误差,你可以检查一下

if abs(a - b) <= error:
print("Almost equal")
关于Python中浮动行为怪异的一些信息: Python 3教程03 - if-else,逻辑运算符和初学者常犯的错误 < / em >

你也可以使用math.isclose来处理相对错误。

使用==是一个简单的好方法,如果你不关心公差精确。

# Python 3.8.5
>>> 1.0000000000001 == 1
False
>>> 1.00000000000001 == 1
True

但是要注意0:

>>> 0 == 0.00000000000000000000000000000000000000000001
False

0总是0。


如果你想控制公差,使用math.isclose

默认的a == b相当于math.isclose(a, b, rel_tol=1e-16, abs_tol=0)


如果你仍然想使用自定义容差的==:

>>> class MyFloat(float):
def __eq__(self, another):
return math.isclose(self, another, rel_tol=0, abs_tol=0.001)


>>> a == MyFloat(0)
>>> a
0.0
>>> a == 0.001
True

到目前为止,我还没有找到为float全局配置它的地方。此外,mock也不能用于float.__eq__

如果你想在测试或TDD上下文中使用pytest包来做到这一点,下面是如何做到的:

import pytest




PRECISION = 1e-3


def assert_almost_equal():
obtained_value = 99.99
expected_value = 100.00
assert obtained_value == pytest.approx(expected_value, PRECISION)

如果你想比较浮点数,上面的选项很好,但在我的情况下,我最终使用Enum的,因为我只有几个有效的浮点数,我的用例可以接受。

from enum import Enum
class HolidayMultipliers(Enum):
EMPLOYED_LESS_THAN_YEAR = 2.0
EMPLOYED_MORE_THAN_YEAR = 2.5

然后运行:

testable_value = 2.0
HolidayMultipliers(testable_value)

如果float是有效的,那就没问题,否则它只会抛出ValueError