Який найкращий спосіб порівняти поплавці для майже рівності в Python?


331

Добре відомо, що порівняння поплавків для рівності трохи хитро пов'язане із питаннями округлення та точності.

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

Який рекомендований спосіб боротьби з цим у Python?

Напевно, є для цього десь стандартна функція бібліотеки?


@tolomea: Оскільки це залежить від вашої програми та ваших даних та вашої проблемної області - і це лише один рядок коду - чому б існувала "стандартна функція бібліотеки"?
С.Лотт

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

@Steve Jessop. Ці функції, орієнтовані на збір, не залежать від застосувань, даних та проблемних доменів, як це робить плаваюча точка. Отже, "однолінійний" явно не так важливий, як реальні причини. Числовий аналіз важкий і не може бути першокласною частиною мовної бібліотеки загального призначення.
S.Lott

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

4
Крім того, "воно зводиться до однієї складно спроектованої лінії коду" - якщо це все-таки однолінійний раз, коли ви робите це правильно, я думаю, ваш монітор ширший, ніж мій ;-). У всякому разі, я думаю, що вся область досить спеціалізована, в тому сенсі, що більшість програмістів (включаючи мене) дуже рідко її використовують. У поєднанні з важкістю вона не збирається потрапити в першу чергу "найбільш шуканого" списку основних бібліотек більшості мов.
Стів Джессоп

Відповіді:


324

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працює.
dbliss

6
@marsh rel_tol- відносна толерантність , вона множиться на більшу величину двох аргументів; як значення збільшуються, так і дозволена різниця між ними, вважаючи їх рівними. abs_tol- це абсолютна толерантність, яка застосовується так, як є у всіх випадках. Якщо різниця менша за будь-який з цих допусків, значення вважаються рівними.
Марк Викуп

5
Щоб не зменшувати значення цієї відповіді (я думаю, що це добре), варто зазначити, що в документації також написано: "Перевірка помилок модуля тощо. Функція поверне результат ..." Іншими словами, iscloseфункція (вище) не є повною реалізацією.
rkersh

5
Вибачення за відродження старої нитки, але, здавалося, варто зазначити, що iscloseзавжди дотримується менш консервативного критерію. Я згадую це лише тому, що така поведінка для мене є протиінтуїтивною. Якби я вказав два критерії, я завжди очікував, що менша толерантність перевищить більшу.
Маккі Мессер

3
@MackieMesser ти, звичайно, маєш право на твою думку, але така поведінка мала для мене ідеальний сенс. За вашим визначенням, нічого ніколи не може бути "близьким до нуля", тому що відносна допуск, помножена на нуль, завжди дорівнює нулю.
Марк Викуп 11

72

Невже щось таке просте, як наступне, недостатньо добре?

return abs(f1 - f2) <= allowed_error

8
Як зазначається в наданому посиланням, віднімання працює лише у тому випадку, якщо ви знаєте приблизну величину чисел заздалегідь.
Гордон Вріглі

8
З мого досвіду, найкращий спосіб для порівняння поплавців: abs(f1-f2) < tol*max(abs(f1),abs(f2)). Цей вид відносної допуску є єдиним значущим способом порівняння поплавків в цілому, оскільки на них зазвичай впливає помилка округлення у малих десяткових колах.
Сесквіпедал

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

45

Я погодився б, що відповідь Гарета, ймовірно, найбільш підходить як легка функція / рішення.

Але я подумав, що було б корисно зауважити, що якщо ви використовуєте NumPy або обмірковуєте це, для цього існує функція, що пакує.

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

Трохи відмови від відповідальності: установка NumPy може бути нетривіальним досвідом залежно від вашої платформи.


1
"Встановлення numpy може бути нетривіальним досвідом залежно від вашої платформи." ... гм Що? На яких платформах "нетривіально" встановлювати numpy? Що саме зробило це нетривіальним?
Іван

10
@John: важко отримати 64-розрядний двійковий файл для Windows. Важко зануритися pipв Windows.
Бен Болкер

@Ternak: Я маю, але деякі мої студенти використовують Windows, тому мені доводиться мати справу з цими речами.
Бен Болкер

4
@BenBolker Якщо вам доведеться встановити платформу з відкритими даними, що працює на Python, найкращим способом є Anaconda continuum.io/downloads (панди, нуме та інше з коробки)
jrovegno

Установка Anaconda тривіальна
ендоліти

13

Використовуйте decimalмодуль Python , який забезпечує Decimalклас.

З коментарів:

Варто зауважити, що якщо ви займаєтесь математично важкою роботою і вам абсолютно не потрібна точність від десяткової, це може насправді заблукати речі. Поплавки - це шлях, набагато швидше впоратися, але неточно. Десяткові знаки надзвичайно точні, але повільні.


11

Мені нічого не відомо в стандартній бібліотеці 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, це не зовсім те саме, що і Доусон, але схоже за духом.


Я не дуже стежу за тим, що ти тут робиш, але це цікаво. Яка різниця між eps, eps1, eps2 та EPS?
Гордон Вріглі

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

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".

Отже, припустимо, ми запитали "Як я порівняю цілі числа для рівності?" в такій ситуації. Єдиної відповіді немає; що ви повинні зробити, залежить від конкретної ситуації, зокрема, які помилки у вас є і що ви хочете досягти.

Ось кілька можливих варіантів.

Якщо ви хочете отримати "справжній" результат, якщо математично точні числа були б рівними, ви можете спробувати використати властивості обчислень, які ви виконуєте, щоб довести, що ви отримуєте однакові помилки в двох числах. Якщо це можливо, і ви порівнюєте два числа, які є результатами виразів, які давали б однакові числа, якщо обчислити точно, то ви отримаєте "правду" від порівняння. Інший підхід полягає в тому, що ви можете проаналізувати властивості обчислень і довести, що помилка ніколи не перевищує певну суму, можливо, абсолютну суму або величину відносно одного з входів або одного з результатів. У такому випадку ви можете запитати, чи відрізняються два обчислені числа щонайменше на цю суму, і повернути "справжнє", якщо вони знаходяться в інтервалі. Якщо ви не можете довести помилку, ви можете здогадатися і сподіватися на найкраще. Один із способів здогадки - це оцінити багато випадкових вибірок і побачити, який розподіл ви отримаєте в результатах.

Звичайно, оскільки ми встановлюємо лише вимогу, що ти отримаєш "справжній", якщо математично точні результати рівні, ми залишаємо відкритими можливість отримати "справжні", навіть якщо вони нерівні. (Насправді, ми можемо задовольнити цю вимогу, завжди повертаючи "справжнє". Це робить розрахунок простим, але, як правило, небажаним, тому я обговорюю покращення ситуації нижче.)

Якщо ви хочете отримати "хибний" результат, якщо математично точні числа були б неоднаковими, вам потрібно довести, що ваша оцінка чисел дає різні числа, якщо математично точні числа були б неоднаковими. Це може бути неможливим для практичних цілей у багатьох загальних ситуаціях. Тож розглянемо альтернативу.

Корисною вимогою може бути те, що ми отримуємо "помилковий" результат, якщо математично точні числа відрізняються більш ніж на певну суму. Наприклад, можливо, ми збираємося обчислити, куди їхав м'яч, кинутий у комп’ютерній грі, і ми хочемо знати, чи вдарив він у біту. У цьому випадку ми, безумовно, хочемо отримати "справжній", якщо м'яч вдарив битою, і ми хочемо отримати "помилковий", якщо м'яч далеко від бити, і ми можемо прийняти неправильну "справжню" відповідь, якщо м'яч в математично точне моделювання пропустило биту, але знаходиться в межах міліметра від удару кажана. У цьому випадку нам потрібно довести (або здогадатися / оцінити), що в нашому розрахунку положення м'яча і положення кажана є комбінована помилка не менше одного міліметра (для всіх цікавих позицій). Це дозволило б нам завжди повертатися "

Отже, те, як ви вирішите, що повернути, порівнюючи числа з плаваючою комою, дуже залежить від вашої конкретної ситуації.

Щодо того, як ви хочете довести межі помилок для розрахунків, це може бути складним предметом. Будь-яка реалізація з плаваючою комою, що використовує стандарт 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 у пітоні.
xApple

@xApple: Я щойно запустив Python 2.7.2 на OS X 10.8.3 і ввійшов (7/3*3 == 7*3/3). Це було надруковано False.
Eric Postpischil

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

3
Це важлива дискусія, але не неймовірно корисна.
Dan Hulme

6

Якщо ви хочете використовувати його в контексті тестування / TDD, я б сказав, що це стандартний спосіб:

from nose.tools import assert_almost_equals

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

5

math.isclose () для цього додано Python 3.5 ( вихідний код ). Ось порт його для Python 2. Відмінність від однокласника Марка Рансома полягає в тому, що він може нормально обробляти "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)

це цікаво, але не дуже практично через str (.1 + .2) == .3
Гордон Вріглі

str (.1 + .2) == str (.3) повертається True
Генріх Кантуні

Чим це відрізняється від f1 == f2 - якщо вони обидва близькі, але все одно відрізняються через точність, представлення рядків також буде неоднаковим.
MrMas

2
.1 + .2 == .3 повертає False, а str (.1 + .2) == str (.3) повертає True
Кресімір

4
У Python 3.7.2 str(.1 + .2) == str(.3)повертає False. Описаний вище метод працює лише для Python 2.
Danibix

1

У деяких випадках, коли ви можете впливати на подання вихідного номера, ви можете представляти їх у вигляді дробів замість плавців, використовуючи цілі чисельник та знаменник. Таким чином ви можете мати точні порівняння.

Детальніше див. Модуль « Фракція» .


1

Мені сподобалося пропозицію @Sesquipedal, але з модифікацією (особливий випадок використання, коли обидва значення 0 повертають помилково). У моєму випадку я був на 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 використовує метод форматування із вбудованого класу 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.