3.1 Métriques et proportions

À partir du moment où la classification ne se fait pas sans erreurs, nous avons la présence de faux positifs et de faux négatifs. Le fait d’être un faux positif ou un faux négatif dépend essentiellement du point de vue, c’est-à-dire, de la classe d’intérêt. Cependant ces erreurs restent des erreurs, quel que soit le point de vue (sauf si nous fusionnons les classes confondues pour simplifier la classification, bien évidemment). Un élément important à considérer est que le taux d’erreur, qu’il soit global ou local (tel que mesuré, par exemple, par le rappel ou la précision pour une classe cible) dépend énormément des proportions d’occurrences observées dans les différentes classes. Et cette affirmation est valable aussi bien dans la phase d’apprentissage que de test ou de déploiement.

Ce problème nous ramène, en réalité à des calculs basiques de probabilités et au théorème de Bayes que nous avons abordés dans le module 6 du premier cours de science des données biologiques. Rappelez-vous, nous avions traité le cas du dépistage d’une maladie, dont le résultat dépendait du fait que la maladie est fréquente ou rare dans la population (sa prévalence). Si vous ne vous souvenez plus de quoi il s’agit, il peut être utile de relire maintenant les sections 6.1.1 à 6.1.3 de ce premier cours, car ce contenu s’applique parfaitement ici.

À vous de jouer !
h5p

Pour illustrer une nouvelle fois ce phénomène, prenons un cas extrême. Admettons que notre classifieur soit capable de discerner sans aucune erreur les individus entre deux classes A et B. Quel sera la précision pour la classe A ? Ça, c’est facile à calculer. Comme il n’y a pas d’erreurs, nous n’aurons aucun faux positif. Donc, la précision qui est \(TP / (TP + FP)\) vaudra toujours 1 ou 100%, et ce, quelles que soient les proportions relatives de A et de B dans notre échantillon. En absence d’erreur, il n’y a pas d’ambiguïté ni de dépendance aux proportions relatives.

Par contre, calculons maintenant la précision pour A, si nous savons que notre même classifieur a tendance à classer 10% des individus B comme des A (faux positifs FP), mais ne fait toujours aucune erreur pour A. La précision étant le rapport \(TP / (TP + FP)\), interviennent ici ces faux positifs qui dépendent eux de ce que notre classifieur est capable de faire par rapport à la classe B. Dix pour cent de faux positifs, oui, mais sur combien d’individus ? Prenons deux cas :

  1. La classe A est prédominante dans l’échantillon, disons qu’elle représente 80% de l’ensemble. Pour 100 individus, nous aurons donc 80 A, tous vrais positifs, et 20 B, dont 10%, soit deux sont faussement classés comme A. La précision est donc de \(80 / (80 + 2)\), soit un peu moins de 98%. C’est un très bon résultat.

  2. Dans notre second cas, les proportions sont inversées. Nous avons 20% de A et 80% de B. Même raisonnement : les vrais positifs pour 100 individus seront de 20 (tous les A) et les faux positifs seront 10% de 80, soit 8. La précision devient donc \(20 / (20 + 8)\), soit un tout petit peu plus de 71%3.

À vous de jouer !
h5p

Dans le second cas, la précision pour la classe A a diminué de manière très nette, rien qu’en changeant les proportions relatives de A et de B dans notre échantillon, les performances de notre classifieur n’ayant pas été modifiées entre les deux situations. Nous venons de démontrer que certaines métriques, dès qu’il y a la moindre erreur de classification possible, sont très sensibles aux proportions relatives des individus dans les classes. Notons que la diminution de la précision dans le cas (2) est liée à la fois à la diminution des vrais positifs (puisqu’il y a moins de A dans notre set), et à l’augmentation des faux positifs, qui dépendent eux de la quantité de B, en augmentation.

À l’extrême, il devient très difficile de classer correctement des individus appartenant à des classes rares à cause de ce phénomène. En effet, si A ne représente plus que 1% de l’échantillon, nous aurons un seul vrai positif A, et 10% de 99 B, soit pratiquement 10 faux positifs B pour un lot de 100 individus. Donc, la précision pour A devient \(1 / (1 + 10)\), soit 9% seulement. Nous verrons alors notre classifieur comme très mauvais à l’examen des items qu’il “prétend” être des A, et pour lesquels la grosse majorité ne le sera pas. Pourtant, il classe A sans aucune erreur et ne fait que 10% d’erreur pour B, ce qui, présenté de la sorte, passe pour un bon classifieur.

Plusieurs métriques qui mesurent les performances de nos classifieurs sont très sensibles aux proportions relatives des différentes classes. Comme l’optimisation des classifieurs se fait sur base de ces métriques, elle est elle-même dépendante des proportions relatives des classes dans le set d’apprentissage.

De plus, si les proportions relatives des items dans les classes diffèrent entre le set de test et les échantillons à classer lors du déploiement du classifieur, les valeurs calculées pendant la phase de test seront biaisées et ne refléteront pas du tout les performances réelles du classifeur, une fois déployé.

3.1.1 Proportions en apprentissage

Les métriques interviennent en test, mais qu’en est-il des proportions relative des classes en apprentissage ? Modifier les proportions relatives des classes dans le set d’apprentissage pour tendre vers une répartition la plus équilibrée possible est souvent intéressant parce que cela va conditionner le comportement de notre classifieur à mieux classer dans les classes rares. Retenez que, si vous voulez améliorer les performances de classification pour une classe cible, vous pouvez y arriver en augmentant ses proportions de manière relative aux autres classes dans le set d’apprentissage. C’est pour cette raison qu’il est souvent conseillé de procéder à un ré-échantillonnage dans le but d’obtenir un effectif à peu près égal entre les différentes classes dans le set d’apprentissage. Par contre, changer les proportions de la sorte dans le set de test nous mène, nous l’avons vu, à une estimation biaisée de plusieurs métriques dont l’erreur globale, la précision, le score F…, dans le sens d’une évaluation optimiste pour les classes rares (moins d’erreur, plus de précision). La désillusion guète lors du déploiement, car nos métriques ne sont alors plus représentatives de la “réalité terrain”. Donc, dissociez bien le set d’apprentissage et le set de test, augmenter les proportions relatives de la ou des classes cibles en apprentissage pour améliorer les performances du classifieur à leur égard, mais faites attention à ce que vous faites en test… ou alors, utilisez la correction que nous allons étudier un peu plus loin dans ce chapitre pour éviter une estimation biaisée des performances de votre classifieur. Dans le challenge, vous aurez à réfléchir à tout ceci !

À vous de jouer !
h5p

Reprenons l’exemple de nos Amérindiens de la tribu Pima confrontés au diabète (en n’utilisant que les cas complets).

SciViews::R("ml", lang = "fr")
pima <- read("PimaIndiansDiabetes2", package = "mlbench")
pima1 <- sdrop_na(pima)
table(pima1$diabetes) |>
  tabularise()

Var1

Count

Percent

neg

262

66.8%

pos

130

33.2%

Total

392

100.0%

Comme nous l’avions déjà signalé, nous avons deux fois plus de cas négatifs que de cas positifs. Revenons sur notre classifieur à forêt aléatoire avec 500 arbres :

set.seed(3631)
pima1_rf <- ml_rforest(data = pima1, diabetes ~ ., ntree = 500)
pima1_rf_conf <- confusion(cvpredict(pima1_rf, cv.k = 10), pima1$diabetes)
summary(pima1_rf_conf)
# 392 items classified with 309 true positives (error = 21.2%)
# 
# Global statistics on reweighted data:
# Error rate: 21.2%, F(micro-average): 0.754, F(macro-average): 0.751
# 
#        Fscore    Recall Precision Specificity       NPV       FPR       FNR
# neg 0.8471455 0.8778626 0.8185053   0.6076923 0.7117117 0.3923077 0.1221374
# pos 0.6556017 0.6076923 0.7117117   0.8778626 0.8185053 0.1221374 0.3923077
#           FDR       FOR     LRPT      LRNT     LRPS      LRNS    BalAcc
# neg 0.1814947 0.2882883 2.237689 0.2009856 2.839190 0.2550115 0.7427775
# pos 0.2882883 0.1814947 4.975481 0.4468896 3.921392 0.3522131 0.7427775
#           MCC    Chisq       Bray Auto Manu A_M  TP FP FN  TN
# neg 0.5073948 100.9202 0.02423469  281  262  19 230 51 32  79
# pos 0.5073948 100.9202 0.02423469  111  130 -19  79 32 51 230

Notez que le rappel (Recall) est plus faible pour les cas positifs (61%) que pour les cas négatifs (88%). Ce n’est pas forcément toujours comme cela, mais relativement fréquent, car notre classifieur est optimisé pour réduire l’erreur globale. Dans cette situation, ayant plus de cas négatifs, il vaut mieux déclarer un cas douteux comme négatif puisque le risque de se tromper est plus faible que de le déclarer positif. La précision pour pos est de 71%. Admettons que nous souhaitons maintenant étudier un maximum de ces Indiennes diabétiques. Le rappel pour la classe pos est notre métrique importante, or c’est la valeur la plus faible actuellement. Comment faire pour l’augmenter sans changer d’algorithme de classification ? Et bien, une des façons de procéder consiste à changer délibérément les proportions des classes dans le set d’apprentissage. Si nous prenons le même nombre d’individus positifs que négatifs en sous-échantillonnant (on laisse tomber la moitié des cas négatifs pour ramener leur nombre à une valeur égale aux cas positifs), cela donne ceci :

# Rééchantillonnage du set
set.seed(845)
pima1 %>.%
  group_by(., diabetes) |>
  sample_n(130L, replace = FALSE) ->
  pima1b
table(pima1b$diabetes) |>
  tabularise()

Var1

Count

Percent

neg

130

50.0%

pos

130

50.0%

Total

260

100.0%

set.seed(854)
pima1b_rf <- ml_rforest(data = pima1b, diabetes ~ ., ntree = 500)
pima1b_rf_conf <- confusion(cvpredict(pima1b_rf, cv.k = 10), pima1b$diabetes)
summary(pima1b_rf_conf)
# 260 items classified with 206 true positives (error = 20.8%)
# 
# Global statistics on reweighted data:
# Error rate: 20.8%, F(micro-average): 0.792, F(macro-average): 0.792
# 
#        Fscore    Recall Precision Specificity       NPV       FPR       FNR
# pos 0.7938931 0.8000000 0.7878788   0.7846154 0.7968750 0.2153846 0.2000000
# neg 0.7906977 0.7846154 0.7968750   0.8000000 0.7878788 0.2000000 0.2153846
#           FDR       FOR     LRPT      LRNT     LRPS      LRNS    BalAcc
# pos 0.2121212 0.2031250 3.714286 0.2549020 3.878788 0.2661913 0.7923077
# neg 0.2031250 0.2121212 3.923077 0.2692308 3.756696 0.2578125 0.7923077
#           MCC    Chisq        Bray Auto Manu A_M  TP FP FN  TN
# pos 0.5846846 88.88258 0.003846154  132  130   2 104 28 26 102
# neg 0.5846846 88.88258 0.003846154  128  130  -2 102 26 28 104

Le rappel pour la classe pos est monté de 60% (pima1) à 80% (pima1b). En première approche, nous devrions nous réjouir de ce résultat, d’autant plus que la précision semble être montée en même temps à presque 79% pour la même classe. Notre nouveau classifieur semble nettement plus efficace pour trouver les Indiennes diabétiques dans l’ensemble de la population. Mais attention ! Ici, nous avons procédé par validation croisée, avec cvpredict(), pour calculer les métriques. Donc, nous avons utilisé les proportions relatives biaisées entre cas positifs et négatifs. N’oublions jamais que certaines métriques sont sensibles aux proportions et que justement, nous venons de “trafiquer” ces dernières. À titre d’exercice, vous pouvez examiner l’effet d’un changement encore plus radical, par exemple, si vous prenez deux ou trois fois plus de cas positifs que négatifs dans votre set d’apprentissage.

À vous de jouer !
h5p

Il existe plusieurs approches pour modifier les proportions relatives dans vos classes pour les sets de test et d’apprentissage. Celle que nous venons d’utiliser consiste à réduire le nombre d’items des classes les plus abondantes. Ici, nous comprenons intuitivement qu’une perte d’information n’est pas forcément une bonne chose.

Nous pouvons aussi manipuler les poids des individus en les augmentant pour les classes rare dans le set d’apprentissage. Si la fonction qui calcule le classifieur supporte cette option, par exemple, via l’argument classwt= de ml_rforest(), c’est facile. Si ce n’est pas le cas, nous pouvons simuler un poids de 2 ou de 3 en dupliquant ou tripliquant ces individus dans le set d’apprentissage (mais alors attention : la validation croisée n’est plus utilisable, car on risque d’avoir les mêmes individus dupliqués ou tripliqués en apprentissage et en test simultanément).

Enfin, il existe des techniques d’augmentation du nombre d’items par classe via la synthèse de cas artificiels en se basant sur l’information contenue dans le tableau de départ. L’une des techniques les plus utilisées s’appelle “SMOTE”. Ce blog l’explique en même temps qu’il montre que cela peut être dangereux. Nous vous conseillons donc d’utiliser plutôt l’une des deux approches expliquées ci-dessus (réduction des classes abondantes ou surpondération, éventuellement en les dupliquant, des items des classes rares dans le set d’apprentissage). Mais n’exagérez jamais trop dans la façon dont vous distordez les proportions par classes par rapport à la réalité, et gardez toujours à l’esprit que vous devez faire attention à ne pas biaiser vos métriques (set de test non modifié, ou correction).

Voici comment nous pouvons surpondérer les classes plus rares par duplication, dans le cas de notre jeu de données pima1. Nous commençons par définir une fonction over_undersample() que vous pouvez réutiliser dans vos projets ou dans le challenge4. Vous lui donnez le vecteur de classes dont il faut modifier les proportions. Vous donnez aussi un vecteur de pondérations (1 = garder tout, < 1 = sous échantillonner la classe dans ces proportions, et > 1 = suréchantillonner la classe). Une pondération de 2 signifie dupliquer tous les items dans la classe, par exemple. Si vous ne donnez pas de vecteur de pondération, la fonction homogénéisera les proportions dans toutes les classes automatiquement. Un argument total= indique la taille souhaitée du jeu de données rééchantillonné. Enfin, l’argument min_warning permettra d’imprimer un message d’avis si l’effectif d’un classe est inférieur à cette valeur (10 par défaut). Le résultat renvoyé doit être utilisé pour sélectionner les lignes du tableau (voir exemple d’application ci-dessous).

over_undersample <- function(x, weights = NULL,
    total = round(sum(weights * table(x))), min_warning = 10) {
  stopifnot(is.factor(x))
  levels_x <- levels(x)
  nlevels <- length(levels_x)
  stopifnot(nlevels >= 2)
  # There may not be empty levels
  table_x <- table(x)
  if (any(table_x == 0))
    stop("There may be no empty levels. Use droplevels() first?")
  if (any(table_x < min_warning))
    warning("Some levels have less than ", min_warning, " items.")
  # Default value for weights = get evenly distributed classes
  if (is.null(weights))
    weights <- length(x) / table_x / nlevels
  stopifnot(is.numeric(weights), length(weights) == nlevels)
  stopifnot(length(total) == 1, is.numeric(total), total > 0)
  # This is an error to generate less than min_warning * nlevels items
  if (total < min_warning * nlevels)
    stop("The total cannot be lower than min_warning * number of levels in x")
  # times is the number of times each item must be multiplied to get the desired
  # total number of items with the desired class distribution
  times <- weights / sum(weights * table_x) * total
  # For integer number of times, we replicate, but for fraction, we randomly
  # subsample
  # For the repetition, it is enough to use rep() with times[x] (times is
  # truncated by rep). times[x] selects the number of times each item must be
  # repeated, depending on its class, because the factor object internally
  # stores the levels as integers
  idx <- rep(1:length(x), times = times[x])
  # For the fraction part of times, we need to subsample each level
  frac <- times %% 1
  split_x <- split(1:length(x), x)
  for (i in 1:nlevels) {
    level <- levels_x[i]
    level_idx <- split_x[[level]]
    split_x[[level]] <- sample(level_idx,
      size = round(frac[[i]] * length(level_idx)))
  }
  # All indices are idx + unlisted split_x
  structure(sort(c(idx, unname(unlist(split_x)))), times = times)
}

Répétons-le : dans le cas du suréchantillonnage, on va dupliquer des items. Cela signifie que l’on ne peut pas diviser ensuite en set d’apprentissage et set de test, ni utiliser la validation croisée. Nous ne pouvons suréchantillonner que le set d’apprentissage, et nous devons nous assurer qu’aucun item dupliqué ne soit ensuite dispatché entre le set d’apprentissage et celui de test (sinon, on aura les mêmes items des deux côtés, ce qui est interdit).

# Séparer d'abord set d'apprentissage et set de test
set.seed(9856)
pima1_split <- initial_split(pima1, 2/3, strata = diabetes)
# Récupération du set d'apprentissage
pima1_train <- training(pima1_split)
# Récupération du set de test
pima1_test <- testing(pima1_split)
table(pima1_train$diabetes) |> # Tableau mal balancé
  tabularise() 

Var1

Count

Percent

neg

174

66.9%

pos

86

33.1%

Total

260

100.0%

Ensuite, nous homogénéisons les classes en dupliquant les cas positifs, mais uniquement dans le set d’apprentissage.

# Pondération double pour les cas positifs en les dupliquant
resample_idx <- over_undersample(pima1_train$diabetes,
  weights = c(neg = 1, pos = 2))
pima1_train <- pima1_train[resample_idx, ]
table(pima1_train$diabetes) |> # Tableau mieux balancé sans perte
  tabularise() 

Var1

Count

Percent

neg

174

50.3%

pos

172

49.7%

Total

346

100.0%

Bien évidemment, si les classes sont extrêmement inégales en effectifs, nous pouvons réaliser les deux approches simultanément : dupliquer, voire tripliquer les classes rares et sous-échantillonner les classes plus abondantes.

3.1.2 Probabilités a priori

Pour estimer quelles sont les proportions relatives des classes dans les données à classer en déploiement, il suffit de réaliser un échantillonnage aléatoire de taille raisonnable (par exemple, un minimum de 100 individus pour exprimer les résultats en pour cent), et de comptabiliser les proportions observées dans chaque classe. Ces proportions seront appelées les probabilités a priori (prior probabilities en anglais). Pour nos Indiennes Pima, les probabilités a priori (si l’échantillonnage de départ est bien aléatoire et réalisé dans les règles de l’art) sont déterminées grâce à une table de contingence du jeu de données initial. Nous devrons fournir cette information sous forme d’un vecteur numérique nommé du nom de chaque classe. Voici comment le faire dans R pour obtenir ce vecteur nommé des proportions par classes :

pima_prior_tab <- table(pima$diabetes) / nrow(pima) # Table de contingence
pima_prior <- structure(as.numeric(pima_prior_tab), names = names(pima_prior_tab))
pima_prior # Vecteur numerique nommé
#       neg       pos 
# 0.6510417 0.3489583

Dans le cas où les probabilités a priori ne sont pas calculée à partir des données mais sont obtenues depuis la littérature, il suffit de créer un vecteur numérique avec c() en nommant les différentes probabilités du même nom que les niveaux de la variable facteur réponse. Par exemple, ici, notre variable réponse a les niveaux neg et pos. Si nous trouvons l’information que la prévalence du diabète dans cette population est de 13.5%, nous écrirons nos probabilités a priori comme pima_prior <- c(neg = 0.865, pos = 0.135). La somme des probabilités doit être de un bien entendu.

À vous de jouer !
h5p

De nombreux algorithmes de classification peuvent nous renvoyer des probabilités d’appartenir aux différentes classes au lieu de la prédiction. Prenons l’exemple d’un classifieur par forêt aléatoire en trois classes A, B et C utilisant 100 arbres. Imaginons que, pour un individu donné, 78 arbres le classe en A, 13 le classe en B et 9 en C. Lorsque vous demandez au classifieur de prédire la classe de cet individu, il vous repondra “A”, car il procède par vote à la majorité de ses arbres. Mais il est aussi possible de demander à ce même classifieur des “pseudo-probabilités” qui correspondent ici au nombre relatif d’arbres ayant voté pour les différentes classes, donc 78% A, 13% B et 9% C. Nous appelerons ces (pseudo-)probabilités, les probabilités a posteriori.

Sans entrer dans le détail du calcul mathématique, il est possible d’utiliser ces informations de probabilités a priori et a posteriori pour corriger les valeurs des métriques qui sont sensibles aux proportions. On se base en fait sur le fameux théorème de Bayes. Nous avions déjà fait un calcul similaire dans le module 6 du cours I. Nous ne le redéveloppons pas ici. Vous êtes invité à relire cette section si vous ne vous en souvenez plus. Pour la suite, il suffit d’accepter que la correction des métriques est possible de cette façon.

Dans {mlearning}, les probabilités a priori peuvent être injectées dans l’objet confusion pour corriger nos métriques en faveur de valeurs plus réalistes. Reprenons un cas concret pour l’illustrer : notre set d’apprentissage pima1b aux proportions modifiées et dont les métriques ont été calculées par validation croisée en absence de set de test. Nous pouvons les corriger, et en même temps, les rendre plus comparables avec les métriques non biaisées calculées sur pima1 en procédant comme suit :

prior(pima1b_rf_conf) <- pima_prior
summary(pima1b_rf_conf)
# 260 items classified with 206 true positives (error = 20.8%)
# 
# Global statistics on reweighted data:
# Error rate: 21%, F(micro-average): 0.782, F(macro-average): 0.778
# 
#        Fscore    Recall Precision Specificity       NPV       FPR       FNR
# neg 0.8294841 0.7846154 0.8797957   0.8000000 0.6656477 0.2000000 0.2153846
# pos 0.7266660 0.8000000 0.6656477   0.7846154 0.8797957 0.2153846 0.2000000
#           FDR       FOR     LRPT      LRNT     LRPS      LRNS    BalAcc
# neg 0.1202043 0.3343523 3.923077 0.2692308 2.631343 0.1805824 0.7923077
# pos 0.3343523 0.1202043 3.714286 0.2549020 5.537639 0.3800340 0.7923077
#           MCC     Chisq       Bray     Auto      Manu         A_M        TP
# neg 0.5646898 0.3188746 0.03521635 0.580609 0.6510417 -0.07043269 0.5108173
# pos 0.5646898 0.3188746 0.03521635 0.419391 0.3489583  0.07043269 0.2791667
#             FP         FN        TN
# neg 0.06979167 0.14022436 0.2791667
# pos 0.14022436 0.06979167 0.5108173

Nous voyons que les valeurs de rappels ne sont pas modifiées par cette correction. Nous avons toujours 80% pour la classe pos. Le gain de 20% de cette métrique pour pos reste acquis ici grâce aux modifications des proportions dans le set d’apprentissage. Par contre, la précision a diminué à 66%, et donc le score F a lui aussi diminué. Notre précision pour les cas pos est donc maintenant moins bonne qu’avec pima1 de 5%, à probabilités a priori égales entre les classes. Ceci est normal. Tout classifieur doit faire un compromis entre rappel et précision. Si nous gagnons pour l’un, nous perdons inévitablement pour l’autre. Notre score F nous indique toutefois un gain global pour pos puisque nous sommes passés de 65% à un peu plus de 72%. L’augmentation artificielle de l’effectif de pos en apprentissage a un effet globalement positif pour cette classe. Mais comme tout est affaire de compromis, nous perdons alors sur les métriques de l’autre classe, neg.

À retenir

Si vous êtes amené à modifier les proportions des différentes classes dans votre set d’apprentissage (pratique conseillée si les proportions sont trop différentes d’une classe à l’autre), n’oubliez pas de repondérer si nécessaire via prior(), afin d’avoir des métriques plus représentatives des performances de votre classifieur en déploiement. Ne comparez jamais entre elles des métriques calculées avec des probabilités a priori différentes !

À vous de jouer !

Vous pouvez déjà réaliser la première partie du tutorile learnr suivant :

Effectuez maintenant les exercices du tutoriel C03La_roc (Proportions par classes et courbes ROC).

BioDataScience3::run("C03La_roc")
À vous de jouer !

Réalisez le travail C03Ia_cardiovascular, partie I.

Travail individuel pour les étudiants inscrits au cours de Science des Données Biologiques III à l’UMONS à terminer avant le 2025-11-10 23:59:59.

Initiez votre projet GitHub Classroom

Voyez les explications dans le fichier README.md, partie I.


  1. Faites le même calcul du rappel, à la fois pour A et pour B dans les deux cas pour vous convaincre que cette métrique n’est pas sensible aux proportions relatives des classes, contrairement à la précision.↩︎

  2. La fonction over_undersample() sera incluse dans une prochaine version du package {mlearning}.↩︎