Fonction pour déterminer si deux nombres sont presque égaux lorsqu'ils sont arrondis à n chiffres décimaux significatifs
On m'a demandé de tester une bibliothèque fournie par un tiers. La bibliothèque est connue pour être précise à n chiffres significatifs. Toutes les erreurs moins significatives peuvent être ignorées en toute sécurité. Je veux écrire une fonction pour m'aider à comparer les résultats:
def nearlyequal( a, b, sigfig=5 ):
Le but de cette fonction est de déterminer si deux nombres à virgule flottante (a et b) sont approximativement égaux. La fonction retournera True si A = = b (correspondance exacte) ou si a et b ont la même valeur lorsqu'ils sont arrondis à sigfig significatif-chiffres lorsqu'ils sont écrits en décimal.
Quelqu'un peut-il Suggérer une bonne implémentation? J'ai écrit un mini-test unitaire. Sauf si vous pouvez voir un bug dans mes tests, une bonne implémentation devrait passer ce qui suit:
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 )
Notes complémentaires:
- les valeurs a et b peuvent être de type int, float ou numpy.float64. Les valeurs a et b seront toujours du même type. Il est vital que la conversion n'introduise pas d'erreur supplémentaire dans la fonction.
- gardons ça numérique, donc les fonctions qui se convertissent en chaînes ou utilisent des astuces non mathématiques ne sont pas idéales. Ce programme sera vérifié par quelqu'un qui est un mathématicien qui veulent être en mesure de prouver que la fonction est ce qu'il est censé faire.
- Vitesse... Je dois comparer beaucoup de chiffres pour que le plus vite soit le mieux.
- j'ai numpy, scipy et la bibliothèque standard. Toute autre chose sera difficile pour moi d'obtenir, surtout pour une si petite partie du projet.
11 réponses
Il y a une fonction assert_approx_equal
dans numpy.testing
(source ici) qui peut être un bon point de départ.
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.
Depuis Python 3.5, la manière standard de le faire (en utilisant la bibliothèque standard) est avec le math.isclose
fonction.
, Il a la signature suivante:
isclose(a, b, rel_tol=1e-9, abs_tol=0.0)
Un exemple d'utilisation avec tolérance d'erreur absolue:
from math import isclose
a = 1.0
b = 1.00000001
assert isclose(a, b, abs_tol=1e-8)
Si vous le voulez avec précision de n chiffres significatifs, remplacez simplement la dernière ligne par:
assert isclose(a, b, abs_tol=10**-n)
Voici une prise.
def nearly_equal(a,b,sig_fig=5):
return ( a==b or
int(a*10**sig_fig) == int(b*10**sig_fig)
)
Je crois que votre question n'est pas assez bien définie, et les tests unitaires que vous présentez le prouvent:
Si par 'arrondir à N décimales sig-fig' vous voulez dire 'n décimales à droite du point décimal', alors le test assert nearlyequal(1e9, 1e9 + 1 , 5)
devrait échouer, car même lorsque vous arrondissez la précision 1000000000 et 1000000001 à 0.00001, ils sont toujours différents.
Et si par "arrondir à N décimales sig-fig" vous voulez dire "les N chiffres les plus significatifs, indépendamment de la virgule décimale", alors le test {[1] } devrait échouer, car 0.000000001 et -0.000000001 sont totalement différents lorsqu'ils sont vus de cette façon.
Si vous vouliez dire la première définition, alors la première réponse sur cette page (par triptyque) est bonne. Si vous vouliez dire la deuxième définition, dites-la, je promets d'y réfléchir: -)
Il y a déjà beaucoup de bonnes réponses, mais voici une réflexion:
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!"
"chiffres significatifs" en décimal est une question d'ajuster le point décimal et de tronquer à un entier.
>>> int(3.1415926 * 10**3)
3141
>>> int(1234567 * 10**-3)
1234
>>>
C'est un problème assez commun avec les nombres à virgule flottante. Je le résous sur la base de la discussion dans la Section 1.5 de Demmel[1]. (1) Calculez l'erreur d'arrondi. (2) Vérifiez que l'erreur d'arrondi est inférieure à certains epsilon. Je n'ai pas utilisé python depuis un certain temps et j'ai seulement la version 2.4.3, mais je vais essayer d'obtenir ceci correct.
Étape 1. Erreur d'arrondi
def roundoff_error(exact, approximate):
return abs(approximate/exact - 1.0)
Étape 2. Égalité en virgule flottante
def float_equal(float1, float2, epsilon=2.0e-9):
return (roundoff_error(float1, float2) < epsilon)
Il y a quelques lacunes évidentes avec cela code.
- Division par erreur zéro si la valeur exacte est zéro.
- ne vérifie pas que les arguments sont des valeurs à virgule flottante.
Révision 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)
C'est un peu mieux. Si la valeur exacte ou approximative est zéro, l'erreur est égale à la valeur de l'autre. Si quelque chose d'autre qu'une valeur à virgule flottante est fourni, un TypeError est déclenché.
, À ce stade, la seule chose difficile est de définir la valeur correcte pour Epsilon. J'ai remarqué dans la documentation de la version 2.6.1 qu'il existe un attribut Epsilon dans sys.float_info, donc j'utiliserais deux fois cette valeur comme epsilon par défaut. Mais la valeur correcte dépend à la fois de votre application et de votre algorithme.
[1] James W. Demmel, algèbre linéaire numérique appliquée , SIAM, 1997.
Oren Shemesh a une partie du problème avec le problème comme indiqué mais il y a plus:
Affirmer nearlyequal( 0.0, 1e-15, 5 )
Échoue également la deuxième définition (et c'est la définition que j'ai apprise à l'école.)
Peu importe le nombre de chiffres que vous regardez, 0 ne sera pas égal à zéro. Cela pourrait s'avérer être un casse-tête pour de tels tests si vous avez un cas dont la bonne réponse est zéro.
Il y a une solution intéressante à cela par B. Dawson (avec du code C++ ) à "Comparaison des nombres à virgule flottante" . Son approche repose sur la représentation IEEE stricte de deux nombres et l'ordre lexicographique imposé lorsque lesdits nombres sont représentés sous forme d'entiers non signés.
Il y a beaucoup de façons de comparer deux nombres pour voir s'ils acceptent N chiffres significatifs. Grosso modo, vous voulez juste vous assurer que leur différence est inférieure à 10^ - N fois la plus grande des deux nombres comparés. C'est assez facile.
Mais, Que faire si l'un des nombres est zéro? Tout le concept de différences relatives ou de chiffres significatifs tombe lorsqu'on compare avec zéro. Pour gérer ce cas, vous devez également avoir une différence absolue, ce qui devrait être spécifié différemment de la différence relative.
Je discute des problèmes de comparaison des nombres à virgule flottante-y compris un cas spécifique de manipulation de zéro-dans cet article de blog:
Http://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/
On m'a demandé de tester une bibliothèque fournie par un tiers
Si vous utilisez la valeur par défaut de Python unittest
cadre, vous pouvez utiliser assertAlmostEqual
self.assertAlmostEqual(a, b, places=5)