Blog

Évaluer un modèle de classification : matrice de confusion, ROC et PR pas à pas

14 دقائق للقراءة

Un modèle de classification produit des nombres : des étiquettes prédites et des probabilités. Le métier, lui, attend des décisions et leur conséquence opérationnelle. Évaluer un modèle, c’est traduire ces nombres en réponses à des questions concrètes : combien de fraudes vais-je détecter ? combien de fausses alertes vais-je générer ? quel seuil de score dois-je fixer ? Ce tutoriel construit pas-à-pas le tableau de bord d’évaluation complet — métriques, matrice de confusion, courbes ROC et précision-rappel, choix du seuil de décision.

Pour le contexte général, voir le guide principal sur la stack data 2026. Ce tutoriel suppose qu’un modèle est déjà entraîné — voir le tutoriel sur la régression et la classification pour la phase précédente.

Prérequis

  • Python 3.10+, scikit-learn 1.8+, matplotlib, pandas, numpy
  • Un modèle de classification entraîné (binaire ou multi-classes)
  • Un jeu de test mis de côté (X_test, y_test)
  • Connaissance basique : précision, rappel, F1
  • Temps estimé : 90 minutes

Étape 1 — Comprendre les quatre quadrants

Toute évaluation de classification binaire repose sur quatre nombres. Les vrais positifs (VP) sont les cas effectivement positifs que le modèle a bien identifiés. Les vrais négatifs (VN) sont les cas négatifs correctement écartés. Les faux positifs (FP) sont des fausses alertes : le modèle dit positif, la réalité dit non. Les faux négatifs (FN) sont les ratés : le modèle dit non, la réalité dit oui. Toutes les métriques dérivent de ces quatre quadrants.

La précision se calcule comme VP / (VP + FP) : sur tous les cas signalés positifs, combien le sont vraiment ? Le rappel se calcule comme VP / (VP + FN) : sur tous les vrais positifs, combien le modèle en attrape ? Le F1 est leur moyenne harmonique. Selon l’enjeu métier — détection de fraude (rappel prioritaire), filtre anti-spam (précision prioritaire) — on n’optimise pas la même métrique. Cette analyse doit être faite avant tout, sous peine de produire un modèle techniquement correct mais opérationnellement inadapté.

Étape 2 — Calculer la matrice de confusion

La matrice de confusion est la représentation des quatre quadrants en tableau 2×2 (en classification binaire). C’est l’objet le plus parlant pour le métier : on voit en un coup d’œil où le modèle se trompe et de quel côté. scikit-learn la calcule en une ligne et matplotlib la dessine élégamment via ConfusionMatrixDisplay.

from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
import matplotlib.pyplot as plt

y_pred = modele.predict(X_test)
cm = confusion_matrix(y_test, y_pred)
print(cm)

fig, ax = plt.subplots(figsize=(5, 5))
ConfusionMatrixDisplay(cm, display_labels=["Négatif", "Positif"]).plot(ax=ax, cmap="Blues", colorbar=False)
ax.set_title("Matrice de confusion")
plt.tight_layout()
plt.savefig("matrice_confusion.png", dpi=120)
plt.show()

Le tableau imprimé est lu par convention : ligne = classe réelle, colonne = classe prédite. La diagonale principale contient les bonnes prédictions, les hors-diagonale contiennent les erreurs. Le graphique en couleurs facilite la lecture quand on présente les résultats à un public non technique. Sur une classification déséquilibrée, normaliser par ligne (normalize="true") aide à voir les taux plutôt que les volumes bruts.

Étape 3 — Le rapport de classification complet

La fonction classification_report agrège précision, rappel, F1 et support pour chaque classe, plus les moyennes pondérées. C’est le tableau le plus utile à inclure dans un rapport ou un commit Git, parce qu’il résume en quelques lignes le comportement global du modèle.

from sklearn.metrics import classification_report

print(classification_report(y_test, y_pred, digits=3, target_names=["Non payé", "Payé"]))

La colonne support donne le nombre d’exemples par classe dans le jeu de test, ce qui rappelle quand l’écart de F1 entre classes est dû à un déséquilibre plutôt qu’à un défaut du modèle. Sur la classe minoritaire avec un faible support, l’écart-type des métriques est plus grand : un test à 100 exemples positifs ne donne pas une estimation aussi fiable qu’un test à 10 000.

Étape 4 — Récupérer les probabilités, pas seulement les prédictions

La méthode predict applique un seuil par défaut de 0,5. Mais ce seuil est presque toujours sous-optimal. Les vraies questions sont : à quel seuil mon rappel atteint-il 90 % ? quel est le coût en faux positifs à ce moment-là ? Pour répondre, il faut récupérer les probabilités brutes via predict_proba et balayer ensuite tous les seuils possibles.

y_proba = modele.predict_proba(X_test)[:, 1]   # proba de la classe positive
print("Distribution des scores :")
print(pd.Series(y_proba).describe().round(3))

L’indice [:, 1] sélectionne la deuxième colonne, qui correspond à la probabilité de la classe positive (les classes sont ordonnées comme dans modele.classes_). La distribution des scores doit idéalement être bimodale : beaucoup de scores proches de 0 pour les négatifs, beaucoup proches de 1 pour les positifs. Si tous les scores se concentrent autour de 0,5, le modèle est peu discriminant et il faudra revenir aux features.

Étape 5 — Tracer la courbe ROC

La courbe ROC (Receiver Operating Characteristic) trace le taux de vrais positifs en fonction du taux de faux positifs, en faisant varier le seuil de 0 à 1. Elle synthétise en une seule image le comportement du modèle à tous les seuils. L’aire sous la courbe (AUC ROC) varie entre 0,5 (aléatoire) et 1 (parfait). Une AUC supérieure à 0,8 est généralement considérée comme bonne, supérieure à 0,9 excellente.

from sklearn.metrics import roc_curve, roc_auc_score

fpr, tpr, seuils = roc_curve(y_test, y_proba)
auc = roc_auc_score(y_test, y_proba)

fig, ax = plt.subplots(figsize=(6, 6))
ax.plot(fpr, tpr, label=f"AUC = {auc:.3f}")
ax.plot([0, 1], [0, 1], linestyle="--", color="gray", label="Aléatoire")
ax.set_xlabel("Taux de faux positifs")
ax.set_ylabel("Taux de vrais positifs")
ax.set_title("Courbe ROC")
ax.legend()
plt.tight_layout()
plt.savefig("courbe_roc.png", dpi=120)
plt.show()

La diagonale grise représente le hasard pur. Plus la courbe s’éloigne vers le coin supérieur gauche, meilleur est le modèle. Attention : l’AUC ROC peut être trompeusement élevée sur des classifications très déséquilibrées. Avec 99 % de négatifs, un modèle qui classe correctement la quasi-totalité des négatifs obtient une AUC élevée même s’il rate la plupart des positifs. C’est pourquoi la courbe précision-rappel est souvent plus pertinente dans ce cas.

Étape 6 — Tracer la courbe précision-rappel

La courbe précision-rappel est la sœur préférée de la ROC sur les classifications déséquilibrées. Elle ignore les vrais négatifs (qui sont la majorité écrasante du jeu) et se concentre sur ce qui compte : à quel point le modèle attrape correctement les positifs sans noyer les utilisateurs sous des fausses alertes. L’aire sous cette courbe (AUC PR ou AP, average precision) est plus informative que l’AUC ROC dans ce contexte.

from sklearn.metrics import precision_recall_curve, average_precision_score

precision, rappel, seuils_pr = precision_recall_curve(y_test, y_proba)
ap = average_precision_score(y_test, y_proba)

fig, ax = plt.subplots(figsize=(6, 6))
ax.plot(rappel, precision, label=f"AP = {ap:.3f}")
ax.set_xlabel("Rappel")
ax.set_ylabel("Précision")
ax.set_title("Courbe précision-rappel")
ax.legend()
plt.tight_layout()
plt.savefig("courbe_pr.png", dpi=120)
plt.show()

Le coin supérieur droit est l’idéal : précision = 1 et rappel = 1. La forme de la courbe révèle le compromis : elle descend généralement à mesure que le rappel monte, parce qu’augmenter le rappel impose d’abaisser le seuil et donc d’admettre plus de faux positifs. Sur cette courbe, on choisit son seuil de production en fonction du compromis voulu — par exemple, sur une détection de fraude, on cherche souvent le point où la précision passe en dessous de 50 % parce qu’au-delà le coût des fausses alertes devient ingérable.

Étape 7 — Choisir le seuil de décision

Le choix du seuil ne doit pas être laissé au hasard ni au défaut 0,5. On formalise un objectif (par exemple : « je veux détecter au moins 80 % des fraudes ») et on cherche le seuil qui le réalise. La fonction precision_recall_curve a déjà calculé tous les couples (précision, rappel, seuil) ; il suffit de les chercher.

import numpy as np

# Trouver le seuil donnant un rappel d'au moins 0.80
masque = rappel[:-1] >= 0.80   # precision_recall_curve renvoie un seuil de moins
indices = np.where(masque)[0]
if len(indices):
    idx = indices[-1]    # dernier seuil qui maintient le rappel
    print(f"Seuil = {seuils_pr[idx]:.3f}  →  rappel = {rappel[idx]:.3f}, précision = {precision[idx]:.3f}")
else:
    print("Aucun seuil n'atteint un rappel de 0.80")

# Appliquer ce seuil
y_pred_seuil = (y_proba >= seuils_pr[idx]).astype(int)
print(classification_report(y_test, y_pred_seuil, digits=3))

Le seuil retenu est ensuite codé en dur dans la pipeline d’inférence — typiquement, un attribut self.threshold dans la classe qui sert le modèle. Il est essentiel de versionner ce seuil avec le modèle parce que sans lui, deux personnes différentes obtiendraient des décisions différentes à partir des mêmes scores. C’est aussi un paramètre que l’on peut ajuster en production sans réentraîner le modèle, ce qui en fait un levier précieux quand le contexte métier change.

Étape 8 — Comparer plusieurs modèles sur les mêmes courbes

Quand on hésite entre deux modèles candidats, comparer leurs courbes ROC et PR sur le même graphique tranche immédiatement le débat. Si les deux courbes se croisent, c’est qu’aucun ne domine partout — le choix dépend alors du seuil opérationnel visé.

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))

for nom, modele_eval in [("LogReg", modele_logreg), ("HGB", modele_hgb)]:
    proba = modele_eval.predict_proba(X_test)[:, 1]
    fpr, tpr, _ = roc_curve(y_test, proba)
    auc = roc_auc_score(y_test, proba)
    ax1.plot(fpr, tpr, label=f"{nom} (AUC={auc:.3f})")

    p, r, _ = precision_recall_curve(y_test, proba)
    ap = average_precision_score(y_test, proba)
    ax2.plot(r, p, label=f"{nom} (AP={ap:.3f})")

ax1.plot([0, 1], [0, 1], "--", color="gray")
ax1.set_xlabel("FPR"); ax1.set_ylabel("TPR"); ax1.set_title("ROC"); ax1.legend()
ax2.set_xlabel("Rappel"); ax2.set_ylabel("Précision"); ax2.set_title("PR"); ax2.legend()
plt.tight_layout()
plt.savefig("comparaison_modeles.png", dpi=120)
plt.show()

Si une courbe est strictement au-dessus de l’autre, le choix est trivial : on prend ce modèle. Si elles se croisent, on regarde où on veut opérer (région à fort rappel ou à forte précision) et on choisit le modèle qui domine dans cette région. Cette inspection visuelle évite de trancher mécaniquement sur une seule métrique agrégée qui pourrait masquer un comportement intéressant.

Étape 9 — Évaluer la calibration des probabilités

Un modèle peut être bon en classification (l’ordre des scores est correct) mais mauvais en calibration (les probabilités prédites ne reflètent pas les fréquences réelles). Si le modèle dit « 70 % de chance de défaut » et qu’en réalité, sur tous les cas où il a dit 70 %, le défaut survient à 50 %, le modèle est mal calibré. C’est crucial dès qu’on utilise les probabilités pour des calculs en aval (espérance de gain, scoring de risque).

from sklearn.calibration import CalibrationDisplay

fig, ax = plt.subplots(figsize=(6, 6))
CalibrationDisplay.from_predictions(y_test, y_proba, n_bins=10, ax=ax)
ax.set_title("Diagramme de fiabilité")
plt.tight_layout()
plt.savefig("calibration.png", dpi=120)
plt.show()

Une calibration parfaite donne une courbe alignée sur la diagonale. Si elle est en dessous, le modèle surestime les probabilités positives ; au-dessus, il les sous-estime. La régression logistique est généralement bien calibrée par construction. Random Forest et SVM le sont moins ; on peut les corriger via CalibratedClassifierCV qui ajuste un modèle de calibration (sigmoïde ou isotonique) sur les sorties.

Étape 10 — Encapsuler dans un rapport reproductible

Toutes les métriques et tous les graphiques précédents doivent être regroupés dans un rapport unique qu’on peut partager. Le plus simple est un notebook Jupyter exporté en HTML, ou une fonction qui produit un PDF avec les graphiques sauvegardés. Cette discipline garantit qu’à chaque ré-entraînement, on peut comparer le rapport de la nouvelle version au précédent.

def rapport_evaluation(modele, X_test, y_test, dossier="rapport_eval"):
    import os
    os.makedirs(dossier, exist_ok=True)
    y_pred = modele.predict(X_test)
    y_proba = modele.predict_proba(X_test)[:, 1]
    with open(f"{dossier}/metriques.txt", "w", encoding="utf-8") as f:
        f.write(classification_report(y_test, y_pred, digits=3))
        f.write(f"\nAUC ROC : {roc_auc_score(y_test, y_proba):.3f}\n")
        f.write(f"AP : {average_precision_score(y_test, y_proba):.3f}\n")
    # ... générer et sauvegarder les graphiques
    return f"{dossier}/metriques.txt"

print(rapport_evaluation(modele, X_test, y_test))

Cette fonction peut être appelée à chaque entraînement, ses sorties versionnées dans Git, et comparées d’une version à l’autre. Couplée à MLflow (voir le tutoriel MLflow), elle remplace les copier-coller de captures d’écran qui finissent toujours par diverger. Un rapport généré automatiquement vaut dix rapports rédigés à la main qui ne sont jamais à jour.

Erreurs fréquentes

ErreurCauseSolution
AUC ROC élevée mais modèle inutile en productionClassification très déséquilibréeToujours regarder aussi la courbe PR et l’AP
predict_proba retourne une seule colonneModèle binaire avec sortie 1DVérifier modele.classes_ pour l’indexation
Métriques différentes à chaque exécutionrandom_state non fixéFixer random_state du modèle et du split
Calibration affreuse sur RandomForestForêts sortent des moyennes par feuilleCalibratedClassifierCV avec method= »isotonic »
Confusion entre classes 0 et 1 dans la matriceLecture inversée ligne/colonneToujours utiliser display_labels et un titre explicite
Seuil 0.5 par défaut donne des résultats catastrophiquesDéséquilibre extrêmeChoisir le seuil sur la courbe PR selon la métrique cible
F1 macro très différent du F1 weightedPerformance hétérogène entre classesExaminer la matrice de confusion par classe

Tutoriels associés

Ressources officielles

FAQ

ROC ou PR : laquelle utiliser ?

Sur une classification équilibrée, ROC et PR donnent des conclusions similaires. Sur une classification déséquilibrée, la courbe PR est plus informative. Présenter les deux est rarement excessif : chacune éclaire une facette différente.

L’AUC dépend-elle du seuil ?

Non. L’AUC est une mesure indépendante du seuil — elle évalue la qualité de l’ordre des prédictions. C’est précisément son intérêt par rapport à l’accuracy ou au F1 qui dépendent d’un seuil fixé.

Comment évaluer un modèle multi-classes ?

Le rapport de classification donne précision, rappel et F1 par classe. La matrice de confusion devient k×k. Pour la ROC et PR, on calcule une courbe par classe en mode « un contre tous », et on moyenne les AUC.

Faut-il évaluer sur le jeu d’entraînement ?

Uniquement pour vérifier qu’il n’y a pas de bug grossier. Le score sur le train est toujours optimiste et n’a aucune valeur prédictive. Seuls comptent le score en validation croisée et le score sur le test final mis de côté.

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité