Quelle est la meilleure façon de comparer les flottants pour une quasi-égalité en Python?

333

Il est bien connu que la comparaison des flottants pour l'égalité est un peu difficile en raison de problèmes d'arrondi et de précision.

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

Quelle est la méthode recommandée pour gérer cela en Python?

Il y a sûrement une fonction de bibliothèque standard pour cela quelque part?

Gordon Wrigley
la source
@tolomea: Comme cela dépend de votre application et de vos données et de votre domaine problématique - et ce n'est qu'une seule ligne de code - pourquoi y aurait-il une "fonction de bibliothèque standard"?
S.Lott
9
@ S. Lott: all, any, max, minsont chacun essentiellement une seule ligne, et ils ne sont pas seulement fournis dans une bibliothèque, ils sont builtin fonctions. Donc, les raisons du BDFL ne sont pas cela. La seule ligne de code que la plupart des gens écrivent est assez simple et ne fonctionne souvent pas, ce qui est une bonne raison de fournir quelque chose de mieux. Bien sûr, tout module fournissant d'autres stratégies devrait également fournir des mises en garde décrivant quand elles sont appropriées, et plus important encore quand elles ne le sont pas. L'analyse numérique est difficile, ce n'est pas une grande honte que les concepteurs de langage n'essaient généralement pas d'outils pour l'aider.
Steve Jessop
@Steve Jessop. Ces fonctions orientées collection n'ont pas les dépendances d'application, de données et de domaine problématique que fait float-point. Donc, le "one-liner" n'est clairement pas aussi important que les vraies raisons. L'analyse numérique est difficile et ne peut pas être une partie de première classe d'une bibliothèque de langues à usage général.
S.Lott
6
@ S.Lott: Je serais probablement d'accord si la distribution Python standard n'était pas fournie avec plusieurs modules pour les interfaces XML. De toute évidence, le fait que différentes applications doivent faire quelque chose de différent n’est pas du tout interdit de placer des modules dans l’ensemble de base pour le faire d’une manière ou d’une autre. Il existe certainement des astuces pour comparer les flotteurs qui sont souvent réutilisés, le plus élémentaire étant un nombre spécifié d'ulps. Je ne suis donc que partiellement d'accord - le problème est que l'analyse numérique est difficile. Python pourrait en principe fournir des outils pour le rendre un peu plus facile, parfois. Je suppose que personne ne s'est porté volontaire.
Steve Jessop
4
En outre, "cela se résume à une ligne de code difficile à concevoir" - si c'est toujours une ligne unique une fois que vous le faites correctement, je pense que votre moniteur est plus large que le mien ;-). Quoi qu'il en soit, je pense que tout le domaine est assez spécialisé, dans le sens où la plupart des programmeurs (y compris moi) l'utilisent très rarement. Combiné avec le fait d'être dur, il ne va pas atteindre le haut de la liste des "bibliothèques les plus recherchées" dans la plupart des langues.
Steve Jessop

Réponses:

326

Python 3.5 ajoute les fonctions math.iscloseetcmath.isclose comme décrit dans PEP 485 .

Si vous utilisez une version antérieure de Python, la fonction équivalente est donnée dans la documentation .

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_tolest une tolérance relative, elle est multipliée par la plus grande des grandeurs des deux arguments; au fur et à mesure que les valeurs deviennent plus grandes, la différence permise entre elles tout en les considérant comme égales.

abs_tolest une tolérance absolue qui est appliquée telle quelle dans tous les cas. Si la différence est inférieure à l'une ou l'autre de ces tolérances, les valeurs sont considérées comme égales.

Mark Ransom
la source
26
noter quand aou best un numpy array, numpy.isclosefonctionne.
dbliss
6
@marsh rel_tolest une tolérance relative , elle est multipliée par la plus grande des grandeurs des deux arguments; au fur et à mesure que les valeurs deviennent plus grandes, la différence permise entre elles tout en les considérant comme égales. abs_tolest une tolérance absolue qui est appliquée telle quelle dans tous les cas. Si la différence est inférieure à l'une ou l'autre de ces tolérances, les valeurs sont considérées comme égales.
Mark Ransom
5
Pour ne pas diminuer la valeur de cette réponse (je pense que c'est une bonne réponse), il convient de noter que la documentation dit également: "Vérification des erreurs Modulo, etc., la fonction retournera le résultat de ..." En d'autres termes, la isclosefonction (ci-dessus) n'est pas une implémentation complète .
rkersh
5
Toutes mes excuses pour avoir fait revivre un vieux fil, mais il nous a semblé utile de souligner qu'il iscloseadhère toujours au critère le moins conservateur. Je ne le mentionne que parce que ce comportement est contre-intuitif pour moi. Si je devais spécifier deux critères, je m'attendrais toujours à ce que la plus petite tolérance remplace la plus grande.
Mackie Messer
3
@MackieMesser, vous avez bien sûr droit à votre opinion, mais ce comportement était parfaitement logique pour moi. Selon votre définition, rien ne pourrait jamais être "proche" de zéro, car une tolérance relative multipliée par zéro est toujours nulle.
Mark Ransom
72

Est-ce que quelque chose d'aussi simple que le suivant n'est pas assez bon?

return abs(f1 - f2) <= allowed_error
Andrew White
la source
8
Comme l'indique le lien que j'ai fourni, la soustraction ne fonctionne que si vous connaissez à l'avance l'ampleur approximative des nombres.
Gordon Wrigley
8
Dans mon expérience, la meilleure méthode pour comparer flotteurs est: abs(f1-f2) < tol*max(abs(f1),abs(f2)). Ce type de tolérance relative est le seul moyen significatif de comparer les flottants en général, car ils sont généralement affectés par une erreur d'arrondi dans les petites décimales.
Sesquipedal
2
Il suffit d'ajouter un exemple simple pour expliquer pourquoi cela peut ne pas fonctionner >>> abs(0.04 - 0.03) <= 0.01:, cela donne False. J'utilisePython 2.7.10 [GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
schatten
3
@schatten pour être juste, cet exemple a plus à voir avec la précision / formats binaires de la machine qu'avec l'algo de comparaison particulier. Lorsque vous avez entré 0,03 dans le système, ce n'est pas vraiment le nombre qui a atteint le CPU.
Andrew White
2
@AndrewWhite cet exemple montre que abs(f1 - f2) <= allowed_errorcela ne fonctionne pas comme prévu.
schatten
45

Je conviens que la réponse de Gareth est probablement la plus appropriée en tant que fonction / solution légère.

Mais j'ai pensé qu'il serait utile de noter que si vous utilisez NumPy ou envisagez de le faire, il existe une fonction packagée pour cela.

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

Un petit avertissement cependant: l'installation de NumPy peut être une expérience non triviale selon votre plate-forme.

J.Makela
la source
1
"L'installation de numpy peut être une expérience non triviale selon votre plateforme." ... euh Quoi? Quelles sont les plates-formes «non triviales» pour installer numpy? Qu'est-ce qui l'a rendu non trivial?
John
10
@John: difficile d'obtenir un binaire 64 bits pour Windows. Difficile d'obtenir numpy via pipWindows.
Ben Bolker
@Ternak: Oui, mais certains de mes étudiants utilisent Windows, donc je dois gérer ce genre de choses.
Ben Bolker
4
@BenBolker Si vous devez installer une plate-forme de science des données ouverte propulsée par Python, le meilleur moyen est Anaconda continuum.io/downloads (pandas, numpy et plus encore)
jrovegno
L'installation d'Anaconda est triviale
endolith
14

Utilisez le decimalmodule de Python , qui fournit la Decimalclasse.

D'après les commentaires:

Il convient de noter que si vous effectuez un travail mathématique et que vous n'avez pas absolument besoin de la précision décimale, cela peut vraiment gâcher les choses. Les flotteurs sont beaucoup plus rapides à gérer, mais imprécis. Les décimales sont extrêmement précises mais lentes.

jathanism
la source
11

Je ne connais rien dans la bibliothèque standard de Python (ou ailleurs) qui implémente la AlmostEqual2sComplementfonction de Dawson . Si c'est le genre de comportement que vous souhaitez, vous devrez l'implémenter vous-même. (Dans ce cas, plutôt que d'utiliser les hacks intelligents au niveau du bit de Dawson, vous feriez probablement mieux d'utiliser des tests plus conventionnels de la forme if abs(a-b) <= eps1*(abs(a)+abs(b)) + eps2ou similaire. Pour obtenir un comportement semblable à Dawson, vous pourriez dire quelque chose comme if abs(a-b) <= eps*max(EPS,abs(a),abs(b))pour certains petits correctifs EPS; ce n'est pas exactement la même chose que Dawson, mais son esprit est similaire.

Gareth McCaughan
la source
Je ne comprends pas très bien ce que vous faites ici, mais c'est intéressant. Quelle est la différence entre eps, eps1, eps2 et EPS?
Gordon Wrigley
eps1et eps2définissez une tolérance relative et absolue: vous êtes prêt à autoriser aet bà différer d'environ eps1fois leur taille, plus eps2. epsest une seule tolérance; vous êtes prêt à autoriser aet bà différer d'environ epsfois leur taille, à condition que tout ce qui est de taille EPSou plus petit soit supposé être de taille EPS. Si vous considérez EPSqu'il s'agit de la plus petite valeur non dénormale de votre type à virgule flottante, cela est très similaire au comparateur de Dawson (à l'exception d'un facteur de 2 ^ # bits car Dawson mesure la tolérance en ulps).
Gareth McCaughan
2
Par ailleurs, je suis d'accord avec S. Lott que la bonne chose dépendra toujours de votre application réelle, c'est pourquoi il n'y a pas une seule fonction de bibliothèque standard pour tous vos besoins de comparaison en virgule flottante.
Gareth McCaughan
@ gareth-mccaughan Comment déterminer la "plus petite valeur non dénormale de votre type à virgule flottante" pour python?
Gordon Wrigley
Cette page docs.python.org/tutorial/floatingpoint.html indique que presque toutes les implémentations python utilisent des flotteurs double précision IEEE-754 et cette page en.wikipedia.org/wiki/IEEE_754-1985 indique que les nombres normalisés les plus proches de zéro sont ± 2 * * −1022.
Gordon Wrigley
11

La sagesse courante selon laquelle les nombres à virgule flottante ne peuvent pas être comparés pour l'égalité est inexacte. Les nombres à virgule flottante ne sont pas différents des entiers: si vous évaluez "a == b", vous obtiendrez vrai si ce sont des nombres identiques et faux sinon (étant entendu que deux NaN ne sont bien sûr pas des nombres identiques).

Le vrai problème est le suivant: si j'ai fait quelques calculs et que je ne suis pas sûr que les deux chiffres que je dois comparer sont exactement corrects, alors quoi? Ce problème est le même pour les virgules flottantes que pour les entiers. Si vous évaluez l'expression entière "7/3 * 3", elle ne sera pas comparable à "7 * 3/3".

Supposons donc que nous ayons demandé "Comment comparer les entiers pour l'égalité?" Dans une telle situation. Il n'y a pas de réponse unique; ce que vous devez faire dépend de la situation spécifique, notamment du type d'erreurs que vous avez et de ce que vous souhaitez obtenir.

Voici quelques choix possibles.

Si vous voulez obtenir un résultat "vrai" si les nombres mathématiquement exacts sont égaux, vous pouvez essayer d'utiliser les propriétés des calculs que vous effectuez pour prouver que vous obtenez les mêmes erreurs dans les deux nombres. Si cela est possible et que vous comparez deux nombres qui résultent d'expressions qui donneraient des nombres égaux s'ils étaient calculés exactement, alors vous obtiendrez «vrai» de la comparaison. Une autre approche consiste à analyser les propriétés des calculs et à prouver que l'erreur ne dépasse jamais un certain montant, peut-être un montant absolu ou un montant relatif à l'une des entrées ou l'une des sorties. Dans ce cas, vous pouvez demander si les deux nombres calculés diffèrent d'au plus ce montant et renvoyer "vrai" s'ils se trouvent dans l'intervalle. Si vous ne pouvez pas prouver une erreur liée, vous pourriez deviner et espérer le meilleur. Une façon de deviner est d'évaluer de nombreux échantillons aléatoires et de voir quel type de distribution vous obtenez dans les résultats.

Bien sûr, puisque nous ne fixons l'exigence que vous devenez "vrai" que si les résultats mathématiquement exacts sont égaux, nous avons laissé ouverte la possibilité que vous obteniez "vrai" même s'ils sont inégaux. (En fait, nous pouvons satisfaire à l'exigence en retournant toujours "vrai". Cela rend le calcul simple mais n'est généralement pas souhaitable, donc je vais discuter de l'amélioration de la situation ci-dessous.)

Si vous voulez obtenir un résultat "faux" si les nombres mathématiquement exacts sont inégaux, vous devez prouver que votre évaluation des nombres donne des nombres différents si les nombres mathématiquement exacts sont inégaux. Cela peut être impossible à des fins pratiques dans de nombreuses situations courantes. Examinons donc une alternative.

Une condition utile pourrait être d'obtenir un résultat "faux" si les nombres mathématiquement exacts diffèrent de plus d'un certain montant. Par exemple, nous allons peut-être calculer où une balle lancée dans un jeu vidéo a voyagé, et nous voulons savoir si elle a frappé une batte. Dans ce cas, nous voulons certainement obtenir "vrai" si la balle frappe la batte, et nous voulons obtenir "faux" si la balle est loin de la batte, et nous pouvons accepter une réponse "vraie" incorrecte si la balle dans une simulation mathématiquement exacte a raté la chauve-souris, mais se situe à un millimètre près de la frappe. Dans ce cas, nous devons prouver (ou deviner / estimer) que notre calcul de la position de la balle et de la position de la batte a une erreur combinée d'au plus un millimètre (pour toutes les positions d'intérêt). Cela nous permettrait de toujours revenir "

Ainsi, la façon dont vous décidez quoi retourner lorsque vous comparez des nombres à virgule flottante dépend beaucoup de votre situation spécifique.

Quant à la façon dont vous allez prouver les limites d'erreur pour les calculs, cela peut être un sujet compliqué. Toute implémentation en virgule flottante utilisant la norme IEEE 754 en mode arrondi au plus proche renvoie le nombre à virgule flottante le plus proche du résultat exact pour toute opération de base (notamment multiplication, division, addition, soustraction, racine carrée). (En cas d'égalité, arrondissez pour que le bit faible soit égal.) (Faites particulièrement attention à la racine carrée et à la division; l'implémentation de votre langage peut utiliser des méthodes qui ne sont pas conformes à IEEE 754 pour celles-ci.) En raison de cette exigence, nous connaissons la l'erreur dans un seul résultat représente au plus la moitié de la valeur du bit le moins significatif. (Si c'était plus, l'arrondi serait allé à un nombre différent qui est dans la moitié de la valeur.)

Partir de là devient beaucoup plus compliqué; l'étape suivante exécute une opération où l'une des entrées a déjà une erreur. Pour les expressions simples, ces erreurs peuvent être suivies à travers les calculs pour atteindre une limite sur l'erreur finale. En pratique, cela ne se fait que dans quelques situations, comme le travail sur une bibliothèque de mathématiques de haute qualité. Et, bien sûr, vous avez besoin d'un contrôle précis sur exactement quelles opérations sont effectuées. Les langages de haut niveau donnent souvent beaucoup de mou au compilateur, vous ne savez donc peut-être pas dans quel ordre les opérations sont effectuées.

Il y a beaucoup plus qui pourrait être (et est) écrit sur ce sujet, mais je dois m'arrêter là. En résumé, la réponse est: il n'y a pas de routine de bibliothèque pour cette comparaison, car il n'y a pas de solution unique qui réponde à la plupart des besoins qui mérite d'être mise dans une routine de bibliothèque. (Si la comparaison avec un intervalle d'erreur relatif ou absolu vous suffit, vous pouvez le faire simplement sans routine de bibliothèque.)

Eric Postpischil
la source
3
D'après la discussion ci-dessus avec Gareth McCaughan, une comparaison correcte avec une erreur relative équivaut essentiellement à "abs (ab) <= eps max (2 * -1022, abs (a), abs (b))", ce n'est pas quelque chose que je décrirais aussi simple et certainement pas quelque chose que j'aurais élaboré par moi-même. De plus, comme le souligne Steve Jessop, il est d'une complexité similaire à max, min, any et all, qui sont tous intégrés. Donc, fournir une comparaison d'erreur relative dans le module mathématique standard semble être une bonne idée.
Gordon Wrigley
(7/3 * 3 == 7 * 3/3) évalue True en python.
xApple
@xApple: Je viens d'exécuter Python 2.7.2 sur OS X 10.8.3 et je suis entré (7/3*3 == 7*3/3). Il a imprimé False.
Eric Postpischil
3
Vous avez probablement oublié de taper from __future__ import division. Si vous ne le faites pas, il n'y a pas de nombres à virgule flottante et la comparaison se fait entre deux entiers.
xApple
3
Il s'agit d'une discussion importante, mais pas incroyablement utile.
Dan Hulme
6

Si vous voulez l'utiliser dans le contexte testing / TDD, je dirais que c'est une manière standard:

from nose.tools import assert_almost_equals

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

math.isclose () a été ajouté à Python 3.5 pour cela ( code source ). Voici un portage de celui-ci vers Python 2. Sa différence avec une ligne de Mark Ransom est qu'il peut gérer "inf" et "-inf" correctement.

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
la source
2

J'ai trouvé la comparaison suivante utile:

str(f1) == str(f2)
Kresimir
la source
c'est intéressant, mais pas très pratique à cause de str (.1 + .2) == .3
Gordon Wrigley
str (.1 + .2) == str (.3) renvoie True
Henrikh Kantuni
En quoi est-ce différent de f1 == f2 - s'ils sont tous deux proches mais toujours différents en raison de la précision, les représentations de chaîne seront également inégales.
MrMas
2
.1 + .2 == .3 renvoie False tandis que str (.1 + .2) == str (.3) renvoie True
Kresimir
4
Dans Python 3.7.2, str(.1 + .2) == str(.3)renvoie False. La méthode décrite ci-dessus ne fonctionne que pour Python 2.
Danibix
1

Dans certains cas où vous pouvez affecter la représentation du numéro source, vous pouvez les représenter sous forme de fractions au lieu de flottants, en utilisant un numérateur et un dénominateur entiers. De cette façon, vous pouvez avoir des comparaisons exactes.

Voir le module Fraction de fractions pour plus de détails.

eis
la source
1

J'ai aimé la suggestion de @Sesquipedal mais avec des modifications (un cas d'utilisation spécial lorsque les deux valeurs sont 0 renvoie False). Dans mon cas, j'étais sur Python 2.7 et j'utilisais juste une fonction simple:

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

Utile dans le cas où vous voulez vous assurer que 2 nombres sont identiques 'jusqu'à la précision', pas besoin de spécifier la tolérance:

  • Trouver la précision minimale des 2 nombres

  • Arrondissez les deux à une précision minimale et comparez

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)                               

Tel qu'il est écrit, ne fonctionne que pour les nombres sans le «e» dans leur représentation sous forme de chaîne (ce qui signifie 0,9999999999995e-4 <nombre <= 0,9999999999995e11)

Exemple:

>>> isclose(10.0,10.049)
True
>>> isclose(10.0,10.05)
False
CptHwK
la source
Le concept illimité de fermeture ne vous servira pas bien. isclose(1.0, 1.1)produit Falseet isclose(0.1, 0.000000000001)retourne True.
kfsone
1

Pour comparer jusqu'à une décimale donnée sans 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
la source
1

C'est peut-être un hack un peu moche, mais cela fonctionne assez bien lorsque vous n'avez pas besoin de plus que la précision de flottement par défaut (environ 11 décimales).

La fonction round_to utilise la méthode de format de la classe str intégrée pour arrondir le flottant à une chaîne qui représente le flottant avec le nombre de décimales nécessaires, puis applique la fonction intégrée eval à la chaîne flottante arrondie pour revenir au type numérique flottant.

La fonction is_close applique simplement une conditionnelle simple au flottant arrondi.

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

Mettre à jour:

Comme suggéré par @stepehjfox, un moyen plus propre de construire une fonction rount_to en évitant "eval" utilise le formatage imbriqué :

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

En suivant la même idée, le code peut être encore plus simple en utilisant les nouvelles grandes chaînes f (Python 3.6+):

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

Donc, nous pourrions même tout résumer dans une fonction simple et propre 'is_close' :

def is_close(a, b, prec):
    return f'{a:.{prec}f}' == f'{b:.{prec}f}'
Albert Alomar
la source
1
Vous n'avez pas besoin d'utiliser eval()pour obtenir une mise en forme paramétrée. Quelque chose comme return '{:.{precision}f'.format(float_num, precision=decimal_precision) ça devrait le faire
stephenjfox
1
Source pour mon commentaire et d' autres exemples: pyformat.info/#param_align
stephenjfox
1
Merci @stephenjfox Je ne connaissais pas le formatage imbriqué. Btw, votre exemple de code n'a pas les accolades finales:return '{:.{precision}}f'.format(float_num, precision=decimal_precision)
Albert Alomar
1
Bonne prise, et surtout une amélioration bien faite avec les cordes f. Avec la mort de Python 2 au coin de la rue, cela deviendra peut-être la norme
stephenjfox
0

En termes d'erreur absolue, vous pouvez simplement vérifier

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

Quelques informations sur les raisons pour lesquelles les flotteurs agissent bizarrement en Python https://youtu.be/v4HhvoNLILk?t=1129

Vous pouvez également utiliser math.isclose pour les erreurs relatives

Rahul Sharma
la source