Choisir un seuil d’alerte avec Isolation Forest : calibration et faux positifs

Comparaison de trois seuils en calibration puis distribution indépendante des scores de test.

Un score Isolation Forest ne suffit pas à décider combien d’alertes examiner. Dans ce cas pratique, nous comparons trois seuils, choisissons celui qui respecte un budget de faux positifs sur un lot de calibration, puis l’évaluons sur un test gardé à part. L’objectif est de rendre ce choix explicite et reproductible.

Le guide Isolation Forest explique les scores et le fonctionnement du modèle. Ici, la question est plus précise : avec des exemples étiquetés, quel compromis accepter entre anomalies retrouvées et fausses alertes ? Les données sont entièrement synthétiques ; une étiquette « anomalie » ne représente ni une fraude ni une panne réelle.

Exécuté le 18 septembre 2026 avec Python 3.12.14, scikit-learn 1.9.1 et NumPy 2.5.3. Télécharger gratuitement le code, les données et les tests.

Trois lots, trois rôles

Lot Composition Utilisation autorisée
Apprentissage 1 200 lignes normales synthétiques Construire les arbres, sans utiliser les étiquettes des autres lots.
Calibration 800 normales et 100 anomalies synthétiques Comparer les seuils et appliquer la règle de choix.
Test final 800 normales et 100 anomalies synthétiques Mesurer une fois le modèle et le seuil figés.

Les trois lots sont des tirages distincts. Les anomalies se superposent partiellement aux observations normales : nous ne construisons pas une démonstration où tout se sépare parfaitement. Les proportions choisies servent au cours ; elles ne prétendent pas représenter un fichier réel.

La calibration suppose ici des étiquettes fiables. Sans elles, on peut organiser un budget d’examen ou classer des scores, mais on ne peut pas annoncer le rappel ni le taux de faux positifs réel. Dans un historique temporel, il faut aussi respecter les dates et la disponibilité des informations.

Définir les chiffres avant de choisir le seuil

  • TP : anomalies signalées correctement.
  • FP : lignes normales signalées à tort.
  • FN : anomalies non signalées.
  • TN : lignes normales non signalées.

La précision vaut TP / (TP + FP) : quelle part des alertes correspond aux anomalies définies dans le générateur ? Le rappel vaut TP / (TP + FN) : quelle part de ces anomalies est retrouvée ? Le taux de faux positifs vaut FP / (FP + TN) et porte sur les lignes normales, pas sur toutes les lignes ni sur les seules alertes.

Nous fixons avant le test cette règle pédagogique : parmi trois seuils candidats, retenir le meilleur rappel avec au plus 5 % de faux positifs en calibration. Ce choix de 5 % n’est pas une recommandation métier. Dans une application réelle, le coût d’examen, celui d’une anomalie manquée et le nombre de lignes doivent guider le budget.

Le code complet

Dans un environnement Python dédié, installez numpy==2.5.3 et scikit-learn==1.9.1. Enregistrez le script sous seuil_isolation.py, puis exécutez python seuil_isolation.py. L’archive inclut aussi un README et les tests à lancer avec python -m unittest test_seuil -v.

"""Choisir le seuil sur calibration ; réserver le test à l'évaluation finale."""
import numpy as np
from sklearn.ensemble import IsolationForest
from sklearn.metrics import confusion_matrix


def lot(rng, normaux, anomalies):
    X = np.vstack((rng.normal(0, 1, (normaux, 2)),
                   rng.normal(2, 1.1, (anomalies, 2))))
    y = np.r_[np.zeros(normaux, dtype=int), np.ones(anomalies, dtype=int)]
    ordre = rng.permutation(len(y))
    return X[ordre], y[ordre]


def bilan(y, scores, seuil):
    tn, fp, fn, tp = confusion_matrix(y, scores >= seuil, labels=[0, 1]).ravel()
    return dict(TP=int(tp), FP=int(fp), FN=int(fn), TN=int(tn),
                precision=float(tp / (tp + fp)) if tp + fp else 0.0,
                rappel=float(tp / (tp + fn)) if tp + fn else 0.0,
                faux_positifs=float(fp / (tn + fp)) if tn + fp else 0.0)


def experience():
    rng = np.random.default_rng(42)
    X_train = rng.normal(0, 1, (1200, 2))
    X_cal, y_cal = lot(rng, 800, 100)
    X_test, y_test = lot(rng, 800, 100)
    modele = IsolationForest(n_estimators=200, contamination="auto",
                            random_state=42, n_jobs=1).fit(X_train)
    # Un score positif plus grand signifie ici une observation plus inhabituelle.
    scores_cal = -modele.score_samples(X_cal)
    essais = []
    for quantile in (0.90, 0.95, 0.98):
        seuil = float(np.quantile(scores_cal[y_cal == 0], quantile, method="higher"))
        essais.append(dict(quantile=quantile, seuil=seuil, **bilan(y_cal, scores_cal, seuil)))
    # Règle fixée avant d'ouvrir le test : rappel maximal sous 5 % de FP en calibration.
    admissibles = [e for e in essais if e["faux_positifs"] <= 0.05]
    choix = max(admissibles, key=lambda e: (e["rappel"], e["seuil"]))
    scores_test = -modele.score_samples(X_test)
    resultat = bilan(y_test, scores_test, choix["seuil"])
    return dict(modele=modele, X_train=X_train, X_cal=X_cal, y_cal=y_cal,
                X_test=X_test, y_test=y_test, scores_cal=scores_cal,
                scores_test=scores_test, essais=essais, choix=choix, test=resultat)


if __name__ == "__main__":
    r = experience()
    print("Calibration : 800 lignes normales et 100 anomalies synthétiques")
    for e in r["essais"]:
        print(f"q={e['quantile']:.2f} seuil={e['seuil']:.6f} "
              f"FP={e['FP']} TP={e['TP']} précision={e['precision']:.3f} "
              f"rappel={e['rappel']:.3f} taux FP={e['faux_positifs']:.3f}")
    print(f"Seuil retenu : {r['choix']['seuil']:.6f}")
    t = r["test"]
    print(f"Test final : TP={t['TP']} FP={t['FP']} FN={t['FN']} TN={t['TN']}")
    print(f"Précision={t['precision']:.3f} rappel={t['rappel']:.3f} taux FP={t['faux_positifs']:.3f}")

La documentation de scikit-learn précise le sens de score_samples. Nous changeons son signe : une valeur plus grande devient plus inhabituelle. Ce nombre reste un score, pas une probabilité. Le seuil est appliqué explicitement ; ce script n’utilise pas predict pour choisir ses alertes.

Les seuils sont les 90e, 95e et 98e percentiles des scores des lignes normales de calibration. Le choix de method="higher" et la comparaison >= rendent la convention explicite. Des scores égaux au seuil peuvent modifier le nombre d’alertes : il faut toujours compter les FP obtenus, pas seulement déduire un pourcentage du percentile. Sur d’autres données, si aucun candidat ne respecte le budget, il faut le signaler et revoir les candidats sur calibration, sans consulter le test pour choisir.

Les résultats de la calibration et du test

Calibration : 800 lignes normales et 100 anomalies synthétiques
q=0.90 seuil=0.546648 FP=80 TP=80 précision=0.500 rappel=0.800 taux FP=0.100
q=0.95 seuil=0.591254 FP=40 TP=73 précision=0.646 rappel=0.730 taux FP=0.050
q=0.98 seuil=0.633768 FP=16 TP=64 précision=0.800 rappel=0.640 taux FP=0.020
Seuil retenu : 0.591254
Test final : TP=73 FP=44 FN=27 TN=756
Précision=0.624 rappel=0.730 taux FP=0.055
Trois seuils comparés sur calibration, puis scores normaux et anormaux du test indépendant.
À gauche, un seuil plus strict réduit les fausses alertes et laisse aussi davantage d’anomalies non détectées. À droite, le seuil est déjà fixé. Cliquez sur le graphique pour l’agrandir.

Le seuil à environ 0,591254 est retenu sur calibration : 40 faux positifs sur 800 normales et 73 anomalies retrouvées sur 100. Le seuil plus strict retrouve seulement 64 anomalies ; le seuil plus souple produit 80 faux positifs et dépasse notre budget.

Sur le test indépendant, le seuil retenu produit 117 alertes : 73 vraies et 44 fausses. La précision est donc 73 / 117, soit environ 62,4 %, le rappel 73 % et le taux de faux positifs 44 / 800, soit 5,5 %. Il manque 27 anomalies. Ces quatre lectures décrivent des conséquences différentes du même réglage.

Pourquoi 5 % devient 5,5 % sur le test

La contrainte a été vérifiée sur un échantillon de calibration, pas sur toutes les observations futures. Les nouveaux tirages varient, même sans changement du générateur. Le résultat de 5,5 % n’autorise pas à déplacer le seuil puis à présenter ce même test comme indépendant. Si la procédure doit évoluer, conserver cette évaluation comme un constat et prévoir une nouvelle évaluation indépendante.

La précision dépend aussi de la fréquence des anomalies. Notre test en contient 100 sur 900. Un flux où les anomalies sont beaucoup plus rares peut contenir une plus grande part de fausses alertes, même avec un taux de faux positifs similaire. Évitez de transposer 62,4 % à un autre fichier.

Deux exercices pour prolonger le cas

  1. Sur la calibration seulement, remplacez le budget de 5 % par 2 %. Quel candidat devient admissible et combien d’anomalies supplémentaires manque-t-il ?
  2. Avec le résultat de test affiché, calculez la part de toutes les lignes qui déclenchent une alerte. Pourquoi ce chiffre diffère-t-il du taux de faux positifs ?
Voir les réponses

À 2 %, le 98e percentile respecte le budget : 16 FP sur 800 normales, 64 TP, soit neuf anomalies de moins que le candidat à 5 % en calibration. Sur le test déjà figé, 117 / 900 vaut 13 %, alors que le taux FP vaut 44 / 800 = 5,5 %. Les numérateurs et les populations ne sont pas les mêmes.

Passer à un CSV en conservant les identifiants

Le projet CSV Isolation Forest montre comment charger un fichier et exporter les lignes à examiner. La page de ressources Python regroupe les téléchargements et la suite de l’apprentissage. Les exemples de cette page fonctionnent sans achat et ne déclenchent aucune suppression automatique de données.

Parcours de lecture : 2 — Distinguer une alerte d’un groupe

Après avoir appris à fixer un seuil, changez de question : cherchez-vous plutôt des familles d’observations similaires ? Le guide HDBSCAN montre le regroupement par densité. Un groupe et une alerte répondent à des besoins différents ; ce passage de lecture ne transforme pas les labels de clusters en vérité métier.