Annulation catastrophique
Cette leçon aborde l'un des pièges les plus dangereux de l'arithmétique flottante : la perte massive de chiffres significatifs lors de la soustraction de deux nombres proches. Ce phénomène, appelé annulation catastrophique (ou cancellation en anglais), peut transformer un calcul apparemment anodin en résultat complètement faux.
Le problème en une phrase
Quand on soustrait deux nombres presque égaux, les chiffres significatifs de tête s'annulent, et il ne reste que les chiffres de queue — qui sont souvent entachés d'erreurs.
Un exemple qui fait peur
Imaginons que vous mesuriez deux longueurs très précisément :
Vous voulez calculer la différence .
Calcul direct :
Tout semble correct. Mais combien de CSE possède ce résultat ?
Analyse détaillée de la perte de précision
Représentation en notation scientifique
Écrivons les valeurs en notation scientifique normalisée :
La soustraction
Renormalisation
Le résultat n'est pas normalisé. On doit décaler :
Le problème révélé
Regardons ce qui s'est passé visuellement :
U* = 0,83672 × 10²
- V* = 0,83666 × 10²
─────────────────────
0,00006 × 10²
= 0,6 × 10⁻²
Les 4 premiers chiffres (8, 3, 6, 6) se sont annulés !
Il ne reste qu'UN SEUL chiffre : le 6.Nous avions 5 CSE dans chaque opérande, mais le résultat n'en a plus qu'un seul !
Pourquoi cela arrive-t-il ?
L'explication intuitive
Les chiffres de tête de et sont identiques : 8, 3, 6, 6. Ces chiffres représentent l'information « commune » aux deux nombres. Quand on soustrait, cette information commune disparaît.
Ce qui reste, ce sont les chiffres où et diffèrent. Mais ces chiffres de queue sont justement ceux qui portent le plus d'incertitude !
L'explication avec les erreurs
Supposons que les vraies valeurs soient :
Nos mesures et ont chacune une erreur d'environ .
Calcul exact :
Calcul avec nos approximations :
Erreur sur le résultat :
Erreur relative sur le résultat :
Nous sommes passés d'une erreur relative de sur les opérandes à une erreur de 8% sur le résultat !
Règle fondamentale
Lors d'une soustraction de nombres proches, les erreurs relatives s'amplifient considérablement. Plus les nombres sont proches, plus l'amplification est dramatique.
Démonstration par intervalles de confiance
Une façon particulièrement visuelle de comprendre l'annulation catastrophique est de raisonner en termes d'intervalles de confiance.
Le principe
Quand une valeur est connue avec une incertitude, on peut l'écrire sous la forme :
L'intervalle représente toutes les valeurs possibles de .
Exemple numérique détaillé
Prenons deux mesures :
Erreurs relatives initiales :
Les deux grandeurs sont connues avec une excellente précision de 1%.
Calcul de la différence
Calculons .
Valeur centrale :
Intervalle de confiance pour z :
Valeur minimale :
Valeur maximale :
Donc :
L'explosion de l'erreur relative
Erreur absolue sur z :
Erreur relative sur z :
L'erreur relative a été multipliée par 100 en une seule soustraction !
La formule générale
Pour une soustraction , l'erreur relative sur le résultat est :
Cette formule révèle le mécanisme de l'annulation :
- Au numérateur : les erreurs absolues s'additionnent (elles ne se compensent pas)
- Au dénominateur : quand , la différence est petite
Le rapport explose quand le dénominateur tend vers zéro !
Loi de l'amplification
Si et ont la même erreur relative et sont proches (), alors :
Plus et sont proches, plus le facteur d'amplification est grand.
Autre exemple : des nombres très proches
Considérons :
Différence :
Erreur relative :
L'erreur relative de 0,1% est devenue 200% — le résultat est totalement non significatif !
def soustraction_avec_incertitude(x, dx, y, dy):
"""
Calcule z = x - y avec propagation des incertitudes.
Paramètres :
x, dx : valeur et incertitude absolue de x
y, dy : valeur et incertitude absolue de y
Retourne :
z, dz, epsilon_z : résultat, incertitude absolue, erreur relative
"""
z = x - y
dz = dx + dy # Les erreurs s'additionnent pour une soustraction
epsilon_z = dz / abs(z) if z != 0 else float('inf')
return z, dz, epsilon_z
# Exemple 1 : x = 102 ± 1, y = 100 ± 1
print("=== Exemple 1 : nombres assez proches ===")
x, dx = 102, 1
y, dy = 100, 1
z, dz, eps = soustraction_avec_incertitude(x, dx, y, dy)
print(f"x = {x} ± {dx} (erreur relative : {dx/x*100:.1f}%)")
print(f"y = {y} ± {dy} (erreur relative : {dy/y*100:.1f}%)")
print(f"x - y = {z} ± {dz} (erreur relative : {eps*100:.1f}%)")
print(f"Facteur d'amplification : ×{eps / (dx/x):.0f}")
print()
# Exemple 2 : x = 1000 ± 1, y = 999 ± 1
print("=== Exemple 2 : nombres très proches ===")
x, dx = 1000, 1
y, dy = 999, 1
z, dz, eps = soustraction_avec_incertitude(x, dx, y, dy)
print(f"x = {x} ± {dx} (erreur relative : {dx/x*100:.2f}%)")
print(f"y = {y} ± {dy} (erreur relative : {dy/y*100:.2f}%)")
print(f"x - y = {z} ± {dz} (erreur relative : {eps*100:.0f}%)")
print(f"Facteur d'amplification : ×{eps / (dx/x):.0f}")Quantifier la perte de CSE
Formule approximative
Quand on soustrait deux nombres et ayant CSE chacun, le résultat perd environ :
Application à notre exemple
On perd environ 4 CSE. Partant de 5 CSE, il en reste environ 1. C'est cohérent avec notre analyse !
Tableau de la catastrophe
| Rapport U/|U-V| | CSE perdus | Situation |
|---|---|---|
| 10 | 1 | Tolérable |
| 100 | 2 | Préoccupant |
| 1 000 | 3 | Problématique |
| 10 000 | 4 | Grave |
| 1 000 000 | 6 | Catastrophique |
Démonstration en Python
import math
# Exemple 1 : Calcul de √(x+1) - √x pour grand x
def difference_racines_naive(x):
return math.sqrt(x + 1) - math.sqrt(x)
# Testons pour des valeurs croissantes de x
print("=== Annulation catastrophique avec √(x+1) - √x ===
")
print(f"{'x':>15} | {'Résultat naïf':>20} | {'Résultat exact':>20}")
print("-" * 60)
for exp in range(1, 16):
x = 10 ** exp
naif = difference_racines_naive(x)
# Valeur "exacte" par développement limité : 1/(2√x) pour grand x
exact = 1 / (2 * math.sqrt(x))
print(f"{x:>15.0e} | {naif:>20.15f} | {exact:>20.15f}")
print("
Observez : pour x = 10¹⁵, le résultat naïf devient 0 !")Ce code montre que pour de grandes valeurs de , le calcul naïf de donne des résultats de plus en plus faux, jusqu'à retourner exactement 0 — alors que la vraie valeur n'est jamais nulle !
Stratégies d'évitement
Heureusement, il existe des techniques pour contourner l'annulation catastrophique.
Stratégie 1 : Reformulation algébrique
L'idée est de réécrire l'expression mathématique sous une forme équivalente qui évite la soustraction problématique. Voyons un exemple détaillé avec la technique de multiplication par le conjugué.
Exemple : Les racines carrées
Problème : Calculer pour grand .
Forme naïve : Soustraction directe → annulation catastrophique.
Reformulation : On utilise la technique de multiplication par le conjugué.
Qu'est-ce que le conjugué ?
Le conjugué d'une expression de la forme est l'expression (on change le signe du milieu).
| Expression | Son conjugué |
|---|---|
Pourquoi c'est utile ?
Quand on multiplie une expression par son conjugué, on obtient une différence de carrés :
C'est l'identité remarquable que vous connaissez depuis le secondaire !
L'intérêt : si et contiennent des racines carrées, celles-ci disparaissent quand on les met au carré.
Exemple simple :
Les racines ont disparu ! Le résultat est un nombre entier.
L'astuce : multiplier ET diviser
Évidemment, on ne peut pas juste multiplier notre expression par son conjugué — cela changerait sa valeur. L'astuce est de multiplier et diviser par la même chose (ce qui revient à multiplier par 1) :
Application pas à pas
Étape 1 : Identifier le conjugué de .
Le conjugué est .
Étape 2 : Multiplier le numérateur (en utilisant ).
Le numérateur se simplifie en... 1 ! Les racines ont disparu.
Étape 3 : Écrire le résultat final.
Pourquoi cette forme est-elle stable ?
Comparons les deux formes :
| Forme | Opération dangereuse ? | Pour x = 10⁸ |
|---|---|---|
| Oui : soustraction de nombres proches | ||
| Non : division simple |
La nouvelle forme ne contient aucune soustraction de nombres proches ! On divise 1 par une somme de deux grands nombres, ce qui est numériquement stable.
import math
def difference_racines_naive(x):
return math.sqrt(x + 1) - math.sqrt(x)
def difference_racines_stable(x):
# Forme reformulée : 1 / (√(x+1) + √x)
return 1 / (math.sqrt(x + 1) + math.sqrt(x))
print("=== Comparaison des deux méthodes ===
")
print(f"{'x':>12} | {'Naïf':>18} | {'Stable':>18} | {'Erreur naïf':>12}")
print("-" * 70)
for exp in range(1, 16):
x = 10 ** exp
naif = difference_racines_naive(x)
stable = difference_racines_stable(x)
erreur = abs(naif - stable) / stable * 100 if stable != 0 else float('inf')
print(f"{x:>12.0e} | {naif:>18.15f} | {stable:>18.15f} | {erreur:>10.2f} %")Stratégie 2 : Utiliser des séries de Taylor
Quand est petit, on peut remplacer une expression par son développement en série.
Exemple 1 : Calculer pour proche de 0.
Problème : Quand , , donc est une soustraction de nombres proches.
Solution : Utiliser le développement de Taylor :
Donc :
Cette formule ne contient aucune soustraction de nombres proches !
Exemple 2 : Calculer pour proche de 0.
Problème : Quand , on calcule , puis on soustrait 1. Les 10 premiers chiffres s'annulent !
Solution : Utiliser le développement de Taylor :
Donc :
Python fournit directement la fonction math.expm1(x) qui implémente ce calcul stable :
import math
x = 1e-15
# Forme naïve : annulation catastrophique
naif = math.exp(x) - 1
print(f"exp({x}) - 1 (naïf) = {naif}")
# Forme stable : fonction dédiée
stable = math.expm1(x)
print(f"expm1({x}) (stable) = {stable}")
# Erreur relative
erreur = abs(naif - stable) / stable * 100
print(f"Erreur relative du calcul naïf : {erreur:.1f}%")Stratégie 3 : Changer l'ordre des opérations
Parfois, réorganiser l'ordre des calculs peut aider.
Exemple : Calculer la somme où les sont de signes alternés.
Mauvaise approche : Additionner dans l'ordre, ce qui crée des sommes partielles qui oscillent.
Meilleure approche : Séparer les termes positifs et négatifs, sommer chaque groupe, puis faire une seule soustraction finale.
Stratégie 4 : Augmenter la précision temporairement
Si aucune reformulation n'est possible, on peut effectuer le calcul critique en précision supérieure.
from decimal import Decimal, getcontext
# Augmenter la précision à 50 chiffres
getcontext().prec = 50
a = Decimal('83.672')
b = Decimal('83.666')
# Soustraction en haute précision
resultat = a - b
print(f"Résultat en haute précision : {resultat}")
# Convertir en float standard pour la suite des calculs
resultat_float = float(resultat)
print(f"Reconverti en float : {resultat_float}")Récapitulatif : Reconnaître et éviter l'annulation
| Signal d'alarme | Action recommandée |
|---|---|
| Soustraction de nombres du même ordre de grandeur | Chercher une reformulation algébrique |
| Expression avec petit | Utiliser un développement de Taylor |
| Calcul de | Multiplier/diviser par |
| Résultat proche de zéro issu de grands nombres | Augmenter la précision ou reformuler |
À retenir
L'annulation catastrophique ne produit pas de message d'erreur. Le calcul s'exécute normalement et retourne un résultat — qui peut être complètement faux. C'est au programmeur d'anticiper ce risque et de choisir des formulations numériquement stables.
Ce qu'il faut retenir
L'annulation catastrophique survient lors de la soustraction de deux nombres proches. Les chiffres significatifs de tête s'annulent, ne laissant que les chiffres de queue — souvent contaminés par les erreurs d'arrondi.
La parade principale est la reformulation algébrique : trouver une expression mathématiquement équivalente mais numériquement stable. Les identités remarquables, les développements de Taylor et la multiplication par le conjugué sont des outils précieux.
Dans la prochaine leçon, nous verrons comment ces erreurs se propagent à travers les calculs, en utilisant les séries de Taylor pour analyser la propagation d'erreurs dans les fonctions.
Exercices de réflexion
Exercice 1 : Détecter le danger
Pour chacune des expressions suivantes, indiquez si elle présente un risque d'annulation catastrophique et dans quelles conditions :
- quand
- quand
- quand
- quand est grand
Exercice 2 : Reformuler
Proposez une formulation stable pour calculer :
Indice : Utilisez l'identité et l'identité .
Exercice 3 : Expérimentation
Écrivez un programme Python qui :
- Calcule pour
- Compare avec la valeur attendue
- Explique les résultats observés
À partir de quelle valeur de le résultat devient-il complètement faux ?