Comparez deux groupes avec Mann-Whitney
Une équipe produit lance un test A/B : la moitié des visiteurs voit l’ancienne page d’accueil (version A), l’autre moitié une nouvelle (version B). On mesure la durée de chaque session. Les deux groupes sont formés de personnes différentes : impossible d’apparier, comme au chapitre précédent. Et les durées de session sont typiquement asymétriques, avec quelques sessions très longues.
Le test de Mann-Whitney est l’outil standard de cette situation. Vous verrez comment il se calcule, ce qu’il teste exactement (ce n’est pas tout à fait une égalité de moyennes), et pourquoi il est, mot pour mot, l’aire sous la courbe ROC que vous utilisez pour évaluer un classifieur.
Le fil rouge : un test A/B sur la durée des sessions
Section intitulée « Le fil rouge : un test A/B sur la durée des sessions »Sur un petit pilote, on a relevé ces durées de session (en minutes) :
| Version A () | 3,2 | 5,1 | 2,4 | 7,8 | 4,0 | 3,6 | |
|---|---|---|---|---|---|---|---|
| Version B () | 6,3 | 8,9 | 4,4 | 12,5 | 7,1 | 5,7 | 9,8 |
Les médianes valent 3,8 minutes pour A et 7,1 minutes pour B. La version B semble retenir les visiteurs plus longtemps. Mais avec 13 sessions, est-ce concluant ?
La somme des rangs
Section intitulée « La somme des rangs »L’idée du test, proposée par Wilcoxon puis reformulée par Mann et Whitney, est la suivante : on mélange les deux échantillons, on classe les valeurs, puis on regarde comment les rangs se répartissent entre les groupes. Si les deux versions sont équivalentes, les rangs de A devraient être éparpillés au hasard parmi les rangs 1 à 13. Si A est systématiquement plus court, ses rangs seront concentrés en bas.
La statistique U et sa loi
Section intitulée « La statistique U et sa loi »On transforme la somme des rangs en une statistique plus parlante :
où est la somme des rangs du groupe 1 et la plus petite valeur possible de (quand le groupe 1 occupe les rangs 1 à ). On définit de même, et .
Ici, et , avec bien .
Pourquoi cette transformation ? Parce que a une interprétation directe :
c’est le nombre de paires (une session A, une session B) dans lesquelles la session A est la plus longue (un ex aequo compte pour 1/2). Sur les paires possibles, seules 5 donnent l’avantage à A : par exemple, la session A de 7,8 minutes bat celles de B qui durent 4,4 ; 5,7 ; 6,3 et 7,1 minutes, et celle de 5,1 minutes bat celle de 4,4.
Loi exacte
Section intitulée « Loi exacte »Sous (les deux échantillons viennent de la même loi continue), toutes les façons de choisir les rangs de A parmi les sont équiprobables. Il y en a . En les énumérant, on trouve 19 configurations avec . La p-valeur bilatérale vaut donc :
On rejette au seuil de 5 % : les sessions B sont plus longues.
Approximation normale
Section intitulée « Approximation normale »Pour de plus grands échantillons, on utilise, sous :
Ici, , , donc et . En présence d’ex aequo, on corrige la variance ; les logiciels ajoutent aussi souvent une correction de continuité de 1/2.
Ce que teste vraiment Mann-Whitney
Section intitulée « Ce que teste vraiment Mann-Whitney »On lit souvent que Mann-Whitney « compare les médianes ». C’est vrai seulement sous une hypothèse supplémentaire. Le test est sensible à la quantité :
où est une observation tirée du groupe 1 et une observation tirée du groupe 2. L’estimateur naturel de est . Sous , : une observation de A a autant de chances de battre une observation de B que l’inverse.
- Si les deux lois ont la même forme et ne diffèrent que par un décalage (modèle de translation, location shift), alors équivaut à une différence de médianes, et le test compare bien les médianes.
- Si les formes diffèrent (par exemple, des variances très différentes), le test peut rejeter alors que les médianes sont égales, ou l’inverse.
Estimer le décalage : Hodges-Lehmann
Section intitulée « Estimer le décalage : Hodges-Lehmann »Une p-valeur ne dit pas de combien B dépasse A. Sous le modèle de translation, l’estimateur de Hodges-Lehmann du décalage est la médiane des différences . Ici, il vaut 3,2 minutes : la version B allonge les sessions d’environ 3 minutes. C’est l’estimation que fournit R avec conf.int = TRUE, accompagnée d’un intervalle de confiance.
En pratique : Python et R
Section intitulée « En pratique : Python et R »import numpy as npfrom scipy import stats
A = np.array([3.2, 5.1, 2.4, 7.8, 4.0, 3.6])B = np.array([6.3, 8.9, 4.4, 12.5, 7.1, 5.7, 9.8])
res = stats.mannwhitneyu(A, B) # bilatéral ; loi exacte pour les petits échantillons sans ex aequoprint(res.statistic, res.pvalue) # 5.0 et environ 0.022
# Estimateur de Hodges-Lehmann du décalage B - Aprint(np.median(B[:, None] - A[None, :])) # 3.2
# Test t de Welch, pour comparaisonprint(stats.ttest_ind(B, A, equal_var=False).pvalue) # environ 0.023scipy renvoie la statistique du premier échantillon passé en argument. En R, la même fonction que pour les données appariées sert, sans paired :
A <- c(3.2, 5.1, 2.4, 7.8, 4.0, 3.6)B <- c(6.3, 8.9, 4.4, 12.5, 7.1, 5.7, 9.8)wilcox.test(A, B) # W = 5, p = 0.02214wilcox.test(B, A, conf.int = TRUE) # estimation du décalage (Hodges-Lehmann) et ICR appelle la statistique , mais c’est bien . D’où les deux noms du test : « Mann-Whitney U » et « test de la somme des rangs de Wilcoxon » (Wilcoxon rank-sum test).
Mann-Whitney et l’AUC, c’est la même chose
Section intitulée « Mann-Whitney et l’AUC, c’est la même chose »Changeons de décor. Un classifieur attribue un score à des exemples positifs (des patients malades, des transactions frauduleuses) et à des exemples négatifs. Son AUC (aire sous la courbe ROC, présentée dans le cours d’Apprentissage automatique) admet une interprétation probabiliste : c’est la probabilité qu’un positif tiré au hasard reçoive un score plus élevé qu’un négatif tiré au hasard, les égalités comptant pour 1/2.
Vous reconnaissez ! L’AUC est exactement , où est la statistique de Mann-Whitney des scores des positifs contre ceux des négatifs.
Cette équivalence a trois conséquences pratiques :
- l’AUC ne dépend que du classement des scores, pas de leurs valeurs : toute transformation croissante des scores la laisse inchangée ;
- le calcul par les rangs coûte , bien moins que l’énumération des paires ;
- tester « AUC = 0,5 » (le modèle ne fait pas mieux que le hasard), c’est faire un test de Mann-Whitney.
Pour notre test A/B, : une session B tirée au hasard a 88 % de chances d’être plus longue qu’une session A. Cette « AUC » est une taille d’effet facile à expliquer, qu’on appelle aussi probabilité de supériorité.
En résumé
Section intitulée « En résumé »- Mann-Whitney compare deux groupes indépendants en classant toutes les observations ensemble.
- compte les paires où le groupe 1 l’emporte ; ; sous , et .
- Le test porte sur ; il compare les médianes seulement si les deux lois ne diffèrent que par un décalage.
- L’estimateur de Hodges-Lehmann (médiane des différences croisées) chiffre le décalage.
- L’AUC d’un classifieur vaut : c’est une statistique de Mann-Whitney.
scipy.stats.mannwhitneyu; en R,wilcox.test(x, y)sanspaired.
Exercices
Section intitulée « Exercices »Exercice · Calculer U à la main
facileDeux méthodes de révision sont testées sur deux groupes d’étudiants. Notes du groupe X : 12, 15, 9, 20. Notes du groupe Y : 18, 25, 22, 30, 16.
- Calculez , puis , de deux façons (par les rangs et par les paires).
- Sous , . Donnez la p-valeur bilatérale et concluez au seuil de 5 %.
Voir la solution
- Classement des 9 notes : 9 (X), 12 (X), 15 (X), 16 (Y), 18 (Y), 20 (X), 22 (Y), 25 (Y), 30 (Y). Les rangs de X sont 1, 2, 3 et 6, donc et . Par les paires : seule la note 20 dépasse des notes de Y (16 et 18), soit 2 paires. Les deux méthodes concordent.
- . On ne rejette pas au seuil de 5 %, même si le groupe Y semble meilleur. Avec 4 et 5 observations, il faudrait une séparation presque parfaite pour conclure.
En code
import numpy as npfrom scipy import stats
X = np.array([12, 15, 9, 20])Y = np.array([18, 25, 22, 30, 16])
rangs = stats.rankdata(np.r_[X, Y])R_X = rangs[:len(X)].sum()print(R_X, R_X - len(X) * (len(X) + 1) / 2) # R_X = 12.0, U_X = 2.0print((X[:, None] > Y[None, :]).sum()) # par les paires : 2
res = stats.mannwhitneyu(X, Y) # loi exacteprint(res.statistic, res.pvalue) # 2.0 0.0635 (= 2 x 4/126)X <- c(12, 15, 9, 20)Y <- c(18, 25, 22, 30, 16)
rangs <- rank(c(X, Y))R_X <- sum(rangs[seq_along(X)])c(R_X, R_X - 4 * 5 / 2) # R_X = 12, U_X = 2sum(outer(X, Y, ">")) # par les paires : 2
wilcox.test(X, Y) # W = 2, p-value = 0.06349 (= 2 x 4/126)Exercice · De U à l'AUC
moyenUn modèle de détection de fraude est évalué sur 50 transactions frauduleuses et 200 transactions normales. La somme des rangs des scores des fraudes, parmi les 250 scores, vaut (aucun ex aequo).
- Calculez et l’AUC.
- Que vaudrait l’AUC si l’on remplaçait chaque score par ?
- Sous l’hypothèse d’un modèle sans pouvoir discriminant, calculez et l’écart-type de . L’AUC observée est-elle compatible avec le hasard ?
Voir la solution
- . AUC .
- Toujours 0,72 : le logarithme est croissant, il conserve l’ordre des scores, donc les rangs et .
- et , soit un écart-type d’environ 457. : on est très loin de ce que produirait le hasard ( de l’ordre de ). Le modèle discrimine réellement, même si une AUC de 0,72 reste modeste.
En code
import numpy as npfrom scipy import stats
n_pos, n_neg, R_pos = 50, 200, 8475U = R_pos - n_pos * (n_pos + 1) / 2print(U, U / (n_pos * n_neg)) # 7200.0 0.72
# Sous H0 : moyenne et écart-type de U, puis score zE_U = n_pos * n_neg / 2sd_U = np.sqrt(n_pos * n_neg * (n_pos + n_neg + 1) / 12)z = (U - E_U) / sd_Uprint(E_U, sd_U, z, 2 * stats.norm.sf(z)) # 5000.0 457.3 4.81 1.5e-06n_pos <- 50; n_neg <- 200; R_pos <- 8475U <- R_pos - n_pos * (n_pos + 1) / 2c(U, U / (n_pos * n_neg)) # 7200 et 0.72
# Sous H0 : moyenne et écart-type de U, puis score zE_U <- n_pos * n_neg / 2sd_U <- sqrt(n_pos * n_neg * (n_pos + n_neg + 1) / 12)z <- (U - E_U) / sd_Uc(E_U, sd_U, z, 2 * pnorm(-z)) # 5000 457.3 4.81 1.5e-06Exercice · Pourquoi U compte les paires
difficileMontrez que est égal au nombre de paires telles que (on suppose qu’il n’y a pas d’ex aequo).
Indication : exprimez le rang de , la -ième plus petite valeur du groupe 1, dans l’échantillon mélangé.
Voir la solution
Dans l’échantillon mélangé, le rang de est égal au nombre de valeurs inférieures ou égales à . Parmi elles, il y a exactement valeurs du groupe 1 (), et un certain nombre de valeurs du groupe 2, celles qui sont plus petites que . Donc .
En sommant sur :
Or compte, pour chaque , le nombre de qu’il dépasse : c’est exactement le nombre de paires avec . D’où . Ce résultat justifie à la fois l’interprétation de et le calcul rapide de l’AUC par les rangs.
En code
import numpy as npfrom scipy import stats
# Vérification numérique sur des échantillons aléatoires sans ex aequorng = np.random.default_rng(0)for n1, n2 in [(4, 5), (10, 7), (30, 40)]: X, Y = rng.normal(size=n1), rng.normal(0.5, 1, size=n2) R1 = stats.rankdata(np.r_[X, Y])[:n1].sum() U_rangs = R1 - n1 * (n1 + 1) / 2 U_paires = (X[:, None] > Y[None, :]).sum() print(n1, n2, U_rangs, U_paires) # les deux colonnes sont toujours égales# Vérification numérique sur des échantillons aléatoires sans ex aequoset.seed(0)for (n in list(c(4, 5), c(10, 7), c(30, 40))) { X <- rnorm(n[1]); Y <- rnorm(n[2], 0.5) R1 <- sum(rank(c(X, Y))[1:n[1]]) U_rangs <- R1 - n[1] * (n[1] + 1) / 2 U_paires <- sum(outer(X, Y, ">")) cat(n, U_rangs, U_paires, "\n") # les deux dernières valeurs sont toujours égales}