--- title: "Multiple imputation with vimpute: pooling, tuning and diagnostics" author: "Matthias Templ" date: "`r Sys.Date()`" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Multiple imputation with vimpute: pooling, tuning and diagnostics} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 4.5) run_mice <- requireNamespace("mice", quietly = TRUE) ``` This vignette walks through a complete multiple-imputation workflow with `vimpute()` — every chunk below is executed: simulate missingness with a known mechanism, impute multiply, check convergence and calibration, pool with Rubin's rules, and validate against the truth. ```{r, message = FALSE} library(VIM) set.seed(2026) data(sleep, package = "VIM") truth <- na.omit(sleep[, c("BodyWgt", "BrainWgt", "NonD", "Sleep", "Span", "Gest")]) truth <- as.data.frame(scale(truth)) # common scale keeps the example compact nrow(truth) ``` ## Simulate missingness with a known mechanism `makeMissing()` generates MCAR/MAR/MNAR missingness in complete data — here MAR: the probability that `Sleep` and `Span` go missing grows with the other (observed) variables. The returned `"where"` attribute marks the amputed cells, so the truth stays available for validation. ```{r} amp <- makeMissing(truth, prop = 0.25, mechanism = "MAR", vars = c("Sleep", "Span"), seed = 1) colSums(is.na(amp)) ``` ## Multiple imputation `m = 5` imputations; with `m > 1`, each imputation refits its models on a bootstrap sample (`boot = TRUE` is the default for multiple imputation) and the default `uncert = "pmm"` draws from observed donor values, so the imputations differ between runs (a prerequisite for Rubin's rules). Per-variable settings use the spec interface; three sequential iterations give the convergence chains something to show. ```{r} mi <- vimpute(amp, spec = list(.default = vs_ranger(num.trees = 100)), m = 5, sequential = TRUE, nseq = 3, seed = 7, verbose = FALSE) mi ``` `print()` already answers the practitioner's first question — *can I trust this?* — with a per-variable model-quality metric (NRMSE, out-of-bag for ranger; PFC for factors). ## Convergence and distribution diagnostics ```{r, fig.height = 5.5} plot(mi) # chains: mean/sd of the imputed values per iteration ``` ```{r} plot(mi, "density") # observed (blue, bold) vs per-imputation imputed (red) ``` ## Pooling with Rubin's rules `with()` fits a model on each completed dataset and returns a mice-compatible `mira`, so the standard pipeline applies unchanged. ```{r, eval = run_mice} fits <- with(mi, lm(Sleep ~ BodyWgt + Span)) pooled <- mice::pool(fits) summary(pooled) ``` Alternatively, convert the whole object: `vim_as_mids(mi)` yields a genuine `mice::mids` for any downstream mice infrastructure. ```{r, eval = run_mice} mids <- vim_as_mids(mi) class(mids) ``` ## Hyperparameter tuning inside the imputation Tuning is controlled per variable (spec) and per call (`tune_control`); with `m > 1` the tuner runs once and all imputations share its parameters. The tuning log records what was chosen. ```{r} mi_tuned <- vimpute(amp, spec = list(Sleep = vs_ranger(num.trees = 100, tune = TRUE), .default = vs_ranger(num.trees = 100)), tune_control = vimpute_tune_control(budget = 4, folds = 3), m = 2, sequential = FALSE, seed = 7, verbose = FALSE) tl <- mi_tuned$tuning_log tail(tl, 1)[[1]][c("variable", "tuned", "tuned_better", "n_evals", "folds")] ``` ## Calibration: overimputation `overimpute()` treats the *observed* cells of a variable as missing (fold by fold), imputes them multiply, and compares observed values with the imputed intervals — a model-agnostic calibration check that needs no ground truth. ```{r} ov <- overimpute(amp, "Sleep", spec = list(.default = vs_ranger(num.trees = 100)), draws = 5, folds = 3, sequential = FALSE, seed = 3) ov plot(ov) ``` ## Validation against the truth Because the missingness was simulated, the imputations can be scored against the true values — the loop `makeMissing()` → `vimpute()` → `evaluation()` that any benchmark study needs. ```{r} completed <- vim_complete(mi, 1) evaluation(truth, completed, where = attr(amp, "where")) ``` ## A note on assumptions Like all conditional imputation, `vimpute()` assumes MAR (which includes MCAR). Under MNAR — missingness driven by the unobserved values themselves — estimates can be biased and no imputation method can repair this from the observed data alone; `makeMissing(mechanism = "MNAR")` supports exactly the sensitivity simulations such situations call for.