---
title: "Multiple imputation with vimpute: pooling, tuning and diagnostics"
author: "Matthias Templ"
date: "2026-09-02"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Multiple imputation with vimpute: pooling, tuning and diagnostics}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---



This vignette walks through a complete multiple-imputation workflow with
`vimpute()`: simulate missingness with a known mechanism, impute multiply,
check convergence and calibration, pool with Rubin's rules, and validate
against the truth. 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.


``` r
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)
#> [1] 42
```

## 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))
#>  BodyWgt BrainWgt     NonD    Sleep     Span     Gest 
#>        0        0        0       10       10        0
```

## 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
#> Multiply imputed dataset (vimmi)
#>   Observations: 42
#>   Variables:    6
#>   Imputations:  m = 5
#>   Bootstrap:    yes
#>   Uncertainty:  pmm
#>   Missing cells: 20 across 2 variables
#>     Sleep: 10 NAs (ranger; NRMSE = 0.401 [oob])
#>     Span: 10 NAs (ranger; NRMSE = 0.727 [oob])
```

`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
plot(mi)             # chains: mean/sd of the imputed values per iteration
```

![plot of chunk unnamed-chunk-5](figures/vimpute-mi-unnamed-chunk-5-1.png)


``` r
plot(mi, "density")  # observed (blue, bold) vs per-imputation imputed (red)
```

![plot of chunk unnamed-chunk-6](figures/vimpute-mi-unnamed-chunk-6-1.png)

## 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
fits <- with(mi, lm(Sleep ~ BodyWgt + Span))
pooled <- mice::pool(fits)
summary(pooled)
#>          term    estimate std.error  statistic       df    p.value
#> 1 (Intercept) -0.06347952 0.1326505 -0.4785471 35.42201 0.63520157
#> 2     BodyWgt -0.20018445 0.1532492 -1.3062675 33.80298 0.20028060
#> 3        Span -0.26789463 0.1527491 -1.7538210 32.48761 0.08889201
```

Alternatively, convert the whole object: `vim_as_mids(mi)` yields a genuine
`mice::mids` for any downstream mice infrastructure.


``` r
mids <- vim_as_mids(mi)
class(mids)
#> [1] "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")]
#> $variable
#> [1] "Span"
#> 
#> $tuned
#> [1] FALSE
#> 
#> $tuned_better
#> [1] FALSE
#> 
#> $n_evals
#> NULL
#> 
#> $folds
#> NULL
```

## 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
#> Overimputation diagnostic for 'Sleep'
#>   32 observed cells, 5 draws each (3 folds)
#>   Empirical coverage of the 90% intervals: 53.1%
#>   Mean absolute error (observed vs imputed mean): 0.4871
plot(ov)
```

![plot of chunk unnamed-chunk-10](figures/vimpute-mi-unnamed-chunk-10-1.png)

## 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"))
#> $err_num
#> [1] 0.3303824
#> 
#> $err_cat
#> [1] 0
#> 
#> $error
#> [1] 0.3303824
```

## 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.
