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
NSIM <- 12 # simulation replications per configuration -- demo scale!
N <- 100 # observations
M <- 5 # imputationsDemo 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.
Linear truth, MAR missingness in the response driven by the observed covariates:
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):
boot = TRUE whenever
m > 1);boot = FALSE) — the single-fit variant, kept for
comparison (before 7.3.0 the m > 1 default produced
identical conditional-mean imputations);vimpute() warns about exactly this configuration: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.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)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))| 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 |
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.