Функция для определения того, являются ли два числа почти равными при округлении до n значащих десятичных разрядов

StackOverflow https://stackoverflow.com/questions/558216

Вопрос

Меня попросили протестировать библиотеку, предоставленную третьей стороной.Библиотека, как известно, с точностью до n значимые цифры.Любые менее существенные ошибки можно смело игнорировать.Я хочу написать функцию, которая поможет мне сравнить результаты:

def nearlyequal( a, b, sigfig=5 ):

Цель этой функции - определить, являются ли два числа с плавающей запятой (a и b) приблизительно равными.Функция вернет значение True, если a==b (точное совпадение) или если a и b имеют одинаковое значение при округлении до сигфиг значимые -цифры, записанные в десятичной системе счисления.

Кто-нибудь может предложить хорошую реализацию?Я написал мини-модульный тест.Если вы не видите ошибку в моих тестах, то хорошая реализация должна соответствовать следующему:

assert nearlyequal(1, 1, 5) 
assert nearlyequal(1.0, 1.0, 5) 
assert nearlyequal(1.0, 1.0, 5) 
assert nearlyequal(-1e-9, 1e-9, 5) 
assert nearlyequal(1e9, 1e9 + 1 , 5) 
assert not nearlyequal( 1e4, 1e4 + 1, 5) 
assert nearlyequal( 0.0, 1e-15, 5 ) 
assert not nearlyequal( 0.0, 1e-4, 6 ) 

Дополнительные примечания:

  1. Значения a и b могут иметь тип int, float или numpy.float64.Значения a и b всегда будут одного и того же типа.Очень важно, чтобы преобразование не приводило к дополнительной ошибке в функции.
  2. Давайте сохраним это числовое значение, поэтому функции, которые преобразуются в строки или используют нематематические приемы, не идеальны.Эта программа будет проверена кем-то, кто является математиком, который захочет иметь возможность доказать, что функция выполняет то, что она должна делать.
  3. Скорость...Мне нужно сравнить множество цифр, так что чем быстрее, тем лучше.
  4. У меня есть numpy, scipy и стандартная библиотека.Что-либо еще мне будет трудно достать, особенно для такой небольшой части проекта.
Это было полезно?

Решение

Существует функция assert_approx_equal в numpy.testing (источник здесь) что может стать хорошей отправной точкой.

def assert_approx_equal(actual,desired,significant=7,err_msg='',verbose=True):
    """
    Raise an assertion if two items are not equal up to significant digits.

    .. note:: It is recommended to use one of `assert_allclose`,
              `assert_array_almost_equal_nulp` or `assert_array_max_ulp`
              instead of this function for more consistent floating point
              comparisons.

    Given two numbers, check that they are approximately equal.
    Approximately equal is defined as the number of significant digits
    that agree.

Другие советы

Начиная с Python 3.5, стандартным способом сделать это (используя стандартную библиотеку) является math.isclose функция.

Он имеет следующую подпись:

isclose(a, b, rel_tol=1e-9, abs_tol=0.0)

Пример использования с абсолютной погрешностью:

from math import isclose
a = 1.0
b = 1.00000001
assert isclose(a, b, abs_tol=1e-8)

Если вы хотите, чтобы это было с точностью n значащие цифры, просто замените последнюю строку на:

assert isclose(a, b, abs_tol=10**-n)

Вот пример.

def nearly_equal(a,b,sig_fig=5):
    return ( a==b or 
             int(a*10**sig_fig) == int(b*10**sig_fig)
           )

Я считаю, что ваш вопрос определен недостаточно хорошо, и представленные вами модульные тесты доказывают это:

Если под "округлить до N знаков после запятой" вы подразумеваете "N знаков после запятой справа от десятичной точки", то тест assert nearlyequal(1e9, 1e9 + 1 , 5) должен завершиться неудачей, потому что даже при округлении 1000000000 и 1000000001 с точностью до 0,00001 они все равно отличаются.

И если под "округлить до N знаков после запятой" вы подразумеваете "N наиболее значимых цифр, независимо от десятичной точки", то тест assert nearlyequal(-1e-9, 1e-9, 5) должен завершиться неудачей, потому что 0.000000001 и -0.000000001 совершенно разные, если рассматривать их таким образом.

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

Уже есть много отличных ответов, но вот что подумайте:

def closeness(a, b):
  """Returns measure of equality (for two floats), in unit
     of decimal significant figures."""
  if a == b:
    return float("infinity")
  difference = abs(a - b)
  avg = (a + b)/2
  return math.log10( avg / difference )


if closeness(1000, 1000.1) > 3:
  print "Joy!"

"Значащие цифры" в десятичной системе счисления - это вопрос настройки десятичной точки и усечения до целого числа.

>>> int(3.1415926 * 10**3)
3141
>>> int(1234567 * 10**-3)
1234
>>>

Это довольно распространенная проблема с числами с плавающей запятой.Я решаю ее на основе обсуждения в разделе 1.5 Деммеля[1].(1) Вычислите ошибку округления.(2) Убедитесь, что ошибка округления меньше некоторого эпсилона.Я некоторое время не использовал python, и у меня есть только версия 2.4.3, но я постараюсь исправить это.

Шаг 1.Ошибка округления

def roundoff_error(exact, approximate):
    return abs(approximate/exact - 1.0)

Шаг 2.Равенство с плавающей запятой

def float_equal(float1, float2, epsilon=2.0e-9):
    return (roundoff_error(float1, float2) < epsilon)

В этом коде есть пара очевидных недостатков.

  1. Ошибка деления на ноль, если точное значение равно нулю.
  2. Не проверяет, что аргументы являются значениями с плавающей запятой.

Пересмотр 1.

def roundoff_error(exact, approximate):
    if (exact == 0.0 or approximate == 0.0):
        return abs(exact + approximate)
    else:
        return abs(approximate/exact - 1.0)

def float_equal(float1, float2, epsilon=2.0e-9):
    if not isinstance(float1,float):
        raise TypeError,"First argument is not a float."
    elif not isinstance(float2,float):
        raise TypeError,"Second argument is not a float."
    else:
        return (roundoff_error(float1, float2) < epsilon)

Так-то немного лучше.Если либо точное, либо приблизительное значение равно нулю, то ошибка равна значению другого.Если указано что-то помимо значения с плавающей запятой, выдается сообщение TypeError .

На данный момент единственная трудность - это установить правильное значение для epsilon.Я заметил в документации для версии 2.6.1, что в sys.float_info есть атрибут epsilon , поэтому я бы использовал вдвое большее значение в качестве epsilon по умолчанию.Но правильное значение зависит как от вашего приложения, так и от вашего алгоритма.

[1] Джеймс У.Деммель, Прикладная Численная Линейная Алгебра, СИАМ, 1997.

Орен Шемеш частично справился с проблемой, как было заявлено, но это еще не все:

утверждать почти равное ( 0.0, 1e-15, 5 )

также не соответствует второму определению (и это определение я выучил в школе).

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

Для этого есть интересное решение от B.Доусон (с кодом на C ++) в "Сравнение чисел с плавающей запятой".Его подход основан на строгом представлении двух чисел по стандарту IEEE и принудительном лексикографическом упорядочении, когда указанные числа представлены в виде целых чисел без знака.

Существует множество способов сравнить два числа, чтобы увидеть, совпадают ли они с N значащими цифрами.Грубо говоря, вы просто хотите убедиться, что их разница меньше, чем в 10 ^-N раз больше самого большого из двух сравниваемых чисел.Это достаточно просто.

Но что, если одно из чисел равно нулю?Вся концепция относительных различий или значащих цифр рушится при сравнении с нулем.Чтобы справиться с этим случаем, вам также необходимо иметь абсолютную разницу, которая должна быть указана иначе, чем относительная разница.

Я обсуждаю проблемы сравнения чисел с плавающей запятой - включая конкретный случай обработки нуля - в этом сообщении в блоге:

http://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/

Меня попросили протестировать библиотеку, предоставленную третьей стороной

Если вы используете Python по умолчанию unittest фреймворк, вы можете использовать assertAlmostEqual

self.assertAlmostEqual(a, b, places=5)
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top