--- title: "Validating multiple-imputation properness: a coverage simulation" author: "Matthias Templ" date: "2026-08-28" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Validating multiple-imputation properness: a coverage simulation} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- Multiple imputation is *proper* when Rubin's rules give pooled standard errors that reflect the real uncertainty — so that nominal 95% confidence intervals cover the truth about 95% of the time. This vignette validates `vimpute()`'s MI behaviour with a known-truth simulation and, just as importantly, shows what goes wrong when a variability source is missing. The chunks below were run when the vignette was precomputed (`vignettes/precompute.R` in the source repository; the code is shown unchanged and runs as is) with ``` r NSIM <- 12 # simulation replications per configuration -- demo scale! N <- 100 # observations M <- 5 # imputations ``` Demo scale means large Monte-Carlo error (a single coverage estimate has a standard error of about 6 percentage points at these settings); the *pooled standard errors* are stable much earlier and carry the qualitative message. `NSIM <- 500`, `M <- 20` (identical code) is the paper-scale setting. ## Design Linear truth, MAR missingness in the response driven by the observed covariates: ``` r library(VIM) set.seed(11) beta_x <- 2 # the estimand sim_data <- function(n) { x <- rnorm(n); z <- rnorm(n) data.frame(x = x, z = z, y = 1 + beta_x * x - z + rnorm(n)) } ``` Four configurations of `vimpute(method = "robust", m = M)`: * **default (boot + pmm)**: bootstrap refits *plus* stochastic donor draws — VIM's default for multiple imputation since 7.3.0 (`boot = TRUE` whenever `m > 1`); * **pmm, no boot**: donor draws with a single model fit (`boot = FALSE`) — the single-fit variant, kept for comparison (before 7.3.0 the `m > 1` default produced identical conditional-mean imputations); * **boot + normalerror**: bootstrap parameter uncertainty *plus* residual noise — the textbook-proper combination; * **boot only, uncert = "none"**: parameter uncertainty without residual variability — *deliberately improper*. `vimpute()` warns about exactly this configuration: ``` r w <- tryCatch( vimpute(sim_data(60) |> transform(y = replace(y, 1:15, NA)), method = "robust", m = 2, boot = TRUE, uncert = "none", sequential = FALSE, verbose = FALSE, seed = 1), warning = function(cond) conditionMessage(cond)) cat(strwrap(w, 70), sep = "\n") #> m > 1 with boot = TRUE but uncert = 'none' and no PMM: imputations #> use conditional means only, so the between-imputation variance is #> underestimated and pooled standard errors will be too small. Add a #> residual-noise mechanism (uncert = 'normalerror' or 'resid') or PMM #> for proper multiple imputation. ``` ## The simulation ``` r configs <- list( "default (boot + pmm)" = list(boot = TRUE, uncert = "pmm"), "pmm, no boot" = list(boot = FALSE, uncert = "pmm"), "boot + normalerror" = list(boot = TRUE, uncert = "normalerror"), "boot, uncert none" = list(boot = TRUE, uncert = "none") ) sim_once <- function(cfg, seed) { dat <- sim_data(N) amp <- makeMissing(dat, prop = 0.3, mechanism = "MAR", vars = "y", seed = seed) mi <- suppressWarnings( vimpute(amp, method = "robust", m = M, boot = cfg$boot, uncert = cfg$uncert, sequential = FALSE, verbose = FALSE)) pooled <- summary(mice::pool(with(mi, lm(y ~ x + z))), conf.int = TRUE) row <- pooled[pooled$term == "x", ] c(est = row$estimate, se = row$std.error, cover = as.numeric(row$`2.5 %` <= beta_x & beta_x <= row$`97.5 %`)) } results <- lapply(names(configs), function(cf) { runs <- t(vapply(seq_len(NSIM), function(i) sim_once(configs[[cf]], i), numeric(3))) data.frame(config = cf, mean_pooled_se = mean(runs[, "se"]), sd_estimates = sd(runs[, "est"]), coverage = mean(runs[, "cover"])) }) results <- do.call(rbind, results) ``` ``` r knitr::kable(results, digits = 3, row.names = FALSE, caption = sprintf( "Pooled inference for the x-coefficient over %d replications (demo scale). A calibrated method has mean pooled SE close to the empirical SD of the estimates and coverage near 0.95.", NSIM)) ``` Table: Pooled inference for the x-coefficient over 12 replications (demo scale). A calibrated method has mean pooled SE close to the empirical SD of the estimates and coverage near 0.95. |config | mean_pooled_se| sd_estimates| coverage| |:--------------------|--------------:|------------:|--------:| |default (boot + pmm) | 0.129| 0.093| 1.00| |pmm, no boot | 0.120| 0.138| 1.00| |boot + normalerror | 0.130| 0.117| 1.00| |boot, uncert none | 0.093| 0.136| 0.75| ## Reading the table The diagnostic comparison is **mean pooled SE vs the empirical SD of the estimates**: for calibrated inference they agree, and coverage lands near the nominal level. The deliberately improper configuration (bootstrap only, no residual noise) shows the audit-documented failure: its pooled SE is visibly *smaller* than the other configurations and than its own sampling variability — anti-conservative intervals whose coverage collapses at paper-scale `NSIM`. The stochastic configurations — the `boot` + `pmm` default, `pmm` without bootstrap, and the textbook `boot + normalerror` combination — produce pooled SEs of the right size, with the bootstrap default adding the model-uncertainty component on top of the donor draws. At `NSIM <- 500`, `M <- 20`, `N <- 300` the same code reproduces the paper's coverage table; only the three constants at the top change.