A general (non-OTIS) policy-evaluation entry point for double/debiased machine learning when observations are clustered – e.g. flights within an airspace corridor, students within a school, patients within a hospital. Standard DML standard errors assume independent observations and are anti-conservative under within-cluster correlation of the treatment or the errors. This cross-fits the AIPW (doubly-robust) score and computes a cluster-robust variance from the per-cluster score sums (Liang & Zeger 1986, one-way; Cameron, Gelbach & Miller 2011, up to two-way).
Usage
morie_dml_clustered(
data,
treatment,
outcome,
covariates,
cluster = NULL,
n_folds = 5L,
seed = 123L,
eps = 0.02,
ps = NULL
)Arguments
- data
A data frame.
- treatment
Binary treatment column name (0/1 or two-valued).
- outcome
Numeric outcome column name.
- covariates
Character vector of confounder column names.
- cluster
Cluster column name (one-way) or length-2 character vector (two-way).
NULLgives the i.i.d. (non-clustered) SE.- n_folds
Cross-fitting folds (default 5).
- seed
Integer seed (default 123).
- eps
Propensity clip bound in
[eps, 1-eps](default 0.02).- ps
Optional length-
nrow(data)vector of externally supplied propensity scores (e.g. from a mixed-effects / cluster-level model); when given it replaces the cross-fitted propensity.
Details
Nuisances are cross-fitted (Chernozhukov et al. 2018): a ridge-logistic
propensity and per-arm ordinary-least-squares outcome regressions, so the
AIPW point estimate is Neyman-orthogonal. Only the SE is cluster-aware; the
point estimate is the usual AIPW ATE. Add own-implemented mixed-effects
propensities via ps if a corridor random effect is needed.
References
Chernozhukov V, et al. (2018). Double/debiased machine learning. The Econometrics Journal 21(1), C1–C68. doi:10.1111/ectj.12097
Cameron AC, Gelbach JB, Miller DL (2011). Robust inference with multiway clustering. JBES 29(2), 238–249. doi:10.1198/jbes.2010.07136
Examples
set.seed(1)
G <- 40L; ng <- 10L; n <- G * ng
g <- rep(seq_len(G), each = ng)
u <- stats::rnorm(G)[g] # cluster effect
x <- stats::rnorm(n)
d <- stats::rbinom(n, 1, stats::plogis(0.5 * x + u))
y <- 2 * d + x + u + stats::rnorm(n) # true ATE = 2
df <- data.frame(y = y, d = d, x = x, corridor = g)
morie_dml_clustered(df, "d", "y", "x", cluster = "corridor")$ate
#> [1] 2.658798
