Aller au contenu

Testez une médiane avec le test du signe et Wilcoxon

Une équipe ajoute un cache à son site et mesure le temps de chargement de dix pages, avant et après. Huit pages sur dix sont plus rapides, deux sont un peu plus lentes. Le cache a-t-il vraiment un effet, ou ce résultat pourrait-il être dû au hasard des mesures ? Avec dix paires seulement et des temps de chargement qui ne sont pas normaux, le test t apparié est discutable.

Ce chapitre présente deux réponses non paramétriques : le test du signe, qui ne regarde que le sens de chaque différence, et le test des rangs signés de Wilcoxon, qui regarde aussi son ampleur. Vous verrez qu’ils peuvent conclure très différemment sur les mêmes données.

Voici les temps de chargement (en ms), mesurés sur les mêmes dix pages :

Page12345678910
Avant312285340298405276330358290321
Après290280325301362270318330294300
Différence DiD_i−22−5−15+3−43−6−12−28+4−21
280300320340360380400420temps de chargement (ms)page 1page 2page 3page 4page 5page 6page 7page 8page 9page 10−22−5−15+3−43−6−12−28+4−21avant le cacheaprès : plus rapideaprès : plus lent
Huit pages sur dix accélèrent, parfois de plusieurs dizaines de millisecondes ; les deux pages ralenties ne perdent que 3 et 4 ms.

Les données sont appariées (paired) : chaque page est mesurée deux fois. On ne compare donc pas deux groupes, on étudie les différences Di=apreˋs−avantD_i = \text{après} - \text{avant}. Un problème apparié se ramène ainsi à un problème à un échantillon : la médiane des différences est-elle nulle ?

On pose les hypothèses :

  • H0H_0 : le cache n’a pas d’effet, la médiane des différences vaut 0 ;
  • H1H_1 : la médiane des différences n’est pas nulle (test bilatéral).

Les mêmes tests servent aussi pour un seul échantillon non apparié : pour tester si la médiane d’une variable vaut m0m_0, on travaille sur Di=Xi−m0D_i = X_i - m_0.

L’idée est d’une simplicité désarmante. Si la médiane des différences est nulle, chaque différence a une chance sur deux d’être positive, indépendamment des autres. Le nombre SS de différences positives suit donc une loi binomiale :

S=#{i:Di>0}∼Binom(n′,1/2)sous H0S = \#\lbrace i : D_i > 0 \rbrace \sim \mathrm{Binom}(n', 1/2) \quad \text{sous } H_0

où n′n' est le nombre de différences non nulles : les différences exactement nulles ne disent rien sur le sens de l’effet, on les retire.

C’est un test exact : la p-valeur se calcule directement avec la loi binomiale, sans aucune approximation, et avec une seule hypothèse (l’indépendance des paires). Il n’exige même pas que la loi soit continue ou symétrique.

Ce résultat est frustrant. Regardez les données : les deux différences positives sont minuscules (+3 et +4 ms), alors que certaines baisses dépassent 20 ou 40 ms. Le test du signe jette cette information, puisqu’il traite une baisse de 43 ms comme une baisse de 5 ms. Peut-on faire mieux sans supposer la normalité ? Oui, en tenant compte de l’ampleur des différences, par leurs rangs.

Le test de Wilcoxon (Wilcoxon signed-rank test) procède en trois étapes :

  1. on calcule les différences DiD_i et on retire celles qui sont nulles ;
  2. on classe les valeurs absolues ∣Di∣|D_i| par ordre croissant : RiR_i est le rang de ∣Di∣|D_i| ;
  3. on additionne les rangs des différences positives :
W+=∑i:Di>0RiW^+ = \sum_{i : D_i > 0} R_i

On définit de même W−W^- pour les différences négatives, et W++W−=n(n+1)/2W^+ + W^- = n(n+1)/2, la somme de tous les rangs de 1 à nn.

12345678910rang de |Dᵢ| (du plus petit au plus grand écart)−50−40−30−20−1001020différence Dᵢ (ms)+3+4−5−6−12−15−21−22−28−43W⁺ = 1 + 2 = 3W⁻ = 3 + 4 + … + 10 = 52
Le test de Wilcoxon regarde à quel rang se trouvent les différences positives. Ici elles sont les deux plus petites : W⁺ = 1 + 2 = 3, très loin des 27,5 attendus sous H₀.

Pourquoi la loi de W+W^+ ne dépend-elle pas de la loi des données ? Sous H0H_0, on suppose que la loi des différences est symétrique autour de 0. Alors, chaque rang 1,2,…,n1, 2, \dots, n a une chance sur deux, indépendamment des autres, de porter un signe « + ». Il y a 2n2^n affectations de signes équiprobables, et W+W^+ est la somme des rangs qui ont reçu un « + ».

Pour n=10n = 10, on peut énumérer les 210=10242^{10} = 1024 cas. Combien donnent W+≤3W^+ \le 3 ? Les ensembles de rangs de somme au plus 3 sont : aucun rang, {1}\lbrace 1 \rbrace, {2}\lbrace 2 \rbrace, {3}\lbrace 3 \rbrace et {1,2}\lbrace 1, 2 \rbrace, soit 5 cas. D’où :

p=2×51024≈0,0098p = 2 \times \frac{5}{1024} \approx 0{,}0098

Cette fois, on rejette H0H_0 au seuil de 5 %, et même de 1 %. Le cache a bien un effet.

Pour nn grand, on n’énumère plus : sous H0H_0,

E(W+)=n(n+1)4,Var(W+)=n(n+1)(2n+1)24E(W^+) = \frac{n(n+1)}{4}, \qquad \mathrm{Var}(W^+) = \frac{n(n+1)(2n+1)}{24}

et Z=(W+−E(W+))/Var(W+)Z = (W^+ - E(W^+)) / \sqrt{\mathrm{Var}(W^+)} suit approximativement la loi normale centrée réduite. Ici, E(W+)=27,5E(W^+) = 27{,}5, Var(W+)=96,25\mathrm{Var}(W^+) = 96{,}25, donc Z=(3−27,5)/9,81≈−2,50Z = (3 - 27{,}5)/9{,}81 \approx -2{,}50 et p≈0,0125p \approx 0{,}0125. C’est proche de la valeur exacte ; l’approximation devient très bonne au-delà d’une vingtaine de paires. Avec des ex aequo parmi les ∣Di∣|D_i|, on leur donne le rang moyen et on corrige la variance.

0246810S (différences positives)0,000,050,100,150,200,250,30probabilité sous H₀observéS = 2p = 112/1 024 ≈ 0,109test du signe : Binom(10 ; 1/2)01020304050W⁺ (somme des rangs positifs)0,000,010,020,030,040,05observéW⁺ = 3p = 2 × 5/1 024 ≈ 0,0098Wilcoxon : 2¹⁰ = 1 024 signes équiprobablesapproximation N(27,5 ; 96,25)
Les deux lois sous H₀ se calculent en dénombrant des cas équiprobables. La valeur S = 2 tombe dans une zone encore fréquente (p ≈ 0,11), alors que W⁺ = 3 est tout au bord de sa loi (p ≈ 0,0098). La courbe normale épouse déjà bien les bâtons pour n = 10.

Pour comparer, le test t apparié donne ici t≈−3,15t \approx -3{,}15 avec 9 degrés de liberté et p≈0,012p \approx 0{,}012. Les trois tests en Python :

import numpy as np
from scipy import stats
avant = np.array([312, 285, 340, 298, 405, 276, 330, 358, 290, 321])
apres = np.array([290, 280, 325, 301, 362, 270, 318, 330, 294, 300])
d = apres - avant
# Test du signe : 2 différences positives sur 10 non nulles
n_pos, n = (d > 0).sum(), (d != 0).sum()
print(stats.binomtest(n_pos, n, p=0.5).pvalue) # 0.109
# Wilcoxon signé (loi exacte pour les petits n sans ex aequo)
res = stats.wilcoxon(apres, avant)
print(res.statistic, res.pvalue) # 3.0 et environ 0.0098
# Test t apparié, pour comparaison
print(stats.ttest_rel(apres, avant).pvalue) # environ 0.012

En bilatéral, scipy renvoie comme statistique le minimum de W+W^+ et W−W^-, ici 3. En R :

avant <- c(312, 285, 340, 298, 405, 276, 330, 358, 290, 321)
apres <- c(290, 280, 325, 301, 362, 270, 318, 330, 294, 300)
d <- apres - avant
binom.test(sum(d > 0), sum(d != 0), p = 0.5) # p = 0.1094
wilcox.test(apres, avant, paired = TRUE) # V = 3, p = 0.009766
wilcox.test(apres, avant, paired = TRUE, conf.int = TRUE) # + estimation du décalage

R appelle la statistique VV et renvoie W+W^+. Avec conf.int = TRUE, il fournit l’estimateur de Hodges-Lehmann du décalage : la médiane de toutes les moyennes de paires (Di+Dj)/2(D_i + D_j)/2 avec i≤ji \le j (les « moyennes de Walsh »). Ici, il vaut −13,5 ms, un résumé robuste de l’effet du cache.

TestUtiliseHypothèse sur la loi des différencesPuissance
Signele sensaucune (continuité non requise)la plus faible
Wilcoxon signéle sens et le rang de l’ampleurcontinue et symétriqueproche du test t
t appariéles valeursnormale (ou nn grand)la meilleure si normale

Une règle simple : Wilcoxon par défaut ; le signe si les données sont très asymétriques, ordinales avec beaucoup d’ex aequo, ou si l’on ne connaît que le sens de chaque différence (« préfère A » ou « préfère B »).

  • Des données appariées se ramènent à un échantillon de différences ; on teste si leur médiane est nulle.
  • Le test du signe compte les différences positives : S∼Binom(n′,1/2)S \sim \mathrm{Binom}(n', 1/2) sous H0H_0, après retrait des différences nulles.
  • Le test de Wilcoxon signé additionne les rangs des ∣Di∣|D_i| associés aux différences positives : W+W^+, de moyenne n(n+1)/4n(n+1)/4 et de variance n(n+1)(2n+1)/24n(n+1)(2n+1)/24 sous H0H_0.
  • Wilcoxon est bien plus puissant que le signe, au prix d’une hypothèse de symétrie des différences.
  • Sur notre exemple, le signe donne p≈0,11p \approx 0{,}11 et Wilcoxon p≈0,0098p \approx 0{,}0098 : l’ampleur des différences change la conclusion.
  • scipy.stats.binomtest, scipy.stats.wilcoxon ; en R, binom.test et wilcox.test(..., paired = TRUE).

Exercice · Un test du signe à la main

facile

Quinze utilisateurs testent deux interfaces et indiquent laquelle ils préfèrent. Douze préfèrent la nouvelle, trois l’ancienne. Au seuil de 5 %, peut-on conclure à une préférence ?

Voir la solution

Sous H0H_0 (aucune préférence), le nombre SS de préférences pour la nouvelle interface suit Binom(15,1/2)\mathrm{Binom}(15, 1/2). On a observé S=12S = 12 :

P(S≥12)=(1512)+(1513)+(1514)+(1515)215=455+105+15+132 768=57632 768≈0,0176P(S \ge 12) = \frac{\binom{15}{12} + \binom{15}{13} + \binom{15}{14} + \binom{15}{15}}{2^{15}} = \frac{455 + 105 + 15 + 1}{32\,768} = \frac{576}{32\,768} \approx 0{,}0176

La p-valeur bilatérale vaut 2×0,0176≈0,0352 \times 0{,}0176 \approx 0{,}035 : on rejette H0H_0 au seuil de 5 %. Ici, seul le test du signe s’applique, puisqu’on ne dispose que du sens de chaque préférence, pas d’une ampleur.

En code

from scipy import stats
# 12 préférences pour la nouvelle interface sur 15, Binom(15, 1/2) sous H0
print(stats.binom.sf(11, 15, 0.5)) # P(S >= 12) = 0.0176
print(stats.binomtest(12, 15, p=0.5).pvalue) # bilatéral : 0.0352

Exercice · Wilcoxon sur six paires

moyen

Six coureurs mesurent leur temps sur 5 km avec deux paires de chaussures. Les différences (ancienne − nouvelle, en secondes) sont : 1,5 ; −0,4 ; 2,1 ; 0,8 ; −1,1 ; 3,0.

  1. Calculez W+W^+ et W−W^-.
  2. La loi exacte de W+W^+ pour n=6n = 6 donne P(W+≤4)=7/64P(W^+ \le 4) = 7/64. Calculez la p-valeur bilatérale et concluez.
  3. Quelle est la plus petite p-valeur bilatérale possible avec six paires ?
Voir la solution
  1. Valeurs absolues triées : 0,4 (rang 1, négative), 0,8 (rang 2), 1,1 (rang 3, négative), 1,5 (rang 4), 2,1 (rang 5), 3,0 (rang 6). Donc W−=1+3=4W^- = 1 + 3 = 4 et W+=2+4+5+6=17W^+ = 2 + 4 + 5 + 6 = 17 ; on vérifie que W++W−=21=6×7/2W^+ + W^- = 21 = 6 \times 7/2.
  2. La statistique la plus extrême est min⁡(W+,W−)=4\min(W^+, W^-) = 4 : p=2×7/64≈0,22p = 2 \times 7/64 \approx 0{,}22. On ne peut pas conclure que les nouvelles chaussures font gagner du temps.
  3. Le cas le plus extrême est W+=0W^+ = 0 (ou W−=0W^- = 0), qui n’arrive que pour une seule affectation de signes sur 26=642^6 = 64 : pmin⁡=2/64≈0,031p_{\min} = 2/64 \approx 0{,}031. Avec six paires, il faut donc que toutes les différences aillent dans le même sens pour rejeter au seuil de 5 %. Avec cinq paires, pmin⁡=2/32=0,0625p_{\min} = 2/32 = 0{,}0625 : le rejet à 5 % est impossible, quelles que soient les données.

En code

import numpy as np
from scipy import stats
d = np.array([1.5, -0.4, 2.1, 0.8, -1.1, 3.0])
rangs = stats.rankdata(np.abs(d))
print(rangs[d > 0].sum(), rangs[d < 0].sum()) # W+ = 17.0, W- = 4.0
res = stats.wilcoxon(d) # loi exacte (n = 6, pas d'ex aequo)
print(res.statistic, res.pvalue) # 4.0 0.21875 (= 2 x 7/64)
# Plus petite p-valeur possible : toutes les différences du même signe
print(stats.wilcoxon([1, 2, 3, 4, 5, 6]).pvalue) # 0.03125 (= 2/64)
print(stats.wilcoxon([1, 2, 3, 4, 5]).pvalue) # 0.0625 (= 2/32)

Exercice · Signe ou Wilcoxon ?

difficile

Reprenez les dix pages du fil rouge, mais supposez que la page 4 ait donné une différence de +50 ms au lieu de +3 ms (les autres différences sont inchangées).

  1. La p-valeur du test du signe change-t-elle ?
  2. Recalculez W+W^+. Sans calculer la p-valeur exacte, dites si la conclusion de Wilcoxon risque de changer.
  3. Qu’en retenez-vous sur ce que mesure chaque test ?
Voir la solution
  1. Non : il y a toujours 2 différences positives sur 10, donc p≈0,109p \approx 0{,}109. Le test du signe ignore l’ampleur.
  2. Les valeurs absolues triées deviennent 4, 5, 6, 12, 15, 21, 22, 28, 43, 50. Les différences positives sont +4 (rang 1) et +50 (rang 10) : W+=11W^+ = 11, W−=44W^- = 44. On s’éloigne moins de la valeur attendue 27,5 qu’avec W+=3W^+ = 3 : la p-valeur augmente nettement (en approximation normale, Z=(11−27,5)/9,81≈−1,68Z = (11 - 27{,}5)/9{,}81 \approx -1{,}68, soit p≈0,09p \approx 0{,}09 ; la loi exacte donne p≈0,105p \approx 0{,}105). La conclusion change : on ne rejette plus au seuil de 5 %.
  3. Wilcoxon est sensible à l’ampleur relative des différences, via leurs rangs. Une page nettement ralentie pèse autant que la plus forte accélération. Le test du signe, lui, ne voit que la proportion de pages ralenties. C’est pourquoi Wilcoxon est plus puissant quand l’effet est cohérent, mais aussi pourquoi il suppose une loi symétrique.

En code

import numpy as np
from scipy import stats
d = np.array([-22, -5, -15, 50, -43, -6, -12, -28, 4, -21]) # page 4 : +50 au lieu de +3
# 1. Test du signe : toujours 2 différences positives sur 10
print(stats.binomtest((d > 0).sum(), (d != 0).sum()).pvalue) # 0.109
# 2. Wilcoxon : W+ et p-valeurs exacte et approchée
rangs = stats.rankdata(np.abs(d))
print(rangs[d > 0].sum()) # W+ = 11.0
print(stats.wilcoxon(d).pvalue) # exacte : 0.105
z = (11 - 27.5) / np.sqrt(96.25)
print(z, 2 * stats.norm.cdf(z)) # -1.68 0.093