Comparez plusieurs groupes avec Kruskal-Wallis et Friedman
Une entreprise hésite entre trois prestataires de livraison et mesure les délais de cinq colis chez chacun. Une équipe de data science compare quatre modèles sur six jeux de données. Dans les deux cas, il y a plus de deux groupes à comparer, et l’on voudrait savoir, d’un seul test, si au moins un groupe se distingue.
En statistique paramétrique, c’est le rôle de l’analyse de la variance (ANOVA). Ce chapitre présente ses deux équivalents de rang : le test de Kruskal-Wallis pour des groupes indépendants, et le test de Friedman pour des mesures organisées en blocs. Vous verrez aussi comment trouver quels groupes diffèrent, sans multiplier les faux positifs.
Plusieurs groupes indépendants : Kruskal-Wallis
Section intitulée « Plusieurs groupes indépendants : Kruskal-Wallis »Le fil rouge : trois prestataires
Section intitulée « Le fil rouge : trois prestataires »Délais de livraison (en heures) de cinq colis par prestataire :
| Prestataire A | 32 | 41 | 28 | 35 | 39 |
|---|---|---|---|---|---|
| Prestataire B | 45 | 52 | 38 | 49 | 56 |
| Prestataire C | 30 | 27 | 36 | 33 | 25 |
Pourquoi ne pas faire trois tests de Mann-Whitney, A contre B, A contre C, B contre C ? Parce que chaque test a 5 % de chances de conclure à tort. Avec trois tests, le risque de se tromper au moins une fois monte jusqu’à (si les tests étaient indépendants). Il faut d’abord un test global.
La statistique H
Section intitulée « La statistique H »Le principe prolonge celui de Mann-Whitney : on classe les observations ensemble, puis on compare les rangs moyens des groupes. Sous (tous les groupes viennent de la même loi), chaque rang moyen devrait être proche du rang moyen global . La statistique de Kruskal-Wallis mesure l’écart à cette situation :
- est le nombre de groupes et l’effectif du groupe ;
- est l’effectif total ;
- est la somme des rangs du groupe dans le classement global.
Sous , et si chaque groupe compte au moins cinq observations environ, suit approximativement une loi du à degrés de liberté. On rejette quand est grand.
Une mesure de taille d’effet simple est , entre 0 et 1 : ici , un effet très fort.
Trouver quels groupes diffèrent
Section intitulée « Trouver quels groupes diffèrent »Le test global dit « au moins un groupe diffère », sans dire lequel. On enchaîne alors des comparaisons deux à deux (post hoc), en corrigeant le seuil pour le nombre de comparaisons. Le test de Dunn compare les rangs moyens de deux groupes, en réutilisant le classement global :
où est le rang moyen du groupe . On compare à la loi normale, puis on corrige les p-valeurs, par exemple avec la méthode de Bonferroni (multiplier chaque p-valeur par le nombre de comparaisons) ou celle, moins conservatrice, de Holm.
| Comparaison | brute | Bonferroni (× 3) | |
|---|---|---|---|
| B contre C | 2,97 | 0,003 | 0,009 |
| A contre B | −1,91 | 0,056 | 0,17 |
| A contre C | 1,06 | 0,29 | 0,87 |
Seul l’écart entre B (le plus lent) et C (le plus rapide) est significatif après correction.
Des mesures en blocs : Friedman
Section intitulée « Des mesures en blocs : Friedman »Le fil rouge : quatre modèles, six jeux de données
Section intitulée « Le fil rouge : quatre modèles, six jeux de données »Quatre modèles de classification (régression logistique, arbre de décision, forêt aléatoire, boosting) sont évalués sur six jeux de données. Voici leurs taux de bonnes réponses :
| Jeu | LogReg | Arbre | Forêt | Boosting |
|---|---|---|---|---|
| 1 | 0,81 | 0,76 | 0,85 | 0,86 |
| 2 | 0,72 | 0,70 | 0,78 | 0,77 |
| 3 | 0,90 | 0,84 | 0,91 | 0,93 |
| 4 | 0,65 | 0,66 | 0,71 | 0,73 |
| 5 | 0,88 | 0,82 | 0,87 | 0,90 |
| 6 | 0,77 | 0,71 | 0,80 | 0,79 |
Les modèles ne sont pas des groupes indépendants : chaque jeu de données est un bloc, sur lequel les quatre modèles sont mesurés. Certains jeux sont faciles (autour de 0,90), d’autres difficiles (autour de 0,70). Si l’on ignorait les blocs, cette variabilité entre jeux noierait les différences entre modèles. C’est le même raisonnement que pour les données appariées du chapitre 2, étendu à plus de deux traitements.
La statistique Q
Section intitulée « La statistique Q »Le test de Friedman classe les traitements à l’intérieur de chaque bloc (ici, 1 pour le meilleur modèle du jeu, 4 pour le moins bon), puis additionne les rangs par traitement. Avec blocs et traitements :
où est la somme des rangs du traitement sur les blocs. Sous (dans chaque bloc, tous les classements sont équiprobables), suit approximativement un .
La quantité , ici , s’appelle le coefficient de concordance de Kendall : elle mesure à quel point les jeux de données s’accordent sur le classement des modèles (0 : aucun accord, 1 : même classement partout).
Le diagramme de différence critique
Section intitulée « Le diagramme de différence critique »Pour localiser les différences entre modèles, la démarche classique en apprentissage automatique (popularisée par un article de Demšar en 2006) utilise le test post hoc de Nemenyi : deux modèles diffèrent significativement si leurs rangs moyens s’écartent d’au moins la différence critique
où est une valeur tabulée (2,569 pour et ). Ici, . Le boosting (1,33) et la forêt (1,83) se distinguent de l’arbre (3,83), mais aucune autre paire ne dépasse l’écart de 1,91 : on ne peut pas départager boosting et forêt, ni boosting et régression logistique (écart de 1,67). Six jeux de données, c’est peu.
En pratique : Python et R
Section intitulée « En pratique : Python et R »from scipy import stats
A = [32, 41, 28, 35, 39]B = [45, 52, 38, 49, 56]C = [30, 27, 36, 33, 25]print(stats.kruskal(A, B, C)) # H = 9.06, p ≈ 0.011
logreg = [0.81, 0.72, 0.90, 0.65, 0.88, 0.77]arbre = [0.76, 0.70, 0.84, 0.66, 0.82, 0.71]foret = [0.85, 0.78, 0.91, 0.71, 0.87, 0.80]boosting = [0.86, 0.77, 0.93, 0.73, 0.90, 0.79]# un argument par traitement, les blocs sont les positions dans les listesprint(stats.friedmanchisquare(logreg, arbre, foret, boosting)) # Q = 13.8, p ≈ 0.003Les tests post hoc (Dunn, Nemenyi) ne sont pas dans scipy ; le paquet scikit-posthocs les fournit (posthoc_dunn, posthoc_nemenyi_friedman). En R :
delais <- c(32, 41, 28, 35, 39, 45, 52, 38, 49, 56, 30, 27, 36, 33, 25)presta <- factor(rep(c("A", "B", "C"), each = 5))kruskal.test(delais ~ presta) # H = 9.06, df = 2, p = 0.0108
acc <- matrix(c(0.81, 0.76, 0.85, 0.86, 0.72, 0.70, 0.78, 0.77, 0.90, 0.84, 0.91, 0.93, 0.65, 0.66, 0.71, 0.73, 0.88, 0.82, 0.87, 0.90, 0.77, 0.71, 0.80, 0.79), nrow = 6, byrow = TRUE, dimnames = list(NULL, c("LogReg", "Arbre", "Foret", "Boosting")))friedman.test(acc) # une ligne par bloc : Q = 13.8, df = 3, p = 0.0032Récapitulatif des tests de rang
Section intitulée « Récapitulatif des tests de rang »| Plan d’expérience | Test paramétrique | Test de rang |
|---|---|---|
| Un échantillon, ou deux mesures appariées | t à un échantillon ou apparié | signe, Wilcoxon signé |
| Deux groupes indépendants | t de Student ou de Welch | Mann-Whitney |
| groupes indépendants | ANOVA à un facteur | Kruskal-Wallis |
| traitements mesurés sur blocs | ANOVA à mesures répétées | Friedman |
En résumé
Section intitulée « En résumé »- Pour plus de deux groupes, on commence par un test global pour éviter l’inflation des faux positifs.
- Kruskal-Wallis compare les rangs moyens de groupes indépendants : sous .
- Friedman classe les traitements à l’intérieur de chaque bloc : sous ; il neutralise la variabilité entre blocs.
- Après un rejet, les comparaisons post hoc (Dunn pour Kruskal-Wallis, Nemenyi pour Friedman) localisent les différences, avec une correction pour tests multiples.
- Pour comparer des modèles sur plusieurs jeux de données : Friedman puis Nemenyi, résumés par un diagramme de différence critique.
scipy.stats.kruskal,scipy.stats.friedmanchisquare; en R,kruskal.testetfriedman.test.
Exercices
Section intitulée « Exercices »Exercice · Un test de Friedman à la main
facileQuatre juges classent trois recettes de cookies (1 = préférée). Classements : juge 1 : (1, 2, 3) ; juge 2 : (1, 3, 2) ; juge 3 : (1, 2, 3) ; juge 4 : (2, 1, 3), pour les recettes (X, Y, Z). Calculez et concluez sachant que .
Voir la solution
Sommes des rangs : , , (total , c’est cohérent). Avec et :
. On ne rejette pas au seuil de 5 %. La recette X semble préférée, mais quatre juges ne suffisent pas pour l’affirmer. (Avec aussi peu de blocs, l’approximation du est d’ailleurs grossière ; des tables exactes existent.)
En code
import numpy as npfrom scipy import stats
# Une ligne par juge, une colonne par recette (X, Y, Z) ; 1 = préféréerangs = np.array([[1, 2, 3], [1, 3, 2], [1, 2, 3], [2, 1, 3]])print(rangs.sum(axis=0)) # [ 5 8 11]
res = stats.friedmanchisquare(*rangs.T) # un argument par recetteprint(res.statistic, res.pvalue) # 4.5 0.105# Une ligne par juge (bloc), une colonne par recette (X, Y, Z) ; 1 = préféréerangs <- matrix(c(1, 2, 3, 1, 3, 2, 1, 2, 3, 2, 1, 3), nrow = 4, byrow = TRUE, dimnames = list(NULL, c("X", "Y", "Z")))colSums(rangs) # 5 8 11friedman.test(rangs) # Friedman chi-squared = 4.5, df = 2, p-value = 0.1054Exercice · Petits groupes et approximation
moyenTrois groupes de trois mesures : G1 = (1,2 ; 3,4 ; 2,2), G2 = (4,1 ; 5,0 ; 3,9), G3 = (0,8 ; 2,9 ; 1,5).
- Calculez .
- L’approximation du donne . Par énumération des répartitions possibles des rangs, on trouve 84 répartitions avec la valeur observée. Quelle est la p-valeur exacte ? Qu’en concluez-vous ?
Voir la solution
- Classement des 9 valeurs : 0,8 (1), 1,2 (2), 1,5 (3), 2,2 (4), 2,9 (5), 3,4 (6), 3,9 (7), 4,1 (8), 5,0 (9). Sommes de rangs : G1 = 2 + 6 + 4 = 12, G2 = 8 + 9 + 7 = 24, G3 = 1 + 5 + 3 = 9. Alors :
- . L’approximation (0,061) est ici un peu conservatrice. Avec de si petits groupes, il faut utiliser la loi exacte (tables ou énumération) ; l’approximation du n’est fiable qu’à partir d’environ cinq observations par groupe.
En code
import itertoolsimport numpy as npfrom scipy import stats
G1, G2, G3 = [1.2, 3.4, 2.2], [4.1, 5.0, 3.9], [0.8, 2.9, 1.5]res = stats.kruskal(G1, G2, G3)print(res.statistic, res.pvalue) # 5.6 0.061 (approximation du chi2)
def H(r1, r2, r3, N=9): return 12 / (N * (N + 1)) * sum(sum(r) ** 2 / 3 for r in (r1, r2, r3)) - 3 * (N + 1)
# Loi exacte : toutes les répartitions des rangs 1..9 en trois groupes de 3rangs = set(range(1, 10))compte = total = 0for r1 in itertools.combinations(rangs, 3): for r2 in itertools.combinations(rangs - set(r1), 3): r3 = rangs - set(r1) - set(r2) total += 1 compte += H(r1, r2, r3) >= 5.6 - 1e-9print(total, compte, compte / total) # 1680 84 0.05G1 <- c(1.2, 3.4, 2.2); G2 <- c(4.1, 5.0, 3.9); G3 <- c(0.8, 2.9, 1.5)kruskal.test(list(G1, G2, G3)) # chi-squared = 5.6, df = 2, p-value = 0.06081
H <- function(r1, r2, r3, N = 9) 12 / (N * (N + 1)) * (sum(r1)^2 + sum(r2)^2 + sum(r3)^2) / 3 - 3 * (N + 1)
# Loi exacte : toutes les répartitions des rangs 1..9 en trois groupes de 3compte <- 0; total <- 0for (i in seq_len(choose(9, 3))) { r1 <- combn(9, 3)[, i]; reste <- setdiff(1:9, r1) for (j in seq_len(choose(6, 3))) { r2 <- reste[combn(6, 3)[, j]]; r3 <- setdiff(reste, r2) total <- total + 1 compte <- compte + (H(r1, r2, r3) >= 5.6 - 1e-9) }}c(total, compte, compte / total) # 1680 84 0.05Exercice · Le bon test pour le bon plan
difficilePour chaque situation, choisissez le test adapté et justifiez.
- Trente clients notent de 1 à 5 trois versions d’une application : chaque client teste les trois.
- On compare la durée de séjour à l’hôpital (très asymétrique) de patients répartis au hasard entre quatre protocoles.
- On compare deux modèles sur les 10 plis d’une même validation croisée.
- Un collègue a comparé quatre modèles sur un seul jeu de données en faisant les six tests de Mann-Whitney deux à deux entre leurs scores par pli, sans correction. Qu’en pensez-vous ?
Voir la solution
- Friedman : chaque client est un bloc, les trois versions sont les traitements, et les notes ordinales se prêtent aux rangs.
- Kruskal-Wallis : quatre groupes indépendants, variable asymétrique. En cas de rejet, test de Dunn avec correction de Holm ou de Bonferroni.
- Wilcoxon signé sur les 10 différences de score (chapitre 2), puisque les deux modèles sont évalués sur les mêmes plis. Avec la réserve que les plis ne sont pas indépendants, ce qui rend la p-valeur optimiste.
- Deux erreurs. D’abord, les scores par pli sont appariés (mêmes plis pour tous les modèles), donc Mann-Whitney, qui suppose des groupes indépendants, est inadapté : il faudrait Friedman, avec les plis comme blocs. Ensuite, six tests non corrigés font grimper le risque de faux positif bien au-delà de 5 %. Enfin, un seul jeu de données ne permet pas de généraliser la conclusion à d’autres problèmes.
En code
import itertoolsimport numpy as npfrom scipy import stats
# Question 4 : quatre modèles équivalents (H0 vraie), 10 plis chacun,# six tests de Mann-Whitney deux à deux sans correctionrng = np.random.default_rng(0)n_sim, faux_positif = 2000, 0for _ in range(n_sim): scores = rng.normal(size=(4, 10)) p = [stats.mannwhitneyu(scores[i], scores[j]).pvalue for i, j in itertools.combinations(range(4), 2)] faux_positif += min(p) < 0.05print(faux_positif / n_sim) # environ 0.19 : près de quatre fois le risque annoncé de 5 %# Question 4 : quatre modèles équivalents (H0 vraie), 10 plis chacun,# six tests de Mann-Whitney deux à deux sans correctionset.seed(0)paires <- combn(4, 2)faux_positif <- replicate(2000, { scores <- matrix(rnorm(40), nrow = 4) p <- apply(paires, 2, function(k) wilcox.test(scores[k[1], ], scores[k[2], ])$p.value) min(p) < 0.05})mean(faux_positif) # environ 0.20 : près de quatre fois le risque annoncé de 5 %