library(tidyverse)
library(ISLR2) # Contient le jeu de données `Wage` sur lequel nous allons travailler
library(broom) # Pour extraire les résultats des modèles de manière "tidy"
library(car) # Pour un test global sur les variables catégorielles
library(MuMIn) # Pour la sélection de modèles exhaustive1 Introduction et Préparation des données
Dans ce cours, nous allons voir comment faire des modèles de régression linéaire multiple et de régression logistique avec R. Une troisième partie va également s’intéresser aux stratégies de sélection de modèles, applicables aussi bien à la régression linéaire qu’à la régression logistique.
Les fonctions fondamentales pour faire ces modèles, qui sont lm() et glm(), sont déjà présentes dans la base de R. Cependant de nombreuses extensions proposent des résultats, diagnostiques et procédures supplémentaires qui s’avèrent très utiles. Les packages nécessaires pour cette session sont donc les suivants :
Tout au long de ce cours, nous allons utiliser le jeu de données Wage du package ISLR. Il contient les informations salariales et démographiques de 3000 travailleurs masculins de la région “Mid-Atlantic” aux États-Unis.
Commençons par le charger, transformons-le en tibble, et examinons-le :
wage_df = as_tibble(Wage)
wage_df# A tibble: 3,000 × 11
year age maritl race education region jobclass health health_ins logwage
<int> <int> <fct> <fct> <fct> <fct> <fct> <fct> <fct> <dbl>
1 2006 18 1. Nev… 1. W… 1. < HS … 2. Mi… 1. Indu… 1. <=… 2. No 4.32
2 2004 24 1. Nev… 1. W… 4. Colle… 2. Mi… 2. Info… 2. >=… 2. No 4.26
3 2003 45 2. Mar… 1. W… 3. Some … 2. Mi… 1. Indu… 1. <=… 1. Yes 4.88
4 2003 43 2. Mar… 3. A… 4. Colle… 2. Mi… 2. Info… 2. >=… 1. Yes 5.04
5 2005 50 4. Div… 1. W… 2. HS Gr… 2. Mi… 2. Info… 1. <=… 1. Yes 4.32
6 2008 54 2. Mar… 1. W… 4. Colle… 2. Mi… 2. Info… 2. >=… 1. Yes 4.85
7 2009 44 2. Mar… 4. O… 3. Some … 2. Mi… 1. Indu… 2. >=… 1. Yes 5.13
8 2008 30 1. Nev… 3. A… 3. Some … 2. Mi… 2. Info… 1. <=… 1. Yes 4.72
9 2006 41 1. Nev… 2. B… 3. Some … 2. Mi… 2. Info… 2. >=… 1. Yes 4.78
10 2004 52 2. Mar… 1. W… 2. HS Gr… 2. Mi… 2. Info… 2. >=… 1. Yes 4.86
# ℹ 2,990 more rows
# ℹ 1 more variable: wage <dbl>
Ce jeu de données est déjà très propre, il n’y a donc pas beaucoup de pré-traitements à effectuer. Cependant, avec de nombreuses méthodes statistiques en R, dont les modèles de régression, définir le bon type des variables (numériques, catégorielles, ordinales) est très important, car les calculs effectués de manière sous-jacente ne seront pas les mêmes.
Ici, les variables catégorielles sont déjà correctement typées. Il nous reste à formater les variables ordinales, ce qui est facile à faire ici car, comme elles commencent par des numéros, l’ordre alphabétique correspond naturellement à l’ordre hiérarchique (sinon, on aurait dû définir l’ordre via le vecteur donné dans l’argument levels) :
wage_df = wage_df %>%
mutate(
education = factor(education, ordered=T),
health = factor(health, ordered=T)
)
head(wage_df$education)[1] 1. < HS Grad 4. College Grad 3. Some College 4. College Grad
[5] 2. HS Grad 4. College Grad
5 Levels: 1. < HS Grad < 2. HS Grad < 3. Some College < ... < 5. Advanced Degree
head(wage_df$health)[1] 1. <=Good 2. >=Very Good 1. <=Good 2. >=Very Good 1. <=Good
[6] 2. >=Very Good
Levels: 1. <=Good < 2. >=Very Good
Nous le verrons plus tard, mais dans le cadre de la régression, les variables ordinales sont traitées de manière très particulière, à mi-chemin entre des variables numériques et des variables catégorielles. Nous allons créer deux autres versions de ces dernières, une numérique et une catégorielle, qui nous permettront de les intégrer dans les modèles de régression d’une manière ciblée :
wage_df = wage_df %>%
mutate(
education_num = as.numeric(education),
education_cat = factor(education, ordered=F),
health_num = as.numeric(health),
health_cat = factor(health, ordered=F)
)
head(wage_df$education_num)[1] 1 4 3 4 2 4
head(wage_df$education_cat)[1] 1. < HS Grad 4. College Grad 3. Some College 4. College Grad
[5] 2. HS Grad 4. College Grad
5 Levels: 1. < HS Grad 2. HS Grad 3. Some College ... 5. Advanced Degree
2 La régression linéaire
2.1 Théorie (rappel)
La régression linéaire (multiple) est un modèle qui postule qu’une variable numérique \(Y\), appelée variable dépendante ou critère, peut s’exprimer en fonction de \(p\) autres variables numériques \(X_1, \ldots X_p\), appelées variables indépendantes ou prédicteurs, selon la formule suivante : \[ Y = b_0 + b_1 X_1 + b_2 X_2 + \ldots + b_p X_p + \epsilon \] où les \(b_i\) sont les coefficients de régression, \(b_0\) est l’ordonnée à l’origine ou l’intercept, et \(\epsilon\) représente le terme d’erreur ou le résidu (c’est-à-dire la part de \(Y\) qui n’est pas expliquée par les variables indépendantes).
Cette relation est dite linéaire car elle suppose qu’il y a une structure sous-jacente de type “hyperplan”, autrement dit une droite dans la cas de la régression simple :
x = runif(50)
y = 2 + 3 * x + rnorm(50, sd=0.5)
df = tibble(x, y)
ggplot(df, aes(x=x, y=y)) +
geom_point() +
geom_abline(intercept=2, slope=3, color="red", linetype="dashed")
On trouve les coefficients de régression par la méthode des moindres carrés ordinaires (OLS). L’algorithme va chercher l’unique droite qui minimise l’erreur carrée moyenne (MSE), c’est-à-dire qui minimise la distance verticale entre chaque point réel \(y_i\) et la prédiction du modèle \(\hat{y}_i\) : \[ \text{MSE} = \tfrac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2 \] avec \(\hat{y}_i = b_0 + b_1 x_{i1} + \ldots + b_p x_{ip}\).
2.2 Construire un modèle linéaire avec lm()
Un modèle linéaire s’obtient avec la fonction lm() de R. Cette dernière demande comme premier argument une formule qui a une structure particulière et qui utilise l’opérateur tilde ~.
La structure usuelle d’une formule est la suivante :
<variable réponse> ~ <variable 1> + <variable 2> + ... + <variable p>
Mais on peut également utiliser d’autres syntaxes :
- En utilisant
<variable réponse> ~ ., on indique que l’on prédit avec toutes les variables du jeu de données. - En ajoutant
-1à la formule, la droite de régression passera par l’origine (on omet l’intercept). <variable i>^npermet d’ajouter l’ordre \(n\) de la variable<variable i>*<variable j>permet d’ajouter les termes d’interaction.- Plusieurs fonctions peuvent être utilisées sur les variables, comme
exp()oulog().
Dans le jeu de données Wage, l’objectif sera de prédire la variable wage (le salaire annuel en milliers de $) en fonction d’autres variables du jeu de données. Pour commencer, nous allons faire un modèle linéaire en utilisant simplement deux autres variables numériques, age et education_num :
lm_res = lm(wage ~ age + education_num, data=wage_df)Nous avons sauvegardé le résultat dans la variable lm_res, car il s’agit d’une sortie qui est utilisable dans plusieurs contextes (une manière de travailler fréquente en R). En premier lieu, cette sortie s’utilise avec summary(), qui donne de nombreuses informations sur la régression effectuée :
summary(lm_res)
Call:
lm(formula = wage ~ age + education_num, data = wage_df)
Residuals:
Min 1Q Median 3Q Max
-104.286 -19.796 -3.664 14.659 220.625
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 38.77503 2.90509 13.35 <2e-16 ***
age 0.58847 0.05723 10.28 <2e-16 ***
education_num 15.93645 0.54341 29.33 <2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 36.08 on 2997 degrees of freedom
Multiple R-squared: 0.2527, Adjusted R-squared: 0.2522
F-statistic: 506.8 on 2 and 2997 DF, p-value: < 2.2e-16
Examinons cette sortie :
- Call: donne la formule utilisée.
- Residuals: donne un aperçu de la distribution des résidus, qui devraient suivre une distribution normale centrée autour de zéro pour que les hypothèses du modèle soient respectées.
- Coefficients: donne les estimations des coefficients de régression, leur erreur standard, la statistique \(t\) et la valeur \(p\) associée. Ces valeurs permettent de tester l’hypothèse nulle selon laquelle le coefficient est égal à zéro (c’est-à-dire que la variable n’a pas d’effet sur la variable dépendante).
- En bas: on trouve plusieurs statistiques globales du modèle, comme le \(R^2\) et le \(R^2\)-ajusté, qui permettent d’évaluer sa qualité globale.
On voit que ce modèle explique environ 25% de la variance totale des salaires, que l’intercept et nos deux coefficients sont significatifs, et que les deux variables associées ont un effet positif sur le salaire.
Notez que l’on peut extraire plusieurs informations de ce résultat via l’opérateur $ :
names(lm_res) [1] "coefficients" "residuals" "effects" "rank"
[5] "fitted.values" "assign" "qr" "df.residual"
[9] "xlevels" "call" "terms" "model"
Comme par exemple, les coefficients de régression :
lm_res$coefficients (Intercept) age education_num
38.7750345 0.5884726 15.9364472
Si l’on cherche à faire des prédictions à l’aide de ce modèle, on peut utiliser la fonction predict(), qui prend en argument la variable contenant le modèle ainsi qu’un nouveau jeu de données (avec des variables ayant le même nom que celles utilisées pour construire le modèle) :
# Les nouvelles données avec 2 individus
new_df = tibble(age=c(30, 40), education_num=c(2, 4))
# La prédiction du salaire sur ces nouveaux individus
predict(lm_res, new_df) 1 2
88.30211 126.05973
2.3 Extraire les résultats avec broom
Bien que summary() soit un standard historique, sa sortie est du texte brut, difficilement manipulable dans un “pipeline” d’opérations. Au sain du tidyverse, c’est le package broom qui s’utilise pour transformer les sorties statistiques complexes de plusieurs modèles en tableaux de données propres (des tibbles).
Pour les coefficents de régression, on utilise la fonction `tidy() :
tidy(lm_res)# A tibble: 3 × 5
term estimate std.error statistic p.value
<chr> <dbl> <dbl> <dbl> <dbl>
1 (Intercept) 38.8 2.91 13.3 1.61e- 39
2 age 0.588 0.0572 10.3 2.14e- 24
3 education_num 15.9 0.543 29.3 1.98e-166
Pour les métriques globales du modèle, on utilise la fonction glance(), qui donne un résumé de la qualité du modèle :
glance(lm_res)# A tibble: 1 × 12
r.squared adj.r.squared sigma statistic p.value df logLik AIC BIC
<dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 0.253 0.252 36.1 507. 2.57e-190 2 -15013. 30034. 30058.
# ℹ 3 more variables: deviance <dbl>, df.residual <int>, nobs <int>
Nous nous pencherons plus en détails sur ces critères dans la Section 4, consacrée à la sélection des modèles.
Pour les résidus et les prédictions du modèle, on utilise la fonction augment(), qui ajoute des colonnes au jeu de données original (avec uniquement les variables retenues) avec les valeurs ajustées, les résidus, et d’autres diagnostics :
augment(lm_res)# A tibble: 3,000 × 9
wage age education_num .fitted .resid .hat .sigma .cooksd .std.resid
<dbl> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 75.0 18 1 65.3 9.74 0.00258 36.1 6.30e-5 0.270
2 70.5 24 4 117. -46.2 0.00147 36.1 8.05e-4 -1.28
3 131. 45 3 113. 17.9 0.000350 36.1 2.88e-5 0.497
4 155. 43 4 128. 26.9 0.000555 36.1 1.03e-4 0.745
5 75.0 50 2 100. -25.0 0.000735 36.1 1.18e-4 -0.694
6 127. 54 4 134. -7.18 0.000854 36.1 1.13e-5 -0.199
7 170. 44 3 112. 57.1 0.000340 36.1 2.83e-4 1.58
8 112. 30 3 104. 7.48 0.000721 36.1 1.03e-5 0.207
9 119. 41 3 111. 8.17 0.000338 36.1 5.79e-6 0.227
10 129. 52 2 101. 27.4 0.000829 36.1 1.60e-4 0.761
# ℹ 2,990 more rows
On peut ainsi facilement visualiser le diagramme des résidus, qui permet de vérifier visuellement les hypothèses de notre modèle (homoscédasticité, linéarité et normalité) :
ggplot(augment(lm_res)) +
geom_point(aes(x=.fitted, y=.resid)) +
geom_hline(yintercept=0, color="red", linetype="dashed") +
labs(title = "Graphique des Résidus vs Valeurs Prédites",
x = "Salaire Prédit",
y = "Résidus")
Ces hypothèses semblent vérifiées ici (les erreurs sont à peu près symétriques autour de la ligne rouge).
2.4 Les prédicteurs catégoriels ou ordinaux
Il est possible d’intégrer directement des variables catégorielles ou ordinales dans un modèle de régression linéaire, en tant que prédicteurs, et R va les traiter automatiquement de la manière la plus adéquate possible.
2.4.1 Les variables catégorielles dans le modèle linéaire
Que se passe-t-il lorsque nous incluons dans notre fonction lm() une variable catégorielle comme maritl, qui contient 5 modalités ?
levels(wage_df$maritl)[1] "1. Never Married" "2. Married" "3. Widowed" "4. Divorced"
[5] "5. Separated"
R gère cela grâce à un processus appelé Dummy Encoding (ou création de variables indicatrices). Si une variable catégorielle possède \(m\) modalités, R va créer \(m-1\) nouvelles variables binaires (contenant uniquement des 0 et des 1).
La raison pour laquelle R crée \(m-1\) modalités au lieu de \(m\) est qu’il faut éviter la redondance parfaite d’information dans le jeu de données, et si l’on ajoute toutes les colonnes, l’une d’elle est déductible des \(m-1\) autres. Pour éviter cela, R enlève toujours une modalité qui devient la catégorie de référence (par défaut, la première dans l’ordre alphabétique, ou la première définie dans le factor). Cette catégorie de référence est comme “absorbée” par l’intercept et les coefficients des autres modalités représentent donc la différence par rapport à cette catégorie de référence.
Pour voir exactement le tableau de données que R construit avant d’ajuster le modèle linéaire, nous pouvons utiliser la fonction model.matrix() :
dummies_encoding = model.matrix(wage ~ maritl, wage_df)
head(dummies_encoding, 10) (Intercept) maritl2. Married maritl3. Widowed maritl4. Divorced
1 1 0 0 0
2 1 0 0 0
3 1 1 0 0
4 1 1 0 0
5 1 0 0 1
6 1 1 0 0
7 1 1 0 0
8 1 0 0 0
9 1 0 0 0
10 1 1 0 0
maritl5. Separated
1 0
2 0
3 0
4 0
5 0
6 0
7 0
8 0
9 0
10 0
Si l’on ajoute donc cette variable catégorielle maritl dans le modèle précédent, on obtient :
lm_res = lm(wage ~ age + education_num + maritl, wage_df)
summary(lm_res)
Call:
lm(formula = wage ~ age + education_num + maritl, data = wage_df)
Residuals:
Min 1Q Median 3Q Max
-107.750 -19.353 -3.366 14.555 217.848
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 36.88259 2.85355 12.925 < 2e-16 ***
age 0.33591 0.06277 5.351 9.4e-08 ***
education_num 15.61140 0.53445 29.210 < 2e-16 ***
maritl2. Married 18.79420 1.76767 10.632 < 2e-16 ***
maritl3. Widowed 1.22593 8.30134 0.148 0.88261
maritl4. Divorced 4.98086 2.98850 1.667 0.09568 .
maritl5. Separated 13.28207 5.02192 2.645 0.00822 **
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 35.33 on 2993 degrees of freedom
Multiple R-squared: 0.2848, Adjusted R-squared: 0.2834
F-statistic: 198.6 on 6 and 2993 DF, p-value: < 2.2e-16
Attention cependant à l’interprétation. Ici, seul les niveaux Married et Separated sont significatifs, mais en référence au niveau Never Married. Le salaire des personnes de ces deux catégories est donc significativement différent par rapport aux personnes n’ayant jamais été mariées. Pour les autres statuts maritaux, l’échantillon ne permet pas de prouver qu’ils gagnent significativement plus ou moins que les gens n’ayant jamais été mariés.
Pour tester si la variable maritl a un impact global sur le modèle (malgré le fait que certaines de ses modalités ne soient pas significatives), la bonne pratique consiste à faire une analyse de variance avec la fonction Anova() du package car :
Anova(lm_res)Anova Table (Type II tests)
Response: wage
Sum Sq Df F value Pr(>F)
age 35734 1 28.636 9.395e-08 ***
education_num 1064746 1 853.245 < 2.2e-16 ***
maritl 167436 4 33.544 < 2.2e-16 ***
Residuals 3734899 2993
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Ici, la valeur p très faible de maritl nous confirme que le statut marital, dans son ensemble, est une variable pertinente à conserver dans notre modèle.
Pour avoir d’autres contrastes, il est possible de changer le niveau de référence d’une variable avec relevel() :
rlv_wage_df = wage_df %>%
mutate(maritl = relevel(maritl, ref="2. Married"))
lm_res = lm(wage ~ age + education_num + maritl, rlv_wage_df)
summary(lm_res)
Call:
lm(formula = wage ~ age + education_num + maritl, data = rlv_wage_df)
Residuals:
Min 1Q Median 3Q Max
-107.750 -19.353 -3.366 14.555 217.848
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 55.67679 3.28002 16.975 < 2e-16 ***
age 0.33591 0.06277 5.351 9.40e-08 ***
education_num 15.61140 0.53445 29.210 < 2e-16 ***
maritl1. Never Married -18.79420 1.76767 -10.632 < 2e-16 ***
maritl3. Widowed -17.56828 8.15103 -2.155 0.0312 *
maritl4. Divorced -13.81334 2.59989 -5.313 1.16e-07 ***
maritl5. Separated -5.51213 4.84299 -1.138 0.2551
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 35.33 on 2993 degrees of freedom
Multiple R-squared: 0.2848, Adjusted R-squared: 0.2834
F-statistic: 198.6 on 6 and 2993 DF, p-value: < 2.2e-16
2.4.2 Les variables ordinales dans le modèle linéaire
Pour ces variables, on sait qu’il y a un ordre, mais on ne sait pas si les écarts entre les modalités sont tous équivalents. R les traite de manière assez particulière.
Prenons directement un exemple avec la variable education (ordinale cette fois). Si l’on crée un modèle avec cette dernière, on obtient :
lm_res = lm(wage ~ age + education, wage_df)
summary(lm_res)
Call:
lm(formula = wage ~ age + education, data = wage_df)
Residuals:
Min 1Q Median 3Q Max
-110.033 -19.635 -3.907 14.441 220.408
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 88.40777 2.53789 34.835 < 2e-16 ***
age 0.56869 0.05719 9.943 < 2e-16 ***
education.L 50.05925 1.86518 26.839 < 2e-16 ***
education.Q 8.13375 1.74709 4.656 3.37e-06 ***
education.C 2.63428 1.43992 1.829 0.0674 .
education^4 0.61756 1.36837 0.451 0.6518
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 35.94 on 2994 degrees of freedom
Multiple R-squared: 0.2593, Adjusted R-squared: 0.2581
F-statistic: 209.6 on 5 and 2994 DF, p-value: < 2.2e-16
On voit que cette variable été incluse avec différentes tendances, résumées par les indications L, Q, C et ^4 :
Tendance Linéaire
L: le modèle teste s’il y a une tendance linéaire globale. Autrement dit, on regarde si le fait de monter d’un niveau de diplôme augmente le salaire de manière régulière et constante.Tendance Quadratique
Q: le modèle teste s’il y a une courbure (une forme en “U”). Ici, la supposition est que les petits diplômes ne changent pas beaucoup le salaire, mais que l’effet augmente lorsque l’on arrive au Master ou au Doctorat.Tendance Cubique
C: le modèle teste s’il y a deux courbures (une forme en “S”). En gros, est-ce qu’on a un gros gain avec les premiers diplômes, puis un plateau (stagnation), puis de nouveau un gros gain avec les plus hauts diplômes.Tendance Quartique
^4: au-delà du cubique, les niveaux deviennent difficiles à interpréter et l’on a presque intérêt à traiter la variable comme une variable catégorielle sans ordre.
Il peut parfois être préférable d’utiliser les versions purement numériques ou catégorielles de cette variable, car l’interprétation s’en trouve allégée. D’une manière générale, si le L est le principal facteur significatif, alors la relation est presque linéaire et on peut utiliser sa version numérique. À l’inverse, si les tendances d’ordres supérieurs (cubique, quartique) sont les seules à être très significatives, la relation d’ordre devient difficile à interpréter et il est peut-être préférable de considérer la variable comme une variable catégorielle non-ordinale.
3 La régression logistique
3.1 Théorie
Jusqu’à présent, nous avons cherché à prédire le salaire (wage), qui est une variable numérique continue. Mais que se passe-t-il si nous voulons prédire une variable catégorielle binaire \(Y\) (0 ou 1, Oui ou Non, Succès ou Échec) ?
Imaginons que nous voulions prédire si un travailleur a un salaire considéré comme “élevé” (que nous définirons arbitrairement à plus de 120 000$ par an). Créons cette variable :
wage_df = wage_df %>%
mutate(high_wage = ifelse(wage > 120, 1, 0))
# On regarde les proportions de chaque catégorie dans le jeu de données
table(wage_df$high_wage)
0 1
2047 953
Si nous utilisons une régression linéaire classique (lm()) pour modéliser cette variable binaire, nous faisons face à un problème : une droite infinie finira inévitablement par prédire des valeurs absurdes, c’est-à-dire négatives ou supérieures à 1 (100%).
Pour résoudre ce problème, la régression logistique n’utilise pas une droite, mais une courbe en forme de “S” la fonction sigmoïde, notée \(\sigma()\), qui s’applique sur la formule de régression pour prédire une probabilité : \[ P(Y = 1) = \sigma(b_0 + b_1 X_1 + b_2 X_2 + \ldots + b_p X_p + \epsilon) \]
La fonction sigmoïde se définit comme : \[ \sigma(z) = \frac{1}{1 + e^{-z}} \] et transforme n’importe quelle valeur réelle \(z\) en une valeur comprise entre 0 et 1, interprétable comme une probabilité. On le voit avec sa courbe :
x = seq(-10, 10, length.out=100)
y = 1 / (1 + exp(-x))
df = tibble(x, y)
ggplot(df, aes(x=x, y=y)) +
geom_line(color="blue") +
labs(title="Fonction Sigmoïde",
x="x",
y="sigmoid(x)")
En réalité, on peut aussi voir la régression logistique comme une modélisation des log-odds (le logarithme du rapport des probabilités) de la variable dépendante avec une relation linéaire : \[ \ln\left(\frac{P(Y=1)}{1 - P(Y=1)}\right) = b_0 + b_1 X_1 + b_2 X_2 + \ldots + b_p X_p \] Nous le verrons plus tard, cette manière de poser le problème permet de mieux interpréter les coefficients de la régression logistique.
Notez que la régression logistique ne minimise pas les moindres carrées pour trouver les coefficients de régression, mais une entropie relative entre les probabilités prédites et les probabilités réelles (i.e. la variable binaire). À cause de cela, sa résolution algorithmique est plus lente.
3.2 Construire un modèle de régression logistique avec glm()
En R, nous n’utilisons plus lm(), mais glm() (pour Generalized Linear Model). La syntaxe de la formule est exactement la même, mais nous devons ajouter l’argument family="binomial" pour indiquer à R d’utiliser la transformation logistique.
Essayons de prédire la probabilité d’avoir un haut salaire en fonction de l’âge et du niveau d’éducation (version numérique) :
glm_res = glm(high_wage ~ age + education_num, wage_df, family="binomial")
summary(glm_res)
Call:
glm(formula = high_wage ~ age + education_num, family = "binomial",
data = wage_df)
Coefficients:
Estimate Std. Error z value Pr(>|z|)
(Intercept) -4.950826 0.231505 -21.385 < 2e-16 ***
age 0.032059 0.003975 8.065 7.35e-16 ***
education_num 0.871085 0.040084 21.732 < 2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
(Dispersion parameter for binomial family taken to be 1)
Null deviance: 3750.6 on 2999 degrees of freedom
Residual deviance: 3083.6 on 2997 degrees of freedom
AIC: 3089.6
Number of Fisher Scoring iterations: 4
Ce qui change fondamentalement ici, c’est l’interprétation des coefficients de régression. Si vous regardez la colonne Estimate, ces chiffres expriment une variation dans les log-odds par rapport à la référence définie par l’intercept (-4.95). Pour rendre ces coefficients interprétables, nous devons annuler le logarithme en appliquant une fonction exponentielle, qui nous donne alors des odds ratios, ou en français, des rapports de cotes.
Le package broom est ici très utile car la fonction tidy() possède un argument (exponentiate=T) qui les calcule automatiquement :
tidy(glm_res, exponentiate=T, conf.int=T)# A tibble: 3 × 7
term estimate std.error statistic p.value conf.low conf.high
<chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 (Intercept) 0.00708 0.232 -21.4 1.83e-101 0.00447 0.0111
2 age 1.03 0.00398 8.06 7.35e- 16 1.02 1.04
3 education_num 2.39 0.0401 21.7 1.03e-104 2.21 2.59
La colonne estimate contient désormais les odds ratios, autrement dit un rapport d’odds ou de cotes entre deux popultions (entre la population avec une différence de 1 dans cette variable). Les odds sont définis de la manière suivantes \[ \text{odds} = \frac{P(Y=1)}{P(Y=0)} \quad \text{ou à l'inverse} \quad P(Y=1) = \frac{\text{odds}}{1 + \text{odds}} \] On peut voir ces odds ratios comme des facteurs multiplicatifs sur les odds : la valeur neutre (l’absence d’effet) n’est plus 0, mais 1, et :
- Si l’odds ratio est > 1 : La variable augmente les chances de succès.
- Si l’odds ratio est < 1 : La variable diminue les chances de succès.
Par exemple, comme l’odds ratio pour education_num vaut 2.39, cela signifie que pour chaque niveau d’éducation supplémentaire acquis, en maintenant l’âge constant, les chances d’avoir un haut salaire sont multipliées par 2.39 par rapport aux chances d’avoir un bas salaire (on passe, p.ex, de 2 chances contre une à 4.78 chances contre une).
Contrairement à la fonction augment() classique (de broom), pour un glm, il faut explicitement préciser que l’on veut récupérer des probabilités (et non des log-odds) en utilisant l’argument type.predict="response" :
glm_augm = augment(glm_res, type.predict = "response")
glm_augm# A tibble: 3,000 × 9
high_wage age education_num .fitted .resid .hat .sigma .cooksd
<dbl> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 0 18 1 0.0292 -0.244 0.000647 1.00 0.00000650
2 0 24 4 0.332 -0.899 0.00187 1.000 0.000312
3 1 45 3 0.290 1.57 0.000455 1.000 0.000371
4 1 43 4 0.478 1.21 0.000642 1.000 0.000234
5 0 50 2 0.167 -0.605 0.000761 1.00 0.0000510
6 1 54 4 0.566 1.07 0.00108 1.000 0.000277
7 1 44 3 0.284 1.59 0.000445 1.000 0.000375
8 0 30 3 0.202 -0.671 0.000847 1.000 0.0000714
9 0 41 3 0.264 -0.784 0.000452 1.000 0.0000543
10 1 52 2 0.176 1.86 0.000853 0.999 0.00133
# ℹ 2,990 more rows
# ℹ 1 more variable: .std.resid <dbl>
.fitted indique alors les probabilités prédites d’avoir un haut salaire pour chaque individu du jeu de données.
Pour évaluer la qualité du modèle, nous devons d’abord transformer ces probabilités continues en décisions d’affectation rigides (0 ou 1, en utilisant le seuil de 0.5), et calculer la matrice de confusion :
glm_augm = glm_augm %>%
mutate(prediction = ifelse(.fitted > 0.5, 1, 0))
table(real=glm_augm$high_wage, prediction=glm_augm$prediction) prediction
real 0 1
0 1782 265
1 510 443
Dans un prochain cours, cette matrice va nous servir à calculer plusieurs métriques d’évaluation du modèle, comme l’exactitude (accuracy), la précision (precision) ou le rappel (recall).
4 La sélection de modèles
Dans cette dernière partie, nous abordons un problème central en statistiques appliquées : comment choisir le “meilleur” modèle parmi toutes les combinaisons possibles de variables ?
On pourrait être tenté d’inclure systématiquement toutes les variables disponibles dans notre jeu de données. Cependant, c’est une très mauvaise pratique à cause du surapprentissage (overfitting). Un modèle trop complexe, avec trop de variables, va apprendre “par coeur” l’échantillon (y compris le bruit aléatoire) et sera incapable de faire de bonnes prédictions sur de nouvelles données. Le principe de base est celui de la parcimonie (le rasoir d’Ockham) : on cherche le modèle qui explique le maximum de variance avec le minimum de complexité.
4.1 Les critères d’information : AIC et BIC
Pour comparer des modèles, nous ne pouvons pas utiliser le simple \(R^2\). En effet, mathématiquement, le \(R^2\) augmente toujours lorsqu’on ajoute une variable, même si celle-ci est totalement absurde.
À la place, nous utilisons les critères d’information, qui mesurent l’erreur du modèle tout en appliquant une pénalité pour chaque variable ajoutée. Ces métriques fonctionnent dans le même sens : on cherche le modèle qui les minimise.
L’AIC (Akaike Information Criterion) : \[AIC = 2k - 2\ln(\hat{L})\] (où \(k\) est le nombre de paramètres et \(\hat{L}\) la vraisemblance du modèle). L’AIC est très utilisé lorsque l’objectif du modèle est de faire de la prédiction.
Le BIC (Bayesian Information Criterion) : \[BIC = \ln(n)k - 2\ln(\hat{L})\] (où \(n\) est la taille de l’échantillon). La pénalité du BIC dépend de la taille de l’échantillon. En pratique, dès que l’on a plus de 8 observations, le BIC pénalise la complexité beaucoup plus sévèrement que l’AIC. Il favorise des modèles plus simples et est idéal lorsque l’objectif est d’expliquer le phénomène et d’identifier les vrais facteurs clés.
Comparons manuellement deux modèles avec la fonction glance() (du package broom) qui nous donne ces différentes métriques :
# Modèle simple
lm_simple = lm(wage ~ age, wage_df)
# Modèle plus complexe
lm_complex = lm(wage ~ age + education_num + jobclass, wage_df)
# Comparaison des métriques
glance(lm_simple)# A tibble: 1 × 12
r.squared adj.r.squared sigma statistic p.value df logLik AIC BIC
<dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 0.0383 0.0380 40.9 119. 2.90e-27 1 -15391. 30789. 30807.
# ℹ 3 more variables: deviance <dbl>, df.residual <int>, nobs <int>
glance(lm_complex)# A tibble: 1 × 12
r.squared adj.r.squared sigma statistic p.value df logLik AIC BIC
<dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 0.256 0.255 36.0 343. 1.96e-191 3 -15007. 30024. 30054.
# ℹ 3 more variables: deviance <dbl>, df.residual <int>, nobs <int>
Le modèle complexe possède un AIC et un BIC légérement inférieurs à ceux du modèle simple. L’ajout des variables education_num et jobclass est donc statistiquement justifié : le gain d’information dépasse la pénalité de complexité.
Regradons maintenant deux méthodes automatiques de recherche de modèles, qui marchent aussi bien avec lm() qu’avec glm().
4.2 L’approche gloutonne
Lorsque nous avons de nombreuses variables potentielles, comparer les modèles à la main devient impossible. R propose une fonction native, step(), qui automatise ce processus.
C’est un algorithme dit glouton ou heuristique, qui part d’un modèle initial, et “avance” (comprendre: retire ou ajoute une variable) dans la direction qui améliore au mieux la métrique choisie. Par exemple, si l’on choisit la direction “backward”, l’algorithme part d’un modèle complet (toutes les variables), évalue quelle variable retirer pour améliorer au mieux le BIC, la retire, et recommence jusqu’à ce que plus aucun retrait n’améliore le score. Regardons justement comment cet exemple fonctionne ici.
On commence par sélectionner toutes le variables qui nous semblent pertinentes pour notre modèle :
wage_reduced_df = wage_df %>%
select(age, maritl, race, education_num, jobclass, wage, health_cat)On va maintenant définir le modèle complet, c’est-à-dire avec toutes les variables restantes :
lm_full = lm(wage ~ ., wage_reduced_df)Avec, step(), on lance la sélection automatique en choisissant la direction="backward" (on peut aussi faire “forward” ou “both”) et en précisant que l’on veut utiliser le BIC pour évaluer les modèles (pour cela on doit définir l’argument k comme \(\ln(n)\), avec \(n\) le nombre d’observations) :
n = nrow(wage_reduced_df)
lm_optimal_wstep = step(lm_full, direction="backward", k=log(n))Start: AIC=21427.77
wage ~ age + maritl + race + education_num + jobclass + health_cat
Df Sum of Sq RSS AIC
- race 3 9757 3684278 21412
<none> 3674521 21428
- jobclass 1 17541 3692062 21434
- health_cat 1 33275 3707796 21447
- age 1 44403 3718924 21456
- maritl 4 146563 3821084 21513
- education_num 1 779121 4453642 21997
Step: AIC=21411.71
wage ~ age + maritl + education_num + jobclass + health_cat
Df Sum of Sq RSS AIC
<none> 3684278 21412
- jobclass 1 15169 3699447 21416
- health_cat 1 34151 3718429 21431
- age 1 43058 3727336 21439
- maritl 4 155662 3839940 21504
- education_num 1 812522 4496800 22002
On peut ensuite examiner le meilleur modèle retenu :
summary(lm_optimal_wstep)
Call:
lm(formula = wage ~ age + maritl + education_num + jobclass +
health_cat, data = wage_reduced_df)
Residuals:
Min 1Q Median 3Q Max
-109.234 -19.580 -2.925 14.571 214.541
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 31.1803 3.0402 10.256 < 2e-16 ***
age 0.3748 0.0634 5.912 3.75e-09 ***
maritl2. Married 18.2215 1.7610 10.347 < 2e-16 ***
maritl3. Widowed 1.4883 8.2501 0.180 0.856851
maritl4. Divorced 4.9779 2.9692 1.677 0.093741 .
maritl5. Separated 12.1753 4.9928 2.439 0.014803 *
education_num 14.5034 0.5647 25.683 < 2e-16 ***
jobclass2. Information 4.7411 1.3510 3.509 0.000456 ***
health_cat2. >=Very Good 7.7031 1.4629 5.265 1.50e-07 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 35.1 on 2991 degrees of freedom
Multiple R-squared: 0.2945, Adjusted R-squared: 0.2926
F-statistic: 156.1 on 8 and 2991 DF, p-value: < 2.2e-16
La limite de cet algorithme est qu’il n’explore pas toutes les possibilités. Il risque donc de se retrouver bloqué dans un optimum local et de rater le meilleur modèle absolu.
4.3 L’approche exhaustive
Pour obtenir le meilleur modèle possible (selon un certain critère), on peut utiliser une approche exhaustive qui va calculer et comparer absolument toutes les combinaisons possibles des variables. Prenons ici un nouvel objectif de prédiction : prédire si les personnes ont une assurance maladie (health_ins), en fonction de plusieurs variables sélectionnées.
On commence par sélectionner nos variables :
wage_reduced2_df = wage_df %>%
select(age, maritl, race, education_num, jobclass, health_ins, wage, health_cat)On fait notre glm complet :
glm_full = glm(health_ins ~ ., wage_reduced2_df, family="binomial")Puis on lance la sélection exhaustive avec la fonction dredge() du package MuMIn, qui va calculer tous les modèles possibles à partir du modèle complet et les classer selon la métrique choisie (ici le BIC) :
# Changement de l'option pour la gestion des NA, requis par MuMIn
options(na.action = "na.fail")
# On lance la sélection exhaustive sur notre modèle complet précédent
all_model_eval = dredge(glm_full, rank="BIC")Fixed term is "(Intercept)"
On peut ensuite examiner tous les différents modèles, dans l’ordre croissant. Ici, on va afficher uniquement les 10 permiers :
head(all_model_eval, 10)Global model call: glm(formula = health_ins ~ ., family = "binomial", data = wage_reduced2_df)
---
Model selection table
(Int) age edc_num hlt_cat jbc wag df logLik BIC delta
74 2.333 -0.01279 + -0.02347 4 -1640.889 3313.8 0.00
76 2.434 -0.01324 -0.08353 + -0.02214 5 -1638.849 3317.7 3.92
73 1.910 + -0.02450 3 -1647.008 3318.0 4.23
78 2.455 -0.01405 + + -0.02304 5 -1639.283 3318.6 4.79
75 1.987 -0.07437 + -0.02334 4 -1645.371 3322.8 8.96
80 2.537 -0.01434 -0.07716 + + -0.02185 6 -1637.560 3323.2 9.35
77 1.959 + + -0.02430 4 -1646.397 3324.8 11.01
68 2.443 -0.01417 -0.11950 -0.02235 4 -1646.410 3324.8 11.04
66 2.292 -0.01361 -0.02443 3 -1650.843 3325.7 11.90
70 2.428 -0.01498 + -0.02393 4 -1648.864 3329.8 15.95
weight
74 0.723
76 0.102
73 0.087
78 0.066
75 0.008
80 0.007
77 0.003
68 0.003
66 0.002
70 0.000
Models ranked by BIC(x)
Si le coefficient de régression apparaît ou une croix est présente, c’est que la variable a été retenue dans le modèle. On voit que le meilleur modèle selon le BIC est celui qui contient les variables age, jobclass et wage.
On peut ensuite obtenir ce modèle avec la fonction get.models(), cela nous permet d’examiner les valeurs de ses coefficients, de faire des prédictions, ou autre :
glm_best = get.models(all_model_eval, 1)[[1]]
tidy(glm_best, exponentiate=T)# A tibble: 4 × 5
term estimate std.error statistic p.value
<chr> <dbl> <dbl> <dbl> <dbl>
1 (Intercept) 10.3 0.201 11.6 4.66e-31
2 age 0.987 0.00367 -3.48 4.99e- 4
3 jobclass2. Information 0.680 0.0866 -4.45 8.55e- 6
4 wage 0.977 0.00158 -14.9 4.14e-50
Attention cependant, dredge() est soumis au problème d’explosion combinatoire. Au delà d’un certain nombre de variables, le nombre de modèles à tester devient trop grand pour un ordinateur standard. Dans ces situations, pour trouver un modèle adéquat, il faudra se tourner vers la sélection gloutonne ou vers des méthodes de régression pénalisée couplées avec des techniques de Machine Learning.