Qual é a melhor maneira de comparar carros alegóricos para quase igualdade no Python?

332

É sabido que comparar carros alegóricos pela igualdade é um pouco complicado devido a problemas de arredondamento e precisão.

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

Qual é a maneira recomendada de lidar com isso no Python?

Certamente existe uma função de biblioteca padrão para isso em algum lugar?

Gordon Wrigley
fonte
@ Tolomea: Como depende da sua aplicação, seus dados e seu domínio do problema - e é apenas uma linha de código - por que haveria uma "função de biblioteca padrão"?
S.Lott
9
@ S. Lott: all, any, max, minsão cada basicamente one-liners, e eles não são apenas fornecidos em uma biblioteca, eles são builtin funções. Portanto, as razões do BDFL não são essa. A única linha de código que a maioria das pessoas escreve é ​​bastante pouco sofisticada e geralmente não funciona, o que é um forte motivo para fornecer algo melhor. É claro que qualquer módulo que forneça outras estratégias também deverá fornecer advertências descrevendo quando elas são apropriadas e, mais importante, quando não são. A análise numérica é difícil, não é uma grande desgraça que os designers de linguagem geralmente não tentem ferramentas para ajudá-lo.
Steve Jessop
@Steve Jessop. Essas funções orientadas a coleções não possuem as dependências de aplicativos, dados e domínio do problema que o ponto flutuante possui. Portanto, o "one-liner" claramente não é tão importante quanto os motivos reais. A análise numérica é difícil e não pode ser uma parte de primeira classe de uma biblioteca de idiomas de uso geral.
precisa saber é o seguinte
6
@ S.Lott: Eu provavelmente concordaria se a distribuição Python padrão não viesse com vários módulos para interfaces XML. Claramente, o fato de que aplicativos diferentes precisam fazer algo diferente não é um obstáculo para colocar os módulos no conjunto básico para fazer isso de uma maneira ou de outra. Certamente, existem truques para comparar carros alegóricos que são reutilizados muito, sendo o mais básico um número especificado de ulps. Portanto, concordo apenas parcialmente - o problema é que a análise numérica é difícil. O Python poderia, em princípio, fornecer ferramentas para torná-lo um pouco mais fácil, em algum momento. Eu acho que ninguém se ofereceu.
18710 Steve Jobs (
4
Além disso, "tudo se resume a uma linha de código difícil de projetar" - se ainda for uma linha quando você estiver fazendo isso corretamente, acho que seu monitor é mais largo que o meu ;-). Enfim, acho que toda a área é bastante especializada, no sentido de que a maioria dos programadores (inclusive eu) raramente a usa. Combinado com a dificuldade, não chegará ao topo da lista "mais procurados" das bibliotecas principais na maioria dos idiomas.
21711 Steve Jobs (

Respostas:

325

O Python 3.5 adiciona as funções math.isclosee cmath.isclose, conforme descrito no PEP 485 .

Se você estiver usando uma versão anterior do Python, a função equivalente é fornecida na documentação .

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é uma tolerância relativa, é multiplicada pela maior das magnitudes dos dois argumentos; à medida que os valores aumentam, também aumenta a diferença permitida entre eles, enquanto os considera iguais.

abs_tolé uma tolerância absoluta aplicada como é em todos os casos. Se a diferença for menor que uma dessas tolerâncias, os valores serão considerados iguais.

Mark Ransom
fonte
26
observe quando aou bé a numpy array, numpy.isclosefunciona.
dbliss
6
@marsh rel_tolé uma tolerância relativa , é multiplicada pela maior das magnitudes dos dois argumentos; à medida que os valores aumentam, também aumenta a diferença permitida entre eles, enquanto os considera iguais. abs_tolé uma tolerância absoluta aplicada como é em todos os casos. Se a diferença for menor que uma dessas tolerâncias, os valores serão considerados iguais.
Mark Ransom
5
Para não diminuir o valor dessa resposta (acho que é boa), vale a pena notar que a documentação também diz: "Verificação de erro do módulo, etc, a função retornará o resultado de ..." Em outras palavras, a isclosefunção (acima) não é uma implementação completa .
rkersh
5
Desculpas por reviver um tópico antigo, mas parecia valer a pena ressaltar que isclosesempre segue o critério menos conservador. Eu apenas menciono isso porque esse comportamento é contra-intuitivo para mim. Se eu especificasse dois critérios, sempre esperaria que a menor tolerância substituísse a maior.
Mackie Messer
3
@MackieMesser, é claro que você tem direito à sua opinião, mas esse comportamento fez todo o sentido para mim. Pela sua definição, nada poderia estar "próximo de" zero, porque uma tolerância relativa multiplicada por zero é sempre zero.
Mark Ransom
72

Algo tão simples como o seguinte não é bom o suficiente?

return abs(f1 - f2) <= allowed_error
Andrew White
fonte
8
Como o link que forneci aponta, subtrair só funciona se você souber a magnitude aproximada dos números com antecedência.
Gordon Wrigley
8
Na minha experiência, o melhor método para comparar carros alegóricos é: abs(f1-f2) < tol*max(abs(f1),abs(f2)). Esse tipo de tolerância relativa é a única maneira significativa de comparar flutuadores em geral, pois geralmente são afetados por erros de arredondamento nas pequenas casas decimais.
Sesquipedal
2
Apenas adicionando um exemplo simples por que ele pode não funcionar >>> abs(0.04 - 0.03) <= 0.01:, ele produz False. Eu usoPython 2.7.10 [GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
schatten
3
@schatten para ser justo, esse exemplo tem mais a ver com precisão / formatos binários da máquina do que algo de comparação em particular. Quando você inseriu 0,03 no sistema, esse não é realmente o número que chegou à CPU.
Andrew White,
2
@AndrewWhite esse exemplo mostra que abs(f1 - f2) <= allowed_errornão funciona conforme o esperado.
schatten
45

Concordo que a resposta de Gareth é provavelmente a mais apropriada como função / solução leve.

Mas pensei que seria útil observar que, se você estiver usando o NumPy ou estiver considerando, existe uma função empacotada para isso.

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

Um pequeno aviso: instalar o NumPy pode ser uma experiência não trivial, dependendo da sua plataforma.

J.Makela
fonte
1
"Instalar o numpy pode ser uma experiência não trivial, dependendo da sua plataforma." ... hum O que? Quais plataformas é "não trivial" instalar numpy? O que exatamente o tornou não trivial?
John
10
@ John: difícil conseguir um binário de 64 bits para Windows. Difícil de ficar entorpecido pipno Windows.
Ben Bolker
@Ternak: Sim, mas alguns de meus alunos usam o Windows, então tenho que lidar com isso.
Ben Bolker
4
@BenBolker Se tiver de instalar plataforma de ciência de dados aberto alimentado por Python, a melhor maneira é Anaconda continuum.io/downloads (pandas, numpy e muito mais fora da caixa)
jrovegno
Instalando Anaconda é trivial
endolith
14

Use o decimalmódulo do Python , que fornece a Decimalclasse.

Dos comentários:

Vale a pena notar que, se você está fazendo um trabalho pesado em matemática e não precisa absolutamente da precisão do decimal, isso pode realmente atrapalhar as coisas. Os carros alegóricos são muito, mais rápidos de lidar, mas imprecisos. Os decimais são extremamente precisos, mas lentos.

jathanism
fonte
11

Não estou ciente de nada na biblioteca padrão do Python (ou em outro lugar) que implemente a AlmostEqual2sComplementfunção de Dawson . Se esse é o tipo de comportamento que você deseja, será necessário implementá-lo. (Nesse caso, em vez de usar os hacks inteligentes e bit a bit de Dawson, você provavelmente faria melhor em usar testes mais convencionais da forma if abs(a-b) <= eps1*(abs(a)+abs(b)) + eps2ou similar. Para obter um comportamento semelhante ao Dawson, você pode dizer algo como if abs(a-b) <= eps*max(EPS,abs(a),abs(b))um pequeno reparo fixo EPS; isso não é exatamente o mesmo que Dawson, mas é semelhante em espírito.

Gareth McCaughan
fonte
Não sigo exatamente o que você está fazendo aqui, mas é interessante. Qual é a diferença entre eps, eps1, eps2 e EPS?
Gordon Wrigley
eps1e eps2defina uma tolerância relativa e uma absoluta: você está preparado para permitir ae bdiferir aproximadamente eps1o tamanho da vantagem eps2. epsé uma tolerância única; você está preparado para permitir ae bdiferir aproximadamente epso tamanho delas, com a condição de que qualquer coisa de tamanho EPSou menor seja assumida como sendo de tamanho EPS. Se você considera EPSo menor valor não-anormal do seu tipo de ponto flutuante, isso é muito semelhante ao comparador de Dawson (exceto por um fator de 2 ^ # bits porque Dawson mede a tolerância em ulps).
Gareth McCaughan
2
Aliás, concordo com S. Lott que a coisa certa sempre dependerá do seu aplicativo real, e é por isso que não existe uma única função de biblioteca padrão para todas as suas necessidades de comparação de ponto flutuante.
Gareth McCaughan
@ gareth-mccaughan Como se determina o "menor valor não-anormal do seu tipo de ponto flutuante" para python?
Gordon Wrigley
Esta página docs.python.org/tutorial/floatingpoint.html diz que quase todas as implementações python usam flutuadores de precisão dupla IEEE-754 e esta página en.wikipedia.org/wiki/IEEE_754-1985 diz que os números normalizados mais próximos de zero são ± 2 * * -1022.
Gordon Wrigley
11

A sabedoria comum de que números de ponto flutuante não podem ser comparados quanto à igualdade é imprecisa. Os números de ponto flutuante não são diferentes dos números inteiros: se você avaliar "a == b", será verdadeiro se eles forem números idênticos e falsos caso contrário (com o entendimento de que dois NaNs não são, obviamente, números idênticos).

O problema real é este: se eu fiz alguns cálculos e não tenho certeza de que os dois números que devo comparar estão exatamente corretos, então o que? Esse problema é o mesmo para o ponto flutuante e para números inteiros. Se você avaliar a expressão inteira "7/3 * 3", ela não será comparada com "7 * 3/3".

Então, suponha que perguntássemos "Como comparo números inteiros para igualdade?" em tal situação. Não há uma resposta única; o que você deve fazer depende da situação específica, principalmente o tipo de erro que você possui e o que deseja obter.

Aqui estão algumas opções possíveis.

Se você deseja obter um resultado "verdadeiro" se os números matematicamente exatos forem iguais, tente usar as propriedades dos cálculos realizados para provar que você obtém os mesmos erros nos dois números. Se isso for possível, e você comparar dois números que resultam de expressões que dariam números iguais se calculados exatamente, você será "verdadeiro" na comparação. Outra abordagem é que você pode analisar as propriedades dos cálculos e provar que o erro nunca excede uma certa quantia, talvez uma quantia absoluta ou uma quantia relativa a uma das entradas ou uma das saídas. Nesse caso, você pode perguntar se os dois números calculados diferem no máximo nesse valor e retornar "true" se estiverem dentro do intervalo. Se você não pode provar um erro vinculado, você pode adivinhar e esperar o melhor. Uma maneira de adivinhar é avaliar muitas amostras aleatórias e ver que tipo de distribuição você obtém nos resultados.

Obviamente, como apenas definimos o requisito de que você seja "verdadeiro" se os resultados matematicamente exatos forem iguais, deixamos em aberto a possibilidade de você ser "verdadeiro", mesmo que sejam desiguais. (De fato, podemos satisfazer o requisito sempre retornando "true". Isso simplifica o cálculo, mas geralmente é indesejável; portanto, discutirei como melhorar a situação abaixo.)

Se você deseja obter um resultado "falso" se os números matematicamente exatos forem desiguais, é necessário provar que sua avaliação dos números gera números diferentes se os números matematicamente exatos forem desiguais. Isso pode ser impossível para fins práticos em muitas situações comuns. Então, vamos considerar uma alternativa.

Um requisito útil pode ser a obtenção de um resultado "falso" se os números matematicamente exatos diferirem mais do que uma certa quantia. Por exemplo, talvez vamos calcular para onde a bola jogada em um jogo de computador viajou e queremos saber se ela atingiu um taco. Nesse caso, certamente queremos ser "verdadeiros" se a bola bater no taco, e queremos ser "falsos" se a bola estiver longe do taco, e podemos aceitar uma resposta "verdadeira" incorreta se a bola uma simulação matematicamente exata perdeu o bastão, mas está a um milímetro de atingi-lo. Nesse caso, precisamos provar (ou adivinhar / estimar) que nosso cálculo da posição da bola e da posição do taco tem um erro combinado de no máximo um milímetro (para todas as posições de interesse). Isso nos permitiria retornar sempre "

Portanto, como você decide o que retornar ao comparar números de ponto flutuante depende muito da sua situação específica.

Quanto à maneira de provar limites de erro para cálculos, isso pode ser um assunto complicado. Qualquer implementação de ponto flutuante usando o padrão IEEE 754 no modo arredondado para o mais próximo retorna o número de ponto flutuante mais próximo do resultado exato para qualquer operação básica (principalmente multiplicação, divisão, adição, subtração, raiz quadrada). (Em caso de empate, arredondar para que o bit mais baixo seja o mesmo.) (Seja particularmente cuidadoso com a raiz quadrada e a divisão; sua implementação de idioma pode usar métodos que não estão em conformidade com o IEEE 754 para esses.) Devido a esse requisito, sabemos o seguinte: erro em um único resultado é no máximo 1/2 do valor do bit menos significativo. (Se fosse mais, o arredondamento teria sido para um número diferente que esteja dentro da metade do valor.)

Continuar a partir daí fica substancialmente mais complicado; a próxima etapa é executar uma operação em que uma das entradas já possui algum erro. Para expressões simples, esses erros podem ser seguidos através dos cálculos para atingir um limite no erro final. Na prática, isso é feito apenas em algumas situações, como trabalhar em uma biblioteca de matemática de alta qualidade. E, é claro, você precisa de um controle preciso sobre exatamente quais operações são executadas. Linguagens de alto nível geralmente oferecem muita folga ao compilador, portanto, você pode não saber em que ordem as operações são executadas.

Há muito mais que poderia ser (e é) escrito sobre esse tópico, mas eu tenho que parar por aí. Em resumo, a resposta é: Não há rotina de biblioteca para essa comparação porque não há uma solução única que atenda à maioria das necessidades que vale a pena colocar em uma rotina de biblioteca. (Se comparar com um intervalo de erro relativo ou absoluto é suficiente para você, você pode fazê-lo simplesmente sem uma rotina de biblioteca.)

Eric Postpischil
fonte
3
Da discussão acima com Gareth McCaughan, comparar corretamente com um erro relativo equivale essencialmente a "abs (ab) <= eps max (2 * -1022, abs (a), abs (b))", isso não é algo que eu descreveria tão simples e certamente não algo que eu teria trabalhado sozinho. Também como Steve Jessop salienta, é de complexidade semelhante ao max, min, any e all, todos integrados. Portanto, fornecer uma comparação relativa de erros no módulo matemático padrão parece uma boa ideia.
Gordon Wrigley
(7/3 * 3 == 7 * 3/3) avalia True em python.
XApple
@ xApple: Acabei de executar o Python 2.7.2 no OS X 10.8.3 e entrei (7/3*3 == 7*3/3). É impresso False.
Eric Postpischil
3
Você provavelmente esqueceu de digitar from __future__ import division. Se você não fizer isso, não haverá números de ponto flutuante e a comparação será entre dois números inteiros.
xApple 31/08
3
Esta é uma discussão importante, mas não incrivelmente útil.
Dan Hulme
6

Se você quiser usá-lo no contexto testing / TDD, diria que esta é uma maneira padrão:

from nose.tools import assert_almost_equals

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

math.isclose () foi adicionado ao Python 3.5 para isso ( código fonte ). Aqui está uma porta para o Python 2. A diferença do one-liner do Mark Ransom é que ele pode manipular "inf" e "-inf" corretamente.

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
user2745509
fonte
2

Achei a seguinte comparação útil:

str(f1) == str(f2)
Kresimir
fonte
é interessante, mas não muito prático devido a str (.1 + .2) == .3
Gordon Wrigley
str (.1 + .2) == str (.3) retorna True
Henrikh Kantuni
Como isso é diferente de f1 == f2 - se os dois são próximos, mas ainda diferentes devido à precisão, as representações das strings também serão desiguais.
MrMas 11/01
2
.1 + .2 == .3 retorna False enquanto str (.1 + .2) == str (.3) retorna True
Kresimir
4
No Python 3.7.2, str(.1 + .2) == str(.3)retorna False. O método descrito acima funciona apenas para o Python 2. #
Danibix 23/03/19
1

Em alguns casos em que você pode afetar a representação do número de origem, é possível representá-los como frações, em vez de flutuadores, usando numerador e denominador inteiro. Dessa forma, você pode ter comparações exatas.

Consulte o módulo Fração de frações para obter detalhes.

eis
fonte
1

Gostei da sugestão de @Sesquipedal, mas com modificações (um caso de uso especial em que ambos os valores são 0 retorna Falso). No meu caso, eu estava no Python 2.7 e apenas usei uma função simples:

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

Útil para o caso em que você deseja garantir que 2 números sejam os mesmos 'até precisão', não há necessidade de especificar a tolerância:

  • Encontre a precisão mínima dos 2 números

  • Arredonde ambos para a precisão mínima e compare

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)                               

Conforme escrito, funciona apenas para números sem o 'e' em sua representação de sequência (significando 0.9999999999995e-4 <número <= 0.9999999999995e11)

Exemplo:

>>> isclose(10.0,10.049)
True
>>> isclose(10.0,10.05)
False
CptHwK
fonte
O conceito ilimitado de fechar não o servirá bem. isclose(1.0, 1.1)produz Falsee isclose(0.1, 0.000000000001)retorna True.
Kfsone # 14/19
1

Para comparar até um determinado decimal sem 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
fonte
1

Talvez isso seja um truque feio, mas funciona muito bem quando você não precisa mais do que a precisão de flutuação padrão (cerca de 11 casas decimais).

A função round_to usa o método format da classe str interna para arredondar o float para uma string que representa o float com o número de casas decimais necessárias e, em seguida, aplica o eval à string float arredondada para voltar para o tipo numérico flutuante.

A função is_close aplica apenas uma condição simples ao float arredondado.

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

Atualizar:

Conforme sugerido por @stepehjfox, uma maneira mais limpa de criar uma função rount_to , evitando "eval", é usando a formatação aninhada :

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

Seguindo a mesma ideia, o código pode ser ainda mais simples usando as excelentes novas f-strings (Python 3.6+):

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

Assim, poderíamos até agrupar tudo em uma função simples e limpa 'is_close' :

def is_close(a, b, prec):
    return f'{a:.{prec}f}' == f'{b:.{prec}f}'
Albert Alomar
fonte
1
Você não precisa usar eval()para obter formatação parametrizada. Algo como return '{:.{precision}f'.format(float_num, precision=decimal_precision) deve fazê-lo
stephenjfox
1
Fonte para o meu comentário e mais exemplos: pyformat.info/#param_align #
stephenjfox
1
Obrigado @stephenjfox Eu não sabia sobre formatação aninhada. Btw, seu código de exemplo não possui os chavetas finais:return '{:.{precision}}f'.format(float_num, precision=decimal_precision)
Albert Alomar
1
Boa captura e aprimoramento especialmente bem feito com as cordas-f. Com a morte do Python 2 ao virar da esquina, talvez isso se torne a norma
stephenjfox
0

Em termos de erro absoluto, você pode apenas verificar

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

Algumas informações sobre por que o float age de maneira estranha no Python https://youtu.be/v4HhvoNLILk?t=1129

Você também pode usar math.isclose para erros relativos

Rahul Sharma
fonte