La régression logistique sous R expliquée à ma fille
La régression logistique n’est pas, en soi, un modèle si compliqué. Mais les résultats qu’on peut en tirer sont nombreux, et la seule fonction glm du package {stats} ne les présente pas toujours de façon simple. Petit tour des possibilités offertes par des packages complémentaires pour tout comprendre des résultats d’un modèle.
Les données : passagers du Titanic
Le package {carData} contient un data.frame appelé TitanicSurvival. Il recense les 1309 passagers du navire avec leur âge, leur sexe et la classe à bord de laquelle ils voyageaient (1e, 2e ou 3e classe). Pour chaque passager, une indication est donnée de leur survie.
library(carData) # données
library(tibble) # visualisation des données
data("TitanicSurvival")
titanic <- TitanicSurvival # copie du data.frame sous un nom plus court
glimpse(titanic)
## Observations: 1,309
## Variables: 4
## $ survived <fct> yes, yes, no, no, no, yes, yes, no, yes, no, no, yes...
## $ sex <fct> female, male, female, male, female, male, female, ma...
## $ age <dbl> 29.0000, 0.9167, 2.0000, 30.0000, 25.0000, 48.0000, ...
## $ passengerClass <fct> 1st, 1st, 1st, 1st, 1st, 1st, 1st, 1st, 1st, 1st, 1s...
Un petit peu de statistique descriptive nous informe sur le contenu de la table.
library(skimr)
pander(skim(titanic), split.tables=Inf)
skim_type | skim_variable | n_missing | complete_rate | factor.ordered | factor.n_unique | factor.top_counts | numeric.mean | numeric.sd | numeric.p0 | numeric.p25 | numeric.p50 | numeric.p75 | numeric.p100 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
factor | survived | 0 | 1 | FALSE | 2 | no: 809, yes: 500 | NA | NA | NA | NA | NA | NA | NA |
factor | sex | 0 | 1 | FALSE | 2 | mal: 843, fem: 466 | NA | NA | NA | NA | NA | NA | NA |
factor | passengerClass | 0 | 1 | FALSE | 3 | 3rd: 709, 1st: 323, 2nd: 277 | NA | NA | NA | NA | NA | NA | NA |
numeric | age | 263 | 0.7991 | NA | NA | NA | 29.88 | 14.41 | 0.1667 | 21 | 28 | 39 | 80 |
On voit que les naufragés sont majoritaires (809/1309, soit environ 62%), que les femmes sont minoritaires (466 pour 1309 passagers) et que 263 passagers sont d’âge inconnu.
Que faire des données quantitatives ?
Une régression logistique peut utiliser une variable quantitative telle quelle, à condition : * de n’utiliser que les observations renseignées (les passagers sans âge ne seront pas pris en compte) * que la relation entre cette variable quantitative et le weight of evidence (ou woe, ou logit, ou log(probabilité(évènement)/(1-probabilité(évènement)))) soit linéaire.
Dans notre cas, abandonner 263 observations sur 1309 (soit 20% des passagers) serait une grosse perte d’information. On va donc découper la variable age en tranches, et créer une tranche spécifique pour les âges inconnus.
Commençons par regarder, pour les âges renseignés, la forme de la relation avec le weight of evidence.
library(dplyr) # préparation des données
library(ggplot2) # graphiques
titanic %>%
filter(! is.na(age)) %>%
group_by(age) %>%
summarise(prob_survie = mean(survived == "yes"),
n = n()) %>%
mutate(logit = log((prob_survie+0.5/n)/(1-prob_survie+0.5/n))) %>%
ggplot() +
aes(x=age, y=logit, size=n) +
geom_point() +
geom_smooth(method="loess", se=FALSE, aes(weight=n), color="red") +
geom_smooth(method="lm" , se=FALSE, aes(weight=n), color="gray")
## `geom_smooth()` using formula 'y ~ x'
## `geom_smooth()` using formula 'y ~ x'
La relation entre les bulles (chacune représente un âge, la taille de la bulle étant proportionnelle au nombre de passagers de cet âge) ne semble pas spécialement linéaire. Si on opte pour un découpage en tranches, plutôt que d’ajuster le nuage des bulles par une droite oblique on l’ajustera par une courbe en escalier, dont chaque palier correspond à une tranche. On fait donc des tranches peu larges quand la relation évolue rapidement et des tranches plus larges quand les bulles sont à peu près toutes à la même hauteur dans le graphique.
Un premier découpage serait :
library(forcats) # gestion des factors
titanic <- titanic %>%
mutate(age_cl = cut(age, breaks=c(-Inf,5,10,15,20,40,60,70,Inf)),
age_cl = fct_explicit_na(age_cl))
On obtient ce nouveau graphique.
titanic %>%
group_by(age_cl) %>%
summarise(prob_survie = mean(survived == "yes"),
n = n()) %>%
mutate(logit = log((prob_survie+0.5)/(1-prob_survie+0.5))) %>%
ggplot() +
aes(x=age_cl, y=logit) +
geom_bar(stat="identity")
Il importe sur ce graphique que des tranches adjacentes ne correspondent pas à des barres de même hauteur. Cela semble correct ici.
Le nombre actuel de tranches peut sembler élevé : n’oublions pas qu’un modèle peut suggérer de fusionner des tranches a posteriori, mais pas d’éclater une tranche existante en plusieurs tranches plus petites.
Un premier modèle
La fonction glm permet de construire un premier modèle dans lequel on inclut toutes les variables potentiellement explicatives : l’âge (mis en tranches ci-dessus), le sexe ou la classe. Le premier argument de glm est, comme dans beaucoup de fonctions de modélisation de R, une formule de type Y ~ X1 + X2 où les variables Y, X1 et X2 doivent être dans le même data.frame
reg1 <- glm(survived ~ age_cl + sex + passengerClass, titanic, family = binomial)
pander(summary(reg1))
Estimate | Std. Error | z value | Pr(>|z|) | |
---|---|---|---|---|
(Intercept) | 3.855 | 0.3965 | 9.721 | 2.444e-22 |
age_cl(5,10] | -1.007 | 0.5584 | -1.803 | 0.07139 |
age_cl(10,15] | -0.7562 | 0.5742 | -1.317 | 0.1879 |
age_cl(15,20] | -1.544 | 0.403 | -3.832 | 0.000127 |
age_cl(20,40] | -1.477 | 0.3505 | -4.213 | 2.518e-05 |
age_cl(40,60] | -1.991 | 0.392 | -5.08 | 3.784e-07 |
age_cl(60,70] | -3.032 | 0.6642 | -4.564 | 5.01e-06 |
age_cl(70, Inf] | -2.027 | 1.11 | -1.827 | 0.06776 |
age_cl(Missing) | -1.676 | 0.3714 | -4.514 | 6.372e-06 |
sexmale | -2.5 | 0.1499 | -16.68 | 1.847e-62 |
passengerClass2nd | -1.152 | 0.2112 | -5.458 | 4.827e-08 |
passengerClass3rd | -2.036 | 0.1939 | -10.5 | 8.204e-26 |
(Dispersion parameter for binomial family taken to be 1 )
Null deviance: | 1741 on 1308 degrees of freedom |
Residual deviance: | 1218 on 1297 degrees of freedom |
On peut récupérer les principaux résultats (coefficients et leur significativité) avec la fonction summary comme ci-dessus ou avec la fonction stargazer du package éponyme. Ce dernier sera particulièrement utile si on souhaite comparer plusieurs modèles : leurs caractéristiques s’afficheront côte à côte dans un même tableau.
library(stargazer)
##
## Please cite as:
## Hlavac, Marek (2018). stargazer: Well-Formatted Regression and Summary Statistics Tables.
## R package version 5.2.2. https://CRAN.R-project.org/package=stargazer
stargazer(reg1, type="html")
Dependent variable: | |
survived | |
age_cl(5,10] | -1.007* |
(0.558) | |
age_cl(10,15] | -0.756 |
(0.574) | |
age_cl(15,20] | -1.544*** |
(0.403) | |
age_cl(20,40] | -1.477*** |
(0.350) | |
age_cl(40,60] | -1.991*** |
(0.392) | |
age_cl(60,70] | -3.032*** |
(0.664) | |
age_cl(70, Inf] | -2.027* |
(1.110) | |
age_cl(Missing) | -1.676*** |
(0.371) | |
sexmale | -2.500*** |
(0.150) | |
passengerClass2nd | -1.152*** |
(0.211) | |
passengerClass3rd | -2.036*** |
(0.194) | |
Constant | 3.855*** |
(0.397) | |
Observations | 1,309 |
Log Likelihood | -608.936 |
Akaike Inf. Crit. | 1,241.871 |
Note: | p<0.1; p<0.05; p<0.01 |
Un autre package s’avère utile pour organiser les sorties du modèle au niveau de ses coefficients : il s’agit de {broom}.
library(broom)
kable(tidy(reg1))
term | estimate | std.error | statistic | p.value |
---|---|---|---|---|
(Intercept) | 3.854830 | 0.3965304 | 9.721399 | 0.0000000 |
age_cl(5,10] | -1.006734 | 0.5583757 | -1.802969 | 0.0713931 |
age_cl(10,15] | -0.756158 | 0.5742408 | -1.316796 | 0.1879070 |
age_cl(15,20] | -1.544280 | 0.4029821 | -3.832130 | 0.0001270 |
age_cl(20,40] | -1.476657 | 0.3504871 | -4.213156 | 0.0000252 |
age_cl(40,60] | -1.991385 | 0.3920417 | -5.079525 | 0.0000004 |
age_cl(60,70] | -3.031519 | 0.6641721 | -4.564358 | 0.0000050 |
age_cl(70, Inf] | -2.027379 | 1.1099279 | -1.826586 | 0.0677621 |
age_cl(Missing) | -1.676190 | 0.3713592 | -4.513663 | 0.0000064 |
sexmale | -2.500169 | 0.1498944 | -16.679531 | 0.0000000 |
passengerClass2nd | -1.152482 | 0.2111712 | -5.457573 | 0.0000000 |
passengerClass3rd | -2.036394 | 0.1938525 | -10.504860 | 0.0000000 |
Dans tous les cas on remarque que pour chaque variable explicative qualitative (types texte et facteur) un coefficient n’est pas affiché, celui de la modalité de référence. Il est égal à 0 par construction. Par défaut R choisit la 1e valeur par ordre alphabétique comme référence ; la fonction relevel permet de changer ce choix.
titanic$age_cl <- relevel(titanic$age_cl, "(20,40]")
reg2 <- glm(survived ~ age_cl + sex + passengerClass, titanic, family = binomial)
kable(tidy(reg2))
term | estimate | std.error | statistic | p.value |
---|---|---|---|---|
(Intercept) | 2.3781735 | 0.2080062 | 11.4331882 | 0.0000000 |
age_cl(-Inf,5] | 1.4766569 | 0.3504871 | 4.2131555 | 0.0000252 |
age_cl(5,10] | 0.4699227 | 0.4648991 | 1.0108057 | 0.3121094 |
age_cl(10,15] | 0.7204989 | 0.4829013 | 1.4920209 | 0.1356937 |
age_cl(15,20] | -0.0676231 | 0.2546823 | -0.2655193 | 0.7906095 |
age_cl(40,60] | -0.5147285 | 0.2204532 | -2.3348655 | 0.0195504 |
age_cl(60,70] | -1.5548623 | 0.5775239 | -2.6922911 | 0.0070963 |
age_cl(70, Inf] | -0.5507216 | 1.0611700 | -0.5189758 | 0.6037776 |
age_cl(Missing) | -0.1995336 | 0.2018029 | -0.9887547 | 0.3227832 |
sexmale | -2.5001688 | 0.1498944 | -16.6795309 | 0.0000000 |
passengerClass2nd | -1.1524823 | 0.2111712 | -5.4575727 | 0.0000000 |
passengerClass3rd | -2.0363937 | 0.1938525 | -10.5048596 | 0.0000000 |
Avant de regarder en détail le sens de ces coefficients et des tests associés, intéressons-nous à une éventuelle simplification de ce modèle : peut-on faire aussi bien avec moins d’informations ?
Simplification du modèle
Simplification des variables
Un outil privilégié pour épurer un modèle est de faire des tests sur l’utilité de chaque variable explicative compte tenu de la présence des autres ; on sait ainsi quelle variable s’avère superflue, soit parce qu’elle ne fournit pas d’information pertinente, soit parce qu’elle s’avère redondante avec d’autres. On élimine ensuite la variable avec la p-value la plus élevée à un tel test, communément appelé test “de type III”. Une automatisation de ce processus conduit à une sélection Backward mais dans ce document nous nous limiterons à la stratégie manuelle. La fonction Anova du package {car} permet de réaliser ce test ; on repart de l’objet produit par la fonction glm.
library(car)
kable(Anova(reg2, type="III"))
LR Chisq | Df | Pr(>Chisq) | |
---|---|---|---|
age_cl | 39.35064 | 8 | 4.2e-06 |
sex | 335.50113 | 1 | 0.0e+00 |
passengerClass | 122.83504 | 2 | 0.0e+00 |
Ici toutes nos variables sont significatives, ce qui nous amène à toutes les conserver simultanément dans le modèle.
Simplification des modalités d’une variable
Une fois ce travail fait, on peut encore simplifier, cette fois à l’intérieur de chacune des variables explicatives : est-il nécessaire de conserver le détail de toutes les classes de passagers, de toutes les tranches d’âge dans le modèle ? Ou peut-on fusionner certaines de ces modalités en une valeur unique ? On va encore répondre à cette question par des tests, cette fois-ci avec l’aide du package {emmeans}. Celui-ci permet de combiner les coefficients d’une régression pour produire des prédictions “moyennes”. Dans ces constructions mathématiques, une seule des variables du modèle varie, les autres étant “moyennées” :
- littéralement positionnées à leur valeur moyenne des données pour les variables X quantitatives du modèle
- combinées comme si toutes leurs valeurs étaient représentées à parts égales dans les données pour les X qualitatives, par exemple sexe aura ses coefficients combinés avec chacun un poids de 0,5 comme si les données étaient composées pour moitié d’hommes et de femmes.
Le réalisme des valeurs produites par de telles constructions n’est pas toujours avéré, mais elles permettent au moins de produire des ordres de grandeur.
library(emmeans)
kable(emmeans(reg2, "passengerClass", type="response"))
passengerClass | prob | SE | df | asymp.LCL | asymp.UCL |
---|---|---|---|---|---|
1st | 0.7509343 | 0.0376548 | Inf | 0.6701813 | 0.8173055 |
2nd | 0.4877822 | 0.0536589 | Inf | 0.3846611 | 0.5919538 |
3rd | 0.2823585 | 0.0365134 | Inf | 0.2165359 | 0.3590210 |
Dans le tableau ci-dessus, on fait varier uniquement la classe des passagers. La colonne PROB indique un niveau de probabilité de survie prédit par le modèle : 75% chez les passagers de première classe, 49% chez ceux de deuxième classe et 28% chez ceux de troisième classe.
library(emmeans)
kable(prop.table(table(titanic$passengerClass,
titanic$survived),
1))
no | yes | |
---|---|---|
1st | 0.3808050 | 0.6191950 |
2nd | 0.5703971 | 0.4296029 |
3rd | 0.7447109 | 0.2552891 |
On peut les comparer avec les pourcentages-lignes du tableau de fréquence obtenu sur les données brutes : 62% de survivants chez les passagers de 1e classe, 43% pour les 2e classe, 26% pour les 3e classe. Dans notre cas, les prédictions moyennes du modèle surévaluent les chances de survie des passagers. Le biais n’est pas systématiquement dans ce sens ; ici il est principalement dû aux disparités d’âge et de sexe au sein des classes, et au fait qu’il n’y a pas de parité hommes-femmes chez les passagers, ce qui rend irréalistes les projections de la fonction emmeans. Néanmoins on tire de cette fonction un moyen simple de comparer toutes les classes de passagers entre elles pour savoir s’il est utile au modèle de toutes les distinguer.
library(emmeans)
kable(pairs(emmeans(reg2, "passengerClass")))
contrast | estimate | SE | df | z.ratio | p.value |
---|---|---|---|---|---|
1st – 2nd | 1.1524823 | 0.2111712 | Inf | 5.457573 | 1.0e-07 |
1st – 3rd | 2.0363937 | 0.1938525 | Inf | 10.504860 | 0.0e+00 |
2nd – 3rd | 0.8839114 | 0.1881167 | Inf | 4.698738 | 7.8e-06 |
Toutes les comparaisons sont significatives : il n’y a pas lieu de simplifier. Faisons maintenant les comparaisons entre tranches d’âge.
library(emmeans)
kable(pairs(emmeans(reg2, "age_cl")))
contrast | estimate | SE | df | z.ratio | p.value |
---|---|---|---|---|---|
(20,40] – (-Inf,5] | -1.4766569 | 0.3504871 | Inf | -4.2131555 | 0.0008450 |
(20,40] – (5,10] | -0.4699227 | 0.4648991 | Inf | -1.0108057 | 0.9849954 |
(20,40] – (10,15] | -0.7204989 | 0.4829013 | Inf | -1.4920209 | 0.8597040 |
(20,40] – (15,20] | 0.0676231 | 0.2546823 | Inf | 0.2655193 | 0.9999993 |
(20,40] – (40,60] | 0.5147285 | 0.2204532 | Inf | 2.3348655 | 0.3209575 |
(20,40] – (60,70] | 1.5548623 | 0.5775239 | Inf | 2.6922911 | 0.1503740 |
(20,40] – (70, Inf] | 0.5507216 | 1.0611700 | Inf | 0.5189758 | 0.9998696 |
(20,40] – (Missing) | 0.1995336 | 0.2018029 | Inf | 0.9887547 | 0.9869926 |
(-Inf,5] – (5,10] | 1.0067341 | 0.5583757 | Inf | 1.8029690 | 0.6803412 |
(-Inf,5] – (10,15] | 0.7561580 | 0.5742408 | Inf | 1.3167961 | 0.9266785 |
(-Inf,5] – (15,20] | 1.5442799 | 0.4029821 | Inf | 3.8321302 | 0.0040416 |
(-Inf,5] – (40,60] | 1.9913854 | 0.3920417 | Inf | 5.0795249 | 0.0000134 |
(-Inf,5] – (60,70] | 3.0315192 | 0.6641721 | Inf | 4.5643580 | 0.0001733 |
(-Inf,5] – (70, Inf] | 2.0273785 | 1.1099279 | Inf | 1.8265857 | 0.6644085 |
(-Inf,5] – (Missing) | 1.6761905 | 0.3713592 | Inf | 4.5136629 | 0.0002196 |
(5,10] – (10,15] | -0.2505761 | 0.6493946 | Inf | -0.3858611 | 0.9999866 |
(5,10] – (15,20] | 0.5375458 | 0.5042753 | Inf | 1.0659768 | 0.9789559 |
(5,10] – (40,60] | 0.9846512 | 0.4964923 | Inf | 1.9832153 | 0.5551651 |
(5,10] – (60,70] | 2.0247850 | 0.7300606 | Inf | 2.7734481 | 0.1232186 |
(5,10] – (70, Inf] | 1.0206443 | 1.1511905 | Inf | 0.8865990 | 0.9937014 |
(5,10] – (Missing) | 0.6694563 | 0.4788847 | Inf | 1.3979487 | 0.8990258 |
(10,15] – (15,20] | 0.7881219 | 0.5214683 | Inf | 1.5113515 | 0.8506649 |
(10,15] – (40,60] | 1.2352274 | 0.5117643 | Inf | 2.4136646 | 0.2762289 |
(10,15] – (60,70] | 2.2753612 | 0.7399818 | Inf | 3.0748881 | 0.0541304 |
(10,15] – (70, Inf] | 1.2712205 | 1.1574324 | Inf | 1.0983108 | 0.9746458 |
(10,15] – (Missing) | 0.9200325 | 0.4966329 | Inf | 1.8525401 | 0.6466726 |
(15,20] – (40,60] | 0.4471055 | 0.3050664 | Inf | 1.4656003 | 0.8715346 |
(15,20] – (60,70] | 1.4872393 | 0.6154593 | Inf | 2.4164705 | 0.2747069 |
(15,20] – (70, Inf] | 0.4830985 | 1.0825849 | Inf | 0.4462454 | 0.9999588 |
(15,20] – (Missing) | 0.1319105 | 0.2819856 | Inf | 0.4677918 | 0.9999408 |
(40,60] – (60,70] | 1.0401338 | 0.5869071 | Inf | 1.7722289 | 0.7007376 |
(40,60] – (70, Inf] | 0.0359931 | 1.0663269 | Inf | 0.0337543 | 1.0000000 |
(40,60] – (Missing) | -0.3151949 | 0.2599726 | Inf | -1.2124162 | 0.9540635 |
(60,70] – (70, Inf] | -1.0041407 | 1.1893316 | Inf | -0.8442900 | 0.9954916 |
(60,70] – (Missing) | -1.3553287 | 0.5932384 | Inf | -2.2846275 | 0.3513664 |
(70, Inf] – (Missing) | -0.3511880 | 1.0693059 | Inf | -0.3284261 | 0.9999962 |
Les comparaisons sont nombreuses et leur lecture, fastidieuse. Si on trie le tableau par p-value décroissante, on verra plus rapidement les fusions que le modèle encourage. Attention cependant, la fusion avec la p-value la plus élevée n’est pas forcément une bonne idée : elle peut correspondre concrètement à mélanger des profils incompatibles d’un point de vue métier. Ici, fusionner deux tranches d’âge non consécutives n’est pas souhaitable.
library(emmeans)
pairs(emmeans::emmeans(reg2,"age_cl")) %>%
as.data.frame() %>%
arrange(desc(p.value)) %>%
kable()
contrast | estimate | SE | df | z.ratio | p.value |
---|---|---|---|---|---|
(40,60] – (70, Inf] | 0.0359931 | 1.0663269 | Inf | 0.0337543 | 1.0000000 |
(20,40] – (15,20] | 0.0676231 | 0.2546823 | Inf | 0.2655193 | 0.9999993 |
(70, Inf] – (Missing) | -0.3511880 | 1.0693059 | Inf | -0.3284261 | 0.9999962 |
(5,10] – (10,15] | -0.2505761 | 0.6493946 | Inf | -0.3858611 | 0.9999866 |
(15,20] – (70, Inf] | 0.4830985 | 1.0825849 | Inf | 0.4462454 | 0.9999588 |
(15,20] – (Missing) | 0.1319105 | 0.2819856 | Inf | 0.4677918 | 0.9999408 |
(20,40] – (70, Inf] | 0.5507216 | 1.0611700 | Inf | 0.5189758 | 0.9998696 |
(60,70] – (70, Inf] | -1.0041407 | 1.1893316 | Inf | -0.8442900 | 0.9954916 |
(5,10] – (70, Inf] | 1.0206443 | 1.1511905 | Inf | 0.8865990 | 0.9937014 |
(20,40] – (Missing) | 0.1995336 | 0.2018029 | Inf | 0.9887547 | 0.9869926 |
(20,40] – (5,10] | -0.4699227 | 0.4648991 | Inf | -1.0108057 | 0.9849954 |
(5,10] – (15,20] | 0.5375458 | 0.5042753 | Inf | 1.0659768 | 0.9789559 |
(10,15] – (70, Inf] | 1.2712205 | 1.1574324 | Inf | 1.0983108 | 0.9746458 |
(40,60] – (Missing) | -0.3151949 | 0.2599726 | Inf | -1.2124162 | 0.9540635 |
(-Inf,5] – (10,15] | 0.7561580 | 0.5742408 | Inf | 1.3167961 | 0.9266785 |
(5,10] – (Missing) | 0.6694563 | 0.4788847 | Inf | 1.3979487 | 0.8990258 |
(15,20] – (40,60] | 0.4471055 | 0.3050664 | Inf | 1.4656003 | 0.8715346 |
(20,40] – (10,15] | -0.7204989 | 0.4829013 | Inf | -1.4920209 | 0.8597040 |
(10,15] – (15,20] | 0.7881219 | 0.5214683 | Inf | 1.5113515 | 0.8506649 |
(40,60] – (60,70] | 1.0401338 | 0.5869071 | Inf | 1.7722289 | 0.7007376 |
(-Inf,5] – (5,10] | 1.0067341 | 0.5583757 | Inf | 1.8029690 | 0.6803412 |
(-Inf,5] – (70, Inf] | 2.0273785 | 1.1099279 | Inf | 1.8265857 | 0.6644085 |
(10,15] – (Missing) | 0.9200325 | 0.4966329 | Inf | 1.8525401 | 0.6466726 |
(5,10] – (40,60] | 0.9846512 | 0.4964923 | Inf | 1.9832153 | 0.5551651 |
(60,70] – (Missing) | -1.3553287 | 0.5932384 | Inf | -2.2846275 | 0.3513664 |
(20,40] – (40,60] | 0.5147285 | 0.2204532 | Inf | 2.3348655 | 0.3209575 |
(10,15] – (40,60] | 1.2352274 | 0.5117643 | Inf | 2.4136646 | 0.2762289 |
(15,20] – (60,70] | 1.4872393 | 0.6154593 | Inf | 2.4164705 | 0.2747069 |
(20,40] – (60,70] | 1.5548623 | 0.5775239 | Inf | 2.6922911 | 0.1503740 |
(5,10] – (60,70] | 2.0247850 | 0.7300606 | Inf | 2.7734481 | 0.1232186 |
(10,15] – (60,70] | 2.2753612 | 0.7399818 | Inf | 3.0748881 | 0.0541304 |
(-Inf,5] – (15,20] | 1.5442799 | 0.4029821 | Inf | 3.8321302 | 0.0040416 |
(20,40] – (-Inf,5] | -1.4766569 | 0.3504871 | Inf | -4.2131555 | 0.0008450 |
(-Inf,5] – (Missing) | 1.6761905 | 0.3713592 | Inf | 4.5136629 | 0.0002196 |
(-Inf,5] – (60,70] | 3.0315192 | 0.6641721 | Inf | 4.5643580 | 0.0001733 |
(-Inf,5] – (40,60] | 1.9913854 | 0.3920417 | Inf | 5.0795249 | 0.0000134 |
La première fusion concerne les tranches de 40 à 60 ans et les plus de 70 ans. Elle n’est pas à encourager car ces tranches ne sont pas adjacentes. La seconde fusion, en revanche, concerne les 15-20 ans et les 20-40 ans. On peut donc envisager une tranche unique de 15 à 40 ans.
titanic$age_cl <- fct_recode(titanic$age_cl,
"(15,40]"="(15,20]",
"(15,40]"="(20,40]")
reg2b <- glm(survived ~ age_cl + sex + passengerClass,
titanic,
family="binomial")
pairs(emmeans(reg2b,"age_cl")) %>%
as.data.frame() %>%
arrange(desc(p.value)) %>%
kable()
contrast | estimate | SE | df | z.ratio | p.value |
---|---|---|---|---|---|
(40,60] – (70, Inf] | 0.0367207 | 1.0664081 | Inf | 0.0344340 | 1.0000000 |
(70, Inf] – (Missing) | -0.3549343 | 1.0693071 | Inf | -0.3319292 | 0.9999789 |
(5,10] – (10,15] | -0.2502722 | 0.6493688 | Inf | -0.3854085 | 0.9999416 |
(15,40] – (70, Inf] | 0.5408137 | 1.0606127 | Inf | 0.5099069 | 0.9996178 |
(60,70] – (70, Inf] | -1.0038500 | 1.1893715 | Inf | -0.8440172 | 0.9905450 |
(5,10] – (70, Inf] | 1.0247290 | 1.1511655 | Inf | 0.8901665 | 0.9870160 |
(15,40] – (Missing) | 0.1858794 | 0.1951899 | Inf | 0.9523003 | 0.9807564 |
(15,40] – (5,10] | -0.4839153 | 0.4618941 | Inf | -1.0476758 | 0.9670385 |
(10,15] – (70, Inf] | 1.2750012 | 1.1574435 | Inf | 1.1015667 | 0.9566909 |
(40,60] – (Missing) | -0.3182136 | 0.2597285 | Inf | -1.2251778 | 0.9246878 |
(-Inf,5] – (10,15] | 0.7557670 | 0.5742275 | Inf | 1.3161456 | 0.8930759 |
(5,10] – (Missing) | 0.6697947 | 0.4788389 | Inf | 1.3987891 | 0.8582081 |
(15,40] – (10,15] | -0.7341875 | 0.4801801 | Inf | -1.5289837 | 0.7919186 |
(40,60] – (60,70] | 1.0405708 | 0.5868308 | Inf | 1.7732041 | 0.6384021 |
(-Inf,5] – (5,10] | 1.0060392 | 0.5583158 | Inf | 1.8019179 | 0.6187794 |
(-Inf,5] – (70, Inf] | 2.0307682 | 1.1099472 | Inf | 1.8296079 | 0.5997048 |
(10,15] – (Missing) | 0.9200669 | 0.4966361 | Inf | 1.8525977 | 0.5837882 |
(5,10] – (40,60] | 0.9880082 | 0.4963088 | Inf | 1.9907127 | 0.4881226 |
(60,70] – (Missing) | -1.3587843 | 0.5930383 | Inf | -2.2912254 | 0.2982793 |
(15,40] – (40,60] | 0.5040930 | 0.2167912 | Inf | 2.3252460 | 0.2796266 |
(10,15] – (40,60] | 1.2382804 | 0.5116609 | Inf | 2.4201194 | 0.2313830 |
(15,40] – (60,70] | 1.5446637 | 0.5761783 | Inf | 2.6808778 | 0.1283360 |
(5,10] – (60,70] | 2.0285790 | 0.7298583 | Inf | 2.7794147 | 0.1001215 |
(10,15] – (60,70] | 2.2788512 | 0.7398365 | Inf | 3.0802091 | 0.0432004 |
(15,40] – (-Inf,5] | -1.4899544 | 0.3469218 | Inf | -4.2947852 | 0.0004639 |
(-Inf,5] – (Missing) | 1.6758338 | 0.3713210 | Inf | 4.5131676 | 0.0001722 |
(-Inf,5] – (60,70] | 3.0346182 | 0.6640160 | Inf | 4.5700983 | 0.0001319 |
(-Inf,5] – (40,60] | 1.9940474 | 0.3919127 | Inf | 5.0879888 | 0.0000100 |
Une fois la fusion réalisée avec le package {forcats}, on réitère les comparaisons de tranches, triées par p-value décroissante. La première fusion “acceptable” est la troisième, qui crée une classe unique de 5-15 ans à partir des 5-10 et 10-15.
titanic$age_cl <- fct_recode(titanic$age_cl,
"(5,15]"="(5,10]",
"(5,15]"="(10,15]")
reg2c <- glm(survived ~ age_cl + sex + passengerClass,
titanic,
family="binomial")
pairs(emmeans(reg2c,"age_cl")) %>%
as.data.frame() %>%
arrange(desc(p.value)) %>%
kable()
contrast | estimate | SE | df | z.ratio | p.value |
---|---|---|---|---|---|
(40,60] – (70, Inf] | 0.0369200 | 1.0665027 | Inf | 0.0346178 | 1.0000000 |
(70, Inf] – (Missing) | -0.3557970 | 1.0693989 | Inf | -0.3327075 | 0.9998922 |
(15,40] – (70, Inf] | 0.5415975 | 1.0607053 | Inf | 0.5106013 | 0.9987193 |
(60,70] – (70, Inf] | -1.0039214 | 1.1894807 | Inf | -0.8439998 | 0.9802746 |
(15,40] – (Missing) | 0.1858006 | 0.1952032 | Inf | 0.9518313 | 0.9639260 |
(5,15] – (70, Inf] | 1.1455754 | 1.1073872 | Inf | 1.0344850 | 0.9461272 |
(40,60] – (Missing) | -0.3188770 | 0.2597370 | Inf | -1.2276920 | 0.8835812 |
(40,60] – (60,70] | 1.0408414 | 0.5868891 | Inf | 1.7734890 | 0.5661025 |
(15,40] – (5,15] | -0.6039779 | 0.3400650 | Inf | -1.7760657 | 0.5643439 |
(-Inf,5] – (70, Inf] | 2.0318876 | 1.1100395 | Inf | 1.8304641 | 0.5272403 |
(-Inf,5] – (5,15] | 0.8863122 | 0.4630702 | Inf | 1.9139910 | 0.4708687 |
(5,15] – (Missing) | 0.7897784 | 0.3628780 | Inf | 2.1764297 | 0.3083231 |
(60,70] – (Missing) | -1.3597184 | 0.5930892 | Inf | -2.2926035 | 0.2474133 |
(15,40] – (40,60] | 0.5046776 | 0.2168040 | Inf | 2.3278054 | 0.2305530 |
(15,40] – (60,70] | 1.5455189 | 0.5762329 | Inf | 2.6821083 | 0.1027293 |
(5,15] – (40,60] | 1.1086554 | 0.3843600 | Inf | 2.8844196 | 0.0599418 |
(5,15] – (60,70] | 2.1494968 | 0.6586040 | Inf | 3.2637168 | 0.0189360 |
(15,40] – (-Inf,5] | -1.4902900 | 0.3469426 | Inf | -4.2954943 | 0.0003498 |
(-Inf,5] – (Missing) | 1.6760906 | 0.3713439 | Inf | 4.5135806 | 0.0001296 |
(-Inf,5] – (60,70] | 3.0358090 | 0.6640695 | Inf | 4.5715227 | 0.0000988 |
(-Inf,5] – (40,60] | 1.9949676 | 0.3919308 | Inf | 5.0901023 | 0.0000074 |
Le nouveau modèle encourage la fusion des 60-70 ans avec les 70 ans et plus. Voici les codes pour les fusions suivantes, qui ne seront pas commentées, elles sont toutes du même tonneau.
# 60-70 avec 70-Inf
titanic$age_cl <- fct_recode(titanic$age_cl,
"(60, Inf]"="(60,70]",
"(60, Inf]"="(70, Inf]")
reg2d <- glm(survived ~ age_cl + sex + passengerClass,
titanic,
family="binomial")
pairs(emmeans(reg2d,"age_cl")) %>%
as.data.frame() %>%
arrange(desc(p.value)) %>%
kable()
contrast | estimate | SE | df | z.ratio | p.value |
---|---|---|---|---|---|
(15,40] – (Missing) | 0.1861563 | 0.1951600 | Inf | 0.9538651 | 0.9322304 |
(40,60] – (Missing) | -0.3189034 | 0.2597018 | Inf | -1.2279600 | 0.8232753 |
(40,60] – (60, Inf] | 0.8453906 | 0.5240239 | Inf | 1.6132675 | 0.5896952 |
(15,40] – (5,15] | -0.6039219 | 0.3399531 | Inf | -1.7764859 | 0.4810436 |
(-Inf,5] – (5,15] | 0.8861312 | 0.4629356 | Inf | 1.9141563 | 0.3933051 |
(5,15] – (Missing) | 0.7900782 | 0.3627665 | Inf | 2.1779248 | 0.2480386 |
(60, Inf] – (Missing) | -1.1642941 | 0.5308100 | Inf | -2.1934292 | 0.2406756 |
(15,40] – (40,60] | 0.5050598 | 0.2167652 | Inf | 2.3299854 | 0.1819492 |
(15,40] – (60, Inf] | 1.3504504 | 0.5119808 | Inf | 2.6376972 | 0.0883530 |
(5,15] – (40,60] | 1.1089816 | 0.3842558 | Inf | 2.8860503 | 0.0450935 |
(5,15] – (60, Inf] | 1.9543723 | 0.6031152 | Inf | 3.2404628 | 0.0151378 |
(15,40] – (-Inf,5] | -1.4900530 | 0.3468599 | Inf | -4.2958358 | 0.0002516 |
(-Inf,5] – (Missing) | 1.6762094 | 0.3712539 | Inf | 4.5149949 | 0.0000926 |
(-Inf,5] – (60, Inf] | 2.8405035 | 0.6087754 | Inf | 4.6659300 | 0.0000452 |
(-Inf,5] – (40,60] | 1.9951128 | 0.3918493 | Inf | 5.0915314 | 0.0000053 |
# 40-60 avec 60-Inf
titanic$age_cl <- fct_recode(titanic$age_cl,
"(40, Inf]"="(40,60]",
"(40, Inf]"="(60, Inf]")
reg2e <- glm(survived ~ age_cl + sex + passengerClass,
titanic,
family="binomial")
pairs(emmeans(reg2e,"age_cl")) %>%
as.data.frame() %>%
arrange(desc(p.value)) %>%
kable()
contrast | estimate | SE | df | z.ratio | p.value |
---|---|---|---|---|---|
(15,40] – (Missing) | 0.1850310 | 0.1952045 | Inf | 0.9478829 | 0.8780711 |
(40, Inf] – (Missing) | -0.4244489 | 0.2524640 | Inf | -1.6812258 | 0.4455949 |
(15,40] – (5,15] | -0.6025311 | 0.3402284 | Inf | -1.7709609 | 0.3906355 |
(-Inf,5] – (5,15] | 0.8859840 | 0.4632678 | Inf | 1.9124663 | 0.3105428 |
(5,15] – (Missing) | 0.7875621 | 0.3630164 | Inf | 2.1694943 | 0.1913138 |
(15,40] – (40, Inf] | 0.6094799 | 0.2080931 | Inf | 2.9288806 | 0.0280987 |
(5,15] – (40, Inf] | 1.2120111 | 0.3798372 | Inf | 3.1908702 | 0.0123598 |
(15,40] – (-Inf,5] | -1.4885151 | 0.3470500 | Inf | -4.2890514 | 0.0001744 |
(-Inf,5] – (Missing) | 1.6735461 | 0.3714202 | Inf | 4.5058022 | 0.0000648 |
(-Inf,5] – (40, Inf] | 2.0979950 | 0.3875480 | Inf | 5.4135098 | 0.0000006 |
# 5-15 avec 15-40
titanic$age_cl <- fct_recode(titanic$age_cl,
"(5,40]"="(5,15]",
"(5,40]"="(15,40]")
reg2f <- glm(survived ~ age_cl + sex + passengerClass,
titanic,
family="binomial")
pairs(emmeans(reg2f,"age_cl")) %>%
as.data.frame() %>%
arrange(desc(p.value)) %>%
kable()
contrast | estimate | SE | df | z.ratio | p.value |
---|---|---|---|---|---|
(5,40] – (Missing) | 0.2413447 | 0.1924441 | Inf | 1.254103 | 0.5922897 |
(40, Inf] – (Missing) | -0.4077027 | 0.2522518 | Inf | -1.616253 | 0.3693494 |
(5,40] – (40, Inf] | 0.6490474 | 0.2069428 | Inf | 3.136362 | 0.0092727 |
(5,40] – (-Inf,5] | -1.4347233 | 0.3456244 | Inf | -4.151106 | 0.0001938 |
(-Inf,5] – (Missing) | 1.6760680 | 0.3715880 | Inf | 4.510554 | 0.0000383 |
(-Inf,5] – (40, Inf] | 2.0837707 | 0.3875032 | Inf | 5.377428 | 0.0000005 |
On en arrive à un point où plus rien n’est simplifiable, et où il nous reste 4 tranches : âge inconnu, moins de 5 ans, 5 à 40 ans et plus de 40 ans.
Commentaire du modèle
Utilisons maintenant un autre package, {blorr}, pour avoir les sorties détaillées de ce modèle. Elles sont organisées suivant la présentation de la proc LOGISTIC de SAS et permet aux utilisateurs de ce logiciel de retrouver leurs repères. (La mise en forme HTML pour intégration à une page Web comme dans {rmarkdown} n’est pour le moment pas disponible, ni avec {knitr} ni avec {pander}.)
library(blorr)
blr_regress(reg2f, odd_conf_limit=TRUE)
## - Creating model overview.
## - Creating response profile.
## - Extracting maximum likelihood estimates.
## - Computing odds ratio estimates.
## - Estimating concordant and discordant pairs.
## Model Overview
## ------------------------------------------------------------------------
## Data Set Resp Var Obs. Df. Model Df. Residual Convergence
## ------------------------------------------------------------------------
## data survived 1309 1308 1302 TRUE
## ------------------------------------------------------------------------
##
## Response Summary
## --------------------------------------------------------
## Outcome Frequency Outcome Frequency
## --------------------------------------------------------
## 0 809 1 500
## --------------------------------------------------------
##
## Maximum Likelihood Estimates
## ------------------------------------------------------------------------
## Parameter DF Estimate Std. Error z value Pr(>|z|)
## ------------------------------------------------------------------------
## (Intercept) 1 2.4014 0.2027 11.8463 0.0000
## age_cl(-Inf,5] 1 1.4347 0.3456 4.1511 0.0000
## age_cl(40, Inf] 1 -0.6490 0.2069 -3.1364 0.0017
## age_cl(Missing) 1 -0.2413 0.1924 -1.2541 0.2098
## sexmale 1 -2.5127 0.1493 -16.8278 0.0000
## passengerClass2nd 1 -1.1353 0.2097 -5.4135 0.0000
## passengerClass3rd 1 -2.0031 0.1917 -10.4486 0.0000
## ------------------------------------------------------------------------
##
## Odds Ratio Estimates
## --------------------------------------------------------------------
## Effects Estimate 95% Wald Conf. Limit
## --------------------------------------------------------------------
## age_cl(-Inf,5] 4.1985 2.1514 8.3734
## age_cl(40, Inf] 0.5225 0.3467 0.7810
## age_cl(Missing) 0.7856 0.5370 1.1428
## sexmale 0.0811 0.0602 0.1081
## passengerClass2nd 0.3213 0.2121 0.4830
## passengerClass3rd 0.1349 0.0922 0.1955
## --------------------------------------------------------------------
##
## Association of Predicted Probabilities and Observed Responses
## ---------------------------------------------------------------
## % Concordant 0.8049 Somers' D 0.7121
## % Discordant 0.1353 Gamma 0.6696
## % Tied 0.0597 Tau-a 0.3164
## Pairs 404500 c 0.8348
## ---------------------------------------------------------------
On retrouve dans ces sorties les coefficients de la régression, avec la même présentation que dans la fonction tidy de {broom}.
kable(tidy(reg2f))
term | estimate | std.error | statistic | p.value |
---|---|---|---|---|
(Intercept) | 2.4013701 | 0.2027105 | 11.846301 | 0.0000000 |
age_cl(-Inf,5] | 1.4347233 | 0.3456244 | 4.151106 | 0.0000331 |
age_cl(40, Inf] | -0.6490474 | 0.2069428 | -3.136362 | 0.0017106 |
age_cl(Missing) | -0.2413447 | 0.1924441 | -1.254103 | 0.2098046 |
sexmale | -2.5126650 | 0.1493161 | -16.827829 | 0.0000000 |
passengerClass2nd | -1.1353278 | 0.2097209 | -5.413517 | 0.0000001 |
passengerClass3rd | -2.0030860 | 0.1917088 | -10.448589 | 0.0000000 |
Deux tableaux sont inédits par rapport aux sorties précédentes : les odds-ratios et le tableau de paires (“Association of Predicted Probabilities etc.”).
Odds-ratios
Les odds-ratios (OR) sont les exponentielles des coefficients de régression, qu’on peut interpréter approximativement comme un facteur multiplicatif des chances de survenance de l’évènement. Si on prend l’OR pour SEX=male, on se compare à la référence SEX=female. Cet OR vaut 0,08 – soit environ 1/12. On peut donc dire que les chances de survie d’un homme sont celles d’une femme multipliée par 0,08, autrement dit, divisées par 12. Pour le tourner encore autrement, les chances de survie d’un homme sont 12 fois plus faibles que celles d’une femme. Si l’OR est supérieur à 1, les chances de survenance de l’évènement sont en faveur de la catégorie considérée par rapport à la référence. Sur l’âge, on compare les enfants de moins de 5 ans à la référence des personnes de 5 à 40 ans. Les petits enfants ont 4,2 fois plus de chances de survie que leurs aînés. Ces OR sont assortis d’intervalles de confience ; quand ils incluent la valeur 1, l’OR n’est pas significatif donc pas commentable. Cela correspond à un coefficient dont la p-value est supérieure à 0,05.
On peut aussi les obtenir avec la fonction tidy de {broom} en ajoutant l’option exponentiate=TRUE.
# on change l'âge de référence pour 5-40, la classe pour 2e et le sexe, homme
titanic$age_cl <- relevel(titanic$age_cl , "(5,40]")
titanic$sex <- relevel(titanic$sex , "male")
titanic$passengerClass <- relevel(titanic$passengerClass , "2nd")
reg2g <- glm(survived ~ age_cl + sex + passengerClass,
titanic,
family="binomial")
broom::tidy(reg2g, exponentiate=TRUE, conf.int=TRUE)
## # A tibble: 7 x 7
## term estimate std.error statistic p.value conf.low conf.high
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 (Intercept) 0.287 0.164 -7.59 3.28e-14 0.207 0.395
## 2 age_cl(-Inf,5] 4.20 0.346 4.15 3.31e- 5 2.15 8.37
## 3 age_cl(40, Inf] 0.523 0.207 -3.14 1.71e- 3 0.347 0.781
## 4 age_cl(Missing) 0.786 0.192 -1.25 2.10e- 1 0.537 1.14
## 5 sexfemale 12.3 0.149 16.8 1.53e-63 9.25 16.6
## 6 passengerClass1st 3.11 0.210 5.41 6.18e- 8 2.07 4.71
## 7 passengerClass3rd 0.420 0.186 -4.67 3.00e- 6 0.291 0.604
Avec le résultat de tidy, il est plus simple de produire un graphique.
library(ggplot2)
reg2g %>%
tidy(exponentiate=TRUE, conf.int=TRUE) %>%
ggplot() +
aes(x=estimate, y=term, xmin=conf.low, xmax=conf.high) +
geom_errorbarh(height=0.2) +
geom_point(shape=15, size=3)
Tableau des paires
L’autre résultat inédit de {blorr} est le tableau des paires. Pour rappel le voici.
## - Creating model overview.
## - Creating response profile.
## - Extracting maximum likelihood estimates.
## - Estimating concordant and discordant pairs.
pairs | concordance | discordance | tied | somers_d | gamma | tau | c |
---|---|---|---|---|---|---|---|
404500 | 0.8049172 | 0.135335 | 0.0597478 | 0.7121305 | 0.6695822 | 0.3163771 | 0.8347911 |
Il est construit en constituant depuis les données toutes les paires possibles, chacune composée d’un passager survivant et d’un passager naufragé. Les prédictions du modèle sont calculées sur chaque passager ; on notera pS la probabilité de survie du passager réellement survivant et pN celle du passager naufragé. On compte
- comme concordante une paire où pS > pN, ce qui est la logique espérée : qu’un passager ayant réellement survécu soit aux yeux du modèle celui qui avait le plus de chances de survivre.
- comme discordante une paire où pS < pN.
- comme liée (en anglais tied) une paire où pS=pN.
Le pourcentage de paires concordantes est une bonne approximation (légèrement optimiste) du pourcentage d’individus bien classés en prédiction par le modèle. Mais la combinaison du nombre de paires des différents types produit également d’autres statistiques : D de Somers (également appelé “indice de Gini”), c de Pregibon (ou AUC), ou encore Tau-alpha de Kendall. Plus ces indicateurs sont proches de 1, meilleure est la qualité discriminante du modèle (= sa capacité à reconnaître un survivant comme tel, et un naufragé comme tel).
Prédictions
Cela nous amène à calculer, justement, des prédictions du modèle pour chaque passager. La fonction predict va calculer pour chacun la probabilité de l’évènement, ici la survie.
titanic$p_surv <- predict(reg2g,
titanic,
type="response")
head(titanic$p_surv)
## [1] 0.9169317 0.7897515 0.9788780 0.4722050 0.9169317 0.3185720
De ce nombre entre 0 et 1, on peut faire plusieurs usages…
- soit le transformer en décision binaire (“survie”, “naufrage”) en utilisant un seuil ; il n’y a pas de valeur standard pour ce seuil, il doit être déterminé au mieux pour chaque modèle
- soit l’utiliser comme un critère de classement, s’il s’agit d’identifier les 100 passagers avec les meilleures chances de survie (un classement d’un cynisme achevé, dont on préfère ne pas réfléchir à l’usage). C’est l’usage qu’on fait d’un modèle pour réaliser un ciblage : clients auxquels on envoie une publicité, clients à contacter en priorité pour éviter un départ à la concurrence, entreprises à aider pour éviter un redressement judiciaire, contribuables les plus suspects de fraude… Dans ce cas, pas de seuil, juste un classement décroissant selon les prédictions brutes
Modèle pour classer
Pour classer et obtenir une prédiction binaire, il nous faut donc choisir un seuil. Une possibilité est d’utiliser le taux de survivants (le % d’évènements) dans la base initiale.
mean(titanic$survived == "yes")
## [1] 0.381971
On peut en déduire une matrice de confusion.
confusion <- table(titanic$survived,
ifelse(titanic$p_surv < 0.382,
"prédit \"non\"",
"prédit \"oui\""))
kable(confusion)
prédit “non” | prédit “oui” | |
---|---|---|
no | 622 | 187 |
yes | 112 | 388 |
# taux de bien classés
sum(diag(confusion))/sum(confusion)*100
## [1] 77.15814
En représentant graphiquement la répartition des vrais survivants et vrais naufragés selon la prédiction du modèle, on peut aussi trouver un seuil optimal.
ggplot(titanic) +
aes(x=p_surv, group=survived, colour=survived) +
geom_line(stat="density", lwd=1.2) +
theme_minimal() +
scale_x_continuous(breaks=seq(0,1,by=0.1))
On voit un croisement des courbes vers… 0,38 – exactement le seuil proposé plus haut. C’est une propriété des régressions logistiques qui se vérifiera systématiquement. On peut essayer un seuil à 0,42 pour voir s’il y a une grosse différence dans les taux de bien classés.
confusion <- table(titanic$survived,
ifelse(titanic$p_surv < 0.42,
"prédit \"non\"",
"prédit \"oui\""))
# kable(confusion)
confusion
##
## prédit "non" prédit "oui"
## no 642 167
## yes 120 380
# taux de bien classés
sum(diag(confusion))/sum(confusion)*100
## [1] 78.07487
On peut aussi utiliser une autre visualisation pour définir un seuil “optimal” (au sens où le modèle se tromper de manière équitable dans les deux sens : autant de faux positifs que de faux négatifs) : un diagramme de distributions cumulées. C’est sur ce type de comparaisons qu’est basé le test de Kolmogorov-Smirnov. Ce graphique est facilement produit par le package {blorr}.
reg2g %>%
blr_gains_table() %>%
blr_ks_chart()
Dernier graphique pour les recherches de seuils, la courbe ROC où chaque point correspond à un seuil possible. Le point de la courbe le plus proche du coin supérieur gauche – le point (0,1) – correspond au seuil optimal. De nombreux packages dessinent des courbes ROC, mais pour une régression logistique on peut rester avec {blorr}.
reg2g %>%
blr_gains_table() %>%
blr_roc_curve()
L’aire sous la courbe ROC est l’indicateur AUC (ou c dans les sorties de blr_regress) vu plus haut.
Modèle pour cibler
Pour l’autre utilisation prédictive d’un modèle, le ciblage, où la prédiction ne sert que de critère de tri sans que ses valeurs dans l’absolu soient exploitées, le graphique pertinent est la courbe de lift. Elle sera aussi fournie par {blorr}.
reg2g %>%
blr_gains_table() %>%
plot()
On y voit en abscisses la population exprimée en pourcentage, triée par prédiction décroissante : les 10% des passagers avec le plus fort pourcentage de chances de survie, les 20% des passagers avec le plus fort pourcentage de chances de survie, etc. (c’est cumulatif), jusqu’à l’intégralité des passagers à 100%. L’axe des ordonnées indique le pourcentage de vrais survivants dans cette fraction de la population.
N’importe quel modèle va du point (0,0) au point (100,100) car en ne sélectionnant personne on ne sélectionne aucun individu réellement cible ; et en sélectionnant tout le monde on sélectionne forcément toutes les cibles. Entre ces deux points, beaucoup de trajectoires sont possibles, y compris des courbes qui démarrent sous la diagonale avant de repasser au-dessus ; ce type de modèle est à fuir absolument. Un modèle parfait peut également être facilement dessiné sur cette courbe, il est composé de deux segments de droite. En notant p la proportion de cibles dans la population, le premier segment est oblique et va de (0,0) à (p,100) ; le second segment est horizontal et va de (p,100) à (100,100).
parfait <- titanic %>%
summarise(p=mean(survived == "yes"))
reg2g %>%
blr_gains_table() %>%
plot() +
geom_segment(data=parfait,
aes(x=0, y=0,
xend=p, yend=1),
linetype="dashed") +
geom_segment(data=parfait,
aes(x=p, y=1,
xend=1, yend=1),
linetype="dashed")