Каков наилучший способ сравнения чисел с плавающей точкой для почти равенства в Python?


333

Хорошо известно, что сравнение поплавков на равенство немного затруднительно из-за проблем округления и точности.

Например: https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/

Каков рекомендуемый способ борьбы с этим в Python?

Конечно, есть где-нибудь стандартная библиотечная функция для этого?


@tolomea: Поскольку это зависит от вашего приложения, ваших данных и вашей проблемной области - и это всего лишь одна строка кода - зачем нужна «стандартная библиотечная функция»?
S.Lott

9
@ С. Лотт: all, any, max, minявляются в основном каждый однострочечники, и они не только при условии , в библиотеке, они встроенные функции. Так что причины BDFL не в этом. Одна строка кода, которую пишет большинство людей, довольно проста и часто не работает, что является веской причиной для предоставления чего-то лучшего. Конечно, любой модуль, предоставляющий другие стратегии, должен также предоставлять предостережения, описывающие, когда они уместны, и, что более важно, когда их нет. Числовой анализ сложен, и это не позор, что разработчики языка обычно не пытаются инструменты, чтобы помочь с этим.
Стив Джессоп

@ Стив Джессоп. Эти ориентированные на коллекцию функции не имеют зависимостей приложений, данных и проблемной области, как у float-point. Так что «однострочник» явно не так важен, как реальные причины. Числовой анализ сложен и не может быть первоклассной частью универсальной языковой библиотеки.
S.Lott

6
@ S.Lott: Я бы, наверное, согласился, если бы в стандартном дистрибутиве Python не было нескольких модулей для интерфейсов XML. Ясно, что тот факт, что разные приложения должны делать что-то по-разному, вовсе не мешает помещать модули в базовый набор, чтобы сделать это так или иначе. Конечно, есть приемы для сравнения поплавков, которые часто используются повторно, самым основным из которых является определенное количество ульпов. Так что я только частично согласен - проблема в том, что численный анализ сложен. В принципе, Python мог бы предоставить инструменты, чтобы сделать это несколько проще. Я думаю, что никто не вызвался.
Стив Джессоп

4
Кроме того, «это сводится к одной сложной для разработки строке кода» - если это все-таки одна строка, когда вы делаете это правильно, я думаю, что ваш монитор шире моего ;-). Во всяком случае, я думаю, что вся область довольно специализирована, в том смысле, что большинство программистов (включая меня) очень редко используют ее. В сочетании с тем, что он сложный, он не попадет в топ списка самых популярных для большинства библиотек языков.
Стив Джессоп

Ответы:


326

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является абсолютным допуском, который применяется как есть во всех случаях. Если разница меньше, чем любой из этих допусков, значения считаются равными.


26
обратите внимание, когда aили bявляется numpy array, numpy.iscloseработает.
ДБЛИС

6
@marsh rel_tol- относительный допуск , он умножается на большее из двух аргументов; по мере того как значения становятся больше, увеличивается и допустимая разница между ними, в то же время считая их равными. abs_tolявляется абсолютным допуском, который применяется как есть во всех случаях. Если разница меньше, чем любой из этих допусков, значения считаются равными.
Марк Рэнсом

5
Чтобы не умалять значение этого ответа (я думаю, что он хороший), стоит отметить, что в документации также сказано: «Проверка ошибок по модулю и т. Д., Функция вернет результат ...» Другими словами, iscloseфункция (выше) не является полной реализацией.
rkersh

5
Извиняюсь за возрождение старой темы, но, похоже, стоит отметить, что iscloseвсегда придерживается менее консервативного критерия. Я упоминаю об этом только потому, что это поведение противоречит мне. Если бы я указал два критерия, я бы всегда ожидал, что меньший допуск заменит более высокий.
Маки Мессер

3
@MackieMesser вы, конечно, имеете право на свое мнение, но такое поведение имело для меня смысл. По вашему определению ничто не может быть "близко к" нулю, потому что относительный допуск, умноженный на ноль, всегда равен нулю.
Марк Рэнсом

72

Что-то простое, как следующее, не достаточно хорошо?

return abs(f1 - f2) <= allowed_error

8
Как указывает ссылка, указанная мною, вычитание работает, только если вы заранее знаете приблизительную величину чисел.
Гордон Ригли

8
По моему опыту, лучший способ для сравнения поплавков: abs(f1-f2) < tol*max(abs(f1),abs(f2)). Такого рода относительный допуск является единственным значимым способом сравнения чисел с плавающей запятой в целом, поскольку на них обычно влияет ошибка округления в небольших десятичных разрядах.
Sesquipedal

2
Просто добавив простой пример, почему он может не работать: >>> abs(0.04 - 0.03) <= 0.01он дает False. Я используюPython 2.7.10 [GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
schatten

3
Если честно, @schatten, этот пример больше связан с двоичной точностью / форматами машин, чем с конкретным алгоритмом сравнения. Когда вы вводите 0,03 в систему, это не то число, которое дало это процессору.
Эндрю Уайт

2
@AndrewWhite этот пример показывает, что abs(f1 - f2) <= allowed_errorне работает, как ожидалось.
Schatten

46

Я бы согласился, что ответ Гарета, вероятно, наиболее уместен в качестве облегченной функции / решения.

Но я подумал, что было бы полезно отметить, что если вы используете NumPy или рассматриваете его, для этого есть упакованная функция.

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

Небольшая оговорка: установка NumPy может быть нетривиальным процессом в зависимости от вашей платформы.


1
«Установка NumPy может быть нетривиальным опытом в зависимости от вашей платформы.» ... гм Что? Какие платформы "нетривиально" для установки NumPy? Что именно сделало это нетривиальным?
Джон

10
@John: трудно получить 64-разрядный двоичный файл для Windows. Трудно получить NumPy через pipWindows.
Бен Болкер

@ Тернак: Да, но некоторые из моих учеников используют Windows, поэтому мне приходится разбираться с этим.
Бен Болкер,

4
@BenBolker Если вам нужно установить открытую платформу для научных исследований на платформе Python, лучшим способом будет Anaconda Continumum.io/downloads (панды, numpy и многое другое из коробки)
jrovegno

Установка Anaconda тривиальна
эндолиты

14

Используйте decimalмодуль Python , который предоставляет Decimalкласс.

Из комментариев:

Стоит отметить, что если вы выполняете тяжелую математическую работу и вам не нужна точность десятичной дроби, это может сильно затормозить. Поплавки намного быстрее, но неточны. Десятичные числа очень точные, но медленные.


11

Я не знаю ничего в стандартной библиотеке Python (или где-либо еще), которая реализует AlmostEqual2sComplementфункцию Доусона . Если вы хотите именно такое поведение, вам придется реализовать его самостоятельно. (В этом случае, вместо того, чтобы использовать умные побитовые хаки Доусона, вам, вероятно, лучше использовать более обычные тесты формы if abs(a-b) <= eps1*(abs(a)+abs(b)) + eps2или аналогичные. Чтобы получить поведение, подобное Доусону, вы можете сказать что-то вроде if abs(a-b) <= eps*max(EPS,abs(a),abs(b))небольшого исправления EPS; это не совсем так такой же, как Доусон, но похож по духу.


Я не совсем понимаю, что вы здесь делаете, но это интересно. В чем разница между eps, eps1, eps2 и eps?
Гордон Ригли

eps1и eps2определите относительную и абсолютную терпимость: вы готовы разрешить aи bпримерно в eps1разы различать, насколько они велики eps2. epsэто единая толерантность; вы готовы разрешить aи bпримерно в epsразы различать, насколько они велики, при условии, что EPSпредполагается, что все, что имеет размер или меньше, имеет размер EPS. Если вы примете EPSнаименьшее неденормированное значение вашего типа с плавающей запятой, это очень похоже на компаратор Доусона (за исключением коэффициента 2 ^ # бит, потому что Доусон измеряет допуск в ulps).
Гарет МакКоган

2
Между прочим, я согласен с С. Лоттом, что правильная вещь всегда будет зависеть от вашего реального приложения, поэтому не существует единой стандартной библиотечной функции для всех ваших потребностей сравнения с плавающей точкой.
Гарет МакКоган

@ gareth-mccaughan Как определить «наименьшее неденормированное значение вашего типа с плавающей точкой» для python?
Гордон Ригли

На этой странице docs.python.org/tutorial/floatingpoint.html говорится, что почти все реализации python используют плавающие символы двойной точности IEEE-754, а на этой странице en.wikipedia.org/wiki/IEEE_754-1985 говорится, что нормализованные числа, близкие к нулю, равны ± 2 * * -1022.
Гордон Ригли

11

Общепринятое мнение, что числа с плавающей точкой не могут сравниваться на равенство, является неточным. Числа с плавающей запятой ничем не отличаются от целых чисел: если вы оцените «a == b», вы получите истину, если они будут идентичными числами, и ложью в противном случае (при том понимании, что два NaN, конечно, не являются одинаковыми числами).

Фактическая проблема заключается в следующем: если я провел некоторые вычисления и не уверен, что два числа, которые я должен сравнивать, являются точными, то что? Эта проблема одинакова для чисел с плавающей запятой и целых чисел. Если вы оцените целочисленное выражение «7/3 * 3», оно не будет сравниваться равным «7 * 3/3».

Итак, предположим, мы спросили: «Как сравнить целые числа на равенство?» в такой ситуации. Там нет однозначного ответа; что вы должны сделать, зависит от конкретной ситуации, в частности, от того, какие у вас ошибки и чего вы хотите достичь.

Вот несколько возможных вариантов.

Если вы хотите получить «истинный» результат, если математически точные числа будут равны, то вы можете попытаться использовать свойства вычислений, которые вы выполняете, чтобы доказать, что вы получаете одинаковые ошибки в двух числах. Если это выполнимо, и вы сравниваете два числа, которые являются результатом выражений, которые дали бы равные числа, если вычислить точно, то вы получите «истину» из сравнения. Другой подход заключается в том, что вы можете проанализировать свойства вычислений и доказать, что ошибка никогда не превышает определенную величину, возможно, абсолютную величину или величину по отношению к одному из входов или одному из выходов. В этом случае вы можете спросить, отличаются ли два вычисленных числа не более чем на эту сумму, и вернуть «true», если они находятся в пределах интервала. Если вы не можете доказать ошибку, Вы можете догадаться и надеяться на лучшее. Один из способов угадать - оценить множество случайных выборок и посмотреть, какое распределение вы получите в результатах.

Конечно, поскольку мы устанавливаем требование, что вы получите «true», если математически точные результаты равны, мы оставили открытой возможность того, что вы получите «true», даже если они неравны. (Фактически, мы можем удовлетворить требование, всегда возвращая «true». Это делает расчет простым, но в целом нежелательным, поэтому я расскажу об улучшении ситуации ниже.)

Если вы хотите получить «ложный» результат, если математически точные числа будут неравными, вам нужно доказать, что ваша оценка чисел дает разные числа, если математически точные числа будут неравными. Это может быть невозможно для практических целей во многих общих ситуациях. Итак, давайте рассмотрим альтернативу.

Полезным требованием может быть получение «ложного» результата, если математически точные числа отличаются более чем на определенную величину. Например, возможно, мы собираемся вычислить, куда попал мяч, брошенный в компьютерной игре, и мы хотим знать, ударил ли он по летучей мыши. В этом случае мы, безусловно, хотим получить «истину», если мяч ударяет по бите, и мы хотим получить «ложь», если мяч находится далеко от летучей мыши, и мы можем принять неверный «истинный» ответ, если мяч математически точное моделирование пропустило летучую мышь, но находится в миллиметре от удара по ней. В этом случае нам нужно доказать (или угадать / оценить), что наши расчеты положения мяча и положения летучей мыши имеют общую ошибку не более одного миллиметра (для всех интересующих позиций). Это позволило бы нам всегда возвращаться

То, как вы решите, что возвращать при сравнении чисел с плавающей запятой, очень сильно зависит от вашей конкретной ситуации.

Относительно того, как вы можете доказать границы ошибок для расчетов, это может быть сложным вопросом. Любая реализация с плавающей запятой, использующая стандарт IEEE 754 в режиме округления до ближайшего, возвращает число с плавающей запятой, ближайшее к точному результату для любой базовой операции (в частности, умножение, деление, сложение, вычитание, квадратный корень). (В случае связывания округлите, чтобы младший бит был четным.) (Будьте особенно осторожны с квадратным корнем и делением; ваша языковая реализация может использовать методы, которые не соответствуют IEEE 754 для них.) Из-за этого требования мы знаем ошибка в одном результате составляет не более 1/2 от значения младшего значащего бита. (Если бы это было больше, округление дошло бы до другого числа, которое находится в пределах 1/2 от значения.)

Идти оттуда становится значительно сложнее; Следующим шагом является выполнение операции, когда на одном из входов уже есть какая-то ошибка. Для простых выражений за этими ошибками можно проследить вычисления, чтобы достичь границы окончательной ошибки. На практике это делается только в нескольких ситуациях, например при работе с высококачественной математикой. И, конечно же, вам необходимо точно контролировать, какие именно операции выполняются. Языки высокого уровня часто дают компилятору большую слабость, поэтому вы можете не знать, в каком порядке выполняются операции.

Об этой теме можно написать (и написать) гораздо больше, но на этом я должен остановиться. Итак, ответ таков: для этого сравнения нет библиотечной подпрограммы, потому что нет единого решения, удовлетворяющего большинству потребностей, которое стоит включить в библиотечную подпрограмму. (Если для вас достаточно сравнения с относительным или абсолютным интервалом ошибок, вы можете сделать это просто без библиотечной процедуры.)


3
Из обсуждения выше с Гаретом МакКоганом, правильное сравнение с относительной ошибкой, по сути, составляет «abs (ab) <= eps max (2 * -1022, abs (a), abs (b))», это не то, что я бы описал как простой и, конечно, не то, что я бы разработал сам. Кроме того, как отмечает Стив Джессоп, он имеет сложность, аналогичную максимальной, минимальной, любой и всем, которые являются встроенными. Поэтому сравнение относительной ошибки в стандартном математическом модуле кажется хорошей идеей.
Гордон Ригли

(7/3 * 3 == 7 * 3/3) оценивает True в Python.
xApple

@xApple: я только что запустил Python 2.7.2 на OS X 10.8.3 и вошел (7/3*3 == 7*3/3). Это напечатано False.
Эрик Постпищил

3
Вы, вероятно, забыли напечатать from __future__ import division. Если вы этого не сделаете, чисел с плавающей запятой не существует, и сравнение проводится между двумя целыми числами.
xApple

3
Это важная дискуссия, но не очень полезная.
Дэн Халм

6

Если вы хотите использовать его в контексте тестирования / TDD, я бы сказал, что это стандартный способ:

from nose.tools import assert_almost_equals

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

5

Для этого в 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

2

Я нашел следующее сравнение полезным:

str(f1) == str(f2)

это интересно, но не очень практично из-за стр. (.1 + .2) == .3
Гордон Ригли

str (.1 + .2) == str (.3) возвращает True
Генрих Кантуни

Чем это отличается от f1 == f2 - если они оба близки, но все же различаются из-за точности, строковые представления также будут неравными.
МрМас

2
0,1 + 0,2 == 0,3 возвращает значение False , а Обл (0,1 + 0,2) == ул (0,3) возвращает значение ИСТИНА
Krešimir

4
В Python 3.7.2 str(.1 + .2) == str(.3)возвращает False. Описанный выше метод работает только для Python 2.
Danibix

1

В некоторых случаях, когда вы можете повлиять на представление исходного числа, вы можете представить их в виде дробей, а не чисел, используя целочисленный числитель и знаменатель. Таким образом, вы можете иметь точные сравнения.

См. Фракция из модуля фракций для деталей.


1

Мне понравилось предложение @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))

1

Полезно для случая, когда вы хотите убедиться, что 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

Неограниченная концепция близких вам не поможет. isclose(1.0, 1.1)производит Falseи isclose(0.1, 0.000000000001)возвращает True.
kfsone

1

Для сравнения до заданного десятичного числа без 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 

1

Это может быть немного уродливым взломом, но он работает довольно хорошо, когда вам не нужно больше, чем точность с плавающей запятой по умолчанию (около 11 десятичных знаков).

Функция round_to использует метод format из встроенного класса str для округления числа с плавающей точкой до строки, представляющей число с плавающей запятой с необходимым количеством десятичных знаков, а затем применяет eval встроенную функцию к округленной строке с плавающей запятой для возврата на числовой тип с плавающей точкой.

Функция is_close просто применяет простое условное выражение к округленному с плавающей точкой.

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-строк (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}'

1
Вам не нужно использовать eval()для получения параметризованного форматирования. Нечто подобное return '{:.{precision}f'.format(float_num, precision=decimal_precision) должно сделать это
stephenjfox

1
Источник для моего комментария и других примеров: pyformat.info/#param_align
stephenjfox

1
Спасибо @stephenjfox, я не знал о вложенном форматировании. Кстати, в вашем примере кода отсутствуют заключительные фигурные скобки:return '{:.{precision}}f'.format(float_num, precision=decimal_precision)
Альберт Аломар

1
Хороший улов, и особенно хорошо сделанное улучшение с f-струнами. Со смертью Python 2 за углом, возможно, это станет нормой
stephenjfox

0

С точки зрения абсолютной ошибки, вы можете просто проверить

if abs(a - b) <= error:
    print("Almost equal")

Некоторая информация о том, почему float действует странно в Python https://youtu.be/v4HhvoNLILk?t=1129

Вы также можете использовать math.isclose для относительных ошибок

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.