CausalModel provides causal inference estimators for three settings:
This vignette walks through basic usage of each.
The generate_data() function creates (Y, Z, X) with a
known treatment effect tau. Treatment assignment depends on
covariates through a logistic propensity model, so naive comparisons are
confounded.
Linear regression of Y on (Z, X) with HC0 robust standard errors:
Reweights outcomes by inverse propensity scores. Uses logistic regression by default:
Combines outcome modeling and propensity weighting — consistent if either model is correctly specified:
Nearest-neighbor matching with M matches per unit.
Variance estimation follows Abadie & Imbens (2006):
est_via_matching(obs, num_matches = 3)
#> Causal Model Estimation Result
#> ------------------------------
#> ATE: 10.043697
#> SE: 0.057497
#> z: 174.6809
#> p-value: 0.0000
#> 95% CI: [9.931004, 10.156390]Bias-adjusted matching corrects for residual imbalance in matched covariates:
For continuous treatments, DML with cross-fitting estimates the linear treatment effect. First, generate continuous treatment data:
data_c <- generate_data_continuous(N = 2000, k = 2, tau = 10)
obs_c <- observational(data_c$Y, data_c$Z, data_c$X)
est_via_dml(obs_c)
#> Causal Model Estimation Result
#> ------------------------------
#> ATE: 9.986547
#> SE: 0.021793
#> z: 458.2498
#> p-value: 0.0000
#> 95% CI: [9.943834, 10.029260]The estimate() generic dispatches to AIPW for binary
treatment and DML for continuous:
estimate(obs) # calls est_via_aipw
#> Causal Model Estimation Result
#> ------------------------------
#> ATE: 9.951175
#> SE: 0.052355
#> z: 190.0694
#> p-value: 0.0000
#> 95% CI: [9.848560, 10.053789]
estimate(obs_c) # calls est_via_dml
#> Causal Model Estimation Result
#> ------------------------------
#> ATE: 9.987542
#> SE: 0.021876
#> z: 456.5507
#> p-value: 0.0000
#> 95% CI: [9.944666, 10.030419]In a completely randomized design, treatment is independent of covariates. We simulate this directly:
N <- 1000
X <- matrix(rnorm(N * 2), ncol = 2)
tau <- 5
Z <- sample(c(rep(1, N/2), rep(0, N/2)))
Y <- tau * Z + X %*% c(1, -1) + rnorm(N)
exp_obj <- experimental(Y, Z, X)Adjusts for covariates via Y ~ 1 + Z + X + Z*X with HC0 robust SEs. Achieves efficiency gains when covariates predict the outcome:
Weighted average of within-stratum difference-in-means:
When units within clusters can affect each other’s outcomes, standard
estimators are biased. clustered() implements the framework
of Qu et al. (2021).
cl_obj <- clustered(
Y = cdata$Y,
Z = cdata$Z,
X = cdata$X,
cluster_labels = cdata$cluster_labels,
group_labels = cdata$group_labels,
ingroup_labels = cdata$ingroup_labels,
n_matches = 50
)
result <- est_via_aipw(cl_obj)
#> Warning in predict.lm(model, newdata = df): prediction from rank-deficient fit;
#> attr(*, "non-estim") has doubtful casesThe result is a list of group-specific estimates. Each element
contains beta_g (treatment effects at each neighborhood
configuration) and se (standard errors):
for (j in seq_along(result)) {
cat(sprintf("Group %d:\n", j - 1))
valid <- !is.nan(result[[j]]$beta_g) & result[[j]]$se > 0
df <- data.frame(
g_index = which(valid) - 1,
beta_g = round(result[[j]]$beta_g[valid], 3),
se = round(result[[j]]$se[valid], 3)
)
print(df, row.names = FALSE)
cat("\n")
}
#> Group 0:
#> g_index beta_g se
#> 0 1.261 0.448
#> 1 1.852 0.442
#> 3 1.043 0.197
#> 4 1.811 0.221
#> 6 1.161 0.221
#> 7 1.976 0.261
#> 9 -5.919 0.354
#> 10 0.829 0.409
#>
#> Group 1:
#> g_index beta_g se
#> 0 1.362 0.354
#> 1 1.777 0.247
#> 2 1.783 0.363
#> 3 0.566 0.290
#> 4 1.659 0.194
#> 5 2.355 0.274
#> 6 1.576 0.352
#> 7 1.829 0.225
#> 8 2.715 0.359You can plug in custom ML models by providing a list with
fit() and predict() (or
predict_proba() for classifiers).
rf_out <- cm_random_forest_regressor(ntree = 100)
rf_ps <- cm_random_forest_classifier(ntree = 100)
est_via_aipw(obs, outcome_model = rf_out, propensity_model = rf_ps)
#> Warning in fix_propensity(ps, obj$eps): Propensity scores had 115 values of 0
#> or 1, trimmed to [0.0001, 0.9999].
#> Causal Model Estimation Result
#> ------------------------------
#> ATE: 10.044085
#> SE: 0.026667
#> z: 376.6465
#> p-value: 0.0000
#> 95% CI: [9.991819, 10.096352]Any model wrapper that follows this protocol works:
my_ridge <- list(
fit = function(X, y) {
X <- as.matrix(X)
lambda <- 1.0
p <- ncol(X)
beta <- solve(crossprod(X) + lambda * diag(p), crossprod(X, y))
list(beta = beta)
},
predict = function(model, X) {
as.numeric(as.matrix(X) %*% model$beta)
}
)
est_via_aipw(obs, outcome_model = my_ridge)
#> Causal Model Estimation Result
#> ------------------------------
#> ATE: 10.071861
#> SE: 0.523981
#> z: 19.2218
#> p-value: 0.0000
#> 95% CI: [9.044877, 11.098845]