--- title: "Causal Inference" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Causal Inference} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r knitr-opts, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` ```{r setup} library(deli) ``` ## Overview deli provides estimating equations for several causal inference methods. A key advantage of the M-estimation framework is that all nuisance model parameters (e.g., propensity scores, outcome models) are estimated jointly, so the sandwich variance correctly propagates uncertainty from all stages. This vignette demonstrates: - G-formula (g-computation / standardization) - Inverse probability weighting (IPW) - Augmented IPW (AIPW / doubly robust) ## Simulated data We simulate data with a binary treatment `A`, a confounder `W`, and a continuous outcome `Y`: ```{r simulate-data} set.seed(42) n <- 1000 W1 <- rnorm(n) W2 <- rbinom(n, 1, 0.4) A <- rbinom(n, 1, plogis(-0.5 + 0.5 * W1 + 0.3 * W2)) # True ATE = 1.5 Y <- 2 + 1.5 * A + W1 - 0.5 * W2 + rnorm(n) ``` ## G-formula (g-computation) The g-formula estimates the average causal effect by fitting an outcome model and standardizing (averaging predictions over the covariate distribution): ```{r gformula} X <- cbind(1, A, W1, W2) # Observed design matrix X1 <- cbind(1, 1, W1, W2) # Counterfactual: all treated X0 <- cbind(1, 0, W1, W2) # Counterfactual: all untreated psi_gformula <- function(theta) { ee_gformula(theta, y = Y, X = X, X1 = X1, X0 = X0) } # theta: ACE, E[Y(1)], E[Y(0)], beta0, beta1, beta2, beta3 m_gformula <- m_estimate( stacked_equations = psi_gformula, init = c(0, 0, 0, 0, 0, 0, 0) ) # ACE estimate (true = 1.5) m_gformula@theta[1] # With confidence interval summary(m_gformula) ``` The first parameter is the average causal effect (ACE). ## Inverse probability weighting (IPW) IPW reweights the observed data by the inverse of the treatment probability (propensity score): ```{r ipw} W_ps <- cbind(1, W1, W2) # Propensity score design matrix psi_ipw <- function(theta) { ee_ipw(theta, y = Y, A = A, W = W_ps) } # theta: ACE, E[Y(1)], E[Y(0)], alpha0, alpha1, alpha2 m_ipw <- m_estimate(stacked_equations = psi_ipw, init = c(0, 0, 0, 0, 0, 0)) # ACE m_ipw@theta[1] summary(m_ipw) ``` ### Truncating propensity scores Propensity scores near zero or one produce very large weights, and a handful of large weights can dominate an IPW estimate. `truncate` clips the fitted scores to a range before they are inverted. Whether that changes anything depends on the scores themselves, so look at them first: ```{r ps-range} # Propensity scores implied by the alphas in the fitted stack ps <- plogis(drop(W_ps %*% m_ipw@theta[4:6])) range(ps) # How many scores a tight c(0.3, 0.7) range would clip, by tail c(lower = sum(ps < 0.3), upper = sum(ps > 0.7)) ``` Nothing here is close to zero or one, so a conventional range such as `c(0.1, 0.9)` would leave every score untouched and the fit unchanged. A deliberately tight range shows what truncation does. `c(0.3, 0.7)` clips 166 of the 1000 scores, all but nine of them in the lower tail: ```{r ipw-truncate} psi_ipw_trunc <- function(theta) { ee_ipw(theta, y = Y, A = A, W = W_ps, truncate = c(0.3, 0.7)) } m_ipw_trunc <- m_estimate( stacked_equations = psi_ipw_trunc, init = c(0, 0, 0, 0, 0, 0) ) summary(m_ipw_trunc) ``` Both the ACE and its standard error move relative to the untruncated fit: ```{r compare-truncation} data.frame( fit = c("Untruncated", "Truncated"), estimate = round(c(m_ipw@theta[[1]], m_ipw_trunc@theta[[1]]), 4), std_err = round( sqrt(c(vcov(m_ipw)[1, 1], vcov(m_ipw_trunc)[1, 1])), 4 ) ) ``` Truncation is usually motivated as a variance reduction, so the rise in the standard error is worth dwelling on. Clipping pays for itself only when the weights it caps are genuinely extreme, which is not the case here. It also costs precision that joint estimation would otherwise supply: a clipped score no longer moves with the propensity score parameters, so the sandwich variance credits less of the gain from estimating those parameters rather than treating them as known. Compare a truncated fit against the untruncated one instead of reaching for a range by default. ## Augmented IPW (doubly robust) AIPW combines outcome modeling and IPW. It is "doubly robust": the estimate is consistent if either the outcome model or the propensity score model is correctly specified. It reuses both sets of design matrices built above: ```{r aipw} psi_aipw <- function(theta) { ee_aipw(theta, y = Y, A = A, W = W_ps, # Propensity score model X = X, X1 = X1, X0 = X0) # Outcome model } # theta: ACE, E[Y(1)], E[Y(0)], alpha (3), beta (4) m_aipw <- m_estimate( stacked_equations = psi_aipw, init = c(0, 0, 0, rep(0, 3), rep(0, 4)) ) summary(m_aipw) ``` ## Comparing methods The g-formula, IPW, and AIPW fits above all target the same average causal effect. Both the outcome model and the propensity score model are correctly specified in this simulation, so the estimates agree closely with one another and with the true value: ```{r compare-methods} data.frame( method = c("G-formula", "IPW", "AIPW"), estimate = round( c(m_gformula@theta[[1]], m_ipw@theta[[1]], m_aipw@theta[[1]]), 3 ), true_ate = 1.5 ) ``` ## Further reading - `?ee_ipw_msm`: Marginal structural models with IPW - `?ee_gestimation_snmm`: G-estimation for structural nested mean models - `?ee_iv_causal`: Instrumental variable estimation - `?ee_2sls`: Two-stage least squares - `?ee_mean_sensitivity_analysis`: Sensitivity analysis for unmeasured confounding