The native engines: DAG to publication table without leaving rmorie

rmorie implements every statistical algorithm it exposes natively — this vignette walks the whole pipeline on one simulated study using only library(rmorie). No MatchIt, no survey, no DoubleML, no dagitty, no fixest: each step below runs on an engine that lives in this package and is cross-validated against the reference implementation in tests/cross/.

library(rmorie)

The data

A simulated observational study: three confounders drive both a binary treatment and a continuous outcome, with a true effect of 0.9.

n <- 600
age  <- rnorm(n); educ <- rnorm(n); re74 <- rnorm(n)
treat <- rbinom(n, 1, plogis(0.4 * age + 0.3 * educ + 0.3 * re74))
re78  <- 1 + 0.9 * treat + 0.5 * age + 0.4 * educ + 0.6 * re74 +
  rnorm(n)
d <- data.frame(age, educ, re74, treat, re78)

0. Guard the categories first (module 25)

Before any model: audit the categorical columns. Silent category-mapping errors — numeric codes imported without their labels, positional recodes, alphabetical releveling — have corrupted published disparity analyses for years at a time. rmorie makes them structurally impossible:

grp_codes <- sample(c("1", "2"), n, replace = TRUE)   # an import hazard
morie_audit_categories(data.frame(group = factor(grp_codes)))
#> Categorical audit: 1 column(s)
#>   group            factor     2 level(s), reference '1'
#>     !! HAZARD: all labels numeric-looking (1,2...): likely imported CODES whose value labels were lost; as.numeric() on this column returns level INDICES, not data

grp <- morie_safe_recode(grp_codes, c("1" = "Control", "2" = "Program"))
morie_crosstab_verify(grp_codes, grp,
                      c("1" = "Control", "2" = "Program"))

1. The DAG (module 13)

dag <- morie_dag(
  edges = c("re74 -> treat", "re74 -> re78",
            "age -> treat",  "age -> re78",
            "educ -> treat", "educ -> re78",
            "treat -> re78"),
  exposure = "treat", outcome = "re78")
dag
#> morie causal DAG: 5 nodes, 7 edges
#>   exposure: treat  outcome: re78 
#>    re74 -> treat 
#>    re74 -> re78 
#>    age -> treat 
#>    age -> re78 
#>    educ -> treat 
#>    educ -> re78 
#>    treat -> re78

2. Identification

Backdoor identification runs Bayes-Ball d-separation with the canonical adjustment set (validated against dagitty in tests/cross/):

id <- morie_dag_identify(dag)
id$identified
#> [1] TRUE
id$adjustment_set
#> [1] "re74" "age"  "educ"

3. Matching on the identified set (module 1)

m <- morie_matching_nearest_neighbor(d, "treat", id$adjustment_set)
#> Warning: Fewer control units than treated units; not all treated units will get
#> a match.
nrow(m$match_pairs)
#> [1] 296
morie_matching_balance(m$matched_data, "treat",
                       id$adjustment_set)$max_smd
#> [1] 0.6107444

4. Estimation through the DAG (modules 10 + 13)

morie_dag_estimate() routes the identified set into the native estimators — here double machine learning (cross-fit ridge + logit nuisances, Neyman-orthogonal score):

ate <- morie_dag_estimate(dag, d, method = "backdoor.dml")
est <- if (!is.null(ate$estimate)) ate$estimate else ate$ate
c(estimate = est)
#>  estimate 
#> 0.9079067

5. Refutation (module 13)

DoWhy-style robustness checks, all native — a placebo treatment should erase the effect; subsets and a random common cause should leave it stable:

for (mth in c("placebo_treatment", "random_common_cause",
              "data_subset")) {
  rf <- morie_dag_refute(dag, d, mth, n_reps = 10L)
  cat(sprintf("%-22s passed: %s\n", mth, rf$passed))
}
#> placebo_treatment      passed: TRUE
#> random_common_cause    passed: TRUE
#> data_subset            passed: TRUE

6. The MRM pipeline and a publication table (module 24)

The MRM flagship composes several native estimators on the same specification, applies a multiple-testing correction across them, pools an inverse-variance consensus, and renders the table with rmorie’s own renderer (text, markdown, LaTeX, or HTML):

eff <- morie_mrm_estimate_causal_effect(
  d, "treat", "re78", id$adjustment_set,
  methods = c("matching", "ate", "dml"))
#> Warning: categorical coding hazards in covariates [re74, age, educ] - run
#> morie_audit_categories(data) and fix before trusting these estimates.
#> Warning: Fewer control units than treated units; not all treated units will get
#> a match.
cat(morie_mrm_report(eff, format = "markdown"), sep = "\n")
#> | Method                   | Estimate | SE     | 95% CI        | p (adj.) |
#> |--------------------------|----------|--------|---------------|----------|
#> | matching (rmorie native) | 1.47***  | 0.105  | [1.26, 1.67]  | 4.63e-38 |
#> | ipw ate (rmorie native)  | 0.906*** | 0.18   | [0.553, 1.26] | 4.85e-07 |
#> | dml plr (rmorie native)  | 0.908*** | 0.0881 | [0.735, 1.08] | 1.31e-24 |
#> Consensus (inverse-variance): 1.11 (SE 0.0631). holm correction; n = 600.

Where to look next

  • Every engine’s cross-validation lives in tests/cross/ (one file per reference package) and its speed in inst/benchmarks/.
  • The quasi-experimental family (DiD, event studies, synthetic control, RDD, ITS, IV) follows the same pattern — see ?morie_did_group_time_att, ?morie_synth_control, ?morie_rdd_sharp, ?morie_its, ?morie_iv_tsls.
  • morie_crypto_pqc_inventory() reports the post-quantum coverage of your build.