Inférence causale et G-computation

De la régression linéaire à l’effet causal moyen, avec application sur les données NHEFS

Auteur·rice

Clément Poupelin

Date de publication

7 novembre 2026

Packages
# install.packages(c("tidyverse", "broom", "boot", "ggdag", "causaldata",
#                    "marginaleffects", "stdReg", "gcomputation"))
# Version de développement de gcomputation :
# remotes::install_github("chupverse/gcomputation")

library(tidyverse)
library(broom)
library(boot)
library(ggdag)
library(causaldata)       # données NHEFS
library(marginaleffects)  # G-computation "généraliste"
library(stdReg)           # standardisation par régression
library(gcomputation)     # package dédié (chupverse)

theme_set(theme_minimal(base_size = 13))
set.seed(2026)

1 Introduction

Quand on ajuste lm(y ~ x + z), le coefficient de x répond à une question descriptive : parmi des individus ayant le même z, de combien diffère en moyenne y entre ceux qui ont x et ceux qui ont x + 1 ?

La question causale est différente : si j’intervenais pour faire passer x à x + 1 chez tout le monde, de combien y changerait-il en moyenne ?

Les deux questions n’ont la même réponse que sous des hypothèses qui ne sont pas des hypothèses statistiques (elles ne se lisent pas sur les résidus) mais des hypothèses sur le mécanisme qui a généré les données. L’inférence causale, c’est essentiellement trois choses :

  1. un langage pour écrire la question (les issues potentielles) ;
  2. des hypothèses d’identification pour relier cette question aux données observées ;
  3. des estimateurs — dont la G-computation — pour calculer la réponse.

Ce tutoriel suit cet ordre, puis applique le tout à un jeu de données réel.

2 De la régression à la causalité : trois simulations

L’avantage d’une simulation : on connaît l’effet causal, puisqu’on l’a écrit.

2.1 Le facteur de confusion

On simule un traitement x dont l’effet réel sur y vaut 2. Une variable z influence à la fois la probabilité d’être traité et l’issue.

Code
n <- 5000
z <- rnorm(n)
x <- rbinom(n, 1, plogis(1.5 * z))   # z pousse vers le traitement
y <- 2 * x + 3 * z + rnorm(n)        # effet causal de x : 2

m1 <- lm(y ~ x)
m2 <- lm(y ~ x + z)

rbind(
  "y ~ x"     = summary(m1)$coefficients["x", c("Estimate", "Std. Error")],
  "y ~ x + z" = summary(m2)$coefficients["x", c("Estimate", "Std. Error")]
)
          Estimate Std. Error
y ~ x     5.160527 0.07713444
y ~ x + z 1.991161 0.03346982

Le modèle y ~ x renvoie un coefficient très supérieur à 2 : les traités ont en moyenne un z plus élevé, et la comparaison brute mélange l’effet de x et celui de z. Le modèle n’est pas « faux » : il estime correctement la différence de moyennes entre traités et non traités. C’est cette différence qui n’est pas l’effet causal.

2.2 Le collisionneur : ajuster n’est pas toujours une bonne idée

On pourrait conclure « ajustons sur tout ce qu’on a ». Contre-exemple : ici x est randomisé (aucune confusion), et c est une conséquence commune de x et de y.

Code
x <- rbinom(n, 1, 0.5)
y <- 2 * x + rnorm(n)                # effet causal de x : 2
c <- x + y + rnorm(n)                # c est causé par x ET par y

m1 <- lm(y ~ x)
m2 <- lm(y ~ x + c)

rbind(
  "y ~ x"     = summary(m1)$coefficients["x", c("Estimate", "Std. Error")],
  "y ~ x + c" = summary(m2)$coefficients["x", c("Estimate", "Std. Error")]
)
          Estimate Std. Error
y ~ x     1.997231 0.02857458
y ~ x + c 0.518548 0.02853671

Cette fois c’est le modèle ajusté qui est biaisé, alors que son \(R^2\) est meilleur. Aucun critère de qualité d’ajustement (AIC, \(R^2\), validation croisée) ne permet de choisir entre les deux : ces critères mesurent la capacité à prédire y, pas à isoler l’effet de x.

ImportantCe qu’il faut retenir

Le choix des variables d’ajustement repose sur un modèle causal du problème (connaissance métier), pas sur les données. On ajuste sur les causes communes du traitement et de l’issue ; on n’ajuste ni sur les conséquences du traitement (médiateurs), ni sur les conséquences communes (collisionneurs).

2.3 Représenter ses hypothèses : le DAG

Un graphe orienté acyclique (DAG) rend ces hypothèses explicites. Une flèche signifie « peut causer ».

Code
dagify(
  Y ~ A + Z,
  A ~ Z,
  C ~ A + Y,
  exposure = "A", outcome = "Y",
  coords = list(x = c(A = 0, Y = 2, Z = 1, C = 1),
                y = c(A = 0, Y = 0, Z = 1, C = -1))
) |>
  ggdag() +
  theme_dag()

  • Z est un facteur de confusion : il ouvre un chemin non causal \(A \leftarrow Z \rightarrow Y\) (dit « porte dérobée »). Ajuster sur Z le ferme.
  • C est un collisionneur : le chemin \(A \rightarrow C \leftarrow Y\) est fermé par défaut. Ajuster sur C l’ouvre.

2.4 Même sans confusion, un coefficient n’est pas toujours l’effet recherché

Dernière simulation, avec une issue binaire et un traitement randomisé. Le coefficient de a dans le modèle logistique vaut 1 par construction.

Code
z <- rnorm(n)
a <- rbinom(n, 1, 0.5)                          # randomisé
y <- rbinom(n, 1, plogis(-1 + 1 * a + 2 * z))

fit <- glm(y ~ a + z, family = binomial)

# OR conditionnel (coefficient du modèle)
exp(coef(fit)["a"])
      a 
2.66721 
Code
# OR marginal : on prédit pour tout le monde sous a = 1 puis sous a = 0
p1 <- mean(predict(fit, newdata = data.frame(a = 1, z = z), type = "response"))
p0 <- mean(predict(fit, newdata = data.frame(a = 0, z = z), type = "response"))
c(risque_traite = p1, risque_non_traite = p0,
  difference_de_risque = p1 - p0,
  OR_marginal = (p1 / (1 - p1)) / (p0 / (1 - p0)))
       risque_traite    risque_non_traite difference_de_risque 
           0.4978684            0.3513602            0.1465082 
         OR_marginal 
           1.8304088 

L’odds ratio conditionnel (proche de \(e^1 \approx 2{,}7\)) et l’odds ratio marginal (nettement plus faible) diffèrent, alors qu’il n’y a ni confusion ni erreur de spécification. C’est la non-collapsibilité de l’odds ratio : l’effet « à z fixé » et l’effet « en population » sont deux quantités différentes. Il en va de même dès qu’un modèle linéaire contient une interaction traitement × covariable : il n’y a plus un coefficient qui résume l’effet.

Les deux lignes p1 / p0 ci-dessus sont déjà une G-computation. Le reste du tutoriel formalise ce qu’elles calculent et à quelles conditions le résultat est causal.

3 Cadre théorique

3.1 Issues potentielles

On note \(A \in \{0, 1\}\) le traitement, \(Y\) l’issue, \(Z\) les covariables. Pour chaque individu \(i\) on définit deux issues potentielles :

  • \(Y_i^{1}\) : l’issue qu’on observerait si \(i\) était traité ;
  • \(Y_i^{0}\) : l’issue qu’on observerait s’il ne l’était pas.

L’effet causal individuel est \(Y_i^{1} - Y_i^{0}\). On n’observe jamais qu’une seule des deux issues : c’est le problème fondamental de l’inférence causale. On vise donc des moyennes :

Estimand Définition Question
ATE \(E[Y^1 - Y^0]\) Que se passerait-il si on traitait toute la population plutôt que personne ?
ATT \(E[Y^1 - Y^0 \mid A = 1]\) Quel a été l’effet du traitement chez ceux qui l’ont reçu ?
ATU \(E[Y^1 - Y^0 \mid A = 0]\) Quel serait l’effet chez ceux qui ne l’ont pas reçu ?

Ces trois quantités ne coïncident que si l’effet est homogène.

3.2 Hypothèses d’identification

Pour exprimer \(E[Y^a]\) à partir de la loi des données observées, on a besoin de trois hypothèses.

  1. Cohérence (consistency) : si \(A_i = a\) alors \(Y_i = Y_i^{a}\). Le traitement est bien défini et l’issue d’un individu ne dépend pas du traitement des autres.
  2. Échangeabilité conditionnelle : \(Y^a \perp A \mid Z\) pour \(a = 0, 1\). À covariables égales, traités et non traités sont comparables — autrement dit, pas de facteur de confusion non mesuré. C’est l’hypothèse forte, et elle est invérifiable sur les données.
  3. Positivité : \(0 < P(A = 1 \mid Z = z) < 1\) pour tout \(z\) rencontré. Dans chaque profil de covariables, on doit pouvoir trouver des traités et des non traités.

Dans un essai randomisé, (2) et (3) sont garanties par le tirage au sort. Dans une étude observationnelle, ce sont des paris qu’on argumente.

3.3 La G-formule

Sous ces hypothèses :

\[ \begin{aligned} E[Y^a] &= E_Z\big[\,E[Y^a \mid Z]\,\big] \\ &= E_Z\big[\,E[Y^a \mid A = a, Z]\,\big] && \text{(échangeabilité, positivité)} \\ &= E_Z\big[\,E[Y \mid A = a, Z]\,\big] && \text{(cohérence)} \end{aligned} \]

Le membre de droite ne contient plus que des quantités observables. C’est la G-formule (Robins 1986), aussi appelée standardisation. Point essentiel : l’espérance extérieure est prise sur la loi marginale de \(Z\), pas sur la loi de \(Z\) dans le groupe \(A = a\). C’est toute la différence avec la moyenne brute du groupe traité, \(E[Y \mid A = 1] = E_{Z \mid A = 1}\big[E[Y \mid A = 1, Z]\big]\).

3.4 L’algorithme de G-computation

On remplace \(E[Y \mid A, Z]\) par un modèle de régression \(\hat{Q}(A, Z)\), appelé Q-model, et l’espérance sur \(Z\) par une moyenne empirique :

\[ \widehat{ATE} = \frac{1}{n} \sum_{i=1}^{n} \Big[ \hat{Q}(1, Z_i) - \hat{Q}(0, Z_i) \Big] \]

En pratique :

  1. ajuster le Q-model de \(Y\) sur \(A\) et \(Z\) (linéaire, logistique, Cox, pénalisé…) ;
  2. dupliquer le jeu de données en fixant \(A = 1\) pour tout le monde, prédire ;
  3. faire de même avec \(A = 0\) ;
  4. moyenner les deux séries de prédictions et les contraster (différence, ratio, OR) ;
  5. obtenir la variance par bootstrap (en répétant les étapes 1 à 4) ou par méthode delta.

Pour l’ATT (resp. l’ATU), on ne moyenne à l’étape 4 que sur les individus réellement traités (resp. non traités).

NoteLien avec ce que vous connaissez déjà
  • Si le Q-model est linéaire sans interaction avec \(A\), alors \(\hat{Q}(1, Z_i) - \hat{Q}(0, Z_i) = \hat\beta_A\) pour tout \(i\) : la G-computation redonne exactement le coefficient. Elle ne devient utile qu’avec des interactions, des liens non linéaires, ou des modèles de survie.
  • Les « moyennes marginales estimées » (emmeans, moyennes ajustées) relèvent de la même mécanique, à ceci près qu’elles moyennent souvent sur une grille équilibrée plutôt que sur la distribution observée de \(Z\).

3.5 Une quatrième hypothèse : le Q-model est bien spécifié

La G-computation repose entièrement sur le modèle de l’issue. S’il est mal spécifié (forme fonctionnelle, interactions oubliées), l’estimateur est biaisé, et il extrapole silencieusement dans les zones où la positivité est fragile. D’où l’intérêt de la comparer à une méthode qui modélise autre chose — la pondération par score de propension (IPW), qui modélise \(P(A \mid Z)\) — ou à un estimateur doublement robuste (AIPW, TMLE) qui combine les deux.

4 Application : arrêt du tabac et prise de poids (NHEFS)

4.1 Les données

On utilise les données NHEFS (NHANES I Epidemiologic Follow-up Study), distribuées librement et fil rouge du livre de référence Causal Inference: What If (Hernán et Robins 2020). Elles sont disponibles dans le package causaldata.

Question : quel est l’effet moyen de l’arrêt du tabac (qsmk, entre 1971 et 1982) sur la variation de poids en kg sur la même période (wt82_71) ?

Code
data("nhefs_complete", package = "causaldata")

num01 <- function(v) as.numeric(as.character(v))

nhefs <- nhefs_complete |>
  transmute(
    wt82_71,
    qsmk           = num01(qsmk),          # 1 = a arrêté de fumer
    sex            = num01(sex),           # 1 = femme
    race           = num01(race),          # 1 = noir ou autre
    age,
    education      = factor(education),    # 5 niveaux
    smokeintensity,                        # cigarettes / jour en 1971
    smokeyrs,                              # années de tabagisme
    exercise       = factor(exercise),     # 3 niveaux
    active         = factor(active),       # 3 niveaux
    wt71                                   # poids en 1971 (kg)
  ) |>
  drop_na() |>
  mutate(
    age2            = age^2,
    smokeintensity2 = smokeintensity^2,
    smokeyrs2       = smokeyrs^2,
    wt71_2          = wt71^2
  )

dim(nhefs)
[1] 1566   15
Code
count(nhefs, qsmk)

On attend 1 566 individus, dont 403 ayant arrêté de fumer.

Note

Les termes quadratiques sont créés comme colonnes plutôt qu’écrits I(age^2) dans la formule : tous les packages utilisés plus bas reçoivent ainsi exactement le même modèle, sans dépendre de la façon dont chacun interprète la formule. C’est sans conséquence ici car seul qsmk est modifié lors des prédictions.

4.2 Le modèle causal supposé

On reprend le jeu d’ajustement de Hernán et Robins : sexe, origine, âge, niveau d’études, intensité et durée du tabagisme, activité physique, et poids initial. Tous sont mesurés avant la décision d’arrêter et sont des causes plausibles à la fois de l’arrêt et de la prise de poids.

Code
dagify(
  prise_poids ~ arret + age + sexe + tabagisme + activite + poids71,
  arret       ~ age + sexe + tabagisme + activite + poids71,
  exposure = "arret", outcome = "prise_poids"
) |>
  ggdag(layout = "circle") +
  theme_dag()

4.3 Analyse naïve

Code
nhefs |>
  group_by(qsmk) |>
  summarise(n = n(), moyenne = mean(wt82_71), ecart_type = sd(wt82_71))
Code
fit_naif <- lm(wt82_71 ~ qsmk, data = nhefs)
tidy(fit_naif, conf.int = TRUE) |> filter(term == "qsmk")

La différence brute attendue est d’environ 2,5 kg (IC 95 % : 1,7 à 3,4). Mais les deux groupes ne se ressemblent pas :

Code
nhefs |>
  group_by(qsmk) |>
  summarise(across(c(age, sex, race, smokeintensity, smokeyrs, wt71), mean))

Ceux qui arrêtent sont notamment plus âgés et plus souvent des hommes : la comparaison brute n’est pas causale.

4.4 Vérifier la positivité

On estime le score de propension \(P(A = 1 \mid Z)\) uniquement pour regarder le recouvrement des deux groupes.

Code
fit_ps <- glm(
  qsmk ~ sex + race + age + age2 + education + smokeintensity + smokeintensity2 +
    smokeyrs + smokeyrs2 + exercise + active + wt71 + wt71_2,
  family = binomial, data = nhefs
)
nhefs$ps <- predict(fit_ps, type = "response")

ggplot(nhefs, aes(ps, fill = factor(qsmk))) +
  geom_density(alpha = 0.5) +
  labs(x = "Score de propension", y = "Densité", fill = "Arrêt du tabac")

Code
nhefs |> group_by(qsmk) |> summarise(min = min(ps), max = max(ps))

Les deux distributions doivent se recouvrir largement. Des scores collés à 0 ou à 1 dans un seul groupe signaleraient des profils pour lesquels le Q-model extrapole.

4.5 G-computation à la main

Étape 1 — le Q-model. On inclut une interaction entre l’arrêt et l’intensité du tabagisme : l’effet de l’arrêt peut dépendre de ce qu’on fumait.

Code
f_q <- wt82_71 ~ qsmk + sex + race + age + age2 + education +
  smokeintensity + smokeintensity2 + smokeyrs + smokeyrs2 +
  exercise + active + wt71 + wt71_2 + qsmk:smokeintensity

fit_q <- glm(f_q, family = gaussian, data = nhefs)
tidy(fit_q) |> filter(str_detect(term, "qsmk"))

Avec l’interaction, le coefficient de qsmk est l’effet chez quelqu’un qui fume 0 cigarette par jour : il n’est plus interprétable seul.

Étapes 2 à 4 — prédire sous chaque scénario et moyenner.

Code
d1 <- mutate(nhefs, qsmk = 1)   # tout le monde arrête
d0 <- mutate(nhefs, qsmk = 0)   # personne n'arrête

y1 <- predict(fit_q, newdata = d1)
y0 <- predict(fit_q, newdata = d0)

c(E_Y1 = mean(y1), E_Y0 = mean(y0), ATE = mean(y1 - y0))
    E_Y1     E_Y0      ATE 
5.273587 1.756213 3.517374 
Code
# ATT et ATU : même calcul, moyenné sur un sous-groupe
c(ATT = mean((y1 - y0)[nhefs$qsmk == 1]),
  ATU = mean((y1 - y0)[nhefs$qsmk == 0]))
     ATT      ATU 
3.427662 3.548461 

L’ATE attendu est d’environ 3,5 kg : c’est la valeur publiée par Hernán et Robins (chapitre 13) avec ce modèle, et un bon point de contrôle.

Étape 5 — intervalle de confiance par bootstrap. Il faut ré-ajuster le Q-model dans chaque rééchantillon.

Code
gcomp_ate <- function(data, indices) {
  d   <- data[indices, ]
  fit <- glm(f_q, family = gaussian, data = d)
  mean(predict(fit, newdata = mutate(d, qsmk = 1)) -
       predict(fit, newdata = mutate(d, qsmk = 0)))
}

boot_ate <- boot(nhefs, gcomp_ate, R = 1000)
ci_main  <- boot.ci(boot_ate, type = "perc")$percent[4:5]

res_main <- tibble(methode = "À la main + bootstrap",
                   estimate = boot_ate$t0,
                   conf.low = ci_main[1], conf.high = ci_main[2])
res_main

4.6 Avec marginaleffects

marginaleffects (Arel-Bundock et al. 2024) est aujourd’hui le package généraliste le plus utilisé pour ce calcul : il fonctionne avec la plupart des classes de modèles et calcule les erreurs standard par méthode delta.

Code
# Moyennes contrefactuelles E[Y^0] et E[Y^1]
avg_predictions(fit_q, variables = "qsmk")

 qsmk Estimate Std. Error    z Pr(>|z|)     S 2.5 % 97.5 %
    0     1.76      0.217  8.1   <0.001  50.7  1.33   2.18
    1     5.27      0.377 14.0   <0.001 144.9  4.53   6.01

Type: response
Code
# ATE
me_ate <- avg_comparisons(fit_q, variables = "qsmk")
me_ate

 Estimate Std. Error    z Pr(>|z|)    S 2.5 % 97.5 %
     3.52       0.44 7.99   <0.001 49.4  2.65   4.38

Term: qsmk
Type: response
Comparison: 1 - 0
Code
# ATT et ATU
avg_comparisons(fit_q, variables = "qsmk", newdata = subset(nhefs, qsmk == 1))

 Estimate Std. Error    z Pr(>|z|)    S 2.5 % 97.5 %
     3.43      0.439 7.81   <0.001 47.3  2.57   4.29

Term: qsmk
Type: response
Comparison: 1 - 0
Code
avg_comparisons(fit_q, variables = "qsmk", newdata = subset(nhefs, qsmk == 0))

 Estimate Std. Error    z Pr(>|z|)    S 2.5 % 97.5 %
     3.55      0.443 8.01   <0.001 49.6  2.68   4.42

Term: qsmk
Type: response
Comparison: 1 - 0
Code
res_me <- tibble(methode = "marginaleffects (delta)",
                 estimate = me_ate$estimate,
                 conf.low = me_ate$conf.low, conf.high = me_ate$conf.high)

Pour une variance robuste à l’hétéroscédasticité, ajouter vcov = "HC3" ; pour un bootstrap, enchaîner avec |> inferences(method = "boot", R = 1000).

Avec un modèle logistique, la même fonction donne la différence de risque par défaut, le risque relatif avec comparison = "ratio" et l’OR marginal avec comparison = "lnoravg", transform = "exp".

4.7 Avec stdReg

stdReg (Sjölander 2016) est le package historique de standardisation par régression (GLM et Cox). Son successeur stdReg2 propose une interface par formule ; la logique est identique.

Code
std <- stdGlm(fit = fit_q, data = as.data.frame(nhefs), X = "qsmk", x = c(0, 1))
summary(std)                                        # E[Y^0] et E[Y^1]

Formula: wt82_71 ~ qsmk + sex + race + age + age2 + education + smokeintensity + 
    smokeintensity2 + smokeyrs + smokeyrs2 + exercise + active + 
    wt71 + wt71_2 + qsmk:smokeintensity
Family: gaussian 
Link function: identity 
Exposure:  qsmk 

  Estimate Std. Error lower 0.95 upper 0.95
0     1.76      0.217       1.33       2.18
1     5.27      0.435       4.42       6.13
Code
s_std <- summary(std, contrast = "difference", reference = 0)
s_std

Formula: wt82_71 ~ qsmk + sex + race + age + age2 + education + smokeintensity + 
    smokeintensity2 + smokeyrs + smokeyrs2 + exercise + active + 
    wt71 + wt71_2 + qsmk:smokeintensity
Family: gaussian 
Link function: identity 
Exposure:  qsmk 
Reference level:  qsmk = 0 
Contrast:  difference 

  Estimate Std. Error lower 0.95 upper 0.95
0     0.00      0.000       0.00       0.00
1     3.52      0.478       2.58       4.45
Code
res_std <- tibble(methode = "stdReg (sandwich)",
                  estimate  = s_std$est.table[2, 1],
                  conf.low  = s_std$est.table[2, 3],
                  conf.high = s_std$est.table[2, 4])

4.8 Avec gcomputation

Le package gcomputation (organisation GitHub chupverse) est dédié à la méthode. Il propose une fonction par type d’issue — gc_continuous, gc_binary, gc_count, gc_times — et ajoute ce que les précédents n’ont pas nativement :

  • un Q-model pénalisé ("lasso", "ridge", "elasticnet") ou sélectionné ("aic", "bic"), en plus du modèle complet ("all") ;
  • le choix direct de l’estimand (effect = "ATE", "ATT", "ATU") ;
  • un bootstrap intégré, classique ("boot") ou avec validation croisée ("bcv") ;
  • l’imputation multiple (boot.mi = TRUE) et la transportabilité (transport()).

Contrainte : la variable de traitement doit être codée 0/1 et figurer dans la formule.

Code
gc_all <- gc_continuous(
  formula     = f_q,
  data        = as.data.frame(dplyr::select(nhefs, -ps)),
  group       = "qsmk",
  effect      = "ATE",
  model       = "all",        # Q-model complet, non pénalisé
  boot.type   = "boot",
  boot.number = 1000,
  progress    = FALSE,
  seed        = 2026
)

summary(gc_all, ci.type = "perc")
all model 
family: poisson(link = 'log')
Call:
wt82_71 ~ qsmk + sex + race + age + age2 + education + smokeintensity + 
    smokeintensity2 + smokeyrs + smokeyrs2 + exercise + active + 
    wt71 + wt71_2 + qsmk:smokeintensity

G-computation: 
      Estimate Lower CI Upper CI Std. Error z value Pr(>|z|)    
m0      1.7593   1.3391   2.1686     0.2184   8.055             
m1      5.2579   4.4259   6.1910     0.4463  11.780             
m1-m0   3.4986   2.5625   4.4880     0.4946   7.074 1.50e-12 ***
m1/m0   3.0364   2.2593   4.0553     0.4682   6.485 8.87e-11 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Unadjusted: 
      Estimate Lower CI Upper CI Std. Error z value Pr(>|z|)    
m0      1.9893   1.5921   2.3959     0.2175   9.147             
m1      4.5115   3.6785   5.3990     0.4433  10.176             
m1-m0   2.5222   1.5418   3.5101     0.4975   5.070 3.98e-07 ***
m1/m0   2.2962   1.6735   3.0595     0.3458   6.641 3.11e-11 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

n= 1566

Avec model = "all" et le même Q-model, l’estimation ponctuelle doit coïncider avec celle obtenue à la main ; seules les bornes de l’intervalle varient avec le tirage bootstrap.

On peut ensuite laisser un lasso choisir parmi un modèle plus riche, où toutes les covariables interagissent avec le traitement :

Code
f_riche <- wt82_71 ~ qsmk * (sex + race + age + age2 + education +
  smokeintensity + smokeintensity2 + smokeyrs + smokeyrs2 +
  exercise + active + wt71 + wt71_2)

gc_lasso <- gc_continuous(
  formula     = f_riche,
  data        = as.data.frame(dplyr::select(nhefs, -ps)),
  group       = "qsmk",
  effect      = "ATE",
  model       = "lasso",
  cv          = 10,
  boot.type   = "bcv",
  boot.number = 500,
  boot.tune   = FALSE,       # lambda estimé une fois sur l'échantillon complet
  progress    = FALSE,
  seed        = 2026
)

summary(gc_lasso, ci.type = "perc")
model: lasso, tuning parameters: lambda=  0.1958
family: poisson(link = 'log')
Call:
wt82_71 ~ qsmk * (sex + race + age + age2 + education + smokeintensity + 
    smokeintensity2 + smokeyrs + smokeyrs2 + exercise + active + 
    wt71 + wt71_2)

G-computation: 
      Estimate Lower CI Upper CI Std. Error z value Pr(>|z|)    
m0      1.8094   1.3984   2.2722     0.2256   8.022             
m1      5.0921   4.2994   5.9930     0.4542  11.212             
m1-m0   3.2827   2.3328   4.2143     0.4824   6.804 1.01e-11 ***
m1/m0   2.8565   2.1163   3.7843     0.4291   6.656 2.80e-11 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Unadjusted: 
      Estimate Lower CI Upper CI Std. Error z value Pr(>|z|)    
m0      1.9890   1.5263   2.4065     0.2239   8.885             
m1      4.4947   3.6861   5.3707     0.4414  10.182             
m1-m0   2.5057   1.5487   3.4259     0.4855   5.161 2.46e-07 ***
m1/m0   2.2885   1.6799   2.9984     0.3416   6.699 2.10e-11 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

n= 1566
Code
plot(gc_lasso)               # graphique de calibration du Q-model

AvertissementSélection de variables et causalité

Un lasso sélectionne ce qui prédit bien \(Y\). Il peut donc écarter un facteur de confusion fortement lié au traitement mais faiblement à l’issue. La pénalisation sert à stabiliser un Q-model flexible, pas à remplacer le choix raisonné du jeu d’ajustement.

4.9 Contrôle croisé par pondération (IPW)

L’IPW modélise le traitement plutôt que l’issue. Si les deux approches concordent, le résultat ne tient pas à la spécification d’un seul modèle.

Code
nhefs <- nhefs |>
  mutate(w = if_else(qsmk == 1, 1 / ps, 1 / (1 - ps)))

ipw_ate <- function(data, indices) {
  d  <- data[indices, ]
  ps <- predict(glm(formula(fit_ps), family = binomial, data = d), type = "response")
  w  <- ifelse(d$qsmk == 1, 1 / ps, 1 / (1 - ps))
  weighted.mean(d$wt82_71[d$qsmk == 1], w[d$qsmk == 1]) -
    weighted.mean(d$wt82_71[d$qsmk == 0], w[d$qsmk == 0])
}

boot_ipw <- boot(nhefs, ipw_ate, R = 1000)
ci_ipw   <- boot.ci(boot_ipw, type = "perc")$percent[4:5]

res_ipw <- tibble(methode = "IPW + bootstrap",
                  estimate = boot_ipw$t0,
                  conf.low = ci_ipw[1], conf.high = ci_ipw[2])

4.10 Synthèse

Code
ci_naif <- tidy(fit_naif, conf.int = TRUE) |> filter(term == "qsmk")

resultats <- bind_rows(
  tibble(methode = "Différence brute", estimate = ci_naif$estimate,
         conf.low = ci_naif$conf.low, conf.high = ci_naif$conf.high),
  res_main, res_me, res_std, res_ipw
) |>
  mutate(methode = fct_rev(fct_inorder(methode)))

resultats
Code
ggplot(resultats, aes(estimate, methode)) +
  geom_pointrange(aes(xmin = conf.low, xmax = conf.high)) +
  labs(x = "Effet de l'arrêt du tabac sur la prise de poids (kg)", y = NULL)

Les résultats de gcomputation se lisent dans les summary() plus haut.

Lecture attendue. Les trois implémentations de la G-computation donnent la même estimation ponctuelle (elles partagent le Q-model) et des intervalles proches ; l’IPW tombe dans la même zone. L’effet ajusté, autour de 3,5 kg, est plus élevé que la différence brute : la confusion masquait ici une partie de l’effet, notamment parce que ceux qui arrêtent sont plus âgés et que les plus âgés prennent moins de poids.

Formulation. « Sous les hypothèses de cohérence, d’absence de confusion non mesurée conditionnellement aux covariables retenues, de positivité et de bonne spécification du modèle, si tous les individus avaient arrêté de fumer, la prise de poids moyenne sur dix ans aurait été supérieure d’environ 3,5 kg à celle observée si aucun n’avait arrêté. »

5 Limites et prolongements

  • Confusion non mesurée. Aucune méthode d’ajustement n’y répond. On peut en quantifier l’impact par une analyse de sensibilité (E-value, package EValue).
  • Sélection. nhefs_complete ne contient que les individus dont le poids de 1982 est connu. Si la sortie d’étude dépend du traitement et de l’issue, on conditionne sur un collisionneur ; What If traite ce point par pondération de censure.
  • Spécification du Q-model. Pour s’en protéger : estimateurs doublement robustes (AIPW via marginaleffects + WeightIt, TMLE via tmle ou lmtp), éventuellement avec apprentissage automatique (SuperLearner).
  • Issues de survie. gc_times et stdReg::stdCoxph standardisent des courbes de survie et donnent des différences de survie ou de RMST marginales, plus interprétables qu’un hazard ratio.
  • Traitements variant dans le temps. Avec des facteurs de confusion eux-mêmes affectés par le traitement passé, la régression classique échoue ; il faut la G-formule paramétrique complète (gfoRmula) ou lmtp.

6 Quel package choisir ?

Package Maturité Points forts Limites
marginaleffects Très utilisé, CRAN, article JSS Presque toutes les classes de modèles, méthode delta rapide, bootstrap, documentation abondante Ne construit pas le Q-model : sélection et pénalisation à gérer soi-même
stdReg / stdReg2 Établi, CRAN GLM et Cox, variance sandwich, courbes standardisées Périmètre de modèles plus étroit
gcomputation Récent, peu d’utilisateurs visibles (7 étoiles GitHub en octobre 2026) Tout-en-un : Q-model pénalisé, ATE/ATT/ATU, bootstrap, imputation multiple, survie, transportabilité Peu de recul extérieur ; API susceptible d’évoluer ; coût de calcul du bootstrap

Une stratégie raisonnable : produire le résultat principal avec un outil éprouvé et utiliser gcomputation pour ce qu’il apporte en propre (pénalisation, imputation, survie), en vérifiant comme ici que les deux concordent sur un modèle commun.

7 Session

Code
sessionInfo()
R version 4.4.2 (2024-10-31)
Platform: x86_64-pc-linux-gnu
Running under: Ubuntu 24.04.1 LTS

Matrix products: default
BLAS:   /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.12.0 
LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.12.0

locale:
 [1] LC_CTYPE=fr_FR.UTF-8       LC_NUMERIC=C              
 [3] LC_TIME=fr_FR.UTF-8        LC_COLLATE=fr_FR.UTF-8    
 [5] LC_MONETARY=fr_FR.UTF-8    LC_MESSAGES=fr_FR.UTF-8   
 [7] LC_PAPER=fr_FR.UTF-8       LC_NAME=C                 
 [9] LC_ADDRESS=C               LC_TELEPHONE=C            
[11] LC_MEASUREMENT=fr_FR.UTF-8 LC_IDENTIFICATION=C       

time zone: Europe/Paris
tzcode source: system (glibc)

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
 [1] gcomputation_0.34     mice_3.19.0           MASS_7.3-60.0.1      
 [4] glmnet_5.0            Matrix_1.6-5          hdnom_6.2.1          
 [7] survival_3.8-3        stdReg_3.4.2          marginaleffects_1.0.0
[10] causaldata_0.1.4      ggdag_0.2.13          boot_1.3-31          
[13] broom_1.0.13          lubridate_1.9.5       forcats_1.0.1        
[16] stringr_1.6.0         dplyr_1.2.1           purrr_1.2.2          
[19] readr_2.2.0           tidyr_1.3.2           tibble_3.3.1         
[22] ggplot2_4.0.3         tidyverse_2.0.0      

loaded via a namespace (and not attached):
 [1] tidyselect_1.2.1    viridisLite_0.4.3   farver_2.1.2       
 [4] viridis_0.6.5       S7_0.2.2            ggraph_2.2.2       
 [7] fastmap_1.2.0       tweenr_2.0.3        ncvreg_3.16.0      
[10] digest_0.6.39       rpart_4.1.24        timechange_0.4.0   
[13] lifecycle_1.0.5     magrittr_2.0.5      compiler_4.4.2     
[16] rlang_1.3.0         tools_4.4.2         igraph_2.3.3       
[19] yaml_2.3.12         data.table_1.18.4   knitr_1.51         
[22] labeling_0.4.3      graphlayouts_1.2.4  htmlwidgets_1.6.4  
[25] curl_7.1.0          dagitty_0.3-4       RColorBrewer_1.1-3 
[28] nleqslv_3.3.7       withr_3.0.2         numDeriv_2016.8-1.1
[31] polyclip_1.10-7     nnet_7.3-20         grid_4.4.2         
[34] jomo_2.7-6          scales_1.4.0        iterators_1.0.14   
[37] insight_1.5.2       cli_3.6.6           rmarkdown_2.31     
[40] reformulas_0.4.4    generics_0.1.4      otel_0.2.0         
[43] rstudioapi_0.19.0   tzdb_0.5.0          cachem_1.1.0       
[46] ggforce_0.5.0       minqa_1.2.8         splines_4.4.2      
[49] drgee_1.1.10-4      vctrs_0.7.3         V8_8.2.0           
[52] jsonlite_2.0.0      hms_1.1.4           ggrepel_0.9.8      
[55] mitml_0.4-5         foreach_1.5.2       glue_1.8.1         
[58] nloptr_2.2.1        pan_2.0             codetools_0.2-20   
[61] stringi_1.8.7       gtable_0.3.6        shape_1.4.6.1      
[64] lme4_2.0-1          pillar_1.11.1       htmltools_0.5.9    
[67] R6_2.6.1            Rdpack_2.6.6        tidygraph_1.3.1    
[70] penalized_0.9-53    evaluate_1.0.5      lattice_0.22-6     
[73] rbibutils_2.4.1     backports_1.5.1     memoise_2.0.1      
[76] Rcpp_1.1.2          checkmate_2.3.4     gridExtra_2.3.1    
[79] nlme_3.1-166        xfun_0.60           pkgconfig_2.0.3    

8 Références

Anders, Simon, et Wolfgang Huber. 2010. « Differential expression analysis for sequence count data ». Genome Biology 11 (10): R106. https://doi.org/10.1186/gb-2010-11-10-r106.
Arel-Bundock, Vincent, Noah Greifer, et Andrew Heiss. 2024. « How to interpret statistical models using marginaleffects for R and Python ». Journal of Statistical Software 111 (9): 1‑32. https://doi.org/10.18637/jss.v111.i09.
chupverse. 2026. gcomputation: an R package for estimating marginal effects using G-computation. https://github.com/chupverse/gcomputation.
Cox, David R. 1972. « Regression Models and Life-Tables ». Journal of the Royal Statistical Society: Series B (Methodological) 34 (2): 187‑202.
Hernán, Miguel A., et James M. Robins. 2020. Causal Inference: What If. Chapman & Hall/CRC. https://miguelhernan.org/whatifbook.
Kaplan, Edward L., et Paul Meier. 1958. « Nonparametric Estimation from Incomplete Observations ». Journal of the American Statistical Association 53 (282): 457‑81.
Kassambara, Alboukadel, Marcin Kosinski, et Przemyslaw Biecek. 2021. survminer: Drawing Survival Curves using ’ggplot2’. https://CRAN.R-project.org/package=survminer.
Kleinbaum, David G., et Mitchel Klein. 2012. Survival Analysis: A Self-Learning Text. 3rd éd. Springer.
Langley, Sarah R., et Manuel Mayr. 2015. « Comparative analysis of statistical methods used for detecting differential expression in label-free mass spectrometry proteomics ». Journal of Proteomics 129: 83‑92. https://doi.org/10.1016/j.jprot.2015.07.012.
Loprinzi, Charles L., John A. Laurie, H. Samuel Wieand, et al. 1994. « Prospective Evaluation of Prognostic Variables from Patient-Completed Questionnaires. North Central Cancer Treatment Group ». Journal of Clinical Oncology 12 (3): 601‑7.
Love, Michael I., Wolfgang Huber, et Simon Anders. 2014. « Moderated estimation of fold change and dispersion for RNA-seq data with DESeq2 ». Genome Biology 15 (12): 550. https://doi.org/10.1186/s13059-014-0550-8.
Ollivaud, Justine, Jean-Michel Galharret, et Nicolas Roussiau. 2023. « Spirituality, self-esteem and the mechanisms of social and temporal comparison ». Spiritual Psychology and Counseling 8 (1): 83‑102. https://doi.org/10.37898/spc.2023.8.1.187.
Ramus, Claire, Agnès Hovasse, Marlène Marcellin, et al. 2015. « Spiked proteomic standard dataset for testing label-free quantitative software and statistical methods ». Data in Brief 6: 286‑94. https://doi.org/10.1016/j.dib.2015.11.063.
Robins, James. 1986. « A new approach to causal inference in mortality studies with a sustained exposure period—application to control of the healthy worker survivor effect ». Mathematical Modelling 7 (9–12): 1393‑512. https://doi.org/10.1016/0270-0255(86)90088-6.
Sjölander, Arvid. 2016. « Regression standardization with the R package stdReg ». European Journal of Epidemiology 31 (6): 563‑74. https://doi.org/10.1007/s10654-016-0157-3.
Snowden, Jonathan M., Sherri Rose, et Kathleen M. Mortimer. 2011. « Implementation of G-computation on a simulated data set: demonstration of a causal inference technique ». American Journal of Epidemiology 173 (7): 731‑38. https://doi.org/10.1093/aje/kwq472.
Therneau, Terry M. 2024. A Package for Survival Analysis in R. https://CRAN.R-project.org/package=survival.
Therneau, Terry M., et Patricia M. Grambsch. 2000. Modeling Survival Data: Extending the Cox Model. Springer.
UCLA Office of Advanced Research Computing, Statistical Methods and Data Analytics. 2025. Introduction to Survival Analysis in R. https://stats.oarc.ucla.edu/r/seminars/survival_analysis_r/.
Vega Yu, Chien, Yafeng Zhu, Catherine Fenselau, Zhengyu Wu, et Jing Wang. 2020. « DEqMS: a method for accurate variance estimation in differential protein expression analysis ». Molecular & Cellular Proteomics 19 (6): 1047‑57. https://doi.org/10.1074/mcp.TIR119.001646.
Zabor, Emily C. 2021. Survival Analysis in R. https://www.emilyzabor.com/survival-analysis-in-r.html.