Code
x = c(rep(15, 20), rep(20, 20), rep(25, 20))
a = c(rep("a", 20), rep("b", 20), rep("c", 20))Ce TP couvre trois thèmes importants :
Le test d’ANOVA (Fisher) teste l’égalité des moyennes entre groupes. La statistique est :
\[F_{obs} = \frac{CMM}{CME}\]
C’est un rapport entre la variance due au modèle et la variance due aux erreurs. Plus la variance de l’erreur est faible, plus on a de chances de rejeter l’égalité.
On suppose 3 niveaux avec 20 observations chacun et des moyennes 15, 20, 25 :
x = c(rep(15, 20), rep(20, 20), rep(25, 20))
a = c(rep("a", 20), rep("b", 20), rep("c", 20))Si on lance une ANOVA sur ce modèle sans erreur :
anova(lm(x ~ a))| Df | Sum Sq | Mean Sq | F value | Pr(>F) | |
|---|---|---|---|---|---|
| <int> | <dbl> | <dbl> | <dbl> | <dbl> | |
| a | 2 | 1.000000e+03 | 5.000000e+02 | 1.966637e+31 | 0 |
| Residuals | 57 | 1.449174e-27 | 2.542411e-29 | NA | NA |
R donne un avertissement : les tests F d’ANOVA sur un ajustement pratiquement parfait ne sont pas fiables. C’est logique : sans erreur, le test n’a pas de sens !
On crée un vecteur d’erreurs normales centrées réduites :
set.seed(42)
err = rnorm(60)On ajoute l’erreur multipliée par un écart-type (commençons par 0.5) :
xe = x + 0.5 * err
anova(lm(xe ~ a))| Df | Sum Sq | Mean Sq | F value | Pr(>F) | |
|---|---|---|---|---|---|
| <int> | <dbl> | <dbl> | <dbl> | <dbl> | |
| a | 2 | 981.25762 | 490.628809 | 1471.204 | 8.849578e-50 |
| Residuals | 57 | 19.00882 | 0.333488 | NA | NA |
Avec un écart-type de 0.5, la p-value est très petite → rejet facile.
anovalue = function(x, a, sigma) {
err = rnorm(length(x))
xe = x + sigma * err
return(anova(lm(xe ~ a))$Pr)
}Faisons varier la valeur de sigma pour voir quand la p-value passe au-dessus de 5% :
cat("sigma = 1 :", anovalue(x, a, 1), "\n")
cat("sigma = 5 :", anovalue(x, a, 5), "\n")
cat("sigma = 10 :", anovalue(x, a, 10), "\n")
cat("sigma = 15 :", anovalue(x, a, 15), "\n")
cat("sigma = 20 :", anovalue(x, a, 20), "\n")sigma = 1 : 6.457851e-38 NA
sigma = 5 : 1.401194e-09 NA
sigma = 10 : 0.00149247 NA
sigma = 15 : 0.04803149 NA
sigma = 20 : 0.2232089 NA
Attention ! Puisque l’erreur est aléatoire, la p-value change à chaque appel. Près du seuil de 5%, relancez plusieurs fois pour observer les fluctuations.
Morale : Voilà, vous savez pondre des études bidons maintenant ! C’est une leçon sur l’importance de ne pas p-hacker ses résultats.
On compare la vitesse de pénétration de 4 marques de mèches en acier sur 5 types de dureté d’acier. Les temps sont mesurés en secondes.
C’est une classification à deux entrées (mesures répétées).
options(OutDec = ",")
durtVit <- read.table("dureteMeche.txt", header = TRUE)
head(durtVit)| Y | D | M | |
|---|---|---|---|
| <int> | <int> | <int> | |
| 1 | 12 | 1 | 1 |
| 2 | 2 | 2 | 1 |
| 3 | 8 | 3 | 1 |
| 4 | 1 | 4 | 1 |
| 5 | 7 | 5 | 1 |
| 6 | 20 | 1 | 2 |
Y = durtVit$Y
D = factor(durtVit$D) # Dureté (5 niveaux)
M = factor(durtVit$M) # Mèche (4 niveaux)Note : Les variables D et M sont codées avec des entiers mais ce sont bien des variables qualitatives (facteurs). On utilise
factor()pour le préciser à R.
interaction.plot(M, D, Y, main = "Graphe d'interaction",
xlab = "Mèche", ylab = "Moyenne de Y",
col = 1:5, lwd = 2)Interprétation : Les lignes ne se croisent pas vraiment → l’interaction des deux facteurs influence peu la variable Y.
On teste l’effet de 2 facteurs sans leur interaction (modèle additif) :
summary(aov(Y ~ D + M)) Df Sum Sq Mean Sq F value Pr(>F)
D 4 184 46,0 23,00 1,49e-05 ***
M 3 310 103,3 51,67 3,91e-07 ***
Residuals 12 24 2,0
---
Signif. codes: 0 ‘***’ 0,001 ‘**’ 0,01 ‘*’ 0,05 ‘.’ 0,1 ‘ ’ 1
Résultat : Chaque facteur influence significativement Y.
summary(aov(Y ~ D * M)) Df Sum Sq Mean Sq
D 4 184 46,0
M 3 310 103,3
D:M 12 24 2,0
anova(lm(Y ~ D * M))| Df | Sum Sq | Mean Sq | F value | Pr(>F) | |
|---|---|---|---|---|---|
| <int> | <dbl> | <dbl> | <dbl> | <dbl> | |
| D | 4 | 184 | 46,0000 | NaN | NaN |
| M | 3 | 310 | 103,3333 | NaN | NaN |
| D:M | 12 | 24 | 2,0000 | NaN | NaN |
| Residuals | 0 | 0 | NaN | NA | NA |
R renvoie un message d’erreur car il n’y a pas assez de données pour estimer l’interaction de manière fiable. Cela nous conforte dans l’idée que l’interaction n’influence pas Y.
Il est fondamental de noter que pris séparément, un facteur ne suffit pas à expliquer Y :
cat("=== ANOVA Y ~ M ===\n")
summary(aov(Y ~ M))
cat("\n=== ANOVA Y ~ D ===\n")
summary(aov(Y ~ D))=== ANOVA Y ~ M ===
Df Sum Sq Mean Sq F value Pr(>F)
M 3 310 103,3 7,949 0,00181 **
Residuals 16 208 13,0
---
Signif. codes: 0 ‘***’ 0,001 ‘**’ 0,01 ‘*’ 0,05 ‘.’ 0,1 ‘ ’ 1
=== ANOVA Y ~ D ===
Df Sum Sq Mean Sq F value Pr(>F)
D 4 184 46,00 2,066 0,136
Residuals 15 334 22,27
Observation : Le facteur M semble expliquer Y seul, mais pas D. Pourtant, dans l’ANOVA à deux facteurs, les deux sont significatifs. C’est leur action jointe qui explique Y. Faire une ANOVA à un facteur sur ce jeu de données est une mauvaise démarche.
Un second jeu de données avec un peu plus de données et une interaction plus marquée :
engrais = read.table("engraisRegion.txt", header = TRUE)
head(engrais)
Y = engrais$Y
R = factor(engrais$R)
E = factor(engrais$E)| Y | R | E | |
|---|---|---|---|
| <int> | <int> | <int> | |
| 1 | 15 | 1 | 1 |
| 2 | 14 | 1 | 1 |
| 3 | 17 | 1 | 1 |
| 4 | 21 | 2 | 1 |
| 5 | 20 | 2 | 1 |
| 6 | 21 | 2 | 1 |
anova(lm(Y ~ R * E))| Df | Sum Sq | Mean Sq | F value | Pr(>F) | |
|---|---|---|---|---|---|
| <int> | <dbl> | <dbl> | <dbl> | <dbl> | |
| R | 3 | 327,1944444 | 109,0648148 | 112,1809524 | 2,954829e-14 |
| E | 2 | 0,8888889 | 0,4444444 | 0,4571429 | 6,384893e-01 |
| R:E | 6 | 99,5555556 | 16,5925926 | 17,0666667 | 1,359421e-07 |
| Residuals | 24 | 23,3333333 | 0,9722222 | NA | NA |
Résultat : Seul le facteur E n’explique pas significativement Y. Par contre, R et l’interaction R:E sont significatifs.
anova(lm(Y ~ R + E))| Df | Sum Sq | Mean Sq | F value | Pr(>F) | |
|---|---|---|---|---|---|
| <int> | <dbl> | <dbl> | <dbl> | <dbl> | |
| R | 3 | 327,1944444 | 109,0648148 | 26,6252260 | 1,349547e-08 |
| E | 2 | 0,8888889 | 0,4444444 | 0,1084991 | 8,975301e-01 |
| Residuals | 30 | 122,8888889 | 4,0962963 | NA | NA |
interaction.plot(R, E, Y, main = "Graphe d'interaction (Engrais)",
col = 1:4, lwd = 2)Les tests non paramétriques n’exigent pas l’hypothèse de normalité. Ils sont basés sur les rangs plutôt que sur les valeurs brutes.
set.seed(42)
x = sample(1:100, 100, replace = TRUE)
quantile(x)
Q1 = quantile(x)[2]
Q3 = quantile(x)[4]On crée X (valeurs extérieures aux quartiles) et Y (valeurs intérieures) :
X = x[x < Q1 | x > Q3]
Y = x[x > Q1 & x < Q3]
# Ajout d'aléa pour les effectifs du test χ²
X = c(X, sample(Q1:Q3, 5))
Y = c(Y, sample(1:Q1, 5), sample(Q3:100, 5))
X = sort(X)
Y = sort(Y)On construit un tableau de contingence basé sur les quartiles :
table_chi = rbind(c(1, 1, 1), c(1, 1, 1))
table_chi[1, 1] = length(X[X < Q1])
table_chi[1, 2] = length(X[X > Q1 & X < Q3])
table_chi[1, 3] = length(X[X > Q3])
table_chi[2, 1] = length(Y[Y < Q1])
table_chi[2, 2] = length(Y[Y > Q1 & Y < Q3])
table_chi[2, 3] = length(Y[Y > Q3])
table_chi| 25 | 4 | 25 |
| 5 | 50 | 4 |
chisq.test(table_chi)
Pearson's Chi-squared test
data: table_chi
X-squared = 67,637, df = 2, p-value = 2,055e-15
Résultat : p-value proche de 0 → le χ² détecte la différence.
On classe toutes les valeurs de X et Y ensemble et on calcule la somme des rangs de X :
matX = rbind(X, rep(1, length(X)))
matY = rbind(Y, rep(0, length(Y)))
matXY = cbind(matX, matY)
mat = matXY[, order(matXY[1, ])]
data_ranks = rbind(mat, 1:length(mat[1, ]))rang = data_ranks[3, ]
W = sum(rang[data_ranks[2, ] == 1])
cat("Statistique W =", W)Statistique W = 3201
L’hypothèse \(H_0\) d’équidistribution est rejetée au seuil \(\alpha\) si :
\[\left|W - \frac{n_X(n+1)}{2}\right| > q_{\alpha/2} \sqrt{\frac{n_X n_Y (n+1)}{12}}\]
U = W - length(X) * (length(X) + length(Y) + 1) / 2
pval_wilcox = 1 - pnorm(abs(U), 0, sqrt(length(X) * length(Y) * (length(X) + length(Y) + 1) / 12))
cat("p-value Wilcoxon =", pval_wilcox)p-value Wilcoxon = 0,4754454
Résultat : Le test de Wilcoxon ne détecte pas la différence → Score : χ² 1 - 0 Wilcoxon.
On redécoupe la variable x différemment :
X2 = x[c(1:12, 25:36, 49:60, 73:84)]
Y2 = x[c(13:24, 37:48, 61:72, 85:96)]Refaisons les deux tests :
# Test χ²
table2 = rbind(c(1, 1, 1), c(1, 1, 1))
table2[1, 1] = length(X2[X2 < Q1])
table2[1, 2] = length(X2[X2 > Q1 & X2 < Q3])
table2[1, 3] = length(X2[X2 > Q3])
table2[2, 1] = length(Y2[Y2 < Q1])
table2[2, 2] = length(Y2[Y2 > Q1 & Y2 < Q3])
table2[2, 3] = length(Y2[Y2 > Q3])
chisq.test(table2)
Pearson's Chi-squared test
data: table2
X-squared = 4,6479, df = 2, p-value = 0,09788
# Test de Wilcoxon (round 2)
matX2 = rbind(X2, rep(1, length(X2)))
matY2 = rbind(Y2, rep(0, length(Y2)))
matXY2 = cbind(matX2, matY2)
mat2 = matXY2[, order(matXY2[1, ])]
data2 = rbind(mat2, 1:length(mat2[1, ]))
rang2 = data2[3, ]
W2 = sum(rang2[data2[2, ] == 1])
U2 = W2 - length(X2) * (length(X2) + length(Y2) + 1) / 2
pval2 = 1 - pnorm(abs(U2), 0, sqrt(length(X2) * length(Y2) * (length(X2) + length(Y2) + 1) / 12))
cat("p-value Wilcoxon round 2 =", pval2)p-value Wilcoxon round 2 = 0,0283781
Résultat : Cette fois, c’est le Wilcoxon qui détecte → Score : 1 partout !
Chaque test est sensible à un type différent de différence entre distributions.
Étude sur la pollution de l’air : données de concentration d’ozone à Rennes durant l’été 2001. 112 observations, 13 variables.
ozone = read.table("ozone.txt", sep = " ", dec = ".", header = TRUE)
head(ozone)| maxO3 | T9 | T12 | T15 | Ne9 | Ne12 | Ne15 | Vx9 | Vx12 | Vx15 | maxO3v | vent | pluie | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| <int> | <dbl> | <dbl> | <dbl> | <int> | <int> | <int> | <dbl> | <dbl> | <dbl> | <int> | <chr> | <chr> | |
| 20010601 | 87 | 15,6 | 18,5 | 18,4 | 4 | 4 | 8 | 0,6946 | -1,7101 | -0,6946 | 84 | Nord | Sec |
| 20010602 | 82 | 17,0 | 18,4 | 17,7 | 5 | 5 | 7 | -4,3301 | -4,0000 | -3,0000 | 87 | Nord | Sec |
| 20010603 | 92 | 15,3 | 17,6 | 19,5 | 2 | 5 | 4 | 2,9544 | 1,8794 | 0,5209 | 82 | Est | Sec |
| 20010604 | 114 | 16,2 | 19,7 | 22,5 | 1 | 1 | 0 | 0,9848 | 0,3473 | -0,1736 | 92 | Nord | Sec |
| 20010605 | 94 | 17,4 | 20,5 | 20,4 | 8 | 8 | 7 | -0,5000 | -2,9544 | -4,3301 | 114 | Ouest | Sec |
| 20010606 | 80 | 17,7 | 19,8 | 18,3 | 6 | 6 | 7 | -5,6382 | -5,0000 | -6,0000 | 94 | Ouest | Pluie |
vent = ozone$vent
maxO3 = ozone$maxO3anova(lm(maxO3 ~ vent))| Df | Sum Sq | Mean Sq | F value | Pr(>F) | |
|---|---|---|---|---|---|
| <int> | <dbl> | <dbl> | <dbl> | <dbl> | |
| vent | 3 | 7586,057 | 2528,6855 | 3,388077 | 0,02073516 |
| Residuals | 108 | 80605,622 | 746,3484 | NA | NA |
On aurait tendance à penser que le vent influence significativement la concentration en ozone.
Il est indispensable de vérifier la normalité pour valider l’ANOVA :
tapply(maxO3, vent, shapiro.test)$Est
Shapiro-Wilk normality test
data: X[[i]]
W = 0,91843, p-value = 0,344
$Nord
Shapiro-Wilk normality test
data: X[[i]]
W = 0,87595, p-value = 0,001906
$Ouest
Shapiro-Wilk normality test
data: X[[i]]
W = 0,7954, p-value = 6,881e-07
$Sud
Shapiro-Wilk normality test
data: X[[i]]
W = 0,95172, p-value = 0,367
Problème : Certains échantillons ne sont pas gaussiens → on devrait éviter l’ANOVA !
Le test de Kruskal-Wallis est un analogue de l’ANOVA, mais sans hypothèse de normalité. Il est basé sur les rangs.
kruskal.test(maxO3 ~ vent)
Kruskal-Wallis rank sum test
data: maxO3 by vent
Kruskal-Wallis chi-squared = 13,645, df = 3, p-value = 0,003431
Résultat : Même conclusion (le vent influence l’ozone), avec une p-value encore plus faible.
On se demande si la température et la composante E-O du vent à 9h sont indépendantes :
Vx9 = ozone$Vx9
T9 = ozone$T9La statistique est le coefficient de corrélation des rangs :
Rstar = cor(rank(Vx9), rank(T9))
cat("Corrélation de Spearman R* =", round(Rstar, 4))Corrélation de Spearman R* = 0,2314
On compare |R*| au quantile \(q_{0.975}\) de \(\mathcal{N}(0, \sqrt{1/(n-1)})\) :
seuil = qnorm(0.975, 0, sqrt(1 / (length(T9) - 1)))
cat("Seuil critique =", round(seuil, 4))
cat("\n|R*| =", round(abs(Rstar), 4))
cat("\nRejet ?", abs(Rstar) > seuil)Seuil critique = 0,186
|R*| = 0,2314
Rejet ? TRUE
cor.test(Vx9, T9, method = "spearman")
Spearman's rank correlation rho
data: Vx9 and T9
S = 179954, p-value = 0,01409
alternative hypothesis: true rho is not equal to 0
sample estimates:
rho
0,2314122
Conclusion : On rejette l’hypothèse de nullité de la corrélation → les variables sont dépendantes.
| Test | Hypothèse de normalité | Utilisation | Fonction R |
|---|---|---|---|
| ANOVA (Fisher) | Oui | Comparer les moyennes de k groupes | anova(lm()) |
| Kruskal-Wallis | Non | Alternative à l’ANOVA | kruskal.test() |
| Wilcoxon | Non | Comparer 2 échantillons | Calcul manuel |
| χ² d’adéquation | Non | Comparer des distributions | chisq.test() |
| Spearman | Non | Tester l’indépendance (corrélation de rangs) | cor.test(method="spearman") |
Points clés :