Évaluation des modèles

CSI 4506 - automne 2026

Marcel Turcotte

Version: sept. 24, 2026 18h03

Préambule

Message du jour

Aperçu du cours

Ce cours développe l’évaluation de la classification à partir de la matrice de confusion. Il introduit l’exactitude, la précision, le rappel et le score F_1, puis examine le déséquilibre des classes et les moyennes micro et macro. Les dernières sections relient les seuils de décision aux courbes précision-rappel et ROC, puis interprètent l’AUROC comme une mesure de la qualité du classement.

Résultats d’apprentissage

  • Déduire les dénombrements binaires et un-contre-tous à partir d’une matrice de confusion.
  • Calculer et interpréter l’exactitude, la précision, le rappel et le score F_1.
  • Expliquer pourquoi le déséquilibre des classes peut rendre l’exactitude trompeuse.
  • Comparer les moyennes micro et macro pour les métriques multiclasses.
  • Relier un seuil de décision à la précision, au rappel et à un point de fonctionnement ROC.
  • Construire une courbe ROC à partir des scores d’un classificateur et interpréter l’AUROC comme une mesure du classement.

Mesures de performance

Matrice de confusion

Positif (Prédit) Négatif (Prédit)
Positif (Réel) Vrai positif (VP) Faux négatif (FN)
Négatif (Réel) Faux positif (FP) Vrai négatif (VN)

ConfusionMatrixDisplay

Code
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression

seed = 42

X, y = make_classification(n_samples = 500, random_state=seed)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state=0)

clf = LogisticRegression(random_state=seed)

clf.fit(X_train, y_train)

predictions = clf.predict(X_test)

cm = confusion_matrix(y_test, predictions, labels=[1, 0])

disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=["Positive", "Negative"])

disp.plot()
plt.show()

Matrice de confusion

Étant donné un ensemble de test avec N exemples et un classificateur h(x) :

C_{i,j} = \sum_{k = 1}^N [y_k = i \wedge h(x_k) = j]

Où C est une matrice l \times l, pour un ensemble de données avec l classes.

Matrice de confusion

  • Le nombre total d’exemples de la classe (réelle) i est C_{i \cdot} = \sum_{j=1}^l C_{i,j}

  • Le nombre total d’exemples assignés à la classe (prédite) j par le classificateur h est C_{\cdot j} = \sum_{i=1}^l C_{i,j}

Matrice de confusion

  • Les termes sur la diagonale indiquent le nombre total d’exemples classés correctement par le classificateur h. Ainsi, le nombre d’exemples correctement classés est \sum_{i=1}^l C_{i,i}

  • Les termes non-diagonaux représentent les erreurs de classification.

Matrice de confusion - multi-classes

Pour évaluer la performance dans un contexte multi-classes, on dérive généralement des métriques “un-contre-tous” pour chaque classe à partir de la matrice de confusion. Ces métriques sont ensuite moyennées en utilisant des schémas de pondération spécifiques.

Matrice de confusion - multi-classes

Matrice de confusion - vrai positif

Matrice de confusion - faux positif

Matrice de confusion - faux négatif

Matrice de confusion - vrai négatif

Matrice de confusion - multi-classes

Multi-classes

Pour évaluer la performance dans un contexte multi-classes, on dérive généralement des métriques “un-contre-tous” pour chaque classe à partir de la matrice de confusion. Ces métriques sont ensuite moyennées en utilisant des schémas de pondération spécifiques.

  • Vrais Positifs (\mathrm{TP}_i) : Entrée diagonale C_{i,i}
  • Faux Positifs (\mathrm{FP}_i) : Somme de la colonne i excluant C_{i,i}
  • Faux Négatifs (\mathrm{FN}_i) : Somme de la ligne i excluant C_{i,i}
  • Vrais Négatifs (\mathrm{TN}_i) : N - (\mathrm{TP}_i + \mathrm{FP}_i + \mathrm{FN}_i)

Multi-classes

Pour évaluer la performance dans un contexte multi-classes, on dérive généralement des métriques “un-contre-tous” pour chaque classe à partir de la matrice de confusion. Ces métriques sont ensuite moyennées en utilisant des schémas de pondération spécifiques.

  • \mathrm{TP}_i = C_{i,i}
  • \mathrm{FP}_i = \sum_{k \ne i} C_{k,i}
  • \mathrm{FN}_i = \sum_{k \ne i} C_{i,k}
  • \mathrm{TN}_i = \sum_{j \ne i} \sum_{k \ne i} C_{j,k}

sklearn.metrics.confusion_matrix

from sklearn.metrics import confusion_matrix

y_actual = [0, 0, 0, 1, 1, 1, 1, 1, 1, 1]
y_pred   = [0, 1, 1, 0, 0, 0, 1, 1, 1, 1]

confusion_matrix(y_actual,y_pred)
array([[1, 2],
       [3, 4]])
tn, fp, fn, tp = confusion_matrix(y_actual, y_pred).ravel().tolist()
(tn, fp, fn, tp)
(1, 2, 3, 4)

Prédiction parfaite

y_actual = [0, 1, 0, 0, 1, 1, 1, 0, 1, 1]
y_pred   = [0, 1, 0, 0, 1, 1, 1, 0, 1, 1]

confusion_matrix(y_actual,y_pred)
array([[4, 0],
       [0, 6]])
tn, fp, fn, tp = confusion_matrix(y_actual, y_pred).ravel().tolist()  
(tn, fp, fn, tp)
(4, 0, 0, 6)

Matrice de confusion : plusieurs classes

Code
from sklearn.datasets import load_digits
import numpy as np

digits = load_digits()

X = digits.data
y = digits.target

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.1,
    random_state=seed,
    stratify=y,
)

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

from sklearn.multiclass import OneVsRestClassifier

clf = OneVsRestClassifier(LogisticRegression(max_iter=1000))
clf.fit(X_train_scaled, y_train)

y_pred = clf.predict(X_test_scaled)

ConfusionMatrixDisplay.from_predictions(y_test, y_pred)
plt.show()

Matrice de confusion à dix classes pour les prédictions de chiffres manuscrits.

Visualisation des erreurs

Code
mask = (y_test == 8) & (y_pred == 1)
images_mal_classees = X_test[mask][:5]

plt.figure(figsize=(8, 2))
for index, image in enumerate(images_mal_classees):
    plt.subplot(1, len(images_mal_classees), index + 1)
    plt.imshow(np.reshape(image, (8,8)), cmap=plt.cm.gray)
    plt.title("Prédit : 1")
    plt.axis("off")

plt.tight_layout()
plt.show()

Deux chiffres huit manuscrits que le modèle a prédits comme des uns.

Exactitude

Quelle est l’exactitude de ce résultat ?

\mathrm{exactitude} = \frac{\mathrm{VP}+\mathrm{VN}}{\mathrm{VP}+\mathrm{VN}+\mathrm{FP}+\mathrm{FN}} = \frac{\mathrm{VP}+\mathrm{VN}}{\mathrm{N}}

from sklearn.metrics import accuracy_score

y_actual = [0, 0, 0, 1, 1, 1, 1, 1, 1, 1]
y_pred   = [0, 1, 1, 0, 0, 0, 1, 1, 1, 1]

accuracy_score(y_actual,y_pred)
0.5

Exactitude

y_actual = [0, 1, 0, 0, 1, 1, 1, 0, 1, 1]
y_pred   = [1, 0, 1, 1, 0, 0, 0, 1, 0, 0]

accuracy_score(y_actual,y_pred)
0.0
y_actual = [0, 1, 0, 0, 1, 1, 1, 0, 1, 1]
y_pred   = [0, 1, 0, 0, 1, 1, 1, 0, 1, 1]

accuracy_score(y_actual,y_pred)
1.0

L’exactitude peut être trompeuse

y_actual = [0, 0, 0, 0, 1, 1, 0, 0, 0, 0]
y_pred   = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

accuracy_score(y_actual,y_pred)
0.8

Précision

Aussi connu sous le nom de valeur prédictive positive (PPV).

\mathrm{précision} = \frac{\mathrm{VP}}{\mathrm{VP}+\mathrm{FP}}

from sklearn.metrics import precision_score

y_actual = [0, 0, 0, 1, 1, 1, 1, 1, 1, 1]
y_pred   = [0, 1, 1, 0, 0, 0, 1, 1, 1, 1]

precision_score(y_actual, y_pred)
0.6666666666666666

La précision seule ne suffit pas

y_actual = [0, 0, 0, 1, 1, 1, 1, 1, 1, 1]
y_pred   = [0, 0, 0, 0, 0, 0, 1, 0, 0, 0]

precision_score(y_actual,y_pred)
1.0

Rappel

Aussi connu sous le nom de sensibilité ou taux de vrais positifs (TPR)

\mathrm{rappel} = \frac{\mathrm{VP}}{\mathrm{VP}+\mathrm{FN}}

from sklearn.metrics import recall_score

y_actual = [0, 0, 0, 1, 1, 1, 1, 1, 1, 1]
y_pred   = [0, 1, 1, 0, 0, 0, 1, 1, 1, 1]

recall_score(y_actual,y_pred)
0.5714285714285714

Score F_1

\begin{align*} F_1~\mathrm{score} &= \frac{2}{\frac{1}{\mathrm{précision}}+\frac{1}{\mathrm{rappel}}} = 2 \times \frac{\mathrm{précision}\times\mathrm{rappel}}{\mathrm{précision}+\mathrm{rappel}} \\ &= \frac{\mathrm{VP}}{\mathrm{VP}+\frac{\mathrm{FN}+\mathrm{FP}}{2}} \end{align*}

from sklearn.metrics import f1_score

y_actual = [0, 0, 0, 1, 1, 1, 1, 1, 1, 1]
y_pred   = [0, 1, 1, 0, 0, 0, 1, 1, 1, 1]

f1_score(y_actual,y_pred)
0.6153846153846154

Moyennes micro et macro

Définition

Le problème de déséquilibre des classes survient lorsqu’une classe contient beaucoup plus d’exemples qu’une autre.

Sans choix appropriés pour l’entraînement et l’évaluation, un modèle peut favoriser la classe majoritaire et obtenir de mauvais résultats sur la classe minoritaire.

Mesures de performance micro

  • La moyenne micro regroupe les vrais positifs, les faux positifs et les faux négatifs de toutes les classes avant de calculer la précision, le rappel ou le score F_1.
  • Elle traite chaque prédiction de la même manière. Les classes fréquentes contribuent donc davantage à la métrique finale.

Mesures de performance macro

  • La moyenne macro calcule la métrique séparément pour chaque classe, puis prend la moyenne arithmétique.
  • Elle traite chaque classe de la même manière, quelle que soit sa fréquence. Une mauvaise performance sur une classe peu fréquente demeure donc visible.

Multi-classe

Lors du calcul de la précision, du rappel et de F_1, on calcule généralement les métriques “un-contre-tous” pour chaque classe. Ensuite, on les moyenne en utilisant des schémas de pondération (macro, micro).

  • Vrais Positifs (\mathrm{TP}_i) : Entrée diagonale C_{i,i}
  • Faux Positifs (\mathrm{FP}_i) : Somme de la colonne i à l’exception de C_{i,i}
  • Faux Négatifs (\mathrm{FN}_i) : Somme de la ligne i à l’exception de C_{i,i}
  • Vrais Négatifs (\mathrm{TN}_i) : N - (\mathrm{TP}_i + \mathrm{FP}_i + \mathrm{FN}_i)

Multi-classe

Lors du calcul de la précision, du rappel et de F_1, on calcule généralement les métriques “un-contre-tous” pour chaque classe. Ensuite, on les moyenne en utilisant des schémas de pondération (macro, micro).

  • \mathrm{TP}_i = C_{i,i}
  • \mathrm{FP}_i = \sum_{k \ne i} C_{k,i}
  • \mathrm{FN}_i = \sum_{k \ne i} C_{i,k}
  • \mathrm{TN}_i = \sum_{j \ne i} \sum_{k \ne i} C_{j,k}

Micro/Macro

from sklearn.metrics import ConfusionMatrixDisplay

# Données d'exemple
y_true = ['Chat'] * 42 + ['Chien'] *  7 + ['Renard'] * 11
y_pred = ['Chat'] * 39 + ['Chien'] *  1 + ['Renard'] *  2 + \
         ['Chat'] *  4 + ['Chien'] *  3 + ['Renard'] *  0 + \
         ['Chat'] *  5 + ['Chien'] *  1 + ['Renard'] *  5

ConfusionMatrixDisplay.from_predictions(y_true, y_pred)

Précision micro/macro

from sklearn.metrics import classification_report, precision_score

print(classification_report(y_true, y_pred), "\n")

precision_micro = precision_score(y_true, y_pred, average="micro")
precision_macro = precision_score(y_true, y_pred, average="macro")
print(f"Précision micro : {precision_micro:.2f}")
print(f"Précision macro : {precision_macro:.2f}")
              precision    recall  f1-score   support

        Chat       0.81      0.93      0.87        42
       Chien       0.60      0.43      0.50         7
      Renard       0.71      0.45      0.56        11

    accuracy                           0.78        60
   macro avg       0.71      0.60      0.64        60
weighted avg       0.77      0.78      0.77        60
 

Précision micro : 0.78
Précision macro : 0.71

Précision micro/macro

  • La précision moyenne macro est calculée comme la moyenne des scores de précision1 pour chaque classe : \frac{0.81 + 0.60 + 0.71}{3} = 0.71.

  • Tandis que, la précision moyenne micro est calculée en utilisant la formule, \frac{TP}{TP+FP} et les données de toute la matrice de confusion \frac{39+3+5}{39+3+5+9+2+2} = \frac{47}{60} = 0.78

Rappel micro/macro

from sklearn.metrics import classification_report, recall_score

print(classification_report(y_true, y_pred), "\n")

micro_recall = recall_score(y_true, y_pred, average="micro")
macro_recall = recall_score(y_true, y_pred, average="macro")
print(f"Rappel micro : {micro_recall:.2f}")
print(f"Rappel macro : {macro_recall:.2f}")
              precision    recall  f1-score   support

        Chat       0.81      0.93      0.87        42
       Chien       0.60      0.43      0.50         7
      Renard       0.71      0.45      0.56        11

    accuracy                           0.78        60
   macro avg       0.71      0.60      0.64        60
weighted avg       0.77      0.78      0.77        60
 

Rappel micro : 0.78
Rappel macro : 0.60

Rappel micro/macro

  • Le rappel moyen macro est calculé comme la moyenne des scores de rappel pour chaque classe : \frac{0.93 + 0.43 + 0.45}{3} = 0.60.

  • Le rappel moyen micro regroupe les décomptes de toute la matrice de confusion : \frac{TP}{TP+FN} = \frac{39+3+5}{39+3+5+3+4+6} = \frac{47}{60} = 0.78.

Déséquilibre des classes dans les données médicales

Mesures micro/macro (données médicales)

Code
# Données d'exemple
y_true = ['Normal'] *  990 + ['Tumour'] *  10
y_pred = ['Normal'] *  985 + ['Tumour'] *   5 + \
         ['Normal'] *    4 + ['Tumour'] *   6

ConfusionMatrixDisplay.from_predictions(y_true, y_pred)

Matrice de confusion pour un exemple médical déséquilibré.

Mesures micro/macro (données médicales)

from sklearn.metrics import classification_report, recall_score

print(classification_report(y_true, y_pred), "\n")

micro_precision = precision_score(y_true, y_pred, average="micro")
macro_precision = precision_score(y_true, y_pred, average="macro")
print(f"Précision micro : {micro_precision:.2f}")
print(f"Précision macro : {macro_precision:.2f}")

print("\n")

micro_recall = recall_score(y_true, y_pred, average="micro")
macro_recall = recall_score(y_true, y_pred, average="macro")
print(f"Rappel micro : {micro_recall:.2f}")
print(f"Rappel macro : {macro_recall:.2f}")

Mesures micro/macro (données médicales)

              precision    recall  f1-score   support

      Normal       1.00      0.99      1.00       990
      Tumour       0.55      0.60      0.57        10

    accuracy                           0.99      1000
   macro avg       0.77      0.80      0.78      1000
weighted avg       0.99      0.99      0.99      1000
 

Précision micro : 0.99
Précision macro : 0.77


Rappel micro : 0.99
Rappel macro : 0.80

Compromis précision-rappel

Chiffres manuscrits (revisités)

Chargement du jeu de données

Code
from sklearn.datasets import fetch_openml

digits = fetch_openml('mnist_784', as_frame=False)
X, y = digits.data, digits.target

Tracé des cinq premiers exemples

Code
plt.figure(figsize=(10,2))
n = 5

for index, (image, label) in enumerate(zip(X[0:n], y[0:n])):
    plt.subplot(1, n, index + 1)
    plt.imshow(np.reshape(image, (28,28)), cmap=plt.cm.gray)
    plt.title(f'y = {label}')

Cinq exemples de chiffres manuscrits provenant du jeu de données MNIST.

Ces images ont des dimensions de 28 \times 28 pixels.

Tâche de classification binaire

# Création d'une tâche de classification binaire (un contre tous)

some_digit = X[0]
some_digit_y = y[0]

y = (y == some_digit_y)
y
array([ True, False, False, ..., False,  True, False], shape=(70000,))
# Création des ensembles d'entraînement et de test
X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.1,
    random_state=seed,
    stratify=y,
)

SGDClassifier

from sklearn.linear_model import SGDClassifier

clf = SGDClassifier(random_state=seed)
clf.fit(X_train, y_train)

Performance

from sklearn.metrics import accuracy_score

y_pred = clf.predict(X_test)

accuracy_score(y_test, y_pred)
0.9657142857142857

Le score semble élevé,

mais la distribution des classes est essentielle pour l’interpréter.

Pas si vite

from sklearn.dummy import DummyClassifier

dummy_clf = DummyClassifier(strategy="most_frequent")

dummy_clf.fit(X_train, y_train)
y_pred = dummy_clf.predict(X_test)

accuracy_score(y_test, y_pred)
0.9098571428571428

Compromis précision-rappel

Compromis précision-rappel

Code
from sklearn.model_selection import cross_val_predict
y_scores = cross_val_predict(
    clf,
    X_train,
    y_train,
    cv=3,
    method="decision_function",
)

from sklearn.metrics import precision_recall_curve

precisions, recalls, thresholds = precision_recall_curve(
    y_train,
    y_scores,
)

threshold = 3000

plt.figure(figsize=(8, 4))
plt.plot(
    thresholds,
    precisions[:-1],
    "b--",
    label="Précision",
    linewidth=2,
)
plt.plot(thresholds, recalls[:-1], "g-", label="Rappel", linewidth=2)
plt.vlines(threshold, 0, 1.0, "k", "dotted", label="Seuil")

idx = (thresholds >= threshold).argmax()
plt.plot(thresholds[idx], precisions[idx], "bo")
plt.plot(thresholds[idx], recalls[idx], "go")
plt.axis([-50000, 50000, 0, 1])
plt.grid()
plt.xlabel("Seuil")
plt.legend(loc="center right")

plt.show()

Précision et rappel en fonction du seuil de décision.

Courbe précision-rappel

Code
import matplotlib.patches as patches

plt.figure(figsize=(5, 5))

plt.plot(
    recalls,
    precisions,
    linewidth=2,
    label="Courbe précision-rappel",
)

plt.plot([recalls[idx], recalls[idx]], [0., precisions[idx]], "k:")
plt.plot([0.0, recalls[idx]], [precisions[idx], precisions[idx]], "k:")
plt.plot(
    [recalls[idx]],
    [precisions[idx]],
    "ko",
    label="Point au seuil 3 000",
)
plt.gca().add_patch(patches.FancyArrowPatch(
    (0.79, 0.60), (0.61, 0.78),
    connectionstyle="arc3,rad=.2",
    arrowstyle="Simple, tail_width=1.5, head_width=8, head_length=10",
    color="#444444"))
plt.text(0.56, 0.62, "Seuil\nplus élevé", color="#333333")
plt.xlabel("Rappel")
plt.ylabel("Précision")
plt.axis([0, 1, 0, 1])
plt.grid()
plt.legend(loc="lower left")

plt.show()

Courbe précision-rappel sur laquelle un seuil particulier est indiqué.

Courbe ROC

Courbe ROC

Courbe caractéristique de fonctionnement du récepteur (ROC)

  • Taux de vrais positifs (TVP) en fonction du taux de faux positifs (TFP)
  • Un classificateur idéal a un TVP proche de 1,0 et un TFP proche de 0,0
  • \mathrm{TVP} = \frac{\mathrm{VP}}{\mathrm{VP}+\mathrm{FN}} (rappel, sensibilité)
  • Le TVP s’approche de un lorsqu’il y a peu de faux négatifs
  • \mathrm{TFP} = \frac{\mathrm{FP}}{\mathrm{FP}+\mathrm{VN}} = 1-\mathrm{spécificité}
  • Le TFP s’approche de zéro lorsqu’il y a peu de faux positifs

Courbe ROC

Courbe ROC

Code
idx_for_90_precision = (precisions >= 0.90).argmax()
threshold_for_90_precision = thresholds[idx_for_90_precision]
y_train_pred_90 = (y_scores >= threshold_for_90_precision)

from sklearn.metrics import roc_curve

fpr, tpr, thresholds = roc_curve(y_train, y_scores)

idx_for_threshold_at_90 = (
    thresholds <= threshold_for_90_precision
).argmax()
tpr_90 = tpr[idx_for_threshold_at_90]
fpr_90 = fpr[idx_for_threshold_at_90]

plt.figure(figsize=(5, 5))
plt.plot(fpr, tpr, linewidth=2, label="Courbe ROC")
plt.plot([0, 1], [0, 1], "k:", label="Courbe ROC d'un classificateur aléatoire")
plt.plot([fpr_90], [tpr_90], "ko", label="Seuil pour une précision de 90 %")

plt.gca().add_patch(patches.FancyArrowPatch(
    (0.20, 0.89), (0.07, 0.70),
    connectionstyle="arc3,rad=.4",
    arrowstyle="Simple, tail_width=1.5, head_width=8, head_length=10",
    color="#444444"))
plt.text(0.12, 0.71, "Seuil\nplus élevé", color="#333333")
plt.xlabel("Taux de faux positifs")
plt.ylabel("Taux de vrais positifs (rappel)")
plt.grid()
plt.axis([0, 1, 0, 1])
plt.legend(loc="lower right", fontsize=13)

plt.show()

Courbe ROC avec le point de fonctionnement correspondant à une précision de 90 pour cent.

Distribution des scores et courbe ROC

Code
from sklearn.metrics import roc_auc_score, roc_curve

def plot_scores_and_roc(
    ax_scores,
    ax_roc,
    negative_scores,
    positive_scores,
    title,
    threshold=None,
):
    """Tracer les distributions des scores et leur courbe ROC."""
    labels = np.concatenate([
        np.zeros(len(negative_scores), dtype=int),
        np.ones(len(positive_scores), dtype=int),
    ])
    scores = np.concatenate([negative_scores, positive_scores])

    bins = np.linspace(0, 1, 21)
    ax_scores.hist(
        negative_scores,
        bins=bins,
        alpha=0.6,
        label="Exemples négatifs",
    )
    ax_scores.hist(
        positive_scores,
        bins=bins,
        alpha=0.6,
        label="Exemples positifs",
    )
    ax_scores.set(
        title=title,
        xlabel="Score de prédiction",
        ylabel="Nombre d'exemples",
        xlim=(0, 1),
    )

    curve_fpr, curve_tpr, _ = roc_curve(labels, scores)
    area = roc_auc_score(labels, scores)
    ax_roc.plot(curve_fpr, curve_tpr, linewidth=2, label="Courbe ROC")
    ax_roc.plot([0, 1], [0, 1], "k--", label="Classement aléatoire")
    ax_roc.set(
        title=f"AUROC = {area:.2f}",
        xlabel="Taux de faux positifs",
        ylabel="Taux de vrais positifs",
        xlim=(0, 1),
        ylim=(0, 1),
    )
    ax_roc.set_aspect("equal", adjustable="box")

    if threshold is not None:
        predictions = (scores >= threshold).astype(int)
        tn, fp, fn, tp = confusion_matrix(
            labels,
            predictions,
            labels=[0, 1],
        ).ravel()
        point_fpr = fp / (fp + tn)
        point_tpr = tp / (tp + fn)
        accuracy = np.mean(predictions == labels)

        ax_scores.axvline(
            threshold,
            color="black",
            linestyle=":",
            label=f"Seuil = {threshold:.2f}",
        )
        ax_scores.text(
            0.02,
            0.95,
            f"Exactitude = {accuracy:.2f}",
            transform=ax_scores.transAxes,
            va="top",
        )
        ax_roc.scatter(
            point_fpr,
            point_tpr,
            color="black",
            zorder=3,
            label="Seuil sélectionné",
        )

    ax_scores.legend(fontsize=8)
    ax_roc.legend(fontsize=8, loc="lower right")


roc_demo_rng = np.random.default_rng(seed)
roc_demo_size = 250
good_negative_scores = np.clip(
    roc_demo_rng.normal(0.35, 0.16, roc_demo_size),
    0,
    1,
)
good_positive_scores = np.clip(
    roc_demo_rng.normal(0.65, 0.16, roc_demo_size),
    0,
    1,
)

fig, axes = plt.subplots(1, 2, figsize=(10, 4.5), constrained_layout=True)
plot_scores_and_roc(
    axes[0],
    axes[1],
    good_negative_scores,
    good_positive_scores,
    "Distributions de scores qui se chevauchent",
    threshold=0.50,
)
plt.show()

Distribution des scores et courbe ROC

Histogrammes des scores de prédiction des classes négative et positive placés à côté de la courbe ROC correspondante; un même seuil est indiqué dans les deux graphiques.

Surface sous la courbe ROC

La surface sous la courbe ROC (AUROC) résume la capacité d’un score à classer les exemples positifs au-dessus des exemples négatifs.

  • \mathrm{AUROC}=1 : chaque exemple positif reçoit un score supérieur à celui de chaque exemple négatif.
  • \mathrm{AUROC}=0,5 : classement aléatoire en moyenne.
  • \mathrm{AUROC}<0,5 : le classement est systématiquement inversé.

L’AUROC compare des classements pour tous les seuils. Elle ne mesure pas l’étalonnage des probabilités et ne choisit pas de seuil de fonctionnement.

Classificateur parfait

Code
perfect_rng = np.random.default_rng(seed + 1)
perfect_negative_scores = np.clip(
    perfect_rng.normal(0.25, 0.07, roc_demo_size),
    0,
    1,
)
perfect_positive_scores = np.clip(
    perfect_rng.normal(0.75, 0.07, roc_demo_size),
    0,
    1,
)

fig, axes = plt.subplots(1, 2, figsize=(10, 4.5), constrained_layout=True)
plot_scores_and_roc(
    axes[0],
    axes[1],
    perfect_negative_scores,
    perfect_positive_scores,
    "Distributions gaussiennes séparées",
)
plt.show()

Classificateur parfait

Distributions gaussiennes séparées des scores négatifs et positifs placées à côté d'une courbe ROC parfaite.

Classificateur aléatoire

Code
random_rng = np.random.default_rng(seed + 2)
random_negative_scores = random_rng.uniform(0.05, 0.95, roc_demo_size)
random_positive_scores = random_rng.uniform(0.05, 0.95, roc_demo_size)

fig, axes = plt.subplots(1, 2, figsize=(10, 4.5), constrained_layout=True)
plot_scores_and_roc(
    axes[0],
    axes[1],
    random_negative_scores,
    random_positive_scores,
    "Échantillons uniformes indépendants",
)
plt.show()

Classificateur aléatoire

Deux distributions uniformes de scores échantillonnées indépendamment placées à côté d'une courbe ROC proche de la diagonale aléatoire.

AUROC inférieure à 0,5

Code
reversed_negative_scores = 1 - good_negative_scores
reversed_positive_scores = 1 - good_positive_scores

fig, axes = plt.subplots(1, 2, figsize=(10, 4.5), constrained_layout=True)
plot_scores_and_roc(
    axes[0],
    axes[1],
    reversed_negative_scores,
    reversed_positive_scores,
    "Ordre des scores inversé",
)
plt.show()

AUROC inférieure à 0,5

Exemples positifs recevant systématiquement des scores inférieurs aux exemples négatifs, placés à côté d'une courbe ROC sous la diagonale aléatoire.

ROC ou précision-rappel?

  • La courbe ROC utilise le rappel et le taux de faux positifs, dont le dénominateur contient tous les exemples négatifs.
  • Lorsque la classe positive est très rare, un grand nombre de vrais négatifs peut rendre le taux de faux positifs faible, même si plusieurs prédictions positives sont erronées.
  • Une courbe précision-rappel se concentre sur la classe positive et montre quelle proportion des prédictions positives est correcte.

Utilisez la courbe qui correspond au problème de décision et examinez toujours la prévalence des classes ainsi qu’un seuil de fonctionnement.

Construction de la courbe ROC et calcul de l’AUROC

Régression logistique

  • Modèle :

    h_\theta(x_i) = \sigma(\theta_0 + x_i^\top\theta) = \frac{1}{1+e^{-(\theta_0+x_i^\top\theta)}}

  • Prédiction :

    • Attribuer y_i = 0 si h_\theta(x_i) < 0,5.
    • Attribuer y_i = 1 si h_\theta(x_i) \geq 0,5.
  • Fonction de perte : entropie croisée

J(\theta) = -\frac{1}{N} \sum_{i=1}^{N} \left[ y_i \log h_\theta(x_i) + (1-y_i) \log\left(1-h_\theta(x_i)\right) \right]

Mise en œuvre : régression logistique

Cette mise en œuvre a été présentée au cours 6. Elle est reprise ici afin que le carnet contienne un modèle fonctionnel permettant de construire la courbe ROC et de calculer l’AUROC.

Code
import numpy as np

class LogisticRegression:

    """
    Régression logistique binaire entraînée par descente de gradient par lot.

    Paramètres
    ----------
    learning_rate : float, default=0.1
        Pas de la descente de gradient.
    max_iter : int, default=1000
        Nombre d'itérations de la descente de gradient.
    Remarques
    ---------
    - Cette mise en œuvre attend des étiquettes binaires {0, 1}.
    - L'ordonnée à l'origine est ajoutée pendant `fit`.
    - Par souci de simplicité, il n'y a ni régularisation ni arrêt précoce.
    """

    def __init__(
        self,
        learning_rate: float = 0.1,
        max_iter: int = 1000,
    ):
        if learning_rate <= 0:
            raise ValueError("learning_rate doit être positif.")
        if max_iter <= 0:
            raise ValueError("max_iter doit être positif.")

        self.learning_rate = learning_rate
        self.max_iter = max_iter

        # Attributs définis après l'ajustement
        self._theta = None
        self._loss_history = None
        self._n_features = None
        self._fitted = False

    def fit(self, X: np.ndarray, y: np.ndarray) -> "LogisticRegression":
        """Ajuster les paramètres du modèle par descente de gradient."""
        X = self._as_2d_array(X, name="X")
        y = self._as_1d_array(y, name="y")

        if X.shape[0] != y.shape[0]:
            raise ValueError(
                "X et y doivent contenir le même nombre d'exemples."
            )
        self._check_binary_labels(y)

        n_examples, n_features = X.shape
        self._n_features = n_features
        Xb = self._add_intercept(X)

        # Une initialisation à zéro suffit puisque l'objectif est convexe.
        self._theta = np.zeros(n_features + 1, dtype=float)
        self._loss_history = []

        for _ in range(self.max_iter):
            probabilities = self._sigmoid(Xb @ self._theta)
            gradient = (Xb.T @ (probabilities - y)) / n_examples
            self._theta -= self.learning_rate * gradient

            updated_probabilities = self._sigmoid(Xb @ self._theta)
            self._loss_history.append(
                self._bce_loss(updated_probabilities, y)
            )

        self._fitted = True
        return self

    def predict_proba(self, X: np.ndarray) -> np.ndarray:
        """Retourner les probabilités prédites de la classe positive."""
        self._ensure_fitted()
        X = self._as_2d_array(X, name="X")
        self._ensure_same_n_features(X)

        Xb = self._add_intercept(X)
        return self._sigmoid(Xb @ self._theta)

    def predict(
        self,
        X: np.ndarray,
        threshold: float = 0.5,
    ) -> np.ndarray:
        """Retourner les classes prédites selon un seuil de probabilité."""
        if not 0 <= threshold <= 1:
            raise ValueError("threshold doit être compris entre 0 et 1.")
        return (self.predict_proba(X) >= threshold).astype(int)

    @property
    def intercept_(self) -> float:
        """Retourner l'ordonnée à l'origine ajustée."""
        self._ensure_fitted()
        return float(self._theta[0])

    @property
    def coef_(self) -> np.ndarray:
        """Retourner une copie des coefficients ajustés."""
        self._ensure_fitted()
        return self._theta[1:].copy()

    def get_loss_history(self) -> list:
        """Retourner une copie des pertes recueillies pendant l'ajustement."""
        self._ensure_fitted()
        return list(self._loss_history)

    @staticmethod
    def _sigmoid(z: np.ndarray) -> np.ndarray:
        """Calculer la sigmoïde sans débordement numérique."""
        z = np.asarray(z, dtype=float)
        result = np.empty_like(z)
        positive = z >= 0
        result[positive] = 1.0 / (1.0 + np.exp(-z[positive]))
        exp_z = np.exp(z[~positive])
        result[~positive] = exp_z / (1.0 + exp_z)
        return result

    @staticmethod
    def _bce_loss(probabilities: np.ndarray, y: np.ndarray) -> float:
        probabilities = np.clip(probabilities, 1e-12, 1.0 - 1e-12)
        return float(
            -np.mean(
                y * np.log(probabilities)
                + (1 - y) * np.log(1 - probabilities)
            )
        )

    @staticmethod
    def _as_2d_array(X, name="X") -> np.ndarray:
        X = np.asarray(X, dtype=float)
        if X.ndim != 2:
            message = (
                f"{name} doit être un tableau 2D de forme "
                "(n_samples, n_features)."
            )
            raise ValueError(message)
        return X

    @staticmethod
    def _as_1d_array(y, name="y") -> np.ndarray:
        y = np.asarray(y, dtype=float)
        if y.ndim != 1:
            message = f"{name} doit avoir la forme (n_samples,)."
            raise ValueError(message)
        return y

    @staticmethod
    def _check_binary_labels(y: np.ndarray) -> None:
        if not np.array_equal(np.unique(y), np.array([0.0, 1.0])):
            raise ValueError(
                "y doit contenir les deux étiquettes binaires 0 et 1."
            )

    @staticmethod
    def _add_intercept(X: np.ndarray) -> np.ndarray:
        return np.column_stack([np.ones(X.shape[0]), X])

    def _ensure_fitted(self) -> None:
        if not self._fitted or self._theta is None:
            raise RuntimeError(
                "Appelez fit(X, y) avant d'utiliser le modèle ajusté."
            )

    def _ensure_same_n_features(self, X: np.ndarray) -> None:
        if X.shape[1] != self._n_features:
            message = (
                f"X contient {X.shape[1]} attributs; "
                f"{self._n_features} étaient attendus."
            )
            raise ValueError(message)

Mise en œuvre : courbe ROC

def compute_roc_curve(y_true, y_scores):
    y_true = np.asarray(y_true)
    y_scores = np.asarray(y_scores, dtype=float)
    thresholds = np.r_[np.inf, np.sort(np.unique(y_scores))[::-1]]
    tpr_list, fpr_list = [], []

    for threshold in thresholds:
        y_pred = (y_scores >= threshold).astype(int)
        tp = np.sum((y_true == 1) & (y_pred == 1))
        fn = np.sum((y_true == 1) & (y_pred == 0))
        fp = np.sum((y_true == 0) & (y_pred == 1))
        tn = np.sum((y_true == 0) & (y_pred == 0))

        tpr_list.append(tp / (tp + fn))  # taux de vrais positifs
        fpr_list.append(fp / (fp + tn))  # taux de faux positifs

    return np.array(fpr_list), np.array(tpr_list), thresholds

Mise en œuvre : AUROC

def compute_auroc(fpr, tpr):

    return np.trapezoid(tpr, fpr)

Exemple : générer les données et les prédictions

Code
from sklearn.datasets import make_blobs

X, y = make_blobs(
    n_samples=1000,
    n_features=2,
    centers=2,
    cluster_std=5,
    random_state=seed,
)

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.3,
    random_state=seed,
    stratify=y,
)

model = LogisticRegression(learning_rate=0.1, max_iter=500)
model.fit(X_train, y_train)

Exemple : tracer

Code
# Calculer les probabilités prédites de la classe positive sur l'ensemble de test
y_probs = model.predict_proba(X_test)

# Calculer la courbe ROC (TFP et TVP pour chaque seuil)
fpr, tpr, thresholds = compute_roc_curve(y_test, y_probs)
auroc_value = compute_auroc(fpr, tpr)

from sklearn.metrics import roc_auc_score
sklearn_auroc = roc_auc_score(y_test, y_probs)

print(f"AUROC manuelle :      {auroc_value:.3f}")
print(f"AUROC scikit-learn :  {sklearn_auroc:.3f}")

# Tracer la courbe ROC
plt.figure(figsize=(8, 6))
plt.plot(
    fpr,
    tpr,
    color="blue",
    lw=2,
    label=f"Courbe ROC (AUROC = {auroc_value:.2f})",
)
plt.plot(
    [0, 1],
    [0, 1],
    color="gray",
    lw=1,
    linestyle="--",
    label="Classificateur aléatoire",
)
plt.xlabel("Taux de faux positifs")
plt.ylabel("Taux de vrais positifs")
plt.title("Courbe caractéristique de fonctionnement du récepteur (ROC)")
plt.legend(loc="lower right")
plt.show()

Exemple : tracer

AUROC manuelle :      0.938
AUROC scikit-learn :  0.938

Courbe ROC produite avec la mise en œuvre de la régression logistique du cours.

Voir aussi

Conclusion

Résumé

  • Dérivé les décomptes binaires et un-contre-tous à partir de matrices de confusion.
  • Calculé et interprété l’exactitude, la précision, le rappel et le score F_1, y compris les limites de l’exactitude en présence d’un déséquilibre des classes.
  • Distingué la moyenne micro, qui accorde le même poids à chaque prédiction, de la moyenne macro, qui accorde le même poids à chaque classe.
  • Relié le seuil de décision au compromis précision-rappel et aux points de fonctionnement de la courbe ROC.
  • Interprété l’AUROC comme une mesure du classement produit par les scores. L’AUROC n’évalue pas l’étalonnage des probabilités et ne sélectionne pas de seuil de décision.

Références principales

  • Sokolova et Lapalme (2009) analyse systématiquement les mesures de performance pour les tâches de classification binaire, multiclasse et multiétiquette.
  • Hanley et McNeil (1982) établit l’interprétation probabiliste de la surface sous une courbe ROC.
  • Davis et Goadrich (2006) explique la relation entre les courbes ROC et précision-rappel, y compris l’effet d’une distribution déséquilibrée des classes.

Évaluation des algorithmes d’apprentissage

  • Ce livre examine le processus d’évaluation, en accordant une attention particulière aux algorithmes de classification (Japkowicz et Shah 2011).

  • Nathalie Japkowicz a été professeure à l’Université d’Ottawa.

  • Mohak Shah a obtenu son doctorat à l’Université d’Ottawa et a occupé plusieurs postes dans l’industrie en intelligence artificielle et en apprentissage automatique.

Prochain cours

  • Nous examinerons la validation croisée et le réglage des hyperparamètres.

Références

Davis, Jesse, et Mark Goadrich. 2006. « The relationship between Precision-Recall and ROC curves ». Proceedings of the 23rd international conference on Machine learning - ICML ’06, 233‑40. https://doi.org/10.1145/1143844.1143874.
Géron, Aurélien. 2022. Hands-on Machine Learning with Scikit-Learn, Keras, and TensorFlow. 3ᵉ éd. O’Reilly Media, Inc.
Hanley, J A, et B J McNeil. 1982. « The meaning and use of the area under a receiver operating characteristic (ROC) curve. » Radiology 143 (1): 29‑36. https://doi.org/10.1148/radiology.143.1.7063747.
Japkowicz, Nathalie, et Mohak Shah. 2011. Evaluating Learning Algorithms: a classification perspective. Cambridge University Press.
Knowler, William C., David J. Pettitt, Peter J. Savage, et Peter H. Bennett. 1981. « Diabetes incidence in Pima indians: contributions of obesity and parental diabetes. » American journal of epidemiology 113 2: 144‑56. https://api.semanticscholar.org/CorpusID:25209675.
Russell, Stuart, et Peter Norvig. 2020. Artificial Intelligence: A Modern Approach. 4ᵉ éd. Pearson. http://aima.cs.berkeley.edu/.
Sokolova, Marina, et Guy Lapalme. 2009. « A systematic analysis of performance measures for classification tasks ». Information Processing and Management 45 (4): 427‑37. https://doi.org/10.1016/j.ipm.2009.03.002.
Unceta, Irene, Paula Subı́as-Beltrán, et Oriol Pujol. 2026. « The epistemic debt of generative AI ». Nature Machine Intelligence 8 (9): 1328‑30. https://doi.org/10.1038/s42256-026-01294-w.

Marcel Turcotte

Marcel.Turcotte@uOttawa.ca

École de science informatique et de génie électrique (SIGE)

Université d’Ottawa

Annexe : 20 Newsgroups

Exemple complet : 20 Newsgroups

Nous utilisons le jeu de données textuelles 20 Newsgroups de scikit-learn.

Le jeu complet contient environ 18 000 messages provenant de 20 groupes de discussion. Cet exemple utilise quatre sujets.

Code
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import RidgeClassifier

categories = [
    "alt.atheism",
    "talk.religion.misc",
    "comp.graphics",
    "sci.space",
]

data_train = fetch_20newsgroups(
    subset="train",
    categories=categories,
    shuffle=True,
    random_state=seed,
)
data_test = fetch_20newsgroups(
    subset="test",
    categories=categories,
    shuffle=True,
    random_state=seed,
)

vectorizer = TfidfVectorizer(
    sublinear_tf=True,
    max_df=0.5,
    min_df=5,
    stop_words="english",
)
X_train = vectorizer.fit_transform(data_train.data)
X_test = vectorizer.transform(data_test.data)
y_train = data_train.target
y_test = data_test.target
target_names = data_train.target_names

clf = RidgeClassifier(tol=1e-2, solver="sparse_cg")
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)

fig, ax = plt.subplots(figsize=(10, 5))
ConfusionMatrixDisplay.from_predictions(y_test, y_pred, ax=ax)
ax.xaxis.set_ticklabels(target_names)
ax.yaxis.set_ticklabels(target_names)
ax.set_title(f"Matrice de confusion pour {clf.__class__.__name__}")
plt.show()

Exemple complet : 20 Newsgroups

Matrice de confusion à quatre classes pour des sujets sélectionnés de 20 Newsgroups.

Matrice de confusion sous forme de tableau

cm = confusion_matrix(y_test, y_pred)
cm
array([[258,   7,  12,  42],
       [  2, 380,   4,   3],
       [  1,  22, 371,   0],
       [ 37,   9,   6, 199]])

Décomptes un-contre-tous

def true_positive(cm, i):
    return cm[i, i]

def false_positive(cm, i):
    return np.sum(cm[:, i]) - cm[i, i]

def false_negative(cm, i):
    return np.sum(cm[i, :]) - cm[i, i]

def true_negative(cm, i):
    n_examples = cm.sum()
    tp = true_positive(cm, i)
    fp = false_positive(cm, i)
    fn = false_negative(cm, i)
    return n_examples - (tp + fp + fn)

Précision micro et macro

def precision_micro(cm):
    _, n_classes = cm.shape
    tp = fp = 0
    for i in range(n_classes):
        tp += true_positive(cm, i)
        fp += false_positive(cm, i)
    return tp / (tp + fp)

def precision_macro(cm):
    _, n_classes = cm.shape
    precision_sum = 0
    for i in range(n_classes):
        tp = true_positive(cm, i)
        fp = false_positive(cm, i)
        precision_sum += tp / (tp + fp)
    return precision_sum / n_classes

print(f"Précision micro : {precision_micro(cm):.3f}")
print(f"Précision macro : {precision_macro(cm):.3f}")
Précision micro : 0.893
Précision macro : 0.884

Précision moyenne micro

\frac{(258+380+371+199)}{(258+380+371+199)+(40+38+22+45)} où

  • 40 = 2 + 1 + 37
  • 38 = 7 + 22 + 9
  • 22 = 12 + 4 + 6
  • 45 = 42 + 3 + 0

Précision moyenne macro

  • \mathrm{Précision}_0 = \frac{258}{258+(2+1+37)} = 0,8657718121
  • \mathrm{Précision}_1 = \frac{380}{380+(7+22+9)} = 0,9090909091
  • \mathrm{Précision}_2 = \frac{371}{371+(12+4+6)} = 0,9440203562
  • \mathrm{Précision}_3 = \frac{199}{199+(42+3+0)} = 0,8155737705

\mathrm{Précision}_{\mathrm{macro}} = \frac{0,8657718121 + 0,9090909091 + 0,9440203562 + 0,8155737705}{4}

Rappel micro et macro

def recall_micro(cm):
    _, n_classes = cm.shape
    tp = fn = 0
    for i in range(n_classes):
        tp += true_positive(cm, i)
        fn += false_negative(cm, i)
    return tp / (tp + fn)

def recall_macro(cm):
    _, n_classes = cm.shape
    recall_sum = 0
    for i in range(n_classes):
        tp = true_positive(cm, i)
        fn = false_negative(cm, i)
        recall_sum += tp / (tp + fn)
    return recall_sum / n_classes

print(f"Rappel micro : {recall_micro(cm):.3f}")
print(f"Rappel macro : {recall_macro(cm):.3f}")
Rappel micro : 0.893
Rappel macro : 0.880

Annexe : comparaison des scores de classificateurs

Jeu de données OpenML

OpenML est une plateforme ouverte de partage de jeux de données, d’algorithmes et d’expériences qui favorise la reproductibilité en apprentissage automatique.

diabetes = fetch_openml(name="diabetes", version=1)
print(diabetes.DESCR)

Jeu de données OpenML

Author: Vincent Sigillito

Source: Obtained from UCI

Please cite: UCI citation policy

  1. Title: Pima Indians Diabetes Database

  2. Sources:

    1. Original owners: National Institute of Diabetes and Digestive and Kidney Diseases
    2. Donor of database: Vincent Sigillito (vgs@aplcen.apl.jhu.edu) Research Center, RMI Group Leader Applied Physics Laboratory The Johns Hopkins University Johns Hopkins Road Laurel, MD 20707 (301) 953-6231
    3. Date received: 9 May 1990
  3. Past Usage:

    1. Smith,J.W., Everhart,J.E., Dickson,W.C., Knowler,W.C., & Johannes,R.S. (1988). Using the ADAP learning algorithm to forecast the onset of diabetes mellitus. In {it Proceedings of the Symposium on Computer Applications and Medical Care} (pp. 261–265). IEEE Computer Society Press.

      The diagnostic, binary-valued variable investigated is whether the patient shows signs of diabetes according to World Health Organization criteria (i.e., if the 2 hour post-load plasma glucose was at least 200 mg/dl at any survey examination or if found during routine medical care). The population lives near Phoenix, Arizona, USA.

      Results: Their ADAP algorithm makes a real-valued prediction between 0 and 1. This was transformed into a binary decision using a cutoff of 0.448. Using 576 training instances, the sensitivity and specificity of their algorithm was 76% on the remaining 192 instances.

  4. Relevant Information: Several constraints were placed on the selection of these instances from a larger database. In particular, all patients here are females at least 21 years old of Pima Indian heritage. ADAP is an adaptive learning routine that generates and executes digital analogs of perceptron-like devices. It is a unique algorithm; see the paper for details.

  5. Number of Instances: 768

  6. Number of Attributes: 8 plus class

  7. For Each Attribute: (all numeric-valued)

    1. Number of times pregnant
    2. Plasma glucose concentration a 2 hours in an oral glucose tolerance test
    3. Diastolic blood pressure (mm Hg)
    4. Triceps skin fold thickness (mm)
    5. 2-Hour serum insulin (mu U/ml)
    6. Body mass index (weight in kg/(height in m)^2)
    7. Diabetes pedigree function
    8. Age (years)
    9. Class variable (0 or 1)
  8. Missing Attribute Values: None

  9. Class Distribution: (class value 1 is interpreted as “tested positive for diabetes”)

    Class Value Number of instances 0 500 1 268

  10. Brief statistical analysis:

    Attribute number: Mean: Standard Deviation:

    1.                 3.8     3.4
    2.               120.9    32.0
    3.                69.1    19.4
    4.                20.5    16.0
    5.                79.8   115.2
    6.                32.0     7.9
    7.                 0.5     0.3
    8.                33.2    11.8

Relabeled values in attribute ‘class’ From: 0 To: tested_negative
From: 1 To: tested_positive

Downloaded from openml.org.

Jeu de données sur le diabète des Indiennes Pima

# Charger le jeu de données sur le diabète des Indiennes Pima
pima = fetch_openml(data_id=37, as_frame=True)

# Extraire les attributs et la cible
X = pima.data
y = pima.target

# Encoder les deux étiquettes cibles avec 0 et 1
y = y.map({"tested_negative": 0, "tested_positive": 1})

# Diviser le jeu de données
X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.3,
    random_state=seed,
    stratify=y,
)

Comparaison de plusieurs classificateurs

from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier

Comparaison de plusieurs classificateurs

Code
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

lr = LogisticRegression(max_iter=1000)
lr.fit(X_train_scaled, y_train)

knn = KNeighborsClassifier()
knn.fit(X_train_scaled, y_train)

dt = DecisionTreeClassifier(random_state=seed)
dt.fit(X_train, y_train)

rf = RandomForestClassifier(random_state=seed)
rf.fit(X_train, y_train)

Courbes ROC et AUROC

Code
from sklearn.metrics import roc_auc_score

y_pred_prob_lr = lr.predict_proba(X_test_scaled)[:, 1]
y_pred_prob_knn = knn.predict_proba(X_test_scaled)[:, 1]
y_pred_prob_dt = dt.predict_proba(X_test)[:, 1]
y_pred_prob_rf = rf.predict_proba(X_test)[:, 1]

# Calculer les courbes ROC
fpr_lr, tpr_lr, _ = roc_curve(y_test, y_pred_prob_lr)
fpr_knn, tpr_knn, _ = roc_curve(y_test, y_pred_prob_knn)
fpr_dt, tpr_dt, _ = roc_curve(y_test, y_pred_prob_dt)
fpr_rf, tpr_rf, _ = roc_curve(y_test, y_pred_prob_rf)

# Calculer les valeurs d'AUROC
auroc_lr = roc_auc_score(y_test, y_pred_prob_lr)
auroc_knn = roc_auc_score(y_test, y_pred_prob_knn)
auroc_dt = roc_auc_score(y_test, y_pred_prob_dt)
auroc_rf = roc_auc_score(y_test, y_pred_prob_rf)

# Tracer les courbes ROC
plt.figure(figsize=(5, 5))
plt.plot(
    fpr_lr,
    tpr_lr,
    color="blue",
    label=f"Régression logistique (AUROC = {auroc_lr:.2f})",
)
plt.plot(
    fpr_knn,
    tpr_knn,
    color="green",
    label=f"k plus proches voisins (AUROC = {auroc_knn:.2f})",
)
plt.plot(
    fpr_dt,
    tpr_dt,
    color="orange",
    label=f"Arbre de décision (AUROC = {auroc_dt:.2f})",
)
plt.plot(
    fpr_rf,
    tpr_rf,
    color="purple",
    label=f"Forêt aléatoire (AUROC = {auroc_rf:.2f})",
)
plt.plot([0, 1], [0, 1], color="red", linestyle="--")
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel("Taux de faux positifs")
plt.ylabel("Taux de vrais positifs")
plt.title("Courbes ROC sur l'ensemble de test Pima")
plt.legend(loc="lower right")
plt.show()

Courbes ROC de quatre classificateurs sur l'ensemble de test du jeu de données sur le diabète des Indiennes Pima.