Qual è il modo migliore per confrontare i float per la quasi-uguaglianza in Python?


333

È noto che il confronto dei galleggianti per l'uguaglianza è un po 'complicato a causa di problemi di arrotondamento e precisione.

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

Qual è il modo consigliato per gestirlo in Python?

Sicuramente esiste una funzione di libreria standard per questo da qualche parte?


@tolomea: poiché dipende dalla tua applicazione, dai tuoi dati e dal tuo dominio problematico - ed è solo una riga di codice - perché dovrebbe esserci una "funzione di libreria standard"?
S.Lott

9
@ S. Lott: all, any, max, minsono ogni fondamentalmente battute, e non sono solo forniti in una biblioteca, sono incorporato funzioni. Quindi le ragioni del BDFL non sono quelle. L'unica riga di codice che la maggior parte delle persone scrive è piuttosto poco sofisticata e spesso non funziona, il che è una buona ragione per fornire qualcosa di meglio. Ovviamente qualsiasi modulo che fornisce altre strategie dovrebbe anche fornire avvertenze che descrivono quando sono appropriate e, cosa più importante, quando non lo sono. L'analisi numerica è difficile, non è una grande vergogna che i designer linguistici di solito non provino strumenti per aiutarla.
Steve Jessop,

@Steve Jessop. Queste funzioni orientate alla raccolta non hanno l'applicazione, i dati e le dipendenze del dominio problematico del float point. Quindi il "one-liner" chiaramente non è importante quanto i veri motivi. L'analisi numerica è difficile e non può essere una parte di prima classe di una biblioteca linguistica per scopi generici.
S.Lott

6
@ S.Lott: probabilmente sarei d'accordo se la distribuzione standard di Python non fosse dotata di più moduli per le interfacce XML. Chiaramente il fatto che diverse applicazioni debbano fare qualcosa di diverso non è affatto un ostacolo nel mettere i moduli nel set di base per farlo in un modo o nell'altro. Certamente ci sono trucchi per confrontare i float che vengono riutilizzati molto, il più semplice è un numero specificato di ulps. Quindi sono solo parzialmente d'accordo - il problema è che l'analisi numerica è difficile. Python in linea di principio potrebbe fornire strumenti per renderlo un po 'più semplice, in alcuni casi. Immagino che nessuno si sia offerto volontario.
Steve Jessop,

4
Inoltre, "si riduce a una riga di codice difficile da progettare" - se è ancora una riga una volta che lo fai correttamente, penso che il tuo monitor sia più largo del mio ;-). Comunque, penso che l'intera area sia piuttosto specializzata, nel senso che la maggior parte dei programmatori (incluso me) la usa molto raramente. Combinato con l'essere duro, non andrà in cima alla lista "più ricercata" per le librerie di base nella maggior parte delle lingue.
Steve Jessop,

Risposte:


326

Python 3.5 aggiunge le funzioni math.iscloseecmath.isclose come descritto in PEP 485 .

Se stai usando una versione precedente di Python, la funzione equivalente è indicata nella documentazione .

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è una tolleranza relativa, moltiplicata per la maggiore delle magnitudini dei due argomenti; man mano che i valori aumentano, aumenta anche la differenza consentita tra loro, pur considerandoli uguali.

abs_tolè una tolleranza assoluta che viene applicata così com'è in tutti i casi. Se la differenza è inferiore a una di queste tolleranze, i valori sono considerati uguali.


26
nota quando ao bè un numpy array, numpy.isclosefunziona.
dbliss,

6
@marsh rel_tolè una tolleranza relativa , moltiplicata per la maggiore delle magnitudini dei due argomenti; man mano che i valori aumentano, aumenta anche la differenza consentita tra loro, pur considerandoli uguali. abs_tolè una tolleranza assoluta che viene applicata così com'è in tutti i casi. Se la differenza è inferiore a una di queste tolleranze, i valori sono considerati uguali.
Mark Ransom,

5
Per non diminuire il valore di questa risposta (penso che sia una buona risposta), vale la pena notare che la documentazione dice anche: "Controllo errori modulo, ecc., La funzione restituirà il risultato di ..." In altre parole, la isclosefunzione (sopra) non è un'implementazione completa .
Rkersh,

5
Ci scusiamo per aver rianimato un vecchio filo, ma sembrava degno di nota che iscloseaderisce sempre al criterio meno conservatore. Lo cito solo perché quel comportamento è controintuitivo per me. Se dovessi specificare due criteri, mi aspetterei sempre che la tolleranza minore superi il maggiore.
Mackie Messer,

3
@MackieMesser hai diritto alla tua opinione ovviamente, ma questo comportamento ha perfettamente senso per me. Per tua definizione, nulla potrebbe mai essere "vicino a" zero, perché una tolleranza relativa moltiplicata per zero è sempre zero.
Mark Ransom,

72

Qualcosa di semplice come il seguente non è abbastanza buono?

return abs(f1 - f2) <= allowed_error

8
Come indica il collegamento che ho indicato, la sottrazione funziona solo se si conosce in anticipo la grandezza approssimativa dei numeri.
Gordon Wrigley,

8
Nella mia esperienza, il metodo migliore per il confronto carri allegorici è: abs(f1-f2) < tol*max(abs(f1),abs(f2)). Questo tipo di tolleranza relativa è l'unico modo significativo per confrontare i galleggianti in generale, in quanto sono generalmente interessati dall'errore di arrotondamento nelle piccole cifre decimali.
Sesquipedal,

2
Basta aggiungere un semplice esempio perché potrebbe non funzionare >>> abs(0.04 - 0.03) <= 0.01:, produce False. Io usoPython 2.7.10 [GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
schatten il

3
Per essere onesti, per esempio, quell'esempio ha più a che fare con la precisione / i formati binari della macchina rispetto al particolare algoritmo di confronto. Quando hai inserito 0.03 nel sistema, questo non è proprio il numero che lo ha reso alla CPU.
Andrew White,

2
@AndrewWhite quell'esempio mostra che abs(f1 - f2) <= allowed_errornon funziona come previsto.
Schatten,

45

Concordo sul fatto che la risposta di Gareth è probabilmente la più appropriata come funzione / soluzione leggera.

Ma ho pensato che sarebbe utile notare che se stai usando NumPy o lo stai prendendo in considerazione, esiste una funzione a pacchetto per questo.

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

Un piccolo disclaimer però: l'installazione di NumPy può essere un'esperienza non banale a seconda della piattaforma.


1
"L'installazione di numpy può essere un'esperienza non banale a seconda della piattaforma." ... um Cosa? Quali piattaforme è "non banale" per installare numpy? Cosa l'ha reso esattamente non banale?
Giovanni

10
@ John: difficile ottenere un binario a 64 bit per Windows. Difficile ottenere intorpidimento via pipsu Windows.
Ben Bolker,

@Ternak: sì, ma alcuni dei miei studenti usano Windows, quindi devo occuparmi di queste cose.
Ben Bolker,

4
@BenBolker Se devi installare una piattaforma open data science basata su Python, il modo migliore è Anaconda continuum.io/downloads (panda, intorpidimento e altro fuori dalla scatola)
jrovegno

L'installazione di Anaconda è banale
endolite il

14

Usa il decimalmodulo di Python , che fornisce la Decimalclasse.

Dai commenti:

Vale la pena notare che se stai facendo un lavoro pesante in matematica e non hai assolutamente bisogno della precisione dai decimali, questo può davvero impantanare le cose. I galleggianti sono molto, molto più veloci da gestire, ma imprecisi. I decimali sono estremamente precisi ma lenti.


11

Non sono a conoscenza di nulla nella libreria standard di Python (o altrove) che attui la AlmostEqual2sComplementfunzione di Dawson . Se questo è il tipo di comportamento che desideri, dovrai implementarlo da solo. (Nel qual caso, invece di usare gli intelligenti hack bit per bit di Dawson probabilmente faresti meglio a usare test più convenzionali del modulo if abs(a-b) <= eps1*(abs(a)+abs(b)) + eps2o simili. Per ottenere un comportamento simile a Dawson potresti dire qualcosa di simile if abs(a-b) <= eps*max(EPS,abs(a),abs(b))per alcuni piccoli risolti EPS; questo non è esattamente lo stesso di Dawson, ma è simile nello spirito.


Non seguo esattamente quello che stai facendo qui, ma è interessante. Qual è la differenza tra eps, eps1, eps2 ed EPS?
Gordon Wrigley,

eps1e eps2definisci una tolleranza relativa e assoluta: sei pronto a consentire ae ba differenziare per eps1volte quanto sono grandi eps2. epsè una singola tolleranza; sei pronto a consentire ae ba differire in base a epsvolte quanto sono grandi, a condizione che EPSsi presuma che qualsiasi dimensione o dimensione sia inferiore EPS. Se EPSconsideri il più piccolo valore non denormale del tuo tipo a virgola mobile, questo è molto simile al comparatore di Dawson (tranne per un fattore di 2 ^ # bit perché Dawson misura la tolleranza in ulps).
Gareth McCaughan,

2
Per inciso, concordo con S. Lott sul fatto che la cosa giusta dipenderà sempre dall'applicazione reale, motivo per cui non esiste un'unica funzione di libreria standard per tutte le esigenze di confronto in virgola mobile.
Gareth McCaughan,

@ gareth-mccaughan Come si determina il "valore non denormale più piccolo del tipo in virgola mobile" per Python?
Gordon Wrigley,

Questa pagina docs.python.org/tutorial/floatingpoint.html afferma che quasi tutte le implementazioni di Python utilizzano float a doppia precisione IEEE-754 e questa pagina en.wikipedia.org/wiki/IEEE_754-1985 afferma che i numeri normalizzati più vicini allo zero sono ± 2 * * -1022.
Gordon Wrigley,

11

La saggezza comune secondo cui i numeri in virgola mobile non possono essere confrontati per l'uguaglianza è inaccurata. I numeri in virgola mobile non sono diversi dagli interi: se si valuta "a == b", si otterrà vero se si tratta di numeri identici e falso altrimenti (con la consapevolezza che due NaN ovviamente non sono numeri identici).

Il vero problema è questo: se ho fatto alcuni calcoli e non sono sicuro che i due numeri che devo confrontare siano esattamente corretti, allora? Questo problema è lo stesso per il virgola mobile come per gli interi. Se si valuta l'espressione intera "7/3 * 3", non comparerà uguale a "7 * 3/3".

Supponiamo quindi di aver chiesto "Come confrontare i numeri interi per l'uguaglianza?" in una situazione del genere. Non esiste una risposta singola; cosa dovresti fare dipende dalla situazione specifica, in particolare che tipo di errori hai e che cosa vuoi ottenere.

Ecco alcune possibili scelte.

Se si desidera ottenere un risultato "vero" se i numeri matematicamente esatti fossero uguali, è possibile provare a utilizzare le proprietà dei calcoli eseguiti per dimostrare che si ottengono gli stessi errori nei due numeri. Se ciò è fattibile e si confrontano due numeri risultanti da espressioni che darebbero uguali numeri se calcolati esattamente, allora si otterrà "vero" dal confronto. Un altro approccio è che è possibile analizzare le proprietà dei calcoli e dimostrare che l'errore non supera mai un determinato importo, forse un importo assoluto o un importo relativo a uno degli input o uno degli output. In tal caso, puoi chiedere se i due numeri calcolati differiscono al massimo da tale importo e restituire "vero" se rientrano nell'intervallo. Se non riesci a provare un limite di errore, potresti indovinare e sperare per il meglio. Un modo di indovinare è valutare molti campioni casuali e vedere quale tipo di distribuzione ottieni nei risultati.

Naturalmente, poiché impostiamo il requisito di ottenere "vero" solo se i risultati matematicamente esatti sono uguali, abbiamo lasciato aperta la possibilità che tu diventi "vero" anche se sono ineguali. (In effetti, possiamo soddisfare il requisito restituendo sempre "vero". Questo semplifica il calcolo ma è generalmente indesiderabile, quindi discuterò di migliorare la situazione di seguito.)

Se si desidera ottenere un risultato "falso" se i numeri matematicamente esatti non fossero uguali, è necessario dimostrare che la valutazione dei numeri produce numeri diversi se i numeri matematicamente esatti non fossero uguali. Ciò può essere impossibile a scopi pratici in molte situazioni comuni. Quindi consideriamo un'alternativa.

Un requisito utile potrebbe essere quello di ottenere un risultato "falso" se i numeri matematicamente esatti differiscono di più di un certo importo. Ad esempio, forse calcoleremo dove ha viaggiato una palla lanciata in un gioco per computer e vogliamo sapere se ha colpito una mazza. In questo caso, vogliamo certamente diventare "veri" se la palla colpisce la mazza e vogliamo diventare "falsi" se la palla è lontana dalla mazza, e possiamo accettare una risposta "vera" errata se la palla dentro una simulazione matematicamente esatta ha mancato la mazza ma si trova a un millimetro di distanza dalla mazza. In tal caso, dobbiamo dimostrare (o indovinare / stimare) che il nostro calcolo della posizione della palla e della posizione della mazza presenta un errore combinato di massimo un millimetro (per tutte le posizioni di interesse). Questo ci consentirebbe di tornare sempre "

Quindi, come decidere cosa restituire quando si confrontano i numeri in virgola mobile dipende molto dalla situazione specifica.

Per quanto riguarda il modo in cui si provano i limiti di errore per i calcoli, questo può essere un argomento complicato. Qualsiasi implementazione in virgola mobile che utilizza lo standard IEEE 754 in modalità arrotondata al più vicino restituisce il numero in virgola mobile più vicino al risultato esatto per qualsiasi operazione di base (in particolare moltiplicazione, divisione, addizione, sottrazione, radice quadrata). (In caso di pareggio, arrotondare in modo che il bit basso sia uniforme.) (Prestare particolare attenzione alla radice quadrata e alla divisione; l'implementazione del linguaggio potrebbe utilizzare metodi non conformi a IEEE 754 per quelli.) A causa di questo requisito, sappiamo che il l'errore in un singolo risultato è al massimo 1/2 del valore del bit meno significativo. (Se fosse più, l'arrotondamento sarebbe andato a un numero diverso che è entro 1/2 del valore.)

Andare avanti da lì diventa sostanzialmente più complicato; il passaggio successivo sta eseguendo un'operazione in cui uno degli ingressi ha già qualche errore. Per espressioni semplici, questi errori possono essere seguiti attraverso i calcoli per raggiungere un limite all'errore finale. In pratica, ciò viene fatto solo in alcune situazioni, come lavorare in una biblioteca di matematica di alta qualità. E, naturalmente, è necessario un controllo preciso su quali operazioni vengono eseguite. I linguaggi di alto livello spesso danno al compilatore un po 'di gioco, quindi potresti non sapere in quale ordine vengono eseguite le operazioni.

C'è molto di più che potrebbe essere (ed è) scritto su questo argomento, ma devo fermarmi qui. In sintesi, la risposta è: non esiste una routine di libreria per questo confronto perché non esiste un'unica soluzione adatta alla maggior parte delle esigenze che vale la pena inserire in una routine di libreria. (Se il confronto con un intervallo di errore relativo o assoluto è sufficiente per te, puoi farlo semplicemente senza una routine di libreria.)


3
Dalla discussione sopra con Gareth McCaughan, il corretto confronto con un errore relativo equivale essenzialmente a "abs (ab) <= eps max (2 * -1022, abs (a), abs (b))", non è qualcosa che descriverei come semplice e certamente non qualcosa che avrei elaborato da solo. Inoltre, come sottolinea Steve Jessop, è di complessità simile a max, min, any e all, che sono tutti builtin. Pertanto, fornire un confronto relativo degli errori nel modulo matematico standard sembra una buona idea.
Gordon Wrigley,

(7/3 * 3 == 7 * 3/3) valuta True in Python.
xApple

@xApple: ho appena eseguito Python 2.7.2 su OS X 10.8.3 e sono entrato (7/3*3 == 7*3/3). Ha stampato False.
Eric Postpischil,

3
Probabilmente hai dimenticato di digitare from __future__ import division. Se non lo fai, non ci sono numeri in virgola mobile e il confronto è tra due numeri interi.
xApple

3
Questa è una discussione importante, ma non incredibilmente utile.
Dan Hulme,

6

Se vuoi usarlo nel contesto testing / TDD, direi che questo è un modo standard:

from nose.tools import assert_almost_equals

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

5

math.isclose () è stato aggiunto a Python 3.5 per questo ( codice sorgente ). Ecco una porta di esso per Python 2. La differenza rispetto a una delle righe di Mark Ransom è che può gestire correttamente "inf" e "-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

Ho trovato utile il seguente confronto:

str(f1) == str(f2)

è interessante, ma non molto pratico a causa di str (.1 + .2) == .3
Gordon Wrigley,

str (.1 + .2) == str (.3) restituisce True
Henrikh Kantuni

In che modo differisce da f1 == f2 - se sono entrambi vicini ma comunque diversi a causa della precisione, anche le rappresentazioni delle stringhe saranno disuguali.
MrMas

2
.1 + .2 == .3 restituisce False mentre str (.1 + .2) == str (.3) restituisce True
Kresimir

4
In Python 3.7.2, str(.1 + .2) == str(.3)restituisce False. Il metodo sopra descritto funziona solo per Python 2.
Danibix del

1

Per alcuni dei casi in cui è possibile influire sulla rappresentazione del numero di origine, è possibile rappresentarli come frazioni anziché float, utilizzando numeratore e denominatore di numeri interi. In questo modo puoi avere confronti esatti.

Vedi Frazione dal modulo frazioni per i dettagli.


1

Mi è piaciuto il suggerimento di @Sesquipedal ma con modifiche (un caso d'uso speciale quando entrambi i valori sono 0 restituisce False). Nel mio caso ero su Python 2.7 e ho usato una semplice funzione:

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

1

Utile nel caso in cui si desideri assicurarsi che 2 numeri siano uguali "fino alla precisione", non è necessario specificare la tolleranza:

  • Trova la precisione minima dei 2 numeri

  • Arrotondare entrambi per la precisione e il confronto minimi

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)                               

Come scritto, funziona solo per i numeri senza la 'e' nella loro rappresentazione di stringa (che significa 0.999999999999995e-4 <numero <= 0.9999999999995e11)

Esempio:

>>> isclose(10.0,10.049)
True
>>> isclose(10.0,10.05)
False

Il concetto illimitato di chiusura non ti servirà bene. isclose(1.0, 1.1)produce Falsee isclose(0.1, 0.000000000001)restituisce True.
kfsone,

1

Per confrontare fino a un determinato decimale senza 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

Questo forse è un po 'brutto, ma funziona abbastanza bene quando non hai bisogno di più della precisione del float predefinita (circa 11 decimali).

La funzione round_to utilizza il metodo di formato dalla classe str incorporata per arrotondare il float a una stringa che rappresenta il float con il numero di decimali necessari, quindi applica la funzione incorporata eval alla stringa float arrotondata per tornare indietro al tipo numerico float.

La funzione is_close applica solo un semplice condizionale al float arrotondato.

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

Aggiornare:

Come suggerito da @stepehjfox, un modo più pulito per creare una funzione rount_to evitando "eval" sta usando la formattazione nidificata :

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

Seguendo la stessa idea, il codice può essere ancora più semplice usando le nuove fantastiche stringhe f (Python 3.6+):

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

Quindi, potremmo anche concludere tutto in una funzione 'is_close' semplice e pulita :

def is_close(a, b, prec):
    return f'{a:.{prec}f}' == f'{b:.{prec}f}'

1
Non è necessario utilizzare eval()per ottenere la formattazione parametrica. Qualcosa del genere return '{:.{precision}f'.format(float_num, precision=decimal_precision) dovrebbe farlo
Stephenjfox,

1
Fonte per il mio commento e altri esempi: pyformat.info/#param_align
stephenjfox

1
Grazie @stephenjfox non sapevo della formattazione nidificata. A proposito, il tuo codice di esempio manca delle parentesi graffe finali:return '{:.{precision}}f'.format(float_num, precision=decimal_precision)
Albert Alomar,

1
Buona cattura e miglioramento soprattutto ben fatto con le corde ad "f". Con la morte di Python 2 dietro l'angolo, forse questo diventerà la norma
stephenjfox

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.