Détection d’anomalies#

Ce qui est normal est ce qui est familier ; ce qui est anormal, c’est ce qui ne ressemble à rien de connu.

Adapté librement de Hawkins (1980)

La détection d’anomalies (anomaly detection, outlier detection) est l’un des problèmes les plus anciens et les plus importants de l’analyse de données. L’idée fondamentale est simple : parmi un ensemble d’observations, certaines se distinguent significativement de la majorité. Ces observations atypiques — les anomalies — peuvent résulter d’erreurs de mesure, de comportements frauduleux, de pannes imminentes, ou simplement de phénomènes rares mais authentiques. Identifier ces anomalies est crucial dans de nombreux domaines, de la détection de fraude bancaire à la cybersécurité, en passant par la maintenance prédictive et le contrôle qualité industriel.

Ce chapitre, qui ouvre la Partie III — Apprentissage non supervisé, présente les principales méthodes de détection d’anomalies, des approches statistiques classiques aux algorithmes modernes d’apprentissage automatique. Nous aborderons les fondements théoriques de chaque méthode, puis les illustrerons avec des implémentations pratiques en Python.

Hide code cell source

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from matplotlib.patches import Ellipse
from scipy import stats
from sklearn.ensemble import IsolationForest
from sklearn.neighbors import LocalOutlierFactor
from sklearn.svm import OneClassSVM
from sklearn.covariance import EllipticEnvelope
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import (
    classification_report, confusion_matrix, precision_recall_curve,
    average_precision_score, roc_auc_score, f1_score
)
from sklearn.datasets import make_blobs, make_moons

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
np.random.seed(42)

Introduction#

Anomalies et nouveautés#

Avant toute chose, il convient de distinguer deux problèmes voisins mais distincts.

Définition 167 (Anomalie)

Une anomalie (outlier) est une observation qui dévie tellement des autres observations qu’elle fait naître le soupçon qu’elle a été générée par un mécanisme différent. Plus formellement, si les données normales sont issues d’une distribution \(P_{\text{normal}}\), une anomalie \(\mathbf{x}^*\) est une observation telle que

\[P_{\text{normal}}(\mathbf{x}^*) < \tau\]

pour un certain seuil \(\tau > 0\) suffisamment petit.

Définition 168 (Détection de nouveautés)

La détection de nouveautés (novelty detection) consiste à identifier, parmi de nouvelles observations, celles qui n’appartiennent pas à la distribution apprise sur un jeu d’entraînement supposé propre (sans anomalies). C’est un problème semi-supervisé : on dispose d’un ensemble d’entraînement constitué uniquement d’observations normales.

Remarque 149

En détection d’anomalies, le jeu d’entraînement contient potentiellement des anomalies que l’on cherche à identifier. En détection de nouveautés, le jeu d’entraînement est propre et l’on cherche à détecter les anomalies parmi les nouvelles observations uniquement. Scikit-learn distingue ces deux cas dans sa documentation et dans le paramètre novelty de certains estimateurs.

Types d’anomalies#

Les anomalies se déclinent en trois grandes catégories.

Définition 169 (Types d’anomalies)

  1. Anomalie ponctuelle (point anomaly) : une observation individuelle qui est anormale par rapport au reste des données. Exemple : une transaction bancaire d’un montant anormalement élevé.

  2. Anomalie contextuelle (contextual anomaly) : une observation qui n’est anormale que dans un contexte donné. Exemple : une température de 35°C est normale en été mais anormale en hiver.

  3. Anomalie collective (collective anomaly) : un ensemble d’observations qui, prises individuellement, ne sont pas nécessairement anormales, mais dont la cooccurrence est anormale. Exemple : une séquence inhabituelle d’accès réseau qui individuellement sont banals.

Applications#

La détection d’anomalies est omniprésente dans les applications réelles :

  • Détection de fraude : identifier les transactions frauduleuses parmi des millions de transactions légitimes.

  • Cybersécurité : détecter les intrusions réseau, les comportements suspects d’utilisateurs ou de processus.

  • Maintenance prédictive : repérer les signaux précurseurs de pannes dans les données de capteurs industriels.

  • Contrôle qualité : identifier les produits défectueux sur une ligne de production.

  • Santé : détecter des anomalies dans des signaux physiologiques (ECG, EEG) ou des résultats d’analyses.

Hide code cell source

# Illustration : données normales et anomalies
rng = np.random.RandomState(42)
n_normal = 300
n_anomalies = 15

# Données normales : deux clusters gaussiens
X_normal = np.vstack([
    rng.randn(n_normal // 2, 2) * 0.8 + np.array([2, 2]),
    rng.randn(n_normal // 2, 2) * 0.6 + np.array([-1, -1])
])
# Anomalies : dispersées uniformément
X_anomalies = rng.uniform(low=-5, high=6, size=(n_anomalies, 2))

fig, ax = plt.subplots(figsize=(8, 6))
ax.scatter(X_normal[:, 0], X_normal[:, 1], c='steelblue', s=30,
           alpha=0.6, edgecolors='k', linewidths=0.3, label='Normal')
ax.scatter(X_anomalies[:, 0], X_anomalies[:, 1], c='crimson', s=80,
           marker='X', edgecolors='k', linewidths=0.5, label='Anomalie')
ax.set_xlabel("$x_1$")
ax.set_ylabel("$x_2$")
ax.set_title("Données normales et anomalies ponctuelles")
ax.legend()
plt.tight_layout()
plt.show()
_images/ae09de8b49ef69b03cc4b9c2ec9a5558d29bc2088c2bb09c188ba1a96219fd96.png

Approches statistiques#

Les approches statistiques constituent le socle historique de la détection d’anomalies. Elles reposent sur l’hypothèse que les données normales suivent une distribution connue, et identifient comme anomalies les observations dont la probabilité est faible sous cette distribution.

Test de Grubbs#

Définition 170 (Test de Grubbs)

Le test de Grubbs (1969) permet de détecter une seule anomalie dans un échantillon univarié supposé gaussien. La statistique de test est

\[G = \frac{\max_{i=1,\ldots,n} |x_i - \bar{x}|}{s}\]

\(\bar{x}\) est la moyenne et \(s\) l’écart-type de l’échantillon. Sous l’hypothèse nulle \(H_0\) : « il n’y a pas d’anomalie », \(G\) suit une distribution dérivée de la loi de Student. On rejette \(H_0\) si

\[G > \frac{n-1}{\sqrt{n}} \sqrt{\frac{t_{\alpha/(2n),\, n-2}^2}{n - 2 + t_{\alpha/(2n),\, n-2}^2}}\]

\(t_{\alpha/(2n),\, n-2}\) est le quantile de la loi de Student à \(n-2\) degrés de liberté.

Z-score et IQR#

Définition 171 (Z-score)

Le Z-score d’une observation \(x_i\) dans un échantillon univarié est

\[z_i = \frac{x_i - \bar{x}}{s}\]

Une observation est considérée comme anomalie si \(|z_i| > \theta\), avec typiquement \(\theta = 3\) (règle des trois sigmas). Le Z-score modifié utilise la médiane et l’écart absolu médian (MAD) pour plus de robustesse :

\[z_i^{\text{mod}} = \frac{0{,}6745\,(x_i - \tilde{x})}{\text{MAD}}\]

\(\tilde{x}\) est la médiane et \(\text{MAD} = \text{median}(|x_i - \tilde{x}|)\).

Définition 172 (Méthode IQR)

La méthode de l’intervalle interquartile (IQR) définit les anomalies comme les observations situées en dehors des « barrières » :

\[\text{anomalie si } x_i < Q_1 - k \cdot \text{IQR} \quad \text{ou} \quad x_i > Q_3 + k \cdot \text{IQR}\]

\(Q_1\) et \(Q_3\) sont les premier et troisième quartiles, \(\text{IQR} = Q_3 - Q_1\), et \(k = 1{,}5\) (anomalies modérées) ou \(k = 3\) (anomalies extrêmes).

Hide code cell source

# Démonstration : Z-score et IQR sur des données univariées
rng = np.random.RandomState(42)
data_normal = rng.randn(200) * 2 + 10
data_outliers = np.array([25, 28, -2, 30])
data = np.concatenate([data_normal, data_outliers])

# Z-score
mean, std = data.mean(), data.std()
z_scores = np.abs((data - mean) / std)
z_anomalies = z_scores > 3

# IQR
Q1, Q3 = np.percentile(data, [25, 75])
IQR = Q3 - Q1
iqr_lower = Q1 - 1.5 * IQR
iqr_upper = Q3 + 1.5 * IQR
iqr_anomalies = (data < iqr_lower) | (data > iqr_upper)

fig, axes = plt.subplots(2, 1, figsize=(9, 9))

# Z-score
ax = axes[0]
ax.scatter(range(len(data)), data, c=np.where(z_anomalies, 'crimson', 'steelblue'),
           s=20, alpha=0.7, edgecolors='k', linewidths=0.3)
ax.axhline(mean + 3 * std, color='orange', linestyle='--', label='$\\bar{x} \\pm 3s$')
ax.axhline(mean - 3 * std, color='orange', linestyle='--')
ax.set_title(f"Z-score ($|z| > 3$) : {z_anomalies.sum()} anomalies")
ax.set_xlabel("Index")
ax.set_ylabel("Valeur")
ax.legend()

# IQR
ax = axes[1]
ax.scatter(range(len(data)), data, c=np.where(iqr_anomalies, 'crimson', 'steelblue'),
           s=20, alpha=0.7, edgecolors='k', linewidths=0.3)
ax.axhline(iqr_upper, color='orange', linestyle='--', label='Barrières IQR')
ax.axhline(iqr_lower, color='orange', linestyle='--')
ax.set_title(f"IQR ($k=1.5$) : {iqr_anomalies.sum()} anomalies")
ax.set_xlabel("Index")
ax.set_ylabel("Valeur")
ax.legend()

plt.tight_layout()
plt.show()
_images/d0fb5f724aa630f7e9fe9773e31a741127c2196cd3daa35bd6ed07ac95c58995.png

Distance de Mahalanobis#

Pour les données multivariées, le Z-score se généralise naturellement à la distance de Mahalanobis.

Définition 173 (Distance de Mahalanobis)

Soit \(\mathbf{x} \in \mathbb{R}^d\) une observation, \(\boldsymbol{\mu}\) le vecteur moyen et \(\boldsymbol{\Sigma}\) la matrice de covariance d’un ensemble de données. La distance de Mahalanobis de \(\mathbf{x}\) au centre de la distribution est

\[D_M(\mathbf{x}) = \sqrt{(\mathbf{x} - \boldsymbol{\mu})^\top \boldsymbol{\Sigma}^{-1} (\mathbf{x} - \boldsymbol{\mu})}\]

Si les données suivent une loi normale multivariée, alors \(D_M^2(\mathbf{x})\) suit une loi \(\chi^2\) à \(d\) degrés de liberté.

Remarque 150

La distance de Mahalanobis tient compte de la corrélation entre les variables, contrairement à la distance euclidienne. Pour des données non corrélées de variance unitaire, elle se réduit à la distance euclidienne au centre. En pratique, l’estimation de \(\boldsymbol{\Sigma}\) peut être instable en haute dimension, ce qui motive l’utilisation de méthodes robustes comme le Minimum Covariance Determinant (MCD), que nous verrons plus loin avec l’Elliptic Envelope.

Hide code cell source

# Distance de Mahalanobis en 2D
rng = np.random.RandomState(42)
n = 300
mean = [2, 3]
cov = [[2, 1.5], [1.5, 3]]
X_gauss = rng.multivariate_normal(mean, cov, n)
X_out = np.array([[8, 1], [-3, 8], [7, 8], [-2, -2]])
X_all = np.vstack([X_gauss, X_out])

# Calcul de la distance de Mahalanobis
mu = X_gauss.mean(axis=0)
Sigma = np.cov(X_gauss.T)
Sigma_inv = np.linalg.inv(Sigma)

def mahalanobis(x, mu, Sigma_inv):
    diff = x - mu
    return np.sqrt(diff @ Sigma_inv @ diff)

d_mahal = np.array([mahalanobis(x, mu, Sigma_inv) for x in X_all])
seuil = np.sqrt(stats.chi2.ppf(0.975, df=2))

fig, ax = plt.subplots(figsize=(8, 6))
colors = np.where(d_mahal > seuil, 'crimson', 'steelblue')
sizes = np.where(d_mahal > seuil, 80, 20)
ax.scatter(X_all[:, 0], X_all[:, 1], c=colors, s=sizes, alpha=0.7,
           edgecolors='k', linewidths=0.3)

# Ellipse de confiance à 97.5%
eigenvalues, eigenvectors = np.linalg.eigh(Sigma)
angle = np.degrees(np.arctan2(eigenvectors[1, 1], eigenvectors[0, 1]))
width, height = 2 * seuil * np.sqrt(eigenvalues)
ellipse = Ellipse(xy=mu, width=width, height=height, angle=angle,
                  facecolor='none', edgecolor='orange', linewidth=2, linestyle='--')
ax.add_patch(ellipse)

ax.set_xlabel("$x_1$")
ax.set_ylabel("$x_2$")
ax.set_title("Distance de Mahalanobis et ellipse de confiance à 97,5 %")
plt.tight_layout()
plt.show()
_images/9aa2b7406611c6bfe693f821c7e4e474367af4ce134bd5cef4a54897368e85b9.png

Remarque 151

Les approches statistiques classiques souffrent de la malédiction de la dimensionnalité. En haute dimension, l’estimation de la matrice de covariance devient instable, les distances perdent leur pouvoir discriminant, et la notion même de « point aberrant » se dilue. C’est pourquoi des méthodes algorithmiques plus sophistiquées ont été développées, comme celles que nous allons maintenant étudier.

Isolation Forest#

L”Isolation Forest (Liu et al., 2008) repose sur une intuition élégante : plutôt que de modéliser les données normales et de chercher les points qui n’y appartiennent pas, on cherche directement à isoler chaque observation. L’idée clé est que les anomalies, étant rares et différentes, sont plus faciles à isoler que les observations normales.

Principe d’isolation#

Définition 174 (Isolation Tree)

Un arbre d’isolation (isolation tree, iTree) est un arbre binaire construit récursivement de la manière suivante :

  1. Choisir aléatoirement une feature \(q\) parmi les \(d\) features.

  2. Choisir aléatoirement un seuil de coupure \(p\) uniformément entre \(\min(x_q)\) et \(\max(x_q)\).

  3. Diviser les données en deux sous-ensembles selon que \(x_q < p\) ou \(x_q \geq p\).

  4. Répéter récursivement jusqu’à ce que chaque feuille contienne une seule observation ou que la profondeur maximale soit atteinte.

Proposition 44 (Score d’anomalie de l’Isolation Forest)

Soit \(h(\mathbf{x})\) la profondeur (nombre d’arêtes de la racine à la feuille) à laquelle l’observation \(\mathbf{x}\) est isolée dans un arbre d’isolation, et \(E[h(\mathbf{x})]\) la profondeur moyenne sur un ensemble de \(T\) arbres. Le score d’anomalie est défini comme

\[s(\mathbf{x}, n) = 2^{-E[h(\mathbf{x})]/c(n)}\]

\(c(n) = 2H(n-1) - 2(n-1)/n\) est la profondeur moyenne d’un arbre de recherche binaire (BST) avec \(n\) observations, et \(H(k) = \ln(k) + \gamma\) est le \(k\)-ième nombre harmonique (\(\gamma \approx 0{,}5772\) est la constante d’Euler-Mascheroni).

Le score \(s\) vérifie :

  • \(s \to 1\) quand \(E[h(\mathbf{x})] \to 0\) : l’observation est très rapidement isolée \(\Rightarrow\) anomalie.

  • \(s \to 0{,}5\) quand \(E[h(\mathbf{x})] \to c(n)\) : profondeur moyenne \(\Rightarrow\) observation normale.

  • \(s \to 0\) quand \(E[h(\mathbf{x})] \to n-1\) : l’observation est très difficile à isoler.

Remarque 152

L’Isolation Forest ne nécessite pas d’hypothèse sur la distribution des données et fonctionne bien en haute dimension grâce à la sélection aléatoire des features. Sa complexité temporelle est \(\mathcal{O}(T \cdot n \log n)\) pour l’entraînement, ce qui le rend efficace sur de grands jeux de données.

Implémentation avec Scikit-learn#

Hide code cell source

# Isolation Forest avec Scikit-learn
rng = np.random.RandomState(42)
n_samples = 500
n_outliers = 25

# Données normales + anomalies
X_train = 0.3 * rng.randn(n_samples, 2)
X_train = np.vstack([X_train, rng.uniform(low=-4, high=4, size=(n_outliers, 2))])

# Ajustement de l'Isolation Forest
clf_if = IsolationForest(n_estimators=100, contamination=n_outliers / (n_samples + n_outliers),
                         random_state=42)
y_pred_if = clf_if.fit_predict(X_train)  # -1 = anomalie, 1 = normal

# Visualisation
xx, yy = np.meshgrid(np.linspace(-5, 5, 200), np.linspace(-5, 5, 200))
Z_if = clf_if.decision_function(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)

fig, ax = plt.subplots(figsize=(8, 6))
ax.contourf(xx, yy, Z_if, levels=np.linspace(Z_if.min(), Z_if.max(), 30),
            cmap='Blues_r', alpha=0.5)
ax.contour(xx, yy, Z_if, levels=[0], linewidths=2, colors='orange')
ax.scatter(X_train[y_pred_if == 1, 0], X_train[y_pred_if == 1, 1],
           c='steelblue', s=20, edgecolors='k', linewidths=0.3, label='Normal')
ax.scatter(X_train[y_pred_if == -1, 0], X_train[y_pred_if == -1, 1],
           c='crimson', s=60, marker='X', edgecolors='k', linewidths=0.5, label='Anomalie')
ax.set_title("Isolation Forest — frontière de décision")
ax.set_xlabel("$x_1$")
ax.set_ylabel("$x_2$")
ax.legend()
plt.tight_layout()
plt.show()
_images/249fa849c712db6160ac68d2eb74988248bc01a263bc8594a5136f7ad1219b70.png

Remarque 153

Le paramètre contamination de IsolationForest spécifie la proportion attendue d’anomalies dans le jeu de données. Il détermine le seuil appliqué au score d’anomalie pour la classification binaire. Si cette proportion est inconnue, on peut utiliser contamination="auto" (valeur par défaut), qui utilise le seuil théorique du papier original.

Local Outlier Factor (LOF)#

Le Local Outlier Factor (Breunig et al., 2000) adopte une approche fondamentalement différente : plutôt que de mesurer la distance globale d’un point aux autres, il compare la densité locale d’un point à celle de ses voisins. Un point qui se trouve dans une région de faible densité par rapport à ses voisins est considéré comme une anomalie.

Densité locale et k-distance#

Définition 175 (\(k\)-distance et \(k\)-voisinage)

Soit \(k \in \mathbb{N}^*\) et \(\mathbf{x}\) une observation. La \(k\)-distance de \(\mathbf{x}\), notée \(d_k(\mathbf{x})\), est la distance entre \(\mathbf{x}\) et son \(k\)-ième plus proche voisin. Le \(k\)-voisinage de \(\mathbf{x}\) est l’ensemble

\[N_k(\mathbf{x}) = \{\mathbf{x}' \in \mathcal{D} : d(\mathbf{x}, \mathbf{x}') \leq d_k(\mathbf{x})\}\]

\(d(\cdot, \cdot)\) est une métrique (typiquement euclidienne). Notons que \(|N_k(\mathbf{x})| \geq k\) car plusieurs points peuvent se trouver à la même distance.

Définition 176 (Distance d’atteignabilité)

La distance d’atteignabilité (reachability distance) de \(\mathbf{x}\) par rapport à \(\mathbf{x}'\) est

\[d_{\text{reach},k}(\mathbf{x}, \mathbf{x}') = \max\big(d_k(\mathbf{x}'), \, d(\mathbf{x}, \mathbf{x}')\big)\]

Cette distance lisse l’effet des fluctuations statistiques en « repoussant » les voisins très proches à une distance minimale \(d_k(\mathbf{x}')\).

Définition 177 (Densité d’atteignabilité locale)

La densité d’atteignabilité locale (local reachability density) de \(\mathbf{x}\) est

\[\text{lrd}_k(\mathbf{x}) = \left(\frac{1}{|N_k(\mathbf{x})|} \sum_{\mathbf{x}' \in N_k(\mathbf{x})} d_{\text{reach},k}(\mathbf{x}, \mathbf{x}')\right)^{-1}\]

C’est l’inverse de la distance d’atteignabilité moyenne aux \(k\) plus proches voisins. Une densité élevée signifie que le point est dans une zone dense.

Définition 178 (Local Outlier Factor)

Le Local Outlier Factor de \(\mathbf{x}\) est

\[\text{LOF}_k(\mathbf{x}) = \frac{1}{|N_k(\mathbf{x})|} \sum_{\mathbf{x}' \in N_k(\mathbf{x})} \frac{\text{lrd}_k(\mathbf{x}')}{\text{lrd}_k(\mathbf{x})}\]

Le LOF compare la densité locale de \(\mathbf{x}\) à celle de ses voisins :

  • \(\text{LOF}_k(\mathbf{x}) \approx 1\) : densité similaire aux voisins \(\Rightarrow\) normal.

  • \(\text{LOF}_k(\mathbf{x}) \gg 1\) : densité beaucoup plus faible que les voisins \(\Rightarrow\) anomalie.

Remarque 154

Le LOF est une mesure locale : un point peut avoir un LOF élevé même s’il est plus dense qu’un point d’un autre cluster, tant qu’il est significativement moins dense que ses propres voisins. C’est cette propriété qui permet au LOF de détecter des anomalies dans des données à densités variables, là où les méthodes globales échouent.

Implémentation avec Scikit-learn#

Hide code cell source

# Local Outlier Factor avec Scikit-learn
rng = np.random.RandomState(42)

# Deux clusters de densités différentes + anomalies
X_dense = 0.3 * rng.randn(200, 2) + np.array([2, 2])
X_sparse = rng.randn(100, 2) + np.array([-3, -3])
X_outliers = rng.uniform(low=-6, high=6, size=(20, 2))
X_lof = np.vstack([X_dense, X_sparse, X_outliers])

# Ajustement du LOF
clf_lof = LocalOutlierFactor(n_neighbors=20, contamination=0.06)
y_pred_lof = clf_lof.fit_predict(X_lof)
lof_scores = -clf_lof.negative_outlier_factor_  # plus c'est grand, plus c'est anomal

fig, axes = plt.subplots(2, 1, figsize=(9, 9))

# Prédictions
ax = axes[0]
ax.scatter(X_lof[y_pred_lof == 1, 0], X_lof[y_pred_lof == 1, 1],
           c='steelblue', s=20, edgecolors='k', linewidths=0.3, label='Normal')
ax.scatter(X_lof[y_pred_lof == -1, 0], X_lof[y_pred_lof == -1, 1],
           c='crimson', s=60, marker='X', edgecolors='k', linewidths=0.5, label='Anomalie')
ax.set_title("LOF — prédictions ($k=20$)")
ax.set_xlabel("$x_1$")
ax.set_ylabel("$x_2$")
ax.legend()

# Scores LOF
ax = axes[1]
scatter = ax.scatter(X_lof[:, 0], X_lof[:, 1], c=lof_scores, cmap='YlOrRd',
                     s=30, edgecolors='k', linewidths=0.3)
plt.colorbar(scatter, ax=ax, label='Score LOF')
ax.set_title("LOF — scores d'anomalie")
ax.set_xlabel("$x_1$")
ax.set_ylabel("$x_2$")

plt.tight_layout()
plt.show()
_images/afbaa0deb1f5c3818cbe4dd0af767d9774216b3811c9dad342ad902faffbdf1d.png

Remarque 155

Le choix du paramètre \(k\) (nombre de voisins) est critique pour le LOF. Un \(k\) trop petit rend l’algorithme sensible au bruit, tandis qu’un \(k\) trop grand peut masquer les anomalies locales. En pratique, on teste souvent plusieurs valeurs de \(k\) (typiquement entre 10 et 50) et on agrège les résultats, ou on utilise la validation visuelle.

One-Class SVM#

Le One-Class SVM (Schölkopf et al., 2001) adapte l’idée des machines à vecteurs de support au cadre non supervisé. L’objectif est de trouver un hyperplan dans l’espace des features qui sépare les données de l”origine avec une marge maximale. Les observations qui se trouvent du mauvais côté de l’hyperplan sont classées comme anomalies.

Définition 179 (One-Class SVM)

Étant donné un noyau \(K(\cdot, \cdot)\) et une application de features associée \(\phi : \mathbb{R}^d \to \mathcal{H}\) (espace de Hilbert à noyau reproduisant), le One-Class SVM résout le problème d’optimisation suivant :

\[\min_{\mathbf{w}, \boldsymbol{\xi}, \rho} \quad \frac{1}{2}\|\mathbf{w}\|^2 + \frac{1}{\nu n} \sum_{i=1}^n \xi_i - \rho\]

sous les contraintes

\[\mathbf{w} \cdot \phi(\mathbf{x}_i) \geq \rho - \xi_i, \quad \xi_i \geq 0, \quad i = 1, \ldots, n\]

\(\nu \in (0, 1]\) est un hyperparamètre qui joue un double rôle :

  • borne supérieure sur la fraction d’anomalies (observations en dehors de la frontière) ;

  • borne inférieure sur la fraction de vecteurs de support.

Proposition 45 (Fonction de décision du One-Class SVM)

La fonction de décision du One-Class SVM est

\[f(\mathbf{x}) = \text{sgn}\left(\sum_{i \in \text{SV}} \alpha_i K(\mathbf{x}_i, \mathbf{x}) - \rho\right)\]

où les \(\alpha_i\) sont les coefficients de Lagrange et SV désigne l’ensemble des vecteurs de support. \(f(\mathbf{x}) = +1\) pour les observations normales et \(f(\mathbf{x}) = -1\) pour les anomalies. Avec un noyau RBF \(K(\mathbf{x}, \mathbf{x}') = \exp(-\gamma\|\mathbf{x} - \mathbf{x}'\|^2)\), la frontière de décision est une surface de niveau dans l’espace d’entrée.

Hide code cell source

# One-Class SVM avec noyau RBF
rng = np.random.RandomState(42)
X_train_oc = 0.5 * rng.randn(300, 2)
X_outliers_oc = rng.uniform(low=-4, high=4, size=(20, 2))
X_all_oc = np.vstack([X_train_oc, X_outliers_oc])

clf_ocsvm = OneClassSVM(kernel='rbf', gamma='scale', nu=0.05)
clf_ocsvm.fit(X_train_oc)
y_pred_ocsvm = clf_ocsvm.predict(X_all_oc)

# Frontière de décision
xx, yy = np.meshgrid(np.linspace(-5, 5, 200), np.linspace(-5, 5, 200))
Z_ocsvm = clf_ocsvm.decision_function(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)

fig, ax = plt.subplots(figsize=(8, 6))
ax.contourf(xx, yy, Z_ocsvm, levels=np.linspace(Z_ocsvm.min(), 0, 15),
            cmap='Blues_r', alpha=0.4)
ax.contour(xx, yy, Z_ocsvm, levels=[0], linewidths=2, colors='orange')
ax.contourf(xx, yy, Z_ocsvm, levels=[0, Z_ocsvm.max()], colors=['lightyellow'],
            alpha=0.3)
ax.scatter(X_all_oc[y_pred_ocsvm == 1, 0], X_all_oc[y_pred_ocsvm == 1, 1],
           c='steelblue', s=20, edgecolors='k', linewidths=0.3, label='Normal')
ax.scatter(X_all_oc[y_pred_ocsvm == -1, 0], X_all_oc[y_pred_ocsvm == -1, 1],
           c='crimson', s=60, marker='X', edgecolors='k', linewidths=0.5, label='Anomalie')
ax.set_title("One-Class SVM — frontière de décision (noyau RBF)")
ax.set_xlabel("$x_1$")
ax.set_ylabel("$x_2$")
ax.legend()
plt.tight_layout()
plt.show()
_images/d8beb27c35c17146025fe6ec5b5cea22c8526f9b5332088a21b33e0b178408d9.png

Remarque 156

Le paramètre \(\nu\) du One-Class SVM est directement interprétable : il correspond approximativement à la proportion d’anomalies attendue. Cependant, le One-Class SVM est sensible au choix du noyau et de ses hyperparamètres (notamment \(\gamma\) pour le noyau RBF). De plus, sa complexité en \(\mathcal{O}(n^2)\) à \(\mathcal{O}(n^3)\) le rend coûteux sur de grands jeux de données.

Elliptic Envelope#

L”Elliptic Envelope suppose que les données normales sont issues d’une distribution gaussienne multivariée et identifie les anomalies comme les observations qui se trouvent en dehors d’une ellipse de confiance.

Définition 180 (Elliptic Envelope)

L”Elliptic Envelope estime les paramètres \((\boldsymbol{\mu}, \boldsymbol{\Sigma})\) d’une distribution gaussienne multivariée et utilise la distance de Mahalanobis comme score d’anomalie. L’estimation est rendue robuste par l’algorithme du Minimum Covariance Determinant (MCD) : on cherche le sous-ensemble de \(h\) observations (avec \(h \leq n\)) dont la matrice de covariance empirique a le plus petit déterminant.

Proposition 46 (Minimum Covariance Determinant)

L’estimateur MCD de Rousseeuw (1984) résout

\[(\hat{\boldsymbol{\mu}}_{\text{MCD}}, \hat{\boldsymbol{\Sigma}}_{\text{MCD}}) = \arg\min_{\boldsymbol{\mu}, \boldsymbol{\Sigma}} \det(\boldsymbol{\Sigma})\]

sous la contrainte que \((\boldsymbol{\mu}, \boldsymbol{\Sigma})\) sont la moyenne et la covariance d’un sous-ensemble \(\mathcal{H} \subseteq \mathcal{D}\) de taille \(|\mathcal{H}| = h\), avec \(h = \lceil (n + d + 1) / 2 \rceil\) par défaut. Cet estimateur a un point de rupture (breakdown point) de \((n - h) / n\), ce qui signifie qu’il reste fiable même si jusqu’à \((n - h)\) observations sont des anomalies.

Hide code cell source

# Elliptic Envelope
rng = np.random.RandomState(42)
X_gauss_ee = rng.multivariate_normal([0, 0], [[2, 1], [1, 2]], 250)
X_out_ee = rng.uniform(low=-6, high=6, size=(15, 2))
X_ee = np.vstack([X_gauss_ee, X_out_ee])

clf_ee = EllipticEnvelope(contamination=0.06, random_state=42)
y_pred_ee = clf_ee.fit_predict(X_ee)

xx, yy = np.meshgrid(np.linspace(-7, 7, 200), np.linspace(-7, 7, 200))
Z_ee = clf_ee.decision_function(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)

fig, ax = plt.subplots(figsize=(8, 6))
ax.contourf(xx, yy, Z_ee, levels=np.linspace(Z_ee.min(), Z_ee.max(), 30),
            cmap='Blues_r', alpha=0.4)
ax.contour(xx, yy, Z_ee, levels=[0], linewidths=2, colors='orange')
ax.scatter(X_ee[y_pred_ee == 1, 0], X_ee[y_pred_ee == 1, 1],
           c='steelblue', s=20, edgecolors='k', linewidths=0.3, label='Normal')
ax.scatter(X_ee[y_pred_ee == -1, 0], X_ee[y_pred_ee == -1, 1],
           c='crimson', s=60, marker='X', edgecolors='k', linewidths=0.5, label='Anomalie')
ax.set_title("Elliptic Envelope — estimation robuste (MCD)")
ax.set_xlabel("$x_1$")
ax.set_ylabel("$x_2$")
ax.legend()
plt.tight_layout()
plt.show()
_images/c5bdcee29eb67d8de38ee4c19246deb9c9927cf45c8ced7daf278f4c8df8dd56.png

Remarque 157

L’Elliptic Envelope est une méthode simple et interprétable, mais elle repose sur l’hypothèse de normalité des données. Elle fonctionne bien lorsque les données normales sont approximativement unimodales et elliptiques, mais elle échoue sur des distributions multimodales ou non gaussiennes. Dans ce cas, les méthodes précédentes (Isolation Forest, LOF) sont préférables.

Autoencodeurs pour la détection d’anomalies#

Les autoencodeurs offrent une approche par apprentissage profond pour la détection d’anomalies. L’idée repose sur un principe simple : un autoencodeur entraîné sur des données normales apprendra à bien reconstruire ces données, mais échouera à reconstruire les anomalies, produisant une erreur de reconstruction élevée.

Définition 181 (Autoencodeur pour la détection d’anomalies)

Un autoencodeur est un réseau de neurones composé d’un encodeur \(f_\theta : \mathbb{R}^d \to \mathbb{R}^p\) (avec \(p \ll d\)) et d’un décodeur \(g_\phi : \mathbb{R}^p \to \mathbb{R}^d\), entraîné pour minimiser l’erreur de reconstruction :

\[\mathcal{L}(\theta, \phi) = \frac{1}{n} \sum_{i=1}^n \|\mathbf{x}_i - g_\phi(f_\theta(\mathbf{x}_i))\|^2\]

Pour la détection d’anomalies, on définit le score d’anomalie d’une observation comme son erreur de reconstruction :

\[\text{score}(\mathbf{x}) = \|\mathbf{x} - g_\phi(f_\theta(\mathbf{x}))\|^2\]

Une observation est classée comme anomalie si \(\text{score}(\mathbf{x}) > \tau\) pour un seuil \(\tau\) choisi, par exemple, comme le quantile à 95 % ou 99 % des erreurs de reconstruction sur l’ensemble d’entraînement.

Remarque 158

L’intuition est la suivante : le goulot d’étranglement (bottleneck) de dimension \(p \ll d\) force l’autoencodeur à apprendre une représentation compacte des données normales. Les anomalies, qui ne partagent pas les mêmes structures que les données normales, sont mal représentées dans cet espace latent et donc mal reconstruites. Les variantes (autoencodeurs variationnels, autoencodeurs débruiteurs) peuvent améliorer les performances. Nous reviendrons en détail sur les architectures d’autoencodeurs au chapitre 21.

Hide code cell source

# Autoencodeur simple pour la détection d'anomalies (avec Scikit-learn MLPRegressor)
from sklearn.neural_network import MLPRegressor

rng = np.random.RandomState(42)
# Données normales en 10 dimensions (structure de faible rang)
n_train = 500
latent = rng.randn(n_train, 2)
W = rng.randn(2, 10)
X_ae_train = latent @ W + 0.1 * rng.randn(n_train, 10)

# Anomalies : bruit aléatoire sans structure
n_test_normal = 100
n_test_anomaly = 20
X_test_normal = rng.randn(n_test_normal, 2) @ W + 0.1 * rng.randn(n_test_normal, 10)
X_test_anomaly = 3 * rng.randn(n_test_anomaly, 10)

# Standardisation
scaler = StandardScaler()
X_ae_train_sc = scaler.fit_transform(X_ae_train)
X_test_normal_sc = scaler.transform(X_test_normal)
X_test_anomaly_sc = scaler.transform(X_test_anomaly)

# Entraîner un autoencodeur (réseau qui reconstruit son entrée)
ae = MLPRegressor(hidden_layer_sizes=(64, 2, 64), activation='relu',
                  max_iter=500, random_state=42, learning_rate_init=0.001)
ae.fit(X_ae_train_sc, X_ae_train_sc)

# Erreur de reconstruction
def reconstruction_error(model, X):
    X_pred = model.predict(X)
    return np.mean((X - X_pred) ** 2, axis=1)

err_normal = reconstruction_error(ae, X_test_normal_sc)
err_anomaly = reconstruction_error(ae, X_test_anomaly_sc)

fig, ax = plt.subplots(figsize=(9, 5))
ax.hist(err_normal, bins=25, alpha=0.7, color='steelblue', label='Normal', density=True)
ax.hist(err_anomaly, bins=15, alpha=0.7, color='crimson', label='Anomalie', density=True)
seuil_ae = np.percentile(
    reconstruction_error(ae, X_ae_train_sc), 95
)
ax.axvline(seuil_ae, color='orange', linestyle='--', linewidth=2,
           label=f'Seuil (95e perc.) = {seuil_ae:.3f}')
ax.set_xlabel("Erreur de reconstruction (MSE)")
ax.set_ylabel("Densité")
ax.set_title("Autoencodeur — distribution des erreurs de reconstruction")
ax.legend()
plt.tight_layout()
plt.show()
_images/cb9a365b513dedd45b24e4e9a82d31a3eef088e738cdf446fea296c94b1701ec.png

Comparaison et recommandations#

Le choix de la méthode de détection d’anomalies dépend de plusieurs facteurs : la dimensionnalité des données, la nature de la distribution, la proportion estimée d’anomalies, et les contraintes computationnelles.

Proposition 47 (Critères de choix)

Les principaux critères pour choisir une méthode de détection d’anomalies sont :

  1. Dimensionnalité : les méthodes statistiques classiques (Z-score, Mahalanobis, Elliptic Envelope) perdent en efficacité en haute dimension. L’Isolation Forest et les autoencodeurs sont mieux adaptés.

  2. Distribution : l’Elliptic Envelope suppose la normalité ; le LOF et l’Isolation Forest sont non paramétriques.

  3. Taille du jeu de données : le One-Class SVM est coûteux pour \(n\) grand ; l’Isolation Forest passe bien à l’échelle.

  4. Interprétabilité : les méthodes statistiques et l’Elliptic Envelope sont les plus interprétables ; l’Isolation Forest offre un score intuitif.

  5. Détection locale vs globale : le LOF excelle pour les anomalies locales ; les autres méthodes sont plutôt globales.

Hide code cell source

# Tableau récapitulatif
recap = pd.DataFrame({
    'Méthode': ['Z-score / IQR', 'Mahalanobis', 'Isolation Forest',
                'LOF', 'One-Class SVM', 'Elliptic Envelope', 'Autoencodeur'],
    'Type': ['Statistique', 'Statistique', 'Arbre', 'Densité',
             'Marge', 'Statistique', 'Réseau de neurones'],
    'Hypothèses': ['Gaussien (univarié)', 'Gaussien multiv.',
                   'Aucune', 'Aucune', 'Aucune',
                   'Gaussien multiv.', 'Aucune'],
    'Haute dim.': ['Non', 'Non', 'Oui', 'Modéré',
                   'Oui (noyau)', 'Non', 'Oui'],
    'Grand n': ['Oui', 'Oui', 'Oui', 'Modéré',
                'Non', 'Oui', 'Oui (GPU)'],
    'Détection locale': ['Non', 'Non', 'Partielle', 'Oui',
                         'Partielle', 'Non', 'Non'],
    'Interprétabilité': ['Élevée', 'Élevée', 'Moyenne',
                         'Moyenne', 'Faible', 'Élevée', 'Faible']
})

print(recap.to_string(index=False))
          Méthode               Type          Hypothèses  Haute dim.   Grand n Détection locale Interprétabilité
    Z-score / IQR        Statistique Gaussien (univarié)         Non       Oui              Non           Élevée
      Mahalanobis        Statistique    Gaussien multiv.         Non       Oui              Non           Élevée
 Isolation Forest              Arbre              Aucune         Oui       Oui        Partielle          Moyenne
              LOF            Densité              Aucune      Modéré    Modéré              Oui          Moyenne
    One-Class SVM              Marge              Aucune Oui (noyau)       Non        Partielle           Faible
Elliptic Envelope        Statistique    Gaussien multiv.         Non       Oui              Non           Élevée
     Autoencodeur Réseau de neurones              Aucune         Oui Oui (GPU)              Non           Faible

Exemple 13 (Guide de choix rapide)

  • Données tabulaires, faible dimension, distribution gaussienne \(\Rightarrow\) Elliptic Envelope ou Mahalanobis.

  • Données tabulaires, haute dimension, grand volume \(\Rightarrow\) Isolation Forest.

  • Données à densités variables, clusters de tailles différentes \(\Rightarrow\) LOF.

  • Détection de nouveautés avec un jeu d’entraînement propre \(\Rightarrow\) One-Class SVM ou Isolation Forest.

  • Données de très haute dimension (images, texte) \(\Rightarrow\) Autoencodeur.

Exemple complet : détection de fraude#

Pour conclure ce chapitre, nous mettons en oeuvre un pipeline complet de détection d’anomalies sur un jeu de données déséquilibré simulant la détection de fraude par carte bancaire. Ce scénario est représentatif des cas réels : les anomalies (fraudes) sont extrêmement rares par rapport aux transactions normales.

Hide code cell source

# Génération d'un jeu de données synthétique de fraude bancaire
rng = np.random.RandomState(42)
n_legit = 5000    # transactions légitimes
n_fraud = 50      # transactions frauduleuses (1 %)

# Transactions légitimes : montants modérés, features corrélées
legit_features = rng.multivariate_normal(
    mean=[50, 30, 5, 0, 0],
    cov=np.array([
        [100, 30, 5, 2, 1],
        [30, 50, 3, 1, 0],
        [5, 3, 4, 0.5, 0],
        [2, 1, 0.5, 1, 0.3],
        [1, 0, 0, 0.3, 1]
    ]),
    size=n_legit
)

# Transactions frauduleuses : montants élevés, comportement atypique
fraud_features = np.column_stack([
    rng.exponential(scale=200, size=n_fraud),       # montants élevés
    rng.uniform(0, 100, size=n_fraud),              # fréquence inhabituelle
    rng.exponential(scale=20, size=n_fraud),         # nb transactions
    rng.normal(3, 1.5, size=n_fraud),                # score de risque élevé
    rng.normal(-2, 2, size=n_fraud)                  # pattern atypique
])

X_fraud = np.vstack([legit_features, fraud_features])
y_fraud = np.array([0] * n_legit + [1] * n_fraud)  # 0 = légitime, 1 = fraude

feature_names = ['Montant', 'Fréquence', 'Nb_transactions', 'Score_risque', 'Pattern']
df_fraud = pd.DataFrame(X_fraud, columns=feature_names)
df_fraud['Fraude'] = y_fraud

print(f"Jeu de données : {len(df_fraud)} transactions")
print(f"  - Légitimes : {(y_fraud == 0).sum()} ({(y_fraud == 0).mean()*100:.1f} %)")
print(f"  - Fraudes   : {(y_fraud == 1).sum()} ({(y_fraud == 1).mean()*100:.1f} %)")
Jeu de données : 5050 transactions
  - Légitimes : 5000 (99.0 %)
  - Fraudes   : 50 (1.0 %)

Hide code cell source

# Visualisation de la distribution des features
fig, axes = plt.subplots(5, 1, figsize=(8, 18))
for i, col in enumerate(feature_names):
    ax = axes[i]
    ax.hist(df_fraud.loc[df_fraud['Fraude'] == 0, col], bins=40,
            alpha=0.7, color='steelblue', label='Légitime', density=True)
    ax.hist(df_fraud.loc[df_fraud['Fraude'] == 1, col], bins=15,
            alpha=0.7, color='crimson', label='Fraude', density=True)
    ax.set_title(col, fontsize=10)
    if i == 0:
        ax.legend(fontsize=8)
plt.suptitle("Distribution des features par classe", fontsize=13, y=1.02)
plt.tight_layout()
plt.show()
_images/814eb7983098a514c75bbcb63893b2d41f47e35aa71e7376ca43c524df9991fa.png

Hide code cell source

# Pipeline complet : standardisation + comparaison des méthodes
scaler_fraud = StandardScaler()
X_scaled = scaler_fraud.fit_transform(X_fraud)

contamination_rate = n_fraud / (n_legit + n_fraud)

# Dictionnaire des méthodes
methods = {
    'Isolation Forest': IsolationForest(n_estimators=200, contamination=contamination_rate,
                                        random_state=42),
    'LOF': LocalOutlierFactor(n_neighbors=20, contamination=contamination_rate),
    'One-Class SVM': OneClassSVM(kernel='rbf', gamma='scale', nu=contamination_rate),
    'Elliptic Envelope': EllipticEnvelope(contamination=contamination_rate, random_state=42)
}

results = {}
for name, clf in methods.items():
    if name == 'LOF':
        y_pred = clf.fit_predict(X_scaled)
    else:
        clf.fit(X_scaled)
        y_pred = clf.predict(X_scaled)

    # Convertir : -1 (anomalie) -> 1 (fraude), 1 (normal) -> 0 (légitime)
    y_pred_binary = (y_pred == -1).astype(int)

    results[name] = {
        'precision': f1_score(y_fraud, y_pred_binary, zero_division=0),
        'y_pred': y_pred_binary
    }

print(f"{'Méthode':<25} {'Precision':<12} {'Recall':<12} {'F1-score':<12}")
print("-" * 61)
for name, res in results.items():
    from sklearn.metrics import precision_score, recall_score
    prec = precision_score(y_fraud, res['y_pred'], zero_division=0)
    rec = recall_score(y_fraud, res['y_pred'], zero_division=0)
    f1 = f1_score(y_fraud, res['y_pred'], zero_division=0)
    print(f"{name:<25} {prec:<12.3f} {rec:<12.3f} {f1:<12.3f}")
Méthode                   Precision    Recall       F1-score    
-------------------------------------------------------------
Isolation Forest          0.900        0.900        0.900       
LOF                       0.920        0.920        0.920       
One-Class SVM             0.450        0.360        0.400       
Elliptic Envelope         0.960        0.960        0.960       

Hide code cell source

# Matrices de confusion pour chaque méthode
fig, axes = plt.subplots(4, 1, figsize=(8, 14))
for ax, (name, res) in zip(axes, results.items()):
    cm = confusion_matrix(y_fraud, res['y_pred'])
    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', ax=ax,
                xticklabels=['Légitime', 'Fraude'],
                yticklabels=['Légitime', 'Fraude'])
    ax.set_title(name, fontsize=10)
    ax.set_xlabel('Prédiction')
    ax.set_ylabel('Réalité')
plt.suptitle("Matrices de confusion — détection de fraude", fontsize=13, y=1.02)
plt.tight_layout()
plt.show()
_images/191178eeb90ead99886a92d5449d15f539a137a99ecbe5708e26daa93302a1d9.png

Hide code cell source

# Courbes Precision-Recall (pour les méthodes avec scores continus)
fig, ax = plt.subplots(figsize=(8, 6))

# Isolation Forest
clf_if_fraud = IsolationForest(n_estimators=200, contamination=contamination_rate,
                                random_state=42)
clf_if_fraud.fit(X_scaled)
scores_if = -clf_if_fraud.decision_function(X_scaled)  # plus c'est grand, plus c'est anomal

prec_if, rec_if, _ = precision_recall_curve(y_fraud, scores_if)
ap_if = average_precision_score(y_fraud, scores_if)
ax.plot(rec_if, prec_if, label=f'Isolation Forest (AP={ap_if:.3f})', linewidth=2)

# Elliptic Envelope
clf_ee_fraud = EllipticEnvelope(contamination=contamination_rate, random_state=42)
clf_ee_fraud.fit(X_scaled)
scores_ee = -clf_ee_fraud.decision_function(X_scaled)

prec_ee, rec_ee, _ = precision_recall_curve(y_fraud, scores_ee)
ap_ee = average_precision_score(y_fraud, scores_ee)
ax.plot(rec_ee, prec_ee, label=f'Elliptic Envelope (AP={ap_ee:.3f})', linewidth=2)

# One-Class SVM
scores_ocsvm = -clf_ocsvm.fit(X_scaled).decision_function(X_scaled)
prec_oc, rec_oc, _ = precision_recall_curve(y_fraud, scores_ocsvm)
ap_oc = average_precision_score(y_fraud, scores_ocsvm)
ax.plot(rec_oc, prec_oc, label=f'One-Class SVM (AP={ap_oc:.3f})', linewidth=2)

ax.set_xlabel("Recall")
ax.set_ylabel("Precision")
ax.set_title("Courbes Precision-Recall — détection de fraude")
ax.legend(loc='best')
ax.set_xlim([0, 1])
ax.set_ylim([0, 1.05])
plt.tight_layout()
plt.show()
_images/f9a5e0c4c99605ddcf893485aff039ff9687a0b4e24237090bf6bcd5b44eaf2e.png

Remarque 159

Dans un contexte de détection de fraude, le recall est souvent plus important que la précision : il vaut mieux signaler quelques fausses alertes (faux positifs) que de laisser passer une fraude non détectée (faux négatif). La courbe Precision-Recall et l’Average Precision (AP) sont des métriques plus informatives que l’AUC-ROC lorsque les classes sont très déséquilibrées, car elles ne sont pas biaisées par le grand nombre de vrais négatifs.

Exemple 14 (Bonnes pratiques pour la détection d’anomalies en production)

  1. Standardiser les features avant d’appliquer les algorithmes (surtout LOF, One-Class SVM, Elliptic Envelope).

  2. Estimer le taux de contamination si possible, ou utiliser des valeurs conservatrices.

  3. Combiner plusieurs méthodes : un point détecté comme anomalie par plusieurs algorithmes est plus probablement une vraie anomalie.

  4. Évaluer sur des données étiquetées quand elles sont disponibles, en utilisant les métriques adaptées aux classes déséquilibrées (Precision, Recall, F1 sur la classe minoritaire, AP).

  5. Surveiller la dérive (drift) : la distribution des données normales peut évoluer dans le temps, nécessitant un réentraînement régulier des modèles.

Résumé#

Ce chapitre a présenté les principales méthodes de détection d’anomalies, des approches statistiques classiques aux algorithmes d’apprentissage automatique modernes. Chaque méthode repose sur une vision différente de ce qui constitue une anomalie :

  • Les approches statistiques (Z-score, IQR, Mahalanobis) modélisent la distribution des données normales et détectent les points de faible probabilité.

  • L”Isolation Forest exploite le fait que les anomalies sont plus faciles à isoler par des coupures aléatoires.

  • Le LOF compare la densité locale d’un point à celle de ses voisins, capturant les anomalies locales.

  • Le One-Class SVM sépare les données de l’origine dans un espace de features de haute dimension.

  • L”Elliptic Envelope ajuste une ellipse robuste aux données en supposant la normalité.

  • Les autoencodeurs détectent les anomalies via l’erreur de reconstruction.

Le choix de la méthode dépend de la nature des données, de leur dimensionnalité, du volume disponible et des contraintes applicatives. En pratique, combiner plusieurs approches et valider sur des données étiquetées (lorsqu’elles existent) est la stratégie la plus fiable. Le chapitre suivant poursuivra l’exploration de l’apprentissage non supervisé avec les méthodes de réduction de dimension.