¿Cuál es la mejor manera de comparar carrozas para casi igualdad en Python?

333

Es bien sabido que comparar flotadores por igualdad es un poco complicado debido a problemas de redondeo y precisión.

Por ejemplo: https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/

¿Cuál es la forma recomendada de lidiar con esto en Python?

¿Seguramente hay una función de biblioteca estándar para esto en alguna parte?

Gordon Wrigley
fuente
@tolomea: Dado que depende de su aplicación y sus datos y su dominio problemático, y es solo una línea de código, ¿por qué habría una "función de biblioteca estándar"?
S.Lott
99
@ S. Lott: all, any, max, minson cada uno, básicamente, de una sola línea, y no son simplemente proporcionan en una biblioteca, son incorporado funciones. Entonces, las razones del BDFL no son eso. La única línea de código que la mayoría de la gente escribe es bastante poco sofisticada y, a menudo, no funciona, lo cual es una razón sólida para proporcionar algo mejor. Por supuesto, cualquier módulo que proporcione otras estrategias también debería proporcionar advertencias que describan cuándo son apropiadas y, lo que es más importante, cuándo no lo son. El análisis numérico es difícil, no es una gran desgracia que los diseñadores de idiomas generalmente no intenten herramientas para ayudarlo.
Steve Jessop
@Steve Jessop. Esas funciones orientadas a la recopilación no tienen la aplicación, los datos y las dependencias del dominio problemático que sí tiene el punto flotante. Entonces, el "one-liner" claramente no es tan importante como las razones reales. El análisis numérico es difícil y no puede ser una parte de primera clase de una biblioteca de idiomas de propósito general.
S.Lott
66
@ S.Lott: Probablemente estaría de acuerdo si la distribución estándar de Python no incluye varios módulos para interfaces XML. Claramente, el hecho de que diferentes aplicaciones necesiten hacer algo diferente no es obstáculo para colocar módulos en el conjunto base para hacerlo de una forma u otra. Ciertamente, hay trucos para comparar flotadores que se reutilizan mucho, el más básico es un número específico de ulps. Así que solo estoy parcialmente de acuerdo: el problema es que el análisis numérico es difícil. Python podría, en principio, proporcionar herramientas para que sea un poco más fácil, algunas veces. Supongo que nadie se ha ofrecido voluntario.
Steve Jessop
44
Además, "se reduce a una línea de código difícil de diseñar": si sigue siendo una línea una vez que lo está haciendo correctamente, creo que su monitor es más ancho que el mío ;-). De todos modos, creo que toda el área es bastante especializada, en el sentido de que la mayoría de los programadores (incluyéndome a mí) rara vez la usan. Combinado con ser difícil, no llegará a la parte superior de la lista de "más buscados" para las bibliotecas principales en la mayoría de los idiomas.
Steve Jessop

Respuestas:

326

Python 3.5 agrega las funciones math.iscloseycmath.isclose como se describe en PEP 485 .

Si está utilizando una versión anterior de Python, la función equivalente se proporciona en la documentación .

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_toles una tolerancia relativa, se multiplica por la mayor de las magnitudes de los dos argumentos; A medida que los valores se hacen más grandes, también lo hace la diferencia permitida entre ellos mientras los consideramos iguales.

abs_toles una tolerancia absoluta que se aplica tal cual en todos los casos. Si la diferencia es menor que cualquiera de esas tolerancias, los valores se consideran iguales.

Mark Ransom
fuente
26
tenga en cuenta cuándo ao bes un numpy array, numpy.isclosefunciona.
dbliss
66
@marsh rel_toles una tolerancia relativa , se multiplica por la mayor de las magnitudes de los dos argumentos; A medida que los valores se hacen más grandes, también lo hace la diferencia permitida entre ellos mientras los consideramos iguales. abs_toles una tolerancia absoluta que se aplica tal cual en todos los casos. Si la diferencia es menor que cualquiera de esas tolerancias, los valores se consideran iguales.
Mark Ransom
55
Para no disminuir el valor de esta respuesta (creo que es buena), vale la pena señalar que la documentación también dice: "Comprobación de errores de módulo, etc., la función devolverá el resultado de ..." En otras palabras, la isclosefunción (arriba) no es una implementación completa .
rkersh
55
Disculpas por revivir un hilo viejo, pero parecía digno de señalar que isclosesiempre se adhiere al criterio menos conservador. Solo lo menciono porque ese comportamiento es contradictorio para mí. Si especificara dos criterios, siempre esperaría que la tolerancia menor reemplazara a la mayor.
Mackie Messer
3
@MackieMesser, tiene derecho a su opinión, por supuesto, pero este comportamiento tiene mucho sentido para mí. Según su definición, nada podría estar "cerca de" cero, porque una tolerancia relativa multiplicada por cero siempre es cero.
Mark Ransom
72

¿Algo tan simple como lo siguiente no es lo suficientemente bueno?

return abs(f1 - f2) <= allowed_error
Andrew White
fuente
8
Como señala el enlace que proporcioné, restar solo funciona si conoce de antemano la magnitud aproximada de los números.
Gordon Wrigley
8
En mi experiencia, el mejor método para comparar los flotadores es: abs(f1-f2) < tol*max(abs(f1),abs(f2)). Este tipo de tolerancia relativa es la única forma significativa de comparar flotadores en general, ya que generalmente se ven afectados por un error de redondeo en los decimales pequeños.
Sesquipedal
2
Simplemente agregando un ejemplo simple de por qué puede no funcionar: >>> abs(0.04 - 0.03) <= 0.01rinde False. Yo usoPython 2.7.10 [GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
schatten
3
@schatten para ser justos, ese ejemplo tiene más que ver con la precisión / formatos binarios de la máquina que con el algoritmo de comparación particular. Cuando ingresó 0.03 en el sistema, ese no es realmente el número que llegó a la CPU.
Andrew White el
2
@AndrewWhite ese ejemplo muestra que abs(f1 - f2) <= allowed_errorno funciona como se esperaba.
schatten
45

Estoy de acuerdo en que la respuesta de Gareth es probablemente la más adecuada como una función / solución ligera.

Pero pensé que sería útil tener en cuenta que si está utilizando NumPy o lo está considerando, hay una función empaquetada para esto.

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

Sin embargo, un pequeño descargo de responsabilidad: instalar NumPy puede ser una experiencia no trivial dependiendo de su plataforma.

J.Makela
fuente
1
"Instalar numpy puede ser una experiencia no trivial dependiendo de su plataforma" ... ¿Qué? ¿Qué plataformas es "no trivial" instalar numpy? ¿Qué lo hizo exactamente no trivial?
John
10
@ John: difícil de obtener un binario de 64 bits para Windows. Difícil de obtener numpy a través pipde Windows.
Ben Bolker
@Ternak: Sí, pero algunos de mis alumnos usan Windows, así que tengo que lidiar con estas cosas.
Ben Bolker
44
@BenBolker Si tiene que instalar una plataforma de ciencia de datos abierta con tecnología de Python, la mejor manera es Anaconda continuum.io/downloads (pandas, numpy y más de forma
inmediata
Instalar Anaconda es trivial
endolith
14

Utilice el decimalmódulo de Python , que proporciona la Decimalclase.

De los comentarios:

Vale la pena señalar que si está haciendo un trabajo matemático pesado y no necesita absolutamente la precisión del decimal, esto realmente puede empantanar las cosas. Los flotadores son mucho más rápidos de tratar, pero imprecisos. Los decimales son extremadamente precisos pero lentos.

jathanism
fuente
11

No conozco nada en la biblioteca estándar de Python (o en otro lugar) que implemente la AlmostEqual2sComplementfunción de Dawson . Si ese es el tipo de comportamiento que desea, deberá implementarlo usted mismo. (En ese caso, en lugar de usar los inteligentes trucos bit a bit de Dawson, probablemente harías mejor en usar pruebas más convencionales de la forma if abs(a-b) <= eps1*(abs(a)+abs(b)) + eps2o similares. Para obtener un comportamiento similar a Dawson, podrías decir algo así como if abs(a-b) <= eps*max(EPS,abs(a),abs(b))para algunos pequeños arreglados EPS; esto no es exactamente lo mismo que Dawson, pero es similar en espíritu.

Gareth McCaughan
fuente
No entiendo muy bien lo que estás haciendo aquí, pero es interesante. ¿Cuál es la diferencia entre eps, eps1, eps2 y EPS?
Gordon Wrigley
eps1y eps2defina una tolerancia relativa y absoluta: está preparado para permitir ay bdiferir aproximadamente en eps1cuanto a su tamaño eps2. epses una sola tolerancia; está preparado para permitir ay bdiferir en aproximadamente el epstamaño de su tamaño, con la condición de que se suponga que cualquier tamaño de tamaño EPSo menor es de tamaño EPS. Si considera que EPSes el valor no nórdico más pequeño de su tipo de punto flotante, esto es muy similar al comparador de Dawson (excepto por un factor de 2 ^ # bits porque Dawson mide la tolerancia en ulps).
Gareth McCaughan
2
Por cierto, estoy de acuerdo con S. Lott en que lo correcto siempre va a depender de su aplicación real, por lo que no existe una única función de biblioteca estándar para todas sus necesidades de comparación de punto flotante.
Gareth McCaughan
@ gareth-mccaughan ¿Cómo se determina el "valor no denormal más pequeño de su tipo de coma flotante" para python?
Gordon Wrigley
Esta página docs.python.org/tutorial/floatingpoint.html dice que casi todas las implementaciones de Python usan flotadores de doble precisión IEEE-754 y esta página en.wikipedia.org/wiki/IEEE_754-1985 dice que los números normalizados más cercanos a cero son ± 2 * * −1022.
Gordon Wrigley
11

La sabiduría común de que los números de coma flotante no se pueden comparar para la igualdad es inexacta. Los números de coma flotante no son diferentes de los enteros: si evalúa "a == b", obtendrá verdadero si son números idénticos y falso de lo contrario (entendiendo que dos NaNs no son números idénticos).

El problema real es este: si he hecho algunos cálculos y no estoy seguro de que los dos números que tengo que comparar son exactamente correctos, ¿entonces qué? Este problema es el mismo para el punto flotante que para los enteros. Si evalúa la expresión entera "7/3 * 3", no se comparará igual a "7 * 3/3".

Supongamos que preguntamos "¿Cómo comparo enteros para la igualdad?" en tal situación. No hay una única respuesta; lo que debe hacer depende de la situación específica, en particular qué tipo de errores tiene y qué desea lograr.

Aquí hay algunas opciones posibles.

Si desea obtener un resultado "verdadero" si los números matemáticamente exactos fueran iguales, entonces podría intentar usar las propiedades de los cálculos que realiza para demostrar que obtiene los mismos errores en los dos números. Si eso es factible, y compara dos números que resultan de expresiones que darían números iguales si se calculan exactamente, entonces obtendrá "verdadero" de la comparación. Otro enfoque es que podría analizar las propiedades de los cálculos y demostrar que el error nunca excede una cierta cantidad, tal vez una cantidad absoluta o una cantidad relativa a una de las entradas o una de las salidas. En ese caso, puede preguntar si los dos números calculados difieren en esa cantidad como máximo y devolver "verdadero" si están dentro del intervalo. Si no puede probar un error vinculado, puedes adivinar y esperar lo mejor. Una forma de adivinar es evaluar muchas muestras aleatorias y ver qué tipo de distribución se obtiene en los resultados.

Por supuesto, dado que solo establecemos el requisito de que sea "verdadero" si los resultados matemáticamente exactos son iguales, dejamos abierta la posibilidad de que sea "verdadero" incluso si son desiguales. (De hecho, podemos satisfacer el requisito siempre devolviendo "verdadero". Esto hace que el cálculo sea simple pero generalmente no es deseable, por lo que discutiremos la mejora de la situación a continuación).

Si desea obtener un resultado "falso" si los números matemáticamente exactos fueran desiguales, debe probar que su evaluación de los números arroja números diferentes si los números matemáticamente exactos fueran desiguales. Esto puede ser imposible para fines prácticos en muchas situaciones comunes. Así que consideremos una alternativa.

Un requisito útil podría ser que obtengamos un resultado "falso" si los números matemáticamente exactos difieren en más de una cierta cantidad. Por ejemplo, tal vez vamos a calcular dónde viajó una pelota lanzada en un juego de computadora, y queremos saber si golpeó un bate. En este caso, ciertamente queremos ser "verdaderos" si la pelota golpea el bate, y queremos ser "falsos" si la pelota está lejos del bate, y podemos aceptar una respuesta "verdadera" incorrecta si la pelota entra una simulación matemáticamente exacta falló el bate pero está a un milímetro de golpear el bate. En ese caso, debemos demostrar (o adivinar / estimar) que nuestro cálculo de la posición de la pelota y la posición del bate tienen un error combinado de como máximo un milímetro (para todas las posiciones de interés). Esto nos permitiría volver siempre "

Entonces, cómo decide qué devolver cuando compara números de punto flotante depende mucho de su situación específica.

En cuanto a cómo probar los límites de error para los cálculos, eso puede ser un tema complicado. Cualquier implementación de punto flotante que use el estándar IEEE 754 en el modo redondeado al más cercano devuelve el número de punto flotante más cercano al resultado exacto para cualquier operación básica (en particular, multiplicación, división, suma, resta, raíz cuadrada). (En caso de empate, redondee para que el bit bajo sea par.) (Tenga especial cuidado con la raíz cuadrada y la división; su implementación de lenguaje podría usar métodos que no se ajustan a IEEE 754 para esos). Debido a este requisito, sabemos que El error en un solo resultado es como máximo la mitad del valor del bit menos significativo. (Si fuera más, el redondeo habría ido a un número diferente que está dentro de la mitad del valor).

Continuar desde allí se vuelve mucho más complicado; El siguiente paso es realizar una operación donde una de las entradas ya tiene algún error. Para expresiones simples, estos errores pueden seguirse a través de los cálculos para alcanzar un límite en el error final. En la práctica, esto solo se hace en algunas situaciones, como trabajar en una biblioteca matemática de alta calidad. Y, por supuesto, necesita un control preciso sobre exactamente qué operaciones se realizan. Los lenguajes de alto nivel a menudo le dan al compilador mucha holgura, por lo que es posible que no sepa en qué orden se realizan las operaciones.

Hay mucho más que podría (y está) escrito sobre este tema, pero tengo que parar allí. En resumen, la respuesta es: no hay una rutina de biblioteca para esta comparación porque no hay una solución única que se adapte a la mayoría de las necesidades que valga la pena poner en una rutina de biblioteca. (Si comparar con un intervalo de error relativo o absoluto es suficiente para usted, puede hacerlo simplemente sin una rutina de biblioteca).

Eric Postpischil
fuente
3
De la discusión anterior con Gareth McCaughan, comparar correctamente con un error relativo esencialmente equivale a "abs (ab) <= eps max (2 * -1022, abs (a), abs (b))", eso no es algo que describiría tan simple y ciertamente no es algo que hubiera resuelto yo solo. Además, como señala Steve Jessop, es de una complejidad similar a max, min, any y all, que son todas incorporadas. Por lo tanto, proporcionar una comparación de errores relativos en el módulo matemático estándar parece una buena idea.
Gordon Wrigley
(7/3 * 3 == 7 * 3/3) evalúa True en python.
xApple
@xApple: Acabo de ejecutar Python 2.7.2 en OS X 10.8.3 e ingresé (7/3*3 == 7*3/3). Impreso False.
Eric Postpischil
3
Probablemente olvidaste escribir from __future__ import division. Si no hace eso, no hay números de coma flotante y la comparación es entre dos enteros.
xApple el
3
Esta es una discusión importante, pero no increíblemente útil.
Dan Hulme
6

Si desea usarlo en pruebas / contexto TDD, diría que esta es una forma estándar:

from nose.tools import assert_almost_equals

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

math.isclose () se ha agregado a Python 3.5 para eso ( código fuente ). Aquí hay un puerto para Python 2. Su diferencia con respecto a Mark Ransom es que puede manejar "inf" y "-inf" correctamente.

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
usuario2745509
fuente
2

La siguiente comparación me pareció útil:

str(f1) == str(f2)
Kresimir
fuente
es interesante, pero no muy práctico debido a str (.1 + .2) == .3
Gordon Wrigley
str (.1 + .2) == str (.3) devuelve True
Henrikh Kantuni el
¿En qué se diferencia esto de f1 == f2? Si ambos están cerca pero siguen siendo diferentes debido a la precisión, las representaciones de cadena también serán desiguales.
MrMas
2
.1 + .2 == .3 devuelve False mientras que str (.1 + .2) == str (.3) devuelve True
Kresimir el
44
En Python 3.7.2, str(.1 + .2) == str(.3)devuelve False. El método descrito anteriormente solo funciona para Python 2.
Danibix
1

Para algunos de los casos en los que puede afectar la representación del número fuente, puede representarlos como fracciones en lugar de flotantes, utilizando un numerador y un denominador enteros. De esa manera puedes tener comparaciones exactas.

Vea la fracción del módulo de fracciones para más detalles.

eis
fuente
1

Me gustó la sugerencia de @Sesquipedal pero con modificaciones (un caso de uso especial cuando ambos valores son 0 devuelve False). En mi caso, estaba en Python 2.7 y solo usé una función simple:

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

Útil para el caso en el que desea asegurarse de que 2 números sean iguales 'hasta la precisión', no es necesario especificar la tolerancia:

  • Encuentra la precisión mínima de los 2 números

  • Redondea ambos con una precisión mínima y compara

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)                               

Tal como está escrito, solo funciona para números sin la 'e' en su representación de cadena (es decir, 0.9999999999995e-4 <número <= 0.9999999999995e11)

Ejemplo:

>>> isclose(10.0,10.049)
True
>>> isclose(10.0,10.05)
False
CptHwK
fuente
El concepto ilimitado de cierre no le servirá bien. isclose(1.0, 1.1)produce Falsey isclose(0.1, 0.000000000001)regresa True.
kfsone
1

Para comparar hasta un decimal dado sin 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 
Vlad
fuente
1

Esto quizás sea un truco un poco feo, pero funciona bastante bien cuando no necesita más que la precisión de flotación predeterminada (aproximadamente 11 decimales).

La función round_to usa el método de formato de la clase str incorporada para redondear el flotante a una cadena que representa el flotante con el número de decimales necesarios, y luego aplica la función incorporada eval a la cadena flotante redondeada para volver al tipo numérico flotante.

La función is_close solo aplica un condicional simple al flotante redondeado.

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

Actualizar:

Según lo sugerido por @stepehjfox, una forma más limpia de construir una función rount_to evitando "eval" es usar el formato anidado :

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

Siguiendo la misma idea, el código puede ser aún más simple usando las nuevas y geniales cadenas f (Python 3.6+):

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

Entonces, incluso podríamos envolverlo todo en una función simple y limpia 'is_close' :

def is_close(a, b, prec):
    return f'{a:.{prec}f}' == f'{b:.{prec}f}'
Albert Alomar
fuente
1
No tiene que usar eval()para obtener un formato parametrizado. Algo así return '{:.{precision}f'.format(float_num, precision=decimal_precision) debería hacerlo
stephenjfox
1
Fuente para mi comentario y más ejemplos: pyformat.info/#param_align
stephenjfox
1
Gracias @stephenjfox No sabía sobre el formato anidado. Por cierto, su código de muestra carece de las llaves finales:return '{:.{precision}}f'.format(float_num, precision=decimal_precision)
Albert Alomar
1
Buena captura y mejora especialmente bien hecha con las cuerdas f. Con la muerte de Python 2 a la vuelta de la esquina, tal vez esto se convierta en la norma
stephenjfox
0

En términos de error absoluto, solo puede verificar

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

Alguna información de por qué flotador actúa raro en Python https://youtu.be/v4HhvoNLILk?t=1129

También puede usar math.isclose para errores relativos

Rahul Sharma
fuente