Comparez plusieurs groupes avec l'analyse de la variance
Au chapitre 7, vous avez comparé deux moyennes. Mais comment comparer trois modes de transport, quatre types de viande dans des saucisses, ou cinq recettes d’un même produit ? Multiplier les tests de Student deux à deux est tentant, et c’est une erreur : plus on fait de tests, plus on finit par trouver une différence par hasard.
L’analyse de la variance (ANOVA) répond à la question globale en un seul test : les moyennes des groupes sont-elles toutes égales ? Malgré son nom, elle compare bien des moyennes, en analysant comment se répartit la variabilité. C’est aussi un cas particulier du modèle linéaire des deux chapitres précédents. Notre fil rouge : des temps de trajet domicile-école.
Le fil rouge : trois façons de venir à l’INSA
Section intitulée « Le fil rouge : trois façons de venir à l’INSA »On relève le temps de trajet (en minutes) d’étudiants habitant à une distance comparable du campus, selon leur mode de transport :
| Mode | Temps de trajet (min) | Effectif | Moyenne |
|---|---|---|---|
| Bus | 28, 34, 25, 31, 37 | 5 | 31,0 |
| Vélo | 18, 22, 15, 20, 24, 19 | 6 | 19,7 |
| Voiture | 24, 30, 21, 27, 23 | 5 | 25,0 |
La moyenne générale vaut minutes. Des boîtes à moustaches côte à côte (boxplot(temps ~ mode) en R) montrent des groupes bien décalés. Mais avec cinq ou six étudiants par groupe, ces écarts pourraient-ils venir du hasard ?
Le vocabulaire : le mode de transport est un facteur (une variable explicative qualitative), et ses valeurs sont ses modalités (ou niveaux).
Le modèle d’ANOVA à un facteur
Section intitulée « Le modèle d’ANOVA à un facteur »On note le temps du -ème étudiant du groupe , avec et ; l’effectif total est . Le modèle s’écrit
où est la moyenne du groupe . Les hypothèses sont celles de la régression : erreurs indépendantes, gaussiennes et de même variance dans tous les groupes.
On l’écrit souvent sous la forme des effets :
où est un niveau de référence et l’effet de la modalité . Mais ce modèle a paramètres pour moyennes : il n’est pas identifiable (on peut ajouter 5 à et retirer 5 à tous les sans rien changer). Il faut une contrainte :
- la contrainte « naturelle » : est alors la moyenne générale, et l’écart du groupe à cette moyenne ;
- la contrainte de R par défaut, : le premier groupe (par ordre alphabétique) sert de référence, est sa moyenne, et chaque est l’écart à ce groupe.
Décomposer la variabilité
Section intitulée « Décomposer la variabilité »L’idée centrale : si les modes de transport ont des effets différents, la variabilité entre les groupes sera grande devant la variabilité à l’intérieur des groupes. On décompose la variabilité totale en deux morceaux :
- (totale) mesure la dispersion de toutes les observations autour de la moyenne générale ;
- (du modèle, ou inter-groupes) mesure la dispersion des moyennes de groupes autour de , pondérée par les effectifs ;
- (résiduelle, ou intra-groupes) mesure la dispersion de chaque observation autour de la moyenne de son groupe.
Les degrés de liberté se décomposent de la même façon : .
Le test de Fisher
Section intitulée « Le test de Fisher »On teste (le facteur n’a pas d’effet) contre : au moins deux moyennes diffèrent. On compare les carrés moyens, c’est-à-dire les sommes de carrés divisées par leurs degrés de liberté :
Sous , numérateur et dénominateur estiment tous deux , et reste proche de 1. Si les moyennes diffèrent, le numérateur gonfle. On rejette si .
On présente le tout dans la table d’ANOVA, exactement comme l’affiche anova() en R :
| Source | ddl | Somme des carrés | Carré moyen | p-valeur | |
|---|---|---|---|---|---|
| Mode (inter) | 2 | 350,4 | 175,2 | 12,03 | 0,0011 |
| Résidus (intra) | 13 | 189,3 | 14,56 |
Avec , on rejette : le temps de trajet moyen dépend du mode de transport. Le carré moyen résiduel estime , soit minutes.
trajets <- data.frame( temps = c(28, 34, 25, 31, 37, 18, 22, 15, 20, 24, 19, 24, 30, 21, 27, 23), mode = factor(rep(c("bus", "velo", "voiture"), times = c(5, 6, 5))))modele <- lm(temps ~ mode, data = trajets)anova(modele) # table d'ANOVA : F = 12.03, p = 0.0011summary(modele) # coefficients avec « bus » comme référencepar(mfrow = c(2, 2)); plot(modele) # graphiques de diagnostic des résidusfrom scipy import stats
bus = [28, 34, 25, 31, 37]velo = [18, 22, 15, 20, 24, 19]voiture = [24, 30, 21, 27, 23]print(stats.f_oneway(bus, velo, voiture)) # statistic 12.03, pvalue 0.0011Avec deux groupes seulement, l’ANOVA redonne exactement le test de Student à variances égales du chapitre 7, avec . Et c’est bien une régression : lm(temps ~ mode) crée automatiquement des variables indicatrices (0 ou 1) pour chaque modalité sauf la référence.
Après le rejet : quels groupes diffèrent ?
Section intitulée « Après le rejet : quels groupes diffèrent ? »Le test global dit qu’au moins deux moyennes diffèrent, pas lesquelles. On compare alors les groupes deux à deux par
C’est un test de Student, mais qui utilise l’estimation de fondée sur tous les groupes (le ), d’où degrés de liberté.
Mais avec groupes, il y a comparaisons. Si chacune a un risque de 5 %, le risque de trouver au moins une fausse différence grimpe vite. La correction de Bonferroni fait chaque test au niveau (ou, ce qui revient au même, multiplie chaque p-valeur par ). Ainsi, le risque global reste inférieur à .
library(emmeans)emmeans(modele, pairwise ~ mode, adjust = "bonferroni")# ou, sans paquet supplémentaire :pairwise.t.test(trajets$temps, trajets$mode, p.adjust.method = "bonferroni")En TP, nous avons suivi cette démarche sur une étude de consommateurs comparant des hot-dogs selon leur composition (bœuf, mélange de viandes, volaille ; une quatrième modalité, le soja, était retirée de l’analyse) : ANOVA sur les calories puis sur le sodium, vérification des hypothèses sur les résidus, puis comparaisons deux à deux avec emmeans et la correction de Bonferroni pour identifier le type le moins calorique.
Vérifier les hypothèses et aller plus loin
Section intitulée « Vérifier les hypothèses et aller plus loin »Les hypothèses
Section intitulée « Les hypothèses »Comme en régression, on vérifie les hypothèses sur les résidus :
- même variance dans tous les groupes : des boîtes à moustaches d’étendues comparables, ou un test de Bartlett (
bartlett.test, sensible à la normalité) ou de Levene ; - normalité : droite de Henry des résidus ;
- indépendance : garantie par le plan d’expérience (des individus différents, tirés au hasard).
L’ANOVA est assez robuste à un léger écart à la normalité, surtout avec des groupes de tailles égales. Si les variances diffèrent beaucoup, on peut utiliser la version de Welch (oneway.test en R).
L’ANOVA à deux facteurs
Section intitulée « L’ANOVA à deux facteurs »On peut croiser deux facteurs. Le modèle additif (sans interaction) s’écrit
où est l’effet de la modalité du premier facteur et celui de la modalité du second. La table d’ANOVA contient alors une ligne par facteur, chacune avec son test de Fisher.
L’intérêt est double. D’abord, étudier deux effets à la fois. Ensuite, retirer du bruit : en TP, des juges notaient l’acidité de trois cafés ; certains juges notent systématiquement plus sévèrement que d’autres. Intégrer le facteur « juge » (on parle de facteur bloc) retire cette variabilité du résidu, ce qui rend le test sur l’effet « café » plus puissant. En R, il suffit d’écrire lm(note ~ cafe + juge).
Si l’effet d’un facteur dépend du niveau de l’autre (un café paraît plus acide pour certains juges seulement), on ajoute un terme d’interaction , écrit cafe * juge en R. Il faut alors plusieurs observations par combinaison de modalités pour pouvoir l’estimer.
En résumé
Section intitulée « En résumé »- ANOVA à un facteur : , avec une contrainte d’identifiabilité (, ou premier groupe en référence comme dans R).
- , avec degrés de liberté ; .
- Test de Fisher : , à comparer à ; la table d’ANOVA résume le tout.
- Après un rejet, comparaisons deux à deux avec le et une correction de Bonferroni ().
- Hypothèses : indépendance, normalité, égalité des variances ; à deux facteurs, un facteur bloc réduit le bruit et l’interaction capte les effets croisés.
Exercices
Section intitulée « Exercices »Exercice · Compléter une table d'ANOVA
facileOn compare 4 méthodes d’apprentissage sur 24 élèves (6 par méthode). On obtient et . Complétez la table d’ANOVA et concluez au niveau 5 % ().
Voir la solution
. Degrés de liberté : et .
| Source | ddl | SC | CM | |
|---|---|---|---|---|
| Méthode | 3 | 120 | 40 | 4,44 |
| Résidus | 20 | 180 | 9 |
: on rejette , la méthode a un effet sur les résultats moyens. La méthode explique de la variabilité.
En code
from scipy import stats
I, n, SST, SSR = 4, 24, 300, 180SSM = SST - SSR # 120ddl_m, ddl_r = I - 1, n - I # 3 et 20CMM, CMR = SSM / ddl_m, SSR / ddl_r # 40 et 9F0 = CMM / CMRprint(F0, stats.f.ppf(0.95, ddl_m, ddl_r)) # 4.44 > 3.10 : on rejette H0print(stats.f.sf(F0, ddl_m, ddl_r), SSM / SST) # p = 0.015, R² = 0.40I <- 4; n <- 24; SST <- 300; SSR <- 180SSM <- SST - SSR # 120ddl_m <- I - 1; ddl_r <- n - I # 3 et 20CMM <- SSM / ddl_m; CMR <- SSR / ddl_r # 40 et 9F0 <- CMM / CMRF0; qf(0.95, ddl_m, ddl_r) # 4.44 > 3.10 : on rejette H01 - pf(F0, ddl_m, ddl_r); SSM / SST # p = 0.015, R² = 0.40Exercice · Interpréter des coefficients de R
moyenUne ANOVA du rendement d’une culture selon l’engrais (modalités A, B, C) donne avec summary(lm(rendement ~ engrais)) : constante 52,0 ; engraisB 4,5 ; engraisC . Les groupes ont tous 8 parcelles.
- Quelles sont les moyennes des trois groupes ?
- Quelle est la moyenne générale, et quels seraient les effets avec la contrainte naturelle ?
Voir la solution
- R utilise A comme référence : , et .
- Les effectifs étant égaux, la moyenne générale vaut . Avec la contrainte naturelle, et les effets sont les écarts à cette moyenne : , et , dont la somme (pondérée par les effectifs égaux) est bien nulle.
En code
import numpy as npimport pandas as pdimport statsmodels.formula.api as smf
# Coefficients lus dans la sortie (A en référence)constante, effet_B, effet_C = 52.0, 4.5, -2.0moyennes = np.array([constante, constante + effet_B, constante + effet_C])print(moyennes) # [52. 56.5 50. ]mu = moyennes.mean() # effectifs égauxprint(mu, moyennes - mu) # 52.83 [-0.83 3.67 -2.83]
# Vérification : 8 parcelles par engrais, construites avec ces moyennesecarts = np.array([-3, -2, -1, 0, 0, 1, 2, 3]) # écarts centrésdf = pd.DataFrame({"engrais": np.repeat(["A", "B", "C"], 8), "rendement": np.concatenate([m + ecarts for m in moyennes])})print(smf.ols("rendement ~ engrais", df).fit().params) # 52.0 4.5 -2.0print(smf.ols("rendement ~ C(engrais, Sum)", df).fit().params) # 52.83 -0.83 3.67# Coefficients lus dans la sortie (A en référence)constante <- 52.0; effet_B <- 4.5; effet_C <- -2.0moyennes <- c(A = constante, B = constante + effet_B, C = constante + effet_C)moyennes # 52.0 56.5 50.0mu <- mean(moyennes) # effectifs égauxmu; moyennes - mu # 52.83 -0.83 3.67 -2.83
# Vérification : 8 parcelles par engrais, construites avec ces moyennesecarts <- c(-3, -2, -1, 0, 0, 1, 2, 3) # écarts centrésdf <- data.frame(engrais = factor(rep(c("A", "B", "C"), each = 8)), rendement = c(outer(ecarts, moyennes, "+")))coef(lm(rendement ~ engrais, data = df)) # 52.0 4.5 -2.0 (A en référence)# Contrainte somme nulle : constante = mu, puis alpha_A et alpha_Bcoef(lm(rendement ~ engrais, data = df, contrasts = list(engrais = "contr.sum")))## 52.83 -0.83 3.67 (alpha_C = -(alpha_A + alpha_B) = -2.83)Exercice · Combien de comparaisons ?
difficileUne ANOVA sur 5 groupes est significative. On veut comparer tous les groupes deux à deux avec un risque global d’au plus 5 %.
- Combien de comparaisons faut-il faire, et à quel niveau individuel avec Bonferroni ?
- Si l’on faisait ces comparaisons à 5 % chacune et qu’elles étaient indépendantes, quelle serait la probabilité d’au moins une fausse découverte lorsque tous les groupes ont la même moyenne ?
Voir la solution
- comparaisons, chacune au niveau .
- La probabilité de n’avoir aucune fausse découverte serait , donc celle d’en avoir au moins une vaut environ . Sans correction, on aurait quatre chances sur dix de « découvrir » une différence inexistante. Les comparaisons ne sont pas vraiment indépendantes (elles partagent des groupes), mais l’ordre de grandeur montre bien le danger.
En code
from itertools import combinationsimport numpy as npfrom scipy import stats
I, alpha = 5, 0.05m = I * (I - 1) // 2print(m, alpha / m, 1 - (1 - alpha)**m) # 10 0.005 0.401
# Simulation : 5 groupes de même moyenne, 10 tests de Student à 5 % chacunrng = np.random.default_rng(0)R, sans_corr, bonf = 5000, 0, 0for _ in range(R): groupes = rng.normal(0, 1, size=(I, 10)) p = [stats.ttest_ind(groupes[i], groupes[j]).pvalue for i, j in combinations(range(I), 2)] sans_corr += min(p) < alpha # au moins une fausse découverte bonf += min(p) < alpha / mprint(sans_corr / R, bonf / R) # 0.29 0.043 : moins que 0.40 (tests liés), Bonferroni sous 5 %I <- 5; alpha <- 0.05m <- choose(I, 2)c(m, alpha / m, 1 - (1 - alpha)^m) # 10 0.005 0.401
# Simulation : 5 groupes de même moyenne, 10 tests de Student à 5 % chacunset.seed(0)groupe <- factor(rep(1:I, each = 10))p_min <- replicate(5000, { y <- rnorm(50) p <- pairwise.t.test(y, groupe, pool.sd = FALSE, var.equal = TRUE, p.adjust.method = "none")$p.value min(p, na.rm = TRUE)})mean(p_min < alpha); mean(p_min < alpha / m) # 0.29 0.037 : moins que 0.40 (tests liés), Bonferroni sous 5 %