Matrice de confusion et courbe ROC en R : évaluer un modèle

Lire une matrice de confusion, calculer accuracy, précision, rappel, sensibilité, spécificité et F1, et tracer la courbe ROC avec l’AUC — et pourquoi l’accuracy seule trompe

Un guide pratique pour évaluer un modèle de classification en R. Construire une matrice de confusion ; calculer accuracy, précision, rappel (sensibilité), spécificité et le score F1 ; tracer la courbe ROC et lire l’AUC ; et voir pourquoi l’accuracy seule trompe sur des données déséquilibrées (utiliser précision-rappel et le seuil de probabilité). Calculé de deux façons — le jeu de métriques yardstick de tidymodels et le caret / pROC de base — sur PimaIndiansDiabetes2 (binaire) et iris (multiclasse, avec moyennage macro/micro).

Date de publication

25 juin 2026

Modifié

11 juillet 2026

AstucePoints clés
  • La matrice de confusion est le tableau 2×2 des prédictions face à la vérité — vrais positifs, vrais négatifs, faux positifs, faux négatifs. Toute métrique de classification en découle, à partir de ses quatre cases.
  • L’accuracy = la part de prédictions correctes. Intuitive, mais elle trompe sur des données déséquilibrées : si 67 % des cas sont négatifs, un modèle qui prédit toujours « négatif » obtient déjà 0.67 tout en n’attrapant aucun positif. Comparez-la toujours à la référence de la classe majoritaire.
  • La précision (« quand je prédis positif, ai-je raison ? »), le rappel / la sensibilité (« parmi les vrais positifs, combien en ai-je attrapé ? »), et la spécificité (« parmi les vrais négatifs, combien en ai-je écarté ? ») révèlent le compromis que l’accuracy masque. Le F1 en est l’équilibre.
  • La courbe ROC trace la sensibilité face à (1 − spécificité) pour chaque seuil de probabilité ; l’ AUC (l’aire sous la courbe) résume le modèle en un seul chiffre — 0.5 = aléatoire, > 0.8 = bon, 1.0 = parfait. ROC + précision-rappel ne dépendent pas du seuil de 0.5, ils résistent donc au déséquilibre des classes.
  • Le seuil de probabilité est un choix, pas une loi : 0.5 est la valeur par défaut, mais l’abaisser échange de la spécificité contre de la sensibilité (attraper plus de positifs, déclencher plus de fausses alertes).
  • Obtenez-les tous avec yardstick (conf_mat, accuracy, sens, spec, precision, recall, f_meas, roc_auc, roc_curve) — ou en base avec caret::confusionMatrix / pROC::roc.
Obtenez le livre — Machine Learning Essentials (PDF)

Introduction

Vous ajustez un classifieur — régression logistique, LDA, une forêt aléatoire — et il annonce « accuracy : 0.76. » Est-ce bon ? Cela dépend entièrement du type d’erreurs qu’il commet. Un dépistage du diabète qui n’attrape que la moitié des vrais cas peut tout de même afficher une accuracy élevée si la plupart des gens sont en bonne santé. L’accuracy seule est le chiffre auquel on accorde le plus de confiance à tort, le plus trompeur en classification.

Cette leçon est la boîte à outils d’évaluation à laquelle renvoie toute la série classification. Nous construirons la matrice de confusion — le tableau dont découle chaque métrique — puis nous calculerons et interpréterons en langage clair l’accuracy, la précision, le rappel (sensibilité), la spécificité et le F1, nous verrons exactement pourquoi l’accuracy trompe sur des données déséquilibrées (et quoi utiliser à la place), et nous tracerons la courbe ROC avec son AUC — le résumé d’un classifieur indépendant du seuil. Nous terminerons par le seuil de probabilité (0.5 est une valeur par défaut, pas une fatalité) et une section multiclasse (moyennage macro/micro/pondéré, ROC un-contre-tous).

Nous calculons chaque métrique de deux façons — la voie tidymodels yardstick utilisée dans tout ce pilier, et la voie de base caret / pROC que la plupart des utilisateurs de R connaissent déjà — pour que vous puissiez lire l’une ou l’autre des sorties que vous croiserez.

NoteÉvaluez sur le jeu de test, jamais sur le jeu d’entraînement

Chaque métrique ici est calculée sur un jeu de test mis de côté que le modèle n’a jamais vu pendant l’entraînement. Évalué sur les données d’entraînement, un classifieur se flatte lui-même. Pour le pourquoi et l’alternative par rééchantillonnage, voir la validation croisée. Ici, nous utilisons une simple division entraînement/test pour garder le focus sur les métriques.

Les données et un modèle rapide

Nous utilisons le jeu de données PimaIndiansDiabetes2 (le package mlbench) : 768 femmes d’origine pima, avec des prédicteurs cliniques (glucose, IMC, âge, …) et une issue binaire diabetespos (testée positive) ou neg. C’est le jeu d’enseignement classique de la classification binaire. Nous supprimons les lignes comportant des valeurs manquantes, puis divisons 80/20 en entraînement et test :

library(mlbench)
library(rsample)
data("PimaIndiansDiabetes2", package = "mlbench")

pima <- na.omit(PimaIndiansDiabetes2)
pima$diabetes <- factor(pima$diabetes, levels = c("neg", "pos"))  # "pos" is the event of interest

set.seed(123)
split      <- initial_split(pima, prop = 0.80, strata = diabetes)
train_data <- training(split)
test_data  <- testing(split)

c(train = nrow(train_data), test = nrow(test_data))
train  test 
  313    79 

Nous fixons levels = c("neg", "pos") pour que la classe positive (pos, les cas de diabète qui nous intéressent) soit le deuxième niveau — cela comptera dans un instant, lorsque nous indiquerons à yardstick quelle classe est l’« événement ».

Ajustons maintenant une régression logistique rapide sur le jeu d’entraînement et obtenons, pour chaque patiente du test, la probabilité prédite par le modèle d’être positive :

library(mlbench)
library(rsample)
data("PimaIndiansDiabetes2", package = "mlbench")
pima <- na.omit(PimaIndiansDiabetes2)
pima$diabetes <- factor(pima$diabetes, levels = c("neg", "pos"))
set.seed(123)
split      <- initial_split(pima, prop = 0.80, strata = diabetes)
train_data <- training(split); test_data <- testing(split)

model <- glm(diabetes ~ ., data = train_data, family = binomial)

# Predicted P(diabetes = pos) for every test patient
prob_pos <- predict(model, test_data, type = "response")
head(round(prob_pos, 3))
   19    21    32    55    64    71 
0.192 0.486 0.663 0.798 0.279 0.147 

Pour obtenir les classes prédites, nous appliquons un seuil — le 0.5 standard : une patiente est étiquetée pos si sa probabilité prédite dépasse 0.5. (Nous reviendrons sur ce seuil plus tard ; c’est un choix, pas une loi.)

library(mlbench)
library(rsample)
data("PimaIndiansDiabetes2", package = "mlbench")
pima <- na.omit(PimaIndiansDiabetes2)
pima$diabetes <- factor(pima$diabetes, levels = c("neg", "pos"))
set.seed(123)
split <- initial_split(pima, prop = 0.80, strata = diabetes)
train_data <- training(split); test_data <- testing(split)
model <- glm(diabetes ~ ., data = train_data, family = binomial)
prob_pos <- predict(model, test_data, type = "response")

predicted <- factor(ifelse(prob_pos > 0.5, "pos", "neg"), levels = c("neg", "pos"))
observed  <- test_data$diabetes

table(head(predicted))   # a peek at the predicted labels

neg pos 
  4   2 

Nous avons maintenant tout ce dont l’évaluation a besoin : les vraies étiquettes (observed), les étiquettes prédites (predicted) et les probabilités prédites (prob_pos).

La matrice de confusion

La matrice de confusion est le point de départ de toute l’évaluation en classification. Elle croise ce que le modèle a prédit avec ce qui était réellement vrai — quatre comptages dont découle toute autre métrique. La version d’une ligne en base R est simplement table() :

library(mlbench)
library(rsample)
data("PimaIndiansDiabetes2", package = "mlbench")
pima <- na.omit(PimaIndiansDiabetes2)
pima$diabetes <- factor(pima$diabetes, levels = c("neg", "pos"))
set.seed(123)
split <- initial_split(pima, prop = 0.80, strata = diabetes)
train_data <- training(split); test_data <- testing(split)
model <- glm(diabetes ~ ., data = train_data, family = binomial)
prob_pos  <- predict(model, test_data, type = "response")
predicted <- factor(ifelse(prob_pos > 0.5, "pos", "neg"), levels = c("neg", "pos"))
observed  <- test_data$diabetes

table(observed, predicted)
        predicted
observed neg pos
     neg  45   8
     pos  11  15

Lisez le tableau par ses quatre cases (avec pos comme classe positive — les cas de diabète) :

  • Vrais positifs (TP) = 15 — prédits pos, et ils étaient pos. Diabète correctement attrapé.
  • Vrais négatifs (TN) = 45 — prédits neg, et ils étaient neg. Correctement écartés.
  • Faux positifs (FP) = 8 — prédits pos, mais ils étaient neg. Une fausse alerte (erreur de type I).
  • Faux négatifs (FN) = 11 — prédits neg, mais ils étaient pos. Un cas manqué (erreur de type II).

La diagonale (TP + TN = 60) correspond aux prédictions correctes ; la hors-diagonale (FP + FN = 19) aux erreurs. Dans un dépistage médical, les deux erreurs ne sont pas aussi coûteuses l’une que l’autre — un faux négatif (un diabétique manqué) est habituellement bien pire qu’une fausse alerte — ce qui explique précisément pourquoi nous regardons au-delà d’un seul chiffre d’accuracy.

yardstick donne le même tableau avec conf_mat(), et une heatmap avec autoplot() :

library(mlbench)
library(rsample)
library(yardstick)
data("PimaIndiansDiabetes2", package = "mlbench")
pima <- na.omit(PimaIndiansDiabetes2)
pima$diabetes <- factor(pima$diabetes, levels = c("neg", "pos"))
set.seed(123)
split <- initial_split(pima, prop = 0.80, strata = diabetes)
train_data <- training(split); test_data <- testing(split)
model <- glm(diabetes ~ ., data = train_data, family = binomial)
prob_pos  <- predict(model, test_data, type = "response")

eval_df <- data.frame(
  truth     = test_data$diabetes,
  predicted = factor(ifelse(prob_pos > 0.5, "pos", "neg"), levels = c("neg", "pos")),
  prob_pos  = prob_pos
)

conf_mat(eval_df, truth = truth, estimate = predicted)
          Truth
Prediction neg pos
       neg  45  11
       pos   8  15
library(mlbench)
library(rsample)
library(yardstick)
library(ggplot2)
data("PimaIndiansDiabetes2", package = "mlbench")
pima <- na.omit(PimaIndiansDiabetes2)
pima$diabetes <- factor(pima$diabetes, levels = c("neg", "pos"))
set.seed(123)
split <- initial_split(pima, prop = 0.80, strata = diabetes)
train_data <- training(split); test_data <- testing(split)
model <- glm(diabetes ~ ., data = train_data, family = binomial)
prob_pos  <- predict(model, test_data, type = "response")
eval_df <- data.frame(
  truth     = test_data$diabetes,
  predicted = factor(ifelse(prob_pos > 0.5, "pos", "neg"), levels = c("neg", "pos"))
)

autoplot(conf_mat(eval_df, truth, predicted), type = "heatmap") +
  scale_fill_gradient(low = "#eaf2fb", high = "#3a86d4") +
  theme_minimal()

Confusion-matrix heatmap with truth on one axis and prediction on the other; the diagonal cells (true negatives = 45, true positives = 15) are darkest, the off-diagonal error cells lighter.

La diagonale sombre correspond aux prédictions correctes ; les cases hors-diagonale plus claires aux erreurs. Un coup d’œil vous dit le modèle se trompe — ici, il manque plus de positifs (11) qu’il ne déclenche de fausses alertes (8).

L’accuracy — et pourquoi elle trompe sur des données déséquilibrées

L’accuracy est la proportion de prédictions qui étaient correctes : (TP + TN) / total. La métrique la plus intuitive — et celle à laquelle on accorde le plus de confiance à tort.

library(mlbench)
library(rsample)
library(yardstick)
data("PimaIndiansDiabetes2", package = "mlbench")
pima <- na.omit(PimaIndiansDiabetes2)
pima$diabetes <- factor(pima$diabetes, levels = c("neg", "pos"))
set.seed(123)
split <- initial_split(pima, prop = 0.80, strata = diabetes)
train_data <- training(split); test_data <- testing(split)
model <- glm(diabetes ~ ., data = train_data, family = binomial)
prob_pos  <- predict(model, test_data, type = "response")
eval_df <- data.frame(
  truth     = test_data$diabetes,
  predicted = factor(ifelse(prob_pos > 0.5, "pos", "neg"), levels = c("neg", "pos"))
)

accuracy(eval_df, truth = truth, estimate = predicted)
# A tibble: 1 × 3
  .metric  .estimator .estimate
  <chr>    <chr>          <dbl>
1 accuracy binary         0.759

Notre modèle obtient une accuracy ≈ 0.76 — il classe correctement environ trois patientes de test sur quatre. Cela paraît correct. Voici le piège. Le diabète est minoritaire ici : seuls ~33 % du jeu de test sont pos, donc ~67 % sont neg. Un modèle qui ne fait rien, prédisant simplement « négatif » pour tout le monde, obtiendrait déjà :

library(mlbench)
library(rsample)
data("PimaIndiansDiabetes2", package = "mlbench")
pima <- na.omit(PimaIndiansDiabetes2)
pima$diabetes <- factor(pima$diabetes, levels = c("neg", "pos"))
set.seed(123)
split <- initial_split(pima, prop = 0.80, strata = diabetes)
test_data <- testing(split)

# "Always predict the majority class (neg)" accuracy = prevalence of neg
mean(test_data$diabetes == "neg")
[1] 0.6708861

Une accuracy de 0.67 tout en n’attrapant aucun diabétique. Le 0.76 de notre modèle n’est que ~9 points au-dessus d’un modèle qui ne fait rien d’utile — et c’est cet écart, pas le 0.76, qui est le vrai signal. Voilà pourquoi l’accuracy seule trompe sur des données déséquilibrées : elle récompense la prédiction de la classe majoritaire et masque si vous attrapez la classe rare qui compte habituellement le plus. La solution consiste à regarder des métriques qui séparent les deux types d’erreur — précision, rappel et spécificité — et des résumés indépendants du seuil (ROC/AUC, précision-rappel) qui ne dépendent pas du seuil de 0.5.

Précision, rappel (sensibilité), spécificité et F1

Ce sont les métriques qui révèlent ce que l’accuracy masque. Chacune répond à une question différente et concrète :

  • Précision = TP / (TP + FP)« quand le modèle prédit positif, à quelle fréquence a-t-il raison ? » Une précision élevée signifie peu de fausses alertes. Surveillez-la quand agir sur un positif coûte cher (une transaction signalée déclenche une vérification coûteuse).
  • Rappel, alias sensibilité ou taux de vrais positifs = TP / (TP + FN)« parmi tous les vrais positifs, combien le modèle en a-t-il attrapé ? » Un rappel élevé signifie peu de cas manqués. Surveillez-le quand manquer un positif est dangereux (une maladie non diagnostiquée).
  • Spécificité, le taux de vrais négatifs = TN / (TN + FP)« parmi tous les vrais négatifs, combien le modèle en a-t-il correctement écartés ? » Le miroir du rappel, pour la classe négative.
  • Score F1 = la moyenne harmonique de la précision et du rappel — un seul chiffre qui n’est élevé que lorsque les deux sont élevés. Le résumé de référence quand la classe positive compte et que les données sont déséquilibrées.

Il y a un compromis inhérent : pousser le rappel vers le haut (attraper chaque positif) fait habituellement baisser la précision et la spécificité (plus de fausses alertes), et inversement. Aucune métrique unique n’est « la » réponse — vous choisissez selon l’erreur qui coûte le plus cher. Calculez-les toutes d’un coup avec un metric_set() de yardstick, en lui indiquant que l’ événement est le deuxième niveau de facteur (pos) :

library(mlbench)
library(rsample)
library(yardstick)
data("PimaIndiansDiabetes2", package = "mlbench")
pima <- na.omit(PimaIndiansDiabetes2)
pima$diabetes <- factor(pima$diabetes, levels = c("neg", "pos"))
set.seed(123)
split <- initial_split(pima, prop = 0.80, strata = diabetes)
train_data <- training(split); test_data <- testing(split)
model <- glm(diabetes ~ ., data = train_data, family = binomial)
prob_pos  <- predict(model, test_data, type = "response")
eval_df <- data.frame(
  truth     = test_data$diabetes,
  predicted = factor(ifelse(prob_pos > 0.5, "pos", "neg"), levels = c("neg", "pos"))
)

class_metrics <- metric_set(accuracy, sens, spec, precision, recall, f_meas)
class_metrics(eval_df, truth = truth, estimate = predicted, event_level = "second")
# A tibble: 6 × 3
  .metric   .estimator .estimate
  <chr>     <chr>          <dbl>
1 accuracy  binary         0.759
2 sens      binary         0.577
3 spec      binary         0.849
4 precision binary         0.652
5 recall    binary         0.577
6 f_meas    binary         0.612

Lisez chaque chiffre en langage clair — c’est tout l’intérêt :

  • Sensibilité / rappel ≈ 0.58 — le modèle n’attrape qu’environ 58 % des vrais diabétiques ; à peu près quatre sur dix passent à travers en faux négatifs. Pour un test de dépistage, c’est le chiffre inquiétant.
  • Spécificité ≈ 0.85 — il écarte correctement 85 % des patientes en bonne santé. Peu de fausses alertes parmi les négatifs.
  • Précision ≈ 0.65 — quand il dit « diabète », il a raison environ 65 % du temps.
  • F1 ≈ 0.61 — l’équilibre précision/rappel. Modeste, tiré vers le bas par le faible rappel — et un résumé bien plus honnête que l’accuracy de 0.76, car il ignore la majorité de négatifs faciles à classer.

Remarquez l’histoire que le seul chiffre d’accuracy ne pouvait pas raconter : ce modèle est bon pour écarter les personnes en bonne santé mais médiocre pour attraper les diabétiques. Dans un dépistage médical, vous voudriez probablement une sensibilité plus élevée, même au prix de plus de fausses alertes — ce que nous pouvons obtenir en abaissant le seuil de probabilité (ci-dessous).

Les mêmes métriques à la base : caret::confusionMatrix

Si vous venez de caret, confusionMatrix() affiche la matrice de confusion et toute la batterie de métriques en un seul appel. Passez positive = "pos" pour qu’il sache quelle classe est l’événement :

library(mlbench)
library(rsample)
library(caret)
data("PimaIndiansDiabetes2", package = "mlbench")
pima <- na.omit(PimaIndiansDiabetes2)
pima$diabetes <- factor(pima$diabetes, levels = c("neg", "pos"))
set.seed(123)
split <- initial_split(pima, prop = 0.80, strata = diabetes)
train_data <- training(split); test_data <- testing(split)
model <- glm(diabetes ~ ., data = train_data, family = binomial)
prob_pos  <- predict(model, test_data, type = "response")
predicted <- factor(ifelse(prob_pos > 0.5, "pos", "neg"), levels = c("neg", "pos"))

confusionMatrix(predicted, test_data$diabetes, positive = "pos")
Confusion Matrix and Statistics

          Reference
Prediction neg pos
       neg  45  11
       pos   8  15
                                          
               Accuracy : 0.7595          
                 95% CI : (0.6502, 0.8486)
    No Information Rate : 0.6709          
    P-Value [Acc > NIR] : 0.05705         
                                          
                  Kappa : 0.4389          
                                          
 Mcnemar's Test P-Value : 0.64636         
                                          
            Sensitivity : 0.5769          
            Specificity : 0.8491          
         Pos Pred Value : 0.6522          
         Neg Pred Value : 0.8036          
             Prevalence : 0.3291          
         Detection Rate : 0.1899          
   Detection Prevalence : 0.2911          
      Balanced Accuracy : 0.7130          
                                          
       'Positive' Class : pos             
                                          

Les mêmes chiffres, en plus grand nombre. L’accuracy en tête d’affiche est 0.76 ; le Kappa ≈ 0.44 est l’accuracy corrigée du hasard (0 = pas mieux que deviner, 1 = parfait — un compagnon utile, sensible au déséquilibre, de l’accuracy brute). Sensitivity (0.58), Specificity (0.85) et Pos Pred Value (= la précision, 0.65) correspondent exactement aux valeurs de yardstick — même évaluation, deux interfaces.

La courbe ROC et l’AUC

Chaque métrique jusqu’ici dépendait du seuil de 0.5. Mais celui-ci est arbitraire — changez-le et la précision, le rappel et le reste bougent tous. La courbe ROC (Receiver Operating Characteristic) vous libère de tout seuil unique : elle trace la sensibilité (taux de vrais positifs) face à 1 − spécificité (taux de faux positifs) à chaque seuil possible, retraçant toute la courbe de compromis du modèle.

Un modèle qui ne fait pas mieux que le hasard donne la ligne diagonale. Un bon modèle s’incurve vers le coin supérieur gauche — sensibilité élevée et spécificité élevée à la fois. L’AUC (Area Under the Curve) ramène toute la courbe à un seul chiffre :

  • AUC = 0.5 → tirage au hasard (la diagonale).
  • AUC > 0.8 → un bon classifieur.
  • AUC = 1.0 → séparation parfaite.

Surtout, la ROC et l’AUC sont calculées à partir des probabilités prédites, et non des classes seuillées à 0.5 — elles ne dépendent donc pas du seuil et résistent au déséquilibre des classes bien mieux que l’accuracy. yardstick calcule l’AUC avec roc_auc() et la courbe avec roc_curve() + autoplot() :

library(mlbench)
library(rsample)
library(yardstick)
data("PimaIndiansDiabetes2", package = "mlbench")
pima <- na.omit(PimaIndiansDiabetes2)
pima$diabetes <- factor(pima$diabetes, levels = c("neg", "pos"))
set.seed(123)
split <- initial_split(pima, prop = 0.80, strata = diabetes)
train_data <- training(split); test_data <- testing(split)
model <- glm(diabetes ~ ., data = train_data, family = binomial)
prob_pos  <- predict(model, test_data, type = "response")
eval_df   <- data.frame(truth = test_data$diabetes, prob_pos = prob_pos)

roc_auc(eval_df, truth = truth, prob_pos, event_level = "second")
# A tibble: 1 × 3
  .metric .estimator .estimate
  <chr>   <chr>          <dbl>
1 roc_auc binary         0.844

L’AUC ≈ 0.84 — au-dessus de la barre « bon » de 0.8. Elle se lit ainsi : prenez un diabétique au hasard et une patiente en bonne santé au hasard, et le modèle attribue au diabétique la probabilité prédite la plus élevée environ 84 % du temps. Un verdict bien plus robuste que l’accuracy de 0.76, car il ignore entièrement le seuil. Maintenant, la figure phare — la courbe elle-même :

library(mlbench)
library(rsample)
library(yardstick)
library(ggplot2)
data("PimaIndiansDiabetes2", package = "mlbench")
pima <- na.omit(PimaIndiansDiabetes2)
pima$diabetes <- factor(pima$diabetes, levels = c("neg", "pos"))
set.seed(123)
split <- initial_split(pima, prop = 0.80, strata = diabetes)
train_data <- training(split); test_data <- testing(split)
model <- glm(diabetes ~ ., data = train_data, family = binomial)
prob_pos  <- predict(model, test_data, type = "response")
eval_df   <- data.frame(truth = test_data$diabetes, prob_pos = prob_pos)

roc_curve(eval_df, truth = truth, prob_pos, event_level = "second") |>
  autoplot() +
  annotate("text", x = 0.62, y = 0.30, label = "AUC = 0.84",
           color = "#3a86d4", fontface = "bold", size = 5) +
  theme_minimal()

ROC curve for the diabetes model rising steeply toward the top-left corner and sitting well above the dashed diagonal no-skill line, indicating a good classifier with AUC around 0.84.

La courbe monte fortement vers le coin supérieur gauche et reste bien au-dessus de la diagonale — la signature visuelle d’un bon classifieur. Le classifieur idéal épouserait exactement le coin supérieur gauche (sensibilité 1, spécificité 1) ; la diagonale est un pile ou face. Chaque point de la courbe est un seuil possible, ce qui est la passerelle vers le choix d’un seuil ensuite.

La même courbe à la base : pROC

L’outil de référence en base R pour l’analyse ROC est pROC. roc() construit l’objet et plot() la dessine avec l’AUC imprimée dessus :

library(mlbench)
library(rsample)
library(pROC)
data("PimaIndiansDiabetes2", package = "mlbench")
pima <- na.omit(PimaIndiansDiabetes2)
pima$diabetes <- factor(pima$diabetes, levels = c("neg", "pos"))
set.seed(123)
split <- initial_split(pima, prop = 0.80, strata = diabetes)
train_data <- training(split); test_data <- testing(split)
model <- glm(diabetes ~ ., data = train_data, family = binomial)
prob_pos  <- predict(model, test_data, type = "response")

res_roc <- roc(test_data$diabetes, prob_pos, levels = c("neg", "pos"), direction = "<")
plot(res_roc, print.auc = TRUE, col = "#3a86d4", legacy.axes = TRUE)

pROC ROC curve for the diabetes model with the AUC value printed on the plot, rising toward the top-left corner above the diagonal reference line.

pROC rapporte la même AUC ≈ 0.84. Les deux moteurs s’accordent exactement — yardstick pour les pipelines tidymodels, pROC pour la voie base R et ses extras supplémentaires (intervalles de confiance, tests de DeLong pour comparer deux courbes).

AstuceROC vs précision-rappel sur des données déséquilibrées

La ROC/AUC résistent au déséquilibre, mais lorsque les positifs sont très rares (fraude, maladie rare), la courbe précision-rappel est encore plus informative — elle se concentre sur la classe positive et n’est pas flattée par l’énorme réservoir de négatifs faciles. Dans yardstick : pr_curve() et pr_auc(), utilisés exactement comme roc_curve()/roc_auc(). Rapportez les deux quand les classes sont fortement déséquilibrées.

Choisir le seuil de probabilité

Tout ce qui précède utilisait le seuil par défaut de 0.5 pour transformer les probabilités en classes. Mais 0.5 est une valeur par défaut, pas une loi. Le point faible de notre modèle était le rappel — il manquait 42 % des diabétiques. Abaissez le seuil (déclarez plus de patientes positives) et vous en attrapez davantage — au prix de plus de fausses alertes (spécificité plus basse). La courbe ROC est précisément le menu de ces compromis.

pROC::coords() trouve le seuil qui équilibre le mieux sensibilité et spécificité (le « meilleur » point sur la courbe ROC, selon l’indice de Youden — le seuil le plus éloigné au-dessus de la diagonale) :

library(mlbench)
library(rsample)
library(pROC)
data("PimaIndiansDiabetes2", package = "mlbench")
pima <- na.omit(PimaIndiansDiabetes2)
pima$diabetes <- factor(pima$diabetes, levels = c("neg", "pos"))
set.seed(123)
split <- initial_split(pima, prop = 0.80, strata = diabetes)
train_data <- training(split); test_data <- testing(split)
model <- glm(diabetes ~ ., data = train_data, family = binomial)
prob_pos  <- predict(model, test_data, type = "response")
res_roc   <- roc(test_data$diabetes, prob_pos, levels = c("neg", "pos"), direction = "<")

coords(res_roc, "best", best.method = "youden",
       ret = c("threshold", "sensitivity", "specificity"))
  threshold sensitivity specificity
1 0.2101307   0.8846154   0.6792453

Le seuil du meilleur équilibre est d’environ 0.21, et non 0.5 — à ce seuil, la sensibilité bondit à ~0.88 (attrapant bien plus de diabétiques) tandis que la spécificité se stabilise autour de ~0.68. Pour un test de dépistage, c’est souvent le bon choix : vous acceptez plus de fausses alertes pour manquer moins de vrais cas. La leçon : le seuil est une décision que vous prenez à partir du coût de chaque type d’erreur — pas un nombre à laisser à 0.5 par défaut.

Évaluation multiclasse

Tout jusqu’ici était binaire. Avec trois classes ou plus, les idées se transposent, mais chaque métrique est calculée par classe (un-contre-tous : cette classe comme positive, toutes les autres comme négatives) puis moyennée entre les classes. Nous utiliserons le jeu de données iris — prédire l’espèce Species (trois classes : setosa, versicolor, virginica) — avec un modèle LDA rapide :

library(rsample)
library(MASS)
data("iris")

set.seed(123)
split      <- initial_split(iris, prop = 0.80, strata = Species)
train_data <- training(split)
test_data  <- testing(split)

iris_model <- lda(Species ~ ., data = train_data)
iris_pred  <- predict(iris_model, test_data)
head(iris_pred$class)
[1] setosa setosa setosa setosa setosa setosa
Levels: setosa versicolor virginica

La matrice de confusion est désormais 3×3 — la diagonale correspond aux prédictions correctes, les cases hors-diagonale montrent quelles espèces sont confondues avec quelles autres :

library(rsample)
library(MASS)
library(yardstick)
data("iris")
set.seed(123)
split <- initial_split(iris, prop = 0.80, strata = Species)
train_data <- training(split); test_data <- testing(split)
iris_model <- lda(Species ~ ., data = train_data)
iris_pred  <- predict(iris_model, test_data)

iris_eval <- data.frame(truth = test_data$Species, predicted = iris_pred$class)
conf_mat(iris_eval, truth = truth, estimate = predicted)
            Truth
Prediction   setosa versicolor virginica
  setosa         10          0         0
  versicolor      0         10         1
  virginica       0          0         9

setosa est parfaitement séparée (elle l’est toujours) ; la seule confusion est une virginica lue à tort comme une versicolor — les deux espèces qui se chevauchent réellement. Maintenant, les métriques moyennées. Le nouvel argument clé est estimator, qui contrôle la façon dont les scores par classe sont combinés :

  • "macro" — la moyenne non pondérée des métriques par classe. Chaque classe compte également, même une classe rare. Utilisez-la quand la performance sur la classe minoritaire compte autant que celle de la majorité.
  • "micro" — met en commun tous les TP/FP/FN des classes en un seul comptage global, puis calcule la métrique une seule fois. Elle est dominée par les classes fréquentes (pour les métriques de type accuracy, micro = accuracy globale).
  • "macro_weighted" — macro, mais chaque classe pondérée par son support (le nombre de cas vrais qu’elle possède) — un compromis.
library(rsample)
library(MASS)
library(yardstick)
data("iris")
set.seed(123)
split <- initial_split(iris, prop = 0.80, strata = Species)
train_data <- training(split); test_data <- testing(split)
iris_model <- lda(Species ~ ., data = train_data)
iris_pred  <- predict(iris_model, test_data)
iris_eval  <- data.frame(truth = test_data$Species, predicted = iris_pred$class)

# Overall accuracy + macro-averaged precision/recall/F1
# (yardstick:: qualifies the calls in case caret is also attached — both export precision/recall)
yardstick::accuracy(iris_eval, truth, predicted)
# A tibble: 1 × 3
  .metric  .estimator .estimate
  <chr>    <chr>          <dbl>
1 accuracy multiclass     0.967
yardstick::precision(iris_eval, truth, predicted, estimator = "macro")
# A tibble: 1 × 3
  .metric   .estimator .estimate
  <chr>     <chr>          <dbl>
1 precision macro          0.970
yardstick::recall(iris_eval, truth, predicted, estimator = "macro")
# A tibble: 1 × 3
  .metric .estimator .estimate
  <chr>   <chr>          <dbl>
1 recall  macro          0.967
yardstick::f_meas(iris_eval, truth, predicted, estimator = "macro")
# A tibble: 1 × 3
  .metric .estimator .estimate
  <chr>   <chr>          <dbl>
1 f_meas  macro          0.967

L’accuracy globale ≈ 0.97, et les précision/rappel/F1 moyennés en macro ≈ 0.97 aussi — excellent, et parce que les trois classes d’iris sont équilibrées, macro et micro donnent presque la même réponse (elles divergent seulement quand les classes sont déséquilibrées). Pour un problème déséquilibré, rapportez le macro afin qu’un modèle ne puisse pas cacher une mauvaise performance sur la classe minoritaire derrière une classe majoritaire forte.

La ROC/AUC s’étendent aussi au multiclasse via le un-contre-tous : roc_auc() calcule une courbe par classe et les moyenne. Les probabilités de classe prédites (colonnes .pred_<class>) la pilotent :

library(rsample)
library(MASS)
library(yardstick)
data("iris")
set.seed(123)
split <- initial_split(iris, prop = 0.80, strata = Species)
train_data <- training(split); test_data <- testing(split)
iris_model <- lda(Species ~ ., data = train_data)
iris_pred  <- predict(iris_model, test_data)

# Assemble truth + the three class-probability columns yardstick expects
probs <- as.data.frame(iris_pred$posterior)
names(probs) <- paste0(".pred_", names(probs))
iris_prob <- cbind(truth = test_data$Species, probs)

# Multiclass AUC: macro-averaged one-vs-all
yardstick::roc_auc(iris_prob, truth = truth,
                   .pred_setosa, .pred_versicolor, .pred_virginica,
                   estimator = "macro")
# A tibble: 1 × 3
  .metric .estimator .estimate
  <chr>   <chr>          <dbl>
1 roc_auc macro              1

Une AUC multiclasse de 1.0 — le modèle LDA sépare les trois espèces essentiellement à la perfection sur ce jeu de test. (L’estimateur multiclasse par défaut de yardstick est celui de Hand & Till, une moyenne un-contre-tous apparentée ; estimator = "macro" rend le moyennage explicite.)

Chaque métrique binaire est un rapport des quatre cases de la matrice de confusion. Avec TP, TN, FP, FN, écrivons l’accuracy comme \(A\), la précision comme \(P\), le rappel (sensibilité) comme \(R\), la spécificité comme \(S\), et le score \(F_1\) comme \(F_1\) :

\[ A = \frac{TP + TN}{TP + TN + FP + FN}, \qquad P = \frac{TP}{TP + FP}, \]

\[ R = \frac{TP}{TP + FN}, \qquad S = \frac{TN}{TN + FP}, \]

\[ F_1 = 2 \cdot \frac{P \cdot R}{P + R}. \]

En injectant nos comptages de diabète (TP = 15, TN = 45, FP = 8, FN = 11) : accuracy = (15 + 45)/79 ≈ 0.76 ; précision = 15/(15 + 8) ≈ 0.65 ; rappel = 15/(15 + 11) ≈ 0.58 ; spécificité = 45/(45 + 8) ≈ 0.85 — exactement les chiffres que yardstick et caret ont rapportés. Le taux de faux positifs tracé sur l’axe des x de la ROC est simplement 1 − specificity = FP/(TN + FP), et l’axe des y est le rappel. Balayer le seuil de probabilité déplace chaque comptage, traçant toute la courbe ROC.

🟢 Avec un agent IA

Vous ne savez pas si votre classifieur est réellement bon, ou si l’accuracy cache un problème ? Demandez à Prova « évalue mon modèle de classification — matrice de confusion, précision/rappel, ROC et AUC — et dis- moi si l’accuracy m’induit en erreur » — elle répond avec du code R que vous pouvez exécuter sur vos propres prédictions, construit la matrice de confusion, calcule chaque métrique, trace la courbe ROC, et vous aide à les lire. The runtime is the judge. Demandez à Prova →

Problèmes courants

  • La précision/le rappel sortent « à l’envers » (ou en NA). yardstick traite par défaut le premier niveau de facteur comme l’événement. Si votre classe positive est le deuxième niveau (comme pos ici), passez event_level = "second" — sinon il évalue la classe négative comme l’événement. Dans caret, fixez positive = "pos". Vérifiez toujours de quelle classe vos métriques parlent.
  • Votre courbe ROC paraît à l’envers (AUC < 0.5). Les probabilités sont orientées vers la mauvaise classe — vous avez fourni P(negative) là où P(positive) était attendue, ou pROC a deviné la direction. Passez la probabilité de la classe positive, et dans pROC fixez levels = c("neg", "pos") + direction = "<" pour corriger l’ orientation. Une AUC de, disons, 0.16 signifie habituellement un 0.84 inversé.
  • Une excellente accuracy en réalité inutile. Sur des données déséquilibrées, l’accuracy peut se situer près de la prévalence de la classe majoritaire tandis que le modèle n’attrape aucun cas de la minorité. Comparez toujours l’accuracy à la référence d’absence de discrimination (mean(truth == majority_class)), et rapportez le rappel, le F1 et l’AUC à côté.
  • ROC/AUC calculées à partir des classes prédites au lieu des probabilités. La ROC a besoin de la probabilité continue (ou du score), pas des étiquettes 0/1 — passez prob_pos, jamais le facteur predicted seuillé, à roc_auc() / roc().

Questions fréquentes

Croisez les vraies étiquettes et les étiquettes prédites. Base R : table(observed, predicted). tidymodels : yardstick::conf_mat(data, truth = truth_col, estimate = pred_col), avec autoplot(..., type = "heatmap") pour une visualisation. Le caret::confusionMatrix(predicted, observed, positive = "pos") de base affiche la matrice et l’accuracy, la sensibilité, la spécificité et plus encore en un seul appel.

À partir des probabilités prédites (et non des classes). tidymodels : yardstick::roc_curve(data, truth, prob_col) |> autoplot(). Base R : library(pROC); roc_obj <- roc(observed, prob_col); plot(roc_obj, print.auc = TRUE). La courbe trace la sensibilité face à 1 − spécificité pour tous les seuils de probabilité.

L’AUC est l’aire sous la courbe ROC. tidymodels : yardstick::roc_auc(data, truth, prob_col, event_level = "second"). Base R : pROC::auc(roc(observed, prob_col)), ou ajoutez print.auc = TRUE lors du tracé. L’AUC va de 0.5 (aléatoire) à 1.0 (parfait) ; au-dessus de 0.8 est généralement considéré comme bon. Calculez-la toujours à partir des probabilités prédites, jamais à partir des classes seuillées.

Précision = TP / (TP + FP) — parmi tout ce que le modèle a prédit positif, quelle part était vraiment positive (peu de fausses alertes). Rappel (= sensibilité) = TP / (TP + FN) — parmi tous les vrais positifs, combien ont été attrapés (peu de manqués). Calculez-les avec yardstick::precision() et yardstick::recall() (ou l’alias sens() de recall()). Le score F1 (f_meas()) est leur moyenne harmonique — élevé seulement quand les deux sont élevés.

La sensibilité (le taux de vrais positifs, = rappel) est la proportion de vrais positifs que le modèle attrape correctement : yardstick::sens(). La spécificité (le taux de vrais négatifs) est la proportion de vrais négatifs qu’il écarte correctement : yardstick::spec(). Un dépistage médical privilégie habituellement la sensibilité (ne pas manquer un patient malade) ; un test de confirmation privilégie la spécificité (ne pas alarmer une personne en bonne santé). Les deux viennent de la même matrice de confusion.

Testez vos connaissances

Un détecteur de fraude est testé sur 10 000 transactions, dont seulement 100 sont des fraudes. Il prédit « pas de fraude » pour chaque transaction. (a) Quelle est son accuracy ? (b) Quels sont son rappel et sa précision pour la classe fraude ? (c) Quelle métrique devriez-vous rapporter, et qu’est-ce que cela enseigne sur l’accuracy sur des données déséquilibrées ?

Avec 9 900 transactions légitimes et 100 fraudes, « toujours pas-de-fraude » classe correctement chaque légitime et se trompe sur chaque fraude. Écrivez TP, TN, FP, FN pour la classe fraude (positive), puis appliquez les formules : accuracy = (TP + TN)/total, rappel = TP/(TP + FN), précision = TP/(TP + FP).

Pour la classe fraude (positive) : TP = 0, FP = 0, TN = 9 900, FN = 100.

  • (a) Accuracy = (0 + 9 900) / 10 000 = 0.99 — paraît excellente.
  • (b) Rappel = 0 / (0 + 100) = 0 — il n’attrape aucune fraude. La précision = 0 / (0 + 0) est indéfinie (il ne prédit jamais positif), rapportée en NA.
  • (c) Rapportez le rappel (et le F1, l’AUC), jamais l’accuracy. Une accuracy de 99 % avec un rappel nul est inutile — le modèle ne sert à rien précisément là où cela importe. C’est la leçon canonique : sur des données déséquilibrées, l’accuracy suit la classe majoritaire ; vérifiez toujours le rappel, le F1 et l’AUC, et comparez l’accuracy à la référence d’absence de discrimination (ici, 0.99).

Vérification rapide. Vous évaluez deux classifieurs du diabète. Le modèle A a une accuracy de 0.78 et une AUC de 0.72 ; le modèle B a une accuracy de 0.75 et une AUC de 0.86. Les classes sont déséquilibrées (la plupart des patientes sont négatives). Quel modèle privilégieriez-vous pour un outil de dépistage, et pourquoi ?

Privilégiez le modèle B. Sur des données déséquilibrées, l’accuracy est gonflée par la majorité facile (les négatifs), donc l’accuracy légèrement plus élevée du modèle A n’est pas digne de confiance. L’AUC est indépendante du seuil et résiste au déséquilibre, et le 0.86 vs 0.72 du modèle B indique qu’il classe les diabétiques au-dessus des patientes en bonne santé bien plus fiablement à tous les seuils — exactement ce dont un outil de dépistage a besoin. Vous choisiriez ensuite, sur le modèle B, un seuil de probabilité qui donne la sensibilité que votre dépistage exige. Jugez les classifieurs par l’AUC (et le rappel/F1), pas par l’accuracy seule, quand les classes sont déséquilibrées.

Conclusion

Vous disposez maintenant de toute la boîte à outils d’évaluation d’un classifieur en R. La matrice de confusion est la source de tout ; l’accuracy est intuitive mais trompe sur des données déséquilibrées — comparez-la toujours à la référence de la classe majoritaire. La précision, le rappel (sensibilité), la spécificité et le F1 séparent les deux types d’erreur pour que vous puissiez juger le modèle selon l’erreur qui coûte le plus cher. La courbe ROC et son AUC résument la performance sur tous les seuils et résistent au déséquilibre, et le seuil de probabilité est une décision que vous prenez à partir de ces coûts — pas un nombre figé à 0.5. Pour trois classes ou plus, les mêmes métriques s’étendent via le moyennage un-contre-tous (macro/micro/pondéré). Calculez le tout avec yardstick dans un pipeline tidymodels, ou caret / pROC en base R — les chiffres s’accordent.

Pour transformer une seule évaluation entraînement/test en une estimation honnête et à faible variance, rééchantillonnez avec la validation croisée. Pour construire les classifieurs que vous évaluez, partez de construire des modèles avec tidymodels.

Leçons connexes

Vous préférez un livre ? Machine Learning Essentials est disponible en PDF téléchargeable — toutes les leçons de cette série, hors ligne et à vous.

Obtenir le livre (PDF) →Les leçons restent gratuites en ligne.
Cette page vous a-t-elle été utile ?

Prouvez que vous savez le faire. Maîtrisez toute la série Classification en R — suivez votre parcours, construisez des projets et obtenez un certificat.

Commencer gratuitement →

Passez à Pro — Prova illimité sur vos propres données et un certificat vérifiable qui atteste la compétence.

dès 15 $/mois facturé annuellement

Passer à Pro →

✓ Vous êtes Pro — continuez. The runtime is the judge.

Recevez les nouvelles leçons R & Python par e-mail

Pratique, reproductible, sans spam. Désinscription à tout moment.

Double opt-in. Nous ne partageons jamais votre e-mail.

Partager cette pageXLinkedInRedditHN
Note

Cette leçon est reproductible : chaque métrique et chaque figure ont été produites par le code montré — copiez n’importe quel bloc et exécutez-le pour les reproduire. The runtime is the judge.

Références

  • James, G., Witten, D., Hastie, T., & Tibshirani, R. An Introduction to Statistical Learning — le traitement canonique de la classification, des courbes ROC et de l’AUC. Gratuit en ligne.
  • Kuhn, M., & Silge, J. Tidy Modeling with R — le chapitre sur les métriques de yardstick (métriques de classification, courbes ROC et PR). Gratuit en ligne (CC BY-NC-SA).
  • Robin, X., et al. pROC: an open-source package for R and S+ to analyze and compare ROC curves. BMC Bioinformatics (2011) — la référence pour le package pROC.

Réutilisation

Citation

BibTeX
@online{2026,
  author = {},
  title = {Matrice de confusion et courbe ROC en R : évaluer un modèle},
  date = {2026-06-25},
  url = {https://www.datanovia.com/learn/machine-learning/classification/classification-model-evaluation},
  langid = {fr}
}
Veuillez citer ce travail comme suit :
“Matrice de confusion et courbe ROC en R : évaluer un modèle.” 2026. June 25. https://www.datanovia.com/learn/machine-learning/classification/classification-model-evaluation.