Каков наилучший способ сравнения чисел с плавающей точкой на почти равенство в Python?
Хорошо известно, что сравнение поплавков на равенство немного затруднительно из-за проблем округления и точности.
Например: https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/
Каков рекомендуемый способ борьбы с этим в Python?
Конечно, есть где-нибудь стандартная библиотечная функция для этого?
18 ответов
Python 3.5 добавляет math.isclose
а также 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
является абсолютным допуском, который применяется как есть во всех случаях. Если разница меньше, чем любой из этих допусков, значения считаются равными.
Что-то простое, как следующее, не достаточно хорошо?
return abs(f1 - f2) <= allowed_error
Я согласен, что ответ Гарета, вероятно, наиболее уместен в качестве облегченной функции / решения.
Но я подумал, что было бы полезно отметить, что если вы используете NumPy или рассматриваете его, для этого есть упакованная функция.
numpy.isclose(a, b, rtol=1e-05, atol=1e-08, equal_nan=False)
Небольшая оговорка: установка NumPy может быть нетривиальным процессом в зависимости от вашей платформы.
Используйте Python decimal
модуль, который обеспечивает Decimal
учебный класс.
Из комментариев:
Стоит отметить, что если вы выполняете тяжелую математическую работу и вам не нужна точность десятичной дроби, это может сильно затормозить. Поплавки намного быстрее, но неточны. Десятичные числа чрезвычайно точны, но медленны.
Общепринятое мнение, что числа с плавающей точкой не могут сравниваться на равенство, является неточным. Числа с плавающей запятой ничем не отличаются от целых чисел: если вы оцените "a == b", вы получите истину, если они будут идентичными числами, и ложью в противном случае (с пониманием, что два NaN, конечно, не являются одинаковыми числами).
Реальная проблема заключается в следующем: если я провел некоторые вычисления и не уверен, что два числа, которые я должен сравнивать, являются точными, то что? Эта проблема одинакова для чисел с плавающей запятой и целых чисел. Если вы оцените целочисленное выражение "7/3*3", оно не будет сравниваться равным "7*3/3".
Итак, предположим, мы спросили: "Как сравнить целые числа на равенство?" в такой ситуации. Там нет однозначного ответа; что вы должны сделать, зависит от конкретной ситуации, в частности, от того, какие ошибки у вас есть и чего вы хотите достичь.
Вот несколько возможных вариантов.
Если вы хотите получить "истинный" результат, если математически точные числа будут равны, то вы можете попытаться использовать свойства вычислений, которые вы выполняете, чтобы доказать, что вы получаете одинаковые ошибки в двух числах. Если это выполнимо, и вы сравниваете два числа, которые являются результатом выражений, которые дали бы равные числа, если вычислить точно, то вы получите "true" из сравнения. Другой подход заключается в том, что вы можете проанализировать свойства вычислений и доказать, что ошибка никогда не превышает определенную величину, возможно, абсолютную величину или величину по отношению к одному из входов или одному из выходов. В этом случае вы можете спросить, отличаются ли два вычисленных числа не более чем на эту сумму, и вернуть "true", если они находятся в пределах интервала. Если вы не можете доказать ошибку, вы можете догадаться и надеяться на лучшее. Один из способов угадать - оценить множество случайных выборок и посмотреть, какое распределение вы получите в результатах.
Конечно, поскольку мы устанавливаем требование, чтобы вы получали значение "истина" только в том случае, если математически точные результаты равны, мы оставили открытой возможность того, что вы получите значение "истина", даже если они неравны. (Фактически, мы можем удовлетворить требование, всегда возвращая "true". Это делает расчет простым, но, как правило, нежелательным, поэтому я расскажу об улучшении ситуации ниже.)
Если вы хотите получить "ложный" результат, если математически точные числа будут неравными, вам нужно доказать, что ваша оценка чисел дает разные числа, если математически точные числа будут неравными. Это может быть невозможно для практических целей во многих общих ситуациях. Итак, давайте рассмотрим альтернативу.
Полезное требование может заключаться в том, что мы получаем "ложный" результат, если математически точные числа отличаются более чем на определенную величину. Например, возможно, мы собираемся вычислить, куда попал мяч, брошенный в компьютерной игре, и мы хотим знать, ударил ли он по летучей мыши. В этом случае мы, безусловно, хотим получить "истину", если мяч ударяет по бите, и мы хотим получить "ложь", если мяч находится далеко от летучей мыши, и мы можем принять неверный "истинный" ответ, если мяч математически точное моделирование пропустило летучую мышь, но находится в миллиметре от удара по ней. В этом случае нам нужно доказать (или угадать / оценить), что наши расчеты положения шара и положения летучей мыши имеют суммарную ошибку не более одного миллиметра (для всех интересующих позиций). Это позволило бы нам всегда возвращать "ложь", если мяч и летучая мышь находятся на расстоянии более миллиметра друг от друга, возвращать "истину", если они касаются, и возвращать "истину", если они достаточно близки, чтобы быть приемлемыми.
То, как вы решите, что возвращать при сравнении чисел с плавающей запятой, очень сильно зависит от вашей конкретной ситуации.
Что касается того, как вы можете доказать границы ошибок для расчетов, это может быть сложным вопросом. Любая реализация с плавающей запятой, использующая стандарт IEEE 754 в режиме округления до ближайшего, возвращает число с плавающей запятой, ближайшее к точному результату для любой базовой операции (особенно умножение, деление, сложение, вычитание, квадратный корень). (В случае связывания округлите, чтобы младший бит был четным.) (Будьте особенно осторожны с квадратным корнем и делением; ваша языковая реализация может использовать методы, которые не соответствуют IEEE 754 для них.) Из-за этого требования мы знаем ошибка в одном результате составляет не более 1/2 от значения младшего значащего бита. (Если бы это было больше, округление пошло бы к другому числу, которое находится в пределах 1/2 от значения.)
Идти оттуда становится значительно сложнее; Следующий шаг - выполнение операции, в которой один из входов уже имеет ошибку. Для простых выражений за этими ошибками можно проследить вычисления, чтобы достичь границы окончательной ошибки. На практике это делается только в нескольких ситуациях, например, при работе с высококачественной математикой. И, конечно же, вам необходимо точно контролировать, какие именно операции выполняются. Языки высокого уровня часто дают компилятору большую слабость, поэтому вы можете не знать, в каком порядке выполняются операции.
Об этой теме можно написать (и написать) гораздо больше, но на этом я должен остановиться. Таким образом, ответ таков: нет библиотечной подпрограммы для этого сравнения, потому что нет единого решения, которое удовлетворяло бы большинству потребностей, которое стоит включить в библиотечную подпрограмму. (Если вам достаточно сравнения с относительным или абсолютным интервалом ошибок, вы можете сделать это просто без библиотечной процедуры.)
Для этого в Python 3.5 был добавлен math.isclose() ( исходный код). Вот его порт для Python 2. Отличие от однострочного в Mark Ransom заключается в том, что он может правильно обрабатывать "inf" и "-inf".
def isclose(a, b, rel_tol=1e-09, abs_tol=0.0):
'''
Python 2 implementation of Python 3.5 math.isclose()
https://hg.python.org/cpython/file/tip/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
Я не знаю ничего в стандартной библиотеке Python (или в другом месте), которая реализует Доусона AlmostEqual2sComplement
функция. Если вы хотите именно такое поведение, вам придется реализовать его самостоятельно. (В этом случае, вместо того, чтобы использовать умные побитовые хаки Доусона, лучше использовать более традиционные тесты вида if abs(a-b) <= eps1*(abs(a)+abs(b)) + eps2
или похожие. Чтобы получить Dawson-подобное поведение, вы можете сказать что-то вроде if abs(a-b) <= eps*max(EPS,abs(a),abs(b))
для небольшого исправления EPS
; это не совсем то же самое, что Доусон, но по духу оно похоже.
Если вы хотите использовать его в контексте тестирования /TDD, я бы сказал, что это стандартный способ:
from nose.tools import assert_almost_equals
assert_almost_equals(x, y, places=7) #default is 7
Что касается абсолютной ошибки, вы можете просто проверить
if abs(a - b) <= error:
print("Almost equal")
Некоторая информация о том, почему float ведет себя странно в Pythonhttps://youtu.be/v4HhvoNLILk?t=1129
Вы также можете использовать math.isclose для относительных ошибок
Полезно для случая, когда вы хотите убедиться, что 2 числа одинаковы "до точности", не нужно указывать допуск:
Найти минимальную точность двух чисел
Округлите их оба с минимальной точностью и сравните
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
Мне понравилось предложение @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))
В некоторых случаях, когда вы можете повлиять на представление исходного числа, вы можете представить их в виде дробей, а не чисел, используя целочисленный числитель и знаменатель. Таким образом, вы можете иметь точные сравнения.
См. Фракция из фракции модуля для деталей.
Если вы хотите сделать это в контексте тестирования или 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)
Сравнивать до заданного десятичного знака без 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
Это может быть немного уродливым взломом, но он работает довольно хорошо, когда вам не нужно больше, чем точность с плавающей точкой по умолчанию (около 11 десятичных знаков). Хорошо работает на Python 2.7.
Функция round_to использует метод format из встроенного класса str для округления числа с плавающей запятой до строки, представляющей число с плавающей запятой с необходимым количеством десятичных знаков, а затем применяет встроенную функцию eval к округленной строке с плавающей запятой, чтобы получить обратно к числовому типу с плавающей точкой.
Функция is_close просто применяет простое условное выражение к округленному с плавающей точкой.
def round_to(float_num, decimal_precision):
return eval("'{:." + str(int(decimal_precision)) + "f}'.format(" + str(float_num) + ")")
def is_close(float_a, float_b, decimal_precision):
if round_to(float_a, decimal_precision) == round_to(float_b, decimal_precision):
return True
return False
a = 10.0 / 3
# Result: 3.3333333333333335
b = 10.0001 / 3
# Result: 3.3333666666666666
print is_close(a, b, decimal_precision=4)
# Result: False
print is_close(a, b, decimal_precision=3)
# Result: True
Если вы хотите сравнить числа с плавающей запятой, приведенные выше варианты великолепны, но в моем случае я остановился на 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)
Если поплавок действителен, все в порядке, но в противном случае он просто выдастValueError
.
Использование - простой хороший способ, если вы не заботитесь о толерантности.
# Python 3.8.5
>>> 1.0000000000001 == 1
False
>>> 1.00000000000001 == 1
True
Но остерегайтесь:
>>> 0 == 0.00000000000000000000000000000000000000000001
False
В
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__
.