Multiverse and specification-curve analyses pool model specifications
that are causally incompatible. Some control sets identify the declared
causal effect; others condition on mediators or colliders and identify
nothing of the kind. dagmv disciplines the multiverse with a set of
candidate causal graphs (DAGs) that encode rival assumptions about the
contested controls. For each graph it determines which specifications
are admissible adjustment sets, and it decomposes the spread of the
multiverse into a within-DAG component (sampling and specification
noise) and a between-DAG component (structural uncertainty). The share
of the between-DAG component is reported as the structural share.
# development version from GitHub
remotes::install_github("sokubo/dagmv")
# or from a local checkout of this repository
# R CMD INSTALL .The package needs only base R (>= 4.1). ggplot2 (plots), MASS
(negative binomial engine) and fixest (panel engine) are optional.
library(dagmv)
# Simulated data: educ confounds union and wage, occ is caused by union,
# exper affects wage only
set.seed(1)
n <- 5000
educ <- rnorm(n)
exper <- rnorm(n)
union <- as.numeric(0.5 * educ + rnorm(n) > 0)
occ <- 0.6 * union + 0.4 * educ + rnorm(n)
wage <- 0.3 * union + 0.5 * occ + 0.4 * educ + 0.3 * exper + rnorm(n)
d <- data.frame(wage, union, occ, educ, exper)
# 1. Encode the disagreement as candidate graphs
g1 <- dag_parse("dag { occ -> union ; occ -> wage ; union -> wage ;
educ -> union ; educ -> wage ; exper -> wage }")
g2 <- dag_parse("dag { union -> occ ; occ -> wage ; union -> wage ;
educ -> union ; educ -> wage ; exper -> wage }")
# 2. What does each world say about each control?
ctrl <- c("occ", "educ", "exper")
mv_classify(g1, ctrl, exposure = "union", outcome = "wage")
mv_classify(g2, ctrl, exposure = "union", outcome = "wage")
# 3. Fit every specification once; map admissibility per world
fits <- mv_run(d, outcome = "wage", exposure = "union", controls = ctrl,
dags = list(G1 = g1, G2 = g2))
fits
# 4. Decompose: how much of the disagreement is structural?
mv_decompose(fits)
mv_plot(fits, true_effect = 0.6) # requires ggplot2Under g1, occ is a confounder and must be included; under g2 it is
a mediator and must be excluded; exper is optional in both. The two
worlds license disjoint specifications, and the decomposition
attributes the spread between them to the graph, not to specification
noise.
The DAG engine is written in base R: a parser for a subset of the
dagitty syntax, d-separation by moralization, the generalized
adjustment criterion (Shpitser, VanderWeele and Robins 2010; Perkovic
et al. 2018) and the associated forbidden set, with latent nodes.
mv_classify() labels each candidate control as required, forbidden or
optional under a graph; the labels summarise the family of admissible
subsets of the candidate pool, which mv_specs() returns in full.
mv_run() fits each control subset once (engines: lm, negative
binomial, fixest fixed effects) and records which subsets are
admissible under each graph, so that adding a graph costs no
estimation. mv_decompose() reports the Young and Holsteen (2017)
metrics per graph and pooled, the within/between-DAG variance split and
the structural share, under equal or user-supplied graph weights.
mv_plot() draws the per-graph distributions against the pooled one.
A candidate graph under which no subset of the candidate pool is
admissible is reported as not identified by adjustment, never dropped:
mv_classify() returns the role "not identified" with attribute
identified = FALSE; mv_run() and mv_decompose() keep the world
with n_specs = 0, exclude it from the decomposition and say so when
printed.
The test suite in tests/testthat/ covers back-door adjustment,
M-bias, butterfly bias, mediators and their descendants, instruments,
colliders and their descendants, a descendant of the exposure off the
causal path, latent-variable restrictions, the eighteen models of
Cinelli, Forney and Pearl (2024), a pair of graphs with identical role
labels but different admissible families, the parser, and the reporting
of worlds that are not identified. Randomised cross-checks on small
DAGs compare adjustment_valid() with dagitty::isAdjustmentSet()
(when dagitty is installed) and, without any additional package, with
path-enumeration d-separation and with exact population OLS bias in
random linear structural equation models. A plain-R battery without
testthat is in inst/battery/test-core.R. From a checkout:
testthat::test_local() # or R CMD checkcitation("dagmv")MIT; see LICENSE.