Et avec R ?

On prépare d’abord les données :

options(OutDec = ",", scipen = 999)
set.seed(67)

library(dplyr)
library(ggplot2)
library(gtsummary)

theme_gtsummary_language(
    language = "fr",
    decimal.mark = ",",
    # 95% CI separator : " - " à " ; "
    ci.sep = " ; ",
    big.mark = " "
)
theme_gtsummary_compact()

# Présentation commune des tableaux de régression
formater_estimation_table <- function(x) {
    gtsummary::style_number(x, digits = 2, decimal.mark = ",", big.mark = " ")
}
formater_ic_table <- function(x) {
    gtsummary::style_number(x, digits = 1, decimal.mark = ",", big.mark = " ")
}
formater_p_table <- function(p) {
    ifelse(is.na(p), NA_character_,
        ifelse(p < 0.001, "< 0,001", formatC(p, format = "f", digits = 3, decimal.mark = ",")))
}
presenter_table_regression <- function(tableau) {
    tableau |>
        modify_fmt_fun(c(conf.low, conf.high) ~ formater_ic_table) |>
        modify_column_merge(
            pattern = "[{conf.low} ; {conf.high}]",
            rows = !is.na(conf.low) & !is.na(conf.high)
        ) |>
        modify_header(
            label = "**Terme**", estimate = "**Estimation**",
            conf.low = "**IC 95 %**", p.value = "**p-valeur**"
        )
}

load("../../data/birthweight.RData")
birthweight <- data

bw <- birthweight |>
    transmute(
        faible_poids = factor(LOW, levels = c(0, 1), labels = c("Non", "Oui")),
        poids_naissance = BWT,
        age = AGE,
        age10 = AGE / 10,
        poids_mere_lb = LWT,
        poids_mere_10lb = LWT / 10,
        tabagisme = factor(SMOKE, levels = c(0, 1), labels = c("Non-fumeuse", "Fumeuse")),
        hypertension = factor(HT, levels = c(0, 1), labels = c("Non", "Oui")),
        origine = factor(RACE, levels = c(1, 2, 3), labels = c("Caucasienne", "Afro-américaine", "Autre"))
    )

Estimer un modèle

Pour estimer un modèle, rien de plus simple :

# Association brute
modele_brut <- lm(
    poids_naissance ~ tabagisme,
    data = bw
)

# Association ajustée
modele_ajuste <- lm(
    poids_naissance ~ age + poids_mere_lb + tabagisme + origine,
    data = bw
)

# Avec une interaction
## on utilise *
modele_interaction <- lm(
    poids_naissance ~ age * tabagisme,
    data = bw
)

## on écrit entièrement la relation (x1*x2 = x1 + x2 + x1:x2)
modele_interaction <- lm(
    poids_naissance ~ age + tabagisme + age:tabagisme,
    data = bw
)

# Avec une variable quantitative transformée
bw$age10 <- bw$age / 10
modele_transforme <- lm(
    poids_naissance ~ age10,
    data = bw
)

Pour avoir un résumé d’un modèle estimé, on propose trois options :

  1. via summary(), qui est la fonction de base de R ;
summary(modele_ajuste)
#> 
#> Call:
#> lm(formula = poids_naissance ~ age + poids_mere_lb + tabagisme + 
#>     origine, data = bw)
#> 
#> Residuals:
#>      Min       1Q   Median       3Q      Max 
#> -2281,79  -447,32    22,18   472,27  1747,79 
#> 
#> Coefficients:
#>                        Estimate Std. Error t value             Pr(>|t|)    
#> (Intercept)            2841,961    321,334   8,844 0,000000000000000767 ***
#> age                      -2,036      9,817  -0,207             0,835894    
#> poids_mere_lb             3,999      1,737   2,302             0,022480 *  
#> tabagismeFumeuse       -400,326    109,207  -3,666             0,000323 ***
#> origineAfro-américaine -511,535    157,028  -3,258             0,001339 ** 
#> origineAutre           -400,607    119,542  -3,351             0,000978 ***
#> ---
#> Signif. codes:  0 '***' 0,001 '**' 0,01 '*' 0,05 '.' 0,1 ' ' 1
#> 
#> Residual standard error: 681,9 on 183 degrees of freedom
#> Multiple R-squared:  0,1484, Adjusted R-squared:  0,1251 
#> F-statistic: 6,377 on 5 and 183 DF,  p-value: 0,00001744
  1. via la librairie broom et ses fonctions tidy() et glance()
broom::tidy(modele_ajuste) # pour les coefficients
term estimate std.error statistic p.value
(Intercept) 2841,961445 321,334241 8,8442534 0,0000000
age -2,036486 9,817054 -0,2074437 0,8358940
poids_mere_lb 3,999113 1,737475 2,3016806 0,0224795
tabagismeFumeuse -400,326420 109,206665 -3,6657691 0,0003230
origineAfro-américaine -511,535272 157,027814 -3,2576093 0,0013392
origineAutre -400,606590 119,541915 -3,3511810 0,0009775
broom::glance(modele_ajuste) # pour les statistiques globales du modèle
r.squared adj.r.squared sigma statistic p.value df logLik AIC BIC deviance df.residual nobs
0,148382 0,1251138 681,8937 6,377017 0,0000174 5 -1498,332 3010,664 3033,356 85091158 183 189
  1. via la librairie gtsummary et sa fonction tbl_regression()
tableau_lineaire <- modele_ajuste |>
    gtsummary::tbl_regression(
        label = list(
            age ~ "Âge maternel (+1 an)",
            poids_mere_lb ~ "Poids maternel (livres)",
            tabagisme ~ "Tabagisme maternel",
            origine ~ "Origine maternelle"
        ),
        estimate_fun = formater_estimation_table,
        pvalue_fun = formater_p_table
    ) |>
    add_global_p(
        include = c(origine),
        # "II" = modèle sans interaction ; "III" = avec interaction
        type = "II",
        keep = TRUE
    ) |>
    bold_labels() |>
    presenter_table_regression()

tableau_lineaire |> as_gt()
Coefficients ajustés du modèle linéaire du poids de naissance (g) selon les caractéristiques maternelles, étude ‘poids de naissance’.
Terme Estimation IC 95 % p-valeur
Âge maternel (+1 an) -2,04 [-21,4 ; 17,3] 0,836
Poids maternel (livres) 4,00 [0,6 ; 7,4] 0,022
Tabagisme maternel


    Non-fumeuse — —
    Fumeuse -400,33 [-615,8 ; -184,9] < 0,001
Origine maternelle

< 0,001
    Caucasienne — —
    Afro-américaine -511,54 [-821,4 ; -201,7] 0,001
    Autre -400,61 [-636,5 ; -164,7] < 0,001
Abréviation: IC = intervalle de confiance

Si vous êtes amenés à faire des analyses univariées, sachez que tbl_uvregression() estime automatiquement un modèle séparé pour chaque variable explicative :

tableau_univarie <- bw |>
    tbl_uvregression(
        y = poids_naissance,
        method = lm,
        include = c(age, tabagisme, origine),
        label = list(
            age ~ "Âge maternel (+1 an)",
            tabagisme ~ "Tabagisme maternel",
            origine ~ "Origine maternelle"
        ),
        estimate_fun = formater_estimation_table,
        pvalue_fun = formater_p_table
    ) %>%
    add_global_p(
        include = c(origine),
        keep = TRUE,
        type = "II"
    ) |>
    bold_labels() |>
    presenter_table_regression()

tableau_univarie |> as_gt()
Associations univariées avec le poids de naissance (g), étude ‘poids de naissance’.
Terme N Estimation IC 95 % p-valeur
Âge maternel (+1 an) 189 12,36 [-7,4 ; 32,1] 0,219
Tabagisme maternel 189


    Non-fumeuse
— —
    Fumeuse
-281,71 [-492,7 ; -70,7] 0,009
Origine maternelle 189

0,008
    Caucasienne
— —
    Afro-américaine
-384,05 [-695,5 ; -72,6] 0,016
    Autre
-299,72 [-524,0 ; -75,5] 0,009
Abréviation: IC = intervalle de confiance

A la limite, un avantage est de pouvoir construire une table qui compare les coefficients bruts et ajustés, grâce à la fonction tbl_merge() :

tbl_merge(
    tbls = list(tableau_univarie, tableau_lineaire),
    tab_spanner = c("**Analyse brute**", "**Analyse ajustée**")
) |>
    as_gt()
Associations brutes et ajustées avec le poids de naissance (g), étude ‘poids de naissance’.
Terme
Analyse brute
Analyse ajustée
N Estimation IC 95 % p-valeur Estimation IC 95 % p-valeur
Âge maternel (+1 an) 189 12,36 [-7,4 ; 32,1] 0,219 -2,04 [-21,4 ; 17,3] 0,836
Tabagisme maternel 189





    Non-fumeuse
— —
— —
    Fumeuse
-281,71 [-492,7 ; -70,7] 0,009 -400,33 [-615,8 ; -184,9] < 0,001
Origine maternelle 189

0,008

< 0,001
    Caucasienne
— —
— —
    Afro-américaine
-384,05 [-695,5 ; -72,6] 0,016 -511,54 [-821,4 ; -201,7] 0,001
    Autre
-299,72 [-524,0 ; -75,5] 0,009 -400,61 [-636,5 ; -164,7] < 0,001
Poids maternel (livres)



4,00 [0,6 ; 7,4] 0,022
Abréviation: IC = intervalle de confiance

Tester les coefficients

On peut être intéressé de tester les coefficients “à la main”. On a présenté dans le cours le test de Student, de Fisher global et de Fisher partiel.

En pratique, le plus simple est de

  • regarder le tableau de summary() du modèle pour les tests d’un seul coefficient ;
  • faire un test du rapport de vraisemblance pour tester un sous-ensemble de coefficients.

Pour ce second point, on estime deux modèles : un modèle de base (qu’on appelle “complet”) et un modèle où on enlève les variables qu’on souhaite tester (qu’on appelle “réduit”). On compare ensuite les deux modèles avec la fonction anova().

Test d’un coefficient

modele_complet <- lm(
    poids_naissance ~ age + tabagisme * origine,
    data = bw
)

modele_reduit <- lm(
    poids_naissance ~ age + tabagisme + origine,
    data = bw
)

summary(modele_complet)
#> 
#> Call:
#> lm(formula = poids_naissance ~ age + tabagisme * origine, data = bw)
#> 
#> Residuals:
#>      Min       1Q   Median       3Q      Max 
#> -2404,31  -419,69    31,89   464,88  1583,17 
#> 
#> Coefficients:
#>                                         Estimate Std. Error t value
#> (Intercept)                             3458,802    279,940  12,356
#> age                                       -1,155      9,998  -0,116
#> tabagismeFumeuse                        -603,710    143,963  -4,193
#> origineAfro-américaine                  -581,277    209,109  -2,780
#> origineAutre                            -618,739    143,354  -4,316
#> tabagismeFumeuse:origineAfro-américaine  258,017    318,482   0,810
#> tabagismeFumeuse:origineAutre            546,798    261,703   2,089
#>                                                     Pr(>|t|)    
#> (Intercept)                             < 0,0000000000000002 ***
#> age                                                  0,90817    
#> tabagismeFumeuse                                   0,0000428 ***
#> origineAfro-américaine                               0,00601 ** 
#> origineAutre                                       0,0000260 ***
#> tabagismeFumeuse:origineAfro-américaine              0,41891    
#> tabagismeFumeuse:origineAutre                        0,03806 *  
#> ---
#> Signif. codes:  0 '***' 0,001 '**' 0,01 '*' 0,05 '.' 0,1 ' ' 1
#> 
#> Residual standard error: 685,3 on 182 degrees of freedom
#> Multiple R-squared:  0,1446, Adjusted R-squared:  0,1164 
#> F-statistic: 5,127 on 6 and 182 DF,  p-value: 0,0000685

Test d’un sous-ensemble de coefficients

Si on veut tester l’interaction, on peut utiliser le test du rapport de vraisemblance, en comparant le modèle complet (avec l’interaction) et le modèle réduit (sans l’interaction) :

anova(modele_reduit, modele_complet)
Res.Df RSS Df Sum of Sq F Pr(>F)
184 87554493 NA NA NA NA
182 85471545 2 2082948 2,217677 0,1117953

On peut aussi utiliser la librairie car, qui permet notamment de tester toutes les variables d’un modèle sans avoir besoin de créer un modèle réduit ! Le code suivant teste chaque variable, avec un test du rapport de vraisemblance (si test.statistic = "LR") ou un test de Fisher partiel (si test.statistic = "F", par défaut) :

# type = "II" de façon générale
# type = "III" si on a des interactions dans le modèle
car::Anova(modele_complet, type = "III", test.statistic = "LR")
Sum Sq Df F value Pr(>F)
(Intercept) 71692213,785 1 152,6587938 0,0000000
age 6265,446 1 0,0133414 0,9081722
tabagisme 8258535,619 1 17,5854255 0,0000428
origine 9239265,867 2 9,8368784 0,0000878
tabagisme:origine 2082948,328 2 2,2176772 0,1117953
Residuals 85471544,628 182 NA NA

Prédire des valeurs

Enfin, pour prédire une valeur à l’aide du modèle de régression, on doit créer un nouveau jeu de données avec les valeurs des variables explicatives, puis utiliser la fonction predict().

Dans l’exemple ci-dessous, on prédit la valeur moyenne du poids de naissance du bébé chez une femme caucasienne non-fumeuse de 20 ans, et chez une femme afro-américaine fumeuse de 30 ans.

# Valeur moyenne prévue et IC pour de nouveaux profils
nouveaux_profils <- data.frame(
    age = c(20, 30),
    tabagisme = factor(c("Non-fumeuse", "Fumeuse"), levels = levels(bw$tabagisme)),
    origine = factor(c("Caucasienne", "Afro-américaine"), levels = levels(bw$origine))
)

predict(
    modele_complet,
    newdata = nouveaux_profils,
    interval = "confidence"
)

On prédit un poids moyen de naissance de 3,325 kg pour la femme caucasienne non-fumeuse de 20 ans, et de 2,475 kg pour la femme afro-américaine fumeuse de 30 ans.

Diagnostics

Pour le cours, nous avons codé les graphiques diagnostiques à la main, et on n’a pas utilisé le package performance. Ainsi, nous avons obscurci certaines choses à vérifier, afin de ne pas rendre plus lourd que nécessaire le cours.

En pratique, le plus simple est d’utiliser le package performance, et nous allons brièvement expliquer ce qu’il faut retenir des graphiques. Côté R, on écrit simplement :

performance::check_model(modele_complet)

Le modèle étudié est un modèle linéaire multiple : poids_naissance ~ age + tabagisme * origine. Les diagnostics portent sur les résidus après ajustement simultané sur l’âge, le tabagisme, l’origine maternelle et leur interaction. On recherche surtout des motifs systématiques, pas un alignement parfait des points.

  1. Posterior Predictive Check (distribution de la réponse). Ce panneau compare la distribution observée (courbe verte) à des distributions simulées par le modèle (courbes bleues). Leur recouvrement et leur forme sont proches : le modèle reproduit raisonnablement la distribution globale des poids de naissance.

Entre nous, vous pouvez l’oublier …

  1. Linéarité. La courbe des résidus en fonction des valeurs ajustées devrait rester horizontale autour de zéro. Sa forme apparente entre 2 800 et 3 300 g vient de l’absence de valeurs dans cet intervalle et d’un problème d’interpolation dans R ; la linéarité semble donc vérifiée.

  2. Homogénéité de la variance. La racine de la valeur absolue des résidus standardisés devrait être horizontale, avec une dispersion comparable. La courbe est plus haute entre 2 800 et 3 300 g, mais la dispersion au sein des blocs reste similaire : l’homogénéité paraît raisonnable.

  3. Observations influentes. Le levier et le résidu standardisé repèrent les observations atypiques ; les pointillés indiquent les niveaux de distance de Cook. La plupart des points restent dans les contours, sans observation clairement très influente.

  4. Colinéarité. Le VIF (Variance Inflation Factor) détecte une colinéarité potentielle, ce qui peut gonfler fortement les coefficients et leurs erreurs-types. Un VIF < 5 est acceptable, 5-10 indique une colinéarité modérée et > 10 une colinéarité forte. Ici, aucun problème n’est détecté.

    Note

    On ne l’a pas abordé dans le cours (il est déjà assez chargé), mais il ne faut jamais avoir de colinéarité quand on estime un modèle de régression.

    En bref, la colinéarité apparaît lorsque deux variables explicatives contiennent une information très redondante. Par exemple, si on essaie d’estimer Y ~ age + 2*age, age et 2*age sont exactement redondantes : le modèle ne pourra pas séparer leurs effets et affichera un message d’erreur. Sans être dans cet extrême, votre modèle peut, à tout moment, présenter une redondance qui rendra les estimations plus instables (coefficient, erreur-type).

    Le VIF permet alors de mesurer l’inflation de variance : il vaut 1 en l’absence de redondance et augmente lorsque l’effet propre d’une variable devient difficile à distinguer de celui des autres.

  5. Normalité des résidus. Le QQ-plot, horizontal ici, devrait suivre la ligne de référence. Les écarts vers le bas dans les queues signalent quelques poids observés supérieurs aux prédictions, mais ne suffisent pas à conclure à une violation nette de la normalité.

Dans l’ensemble, le modèle semble raisonnablement bien ajusté aux données.

Diagnostics de modèles

  • performance pour des diagnostics généraux de modèles

Bonus - Forest plots avec ggstats

Pour finir, on voulait présenter le package ggstats pour construire des forest plots.

Un modèle

ggstats::ggcoef_model(modele_ajuste)

On peut monter en complexité

Si on y passe du temps, voici ce qu’on pourrait obtenir à la fin éventuellement :

formater_estimation <- function(x) {
    valeurs <- scales::label_number(accuracy = 2, decimal.mark = ",")(x)
    valeurs[!is.na(x) & x == 0] <- "Référence"
    valeurs
}

ggstats::ggcoef_table(
    model = modele_ajuste,
    conf.level = 0.95,
    variable_labels = c(
        age = "Âge maternel (+1 an)",
        poids_mere_lb = "Poids maternel (livres)",
        tabagisme = "Tabagisme maternel",
        origine = "Origine maternelle"
    ),
    table_stat = c("estimate", "ci", "p.value"),
    table_header = c("Estimation", "IC 95 %", "p-valeur"),
    table_stat_label = list(
        estimate = formater_estimation,
        conf.low = scales::label_number(accuracy = 2, decimal.mark = ","),
        conf.high = scales::label_number(accuracy = 2, decimal.mark = ","),
        p.value = scales::label_pvalue(accuracy = 0.001, decimal.mark = ",")
    ),
    ci_pattern = "[{conf.low} ; {conf.high}]",
    stripped_rows = FALSE # si TRUE, on a un fond gris une fois sur deux (défaut)
) &
    ggplot2::theme(panel.border = ggplot2::element_blank()) &
    ggplot2::labs(x = "Coefficient (IC 95%)")

Et rien ne nous empêche d’avoir sur la même figure, les résultats de l’analyse univariée et de l’analyse ajustée :

modeles <- list(
    "Univariée" = lm(poids_naissance ~ age, data = bw),
    "Univariée" = lm(poids_naissance ~ tabagisme, data = bw),
    "Univariée" = lm(poids_naissance ~ origine, data = bw),
    "Univariée" = lm(poids_naissance ~ poids_mere_lb, data = bw),
    "Multiple" = modele_ajuste
)

ggstats::ggcoef_compare(
    models = modeles,
    type = "table",
    variable_labels = c(
        age = "Âge maternel (+1 an)",
        poids_mere_lb = "Poids maternel (livres)",
        tabagisme = "Tabagisme maternel",
        origine = "Origine maternelle"
    ),
    table_stat = c("estimate", "ci", "p.value"),
    table_header = c("Estimation", "IC 95 %", "p-valeur"),
    table_stat_label = list(
        estimate = formater_estimation,
        conf.low = scales::label_number(accuracy = 2, decimal.mark = ","),
        conf.high = scales::label_number(accuracy = 2, decimal.mark = ","),
        p.value = scales::label_pvalue(accuracy = 0.001, decimal.mark = ",")
    ),
    ci_pattern = "[{conf.low} ; {conf.high}]"
) &
    ggplot2::theme(panel.border = ggplot2::element_blank()) &
    ggplot2::labs(x = "Coefficient (IC 95%)")