Skip to contents

Introduction

Multiple imputation with mice() creates m imputed data sets by running m chains of the Gibbs sampler. Chain i only uses its own imputations from the previous iteration, so the chains are independent and can run at the same time on different cores or machines.

mice() can do this itself with the argument parallel = TRUE. This builds the parallel run into mice(), instead of the wrapper functions parlmice() (deprecated) and futuremice() (Volker and Vink 2022). The parallel backend is provided by the future framework (Bengtsson 2021), so the same code runs on a laptop, a workstation or a cluster.

This vignette shows how to use parallel = TRUE, what stays the same, and what to watch out for. The companion vignette Parallel imputation: benchmarks shows when running in parallel pays off, and how mice(parallel = TRUE) compares to futuremice().

Parallel imputation needs the future and future.apply packages:

install.packages(c("future", "future.apply"))

Basic use

Add parallel = TRUE to the call:

imp <- mice(nhanes, m = 5, seed = 123, parallel = TRUE, n.core = 2)
#> 
#>  Imputed 5 chains in parallel, 5 iterations (0.61 secs)
imp
#> Class: mids
#> Number of multiple imputations:  5 
#> Imputation methods:
#>   age   bmi   hyp   chl 
#>    "" "pmm" "pmm" "pmm" 
#> PredictorMatrix:
#>     age bmi hyp chl
#> age   0   1   1   1
#> bmi   1   0   1   1
#> hyp   1   1   0   1
#> chl   1   1   1   0

mice() first sets up the imputation model and the starting imputations, exactly as for parallel = FALSE. It then sends chain i, with all its maxit iterations, to a worker, and combines the results into one mids object. With print = TRUE (the default), mice() reports the total time rather than the iteration history.

The argument n.core sets the number of workers. Without n.core, mice() uses the backend that you set with future::plan() (see below), or, if you set none, starts availableCores() - 1 workers. mice() never uses more workers than there are chains, so m = 5 keeps at most five workers busy.

The workers come on top of your own R session, the main process. The main process sets up the imputation model and the starting imputations, sends the chains to the workers, and collects and combines the results. While the chains run, it mostly waits. This is why the default leaves one core free for it (availableCores() - 1).

Choosing and reusing the backend

With n.core, mice() starts a fresh set of background R sessions (a multisession plan) for every call, and closes them afterwards. Starting a worker takes time, mainly because each worker has to load mice and its dependencies. On a recent laptop this costs about a second per call, which is more than the whole imputation of a small data set.

If you impute more than once, start the workers yourself, once:

library(future)
plan(multisession, workers = 4)

imp1 <- mice(nhanes, m = 5, seed = 1, parallel = TRUE, print = FALSE)
imp2 <- mice(nhanes2, m = 5, seed = 1, parallel = TRUE, print = FALSE)

mice() respects a plan set by the user, and does not change or close it. The first call loads mice on the workers; later calls reuse them. Close the workers when you are done:

plan(sequential)

The same mechanism selects other backends. Some examples:

# forked processes, Linux and macOS outside RStudio only
plan(multicore, workers = 8)

# R sessions on other machines
plan(cluster, workers = c("node1", "node2", "node3"))

# jobs on an HPC scheduler, with the future.batchtools package
plan(future.batchtools::batchtools_slurm)

See vignette("future-1-overview", package = "future") for all options. Without parallel = TRUE, mice() ignores the plan and uses the sequential sampler, which performs iteration 1 for all chains, then iteration 2 for all chains, and so on.

Reproducibility

Each chain gets its own random number stream, derived from seed (future.seed = TRUE in future.apply). As a result, the imputations are reproducible with parallel = TRUE, and they do not depend on the number of workers:

imp2 <- mice(nhanes, m = 5, seed = 123, parallel = TRUE, n.core = 2,
             print = FALSE)
imp3 <- mice(nhanes, m = 5, seed = 123, parallel = TRUE, n.core = 3,
             print = FALSE)
identical(complete(imp2, "long"), complete(imp3, "long"))
#> [1] TRUE

The imputations do differ from those of parallel = FALSE with the same seed. The sequential sampler takes the random numbers for all chains from one and the same stream, in the order in which it visits them: iteration 1 of chain 1, iteration 1 of chain 2, and so on. With parallel = TRUE, each chain draws from its own stream:

imps <- mice(nhanes, m = 5, seed = 123, print = FALSE)
identical(complete(imps, "long"), complete(imp2, "long"))
#> [1] FALSE

Both are valid multiple imputations. If you need to reproduce an earlier analysis, use the same setting of parallel as before.

The result is an ordinary mids object

The object returned by mice(parallel = TRUE) has the same structure as that of the sequential sampler. All functions that work on a mids object work as before. Convergence plots use chainMean and chainVar, which hold one trace per chain. We remove bmi from the boys data, because it is a deterministic function of hgt and wgt, which makes the chains mix slowly:

boys2 <- boys[, names(boys) != "bmi"]
imp <- mice(boys2, m = 5, maxit = 10, seed = 1, parallel = TRUE,
            n.core = 2, print = FALSE)
plot(imp, c("hgt", "wgt", "hc"))

The events that the chains log, such as removed predictors, are collected in loggedEvents in the order of the sequential sampler. The usual analysis and pooling steps are unchanged:

fit <- with(imp, lm(wgt ~ age + hgt))
summary(pool(fit))
#>          term   estimate  std.error statistic       df      p.value
#> 1 (Intercept) -9.3559617 2.04663853  -4.57138 742.1823 5.674799e-06
#> 2         age  2.3237179 0.18942725  12.26707 741.6993 1.256016e-31
#> 3         hgt  0.1922411 0.02808239   6.84561 741.9631 1.596930e-11

Adding iterations

mice.mids() adds iterations to an existing mids object. An object created with parallel = TRUE continues in parallel, and the result is the same as that of a single call with all iterations:

imp5 <- mice(nhanes, m = 5, maxit = 5, seed = 1, parallel = TRUE,
             n.core = 2, print = FALSE)
imp10 <- mice.mids(imp5, maxit = 5, n.core = 2, print = FALSE)
imp10$iteration
#> [1] 10

full <- mice(nhanes, m = 5, maxit = 10, seed = 1, parallel = TRUE,
             n.core = 2, print = FALSE)
identical(complete(imp10, "long"), complete(full, "long"))
#> [1] TRUE

This works because the mids object stores whether it was created in parallel (imp$parallel), and the state of the random number stream of each chain after the last iteration (imp$chainSeed).

You can also switch. parallel = TRUE continues a sequential object in parallel, for example to add many iterations to a model that you first checked with a short sequential run. parallel = FALSE continues a parallel object sequentially, for example to see the iteration history or to debug an imputation method:

imp_seq <- mice.mids(imp5, maxit = 1, parallel = FALSE)
#> 
#>  iter imp variable
#>   6   1  bmi  hyp  chl
#>   6   2  bmi  hyp  chl
#>   6   3  bmi  hyp  chl
#>   6   4  bmi  hyp  chl
#>   6   5  bmi  hyp  chl

After a switch, the results are reproducible, but they differ from those of a single call, because the random numbers come from other streams.

Following the progress

For long runs, mice() can report its progress through the progressr package. It signals one step after each iteration of each chain, so m * maxit steps in total. With parallel = TRUE, the workers relay these steps while the chains run. Nothing is shown unless you enable a progress handler:

library(progressr)
handlers("cli")

with_progress(
  imp <- mice(boys2, m = 20, maxit = 20, parallel = TRUE, print = FALSE)
)

To show the progress of every call in the session, use handlers(global = TRUE) instead of with_progress(). The progress bar disappears when the imputation has finished, so for a fast run you may see nothing at all. Set options(progressr.clear = FALSE) to keep it. Progress reporting does not change the imputations.

User-written imputation methods

An imputation method is a function called mice.impute.<name>(). You can write your own and use it in method. With parallel = TRUE, mice() sends such functions to the workers, together with the objects from your session that they use, such as helper functions:

shrink <- function(x, f) mean(x) + f * (x - mean(x))

mice.impute.pmmshrink <- function(y, ry, x, ...) {
  shrink(mice.impute.pmm(y, ry, x, ...), f = 0.5)
}

meth <- make.method(nhanes)
meth["bmi"] <- "pmmshrink"
imp <- mice(nhanes, method = meth, seed = 1, parallel = TRUE,
            n.core = 2, print = FALSE)

Imputation methods from other packages, such as those in miceadds, are loaded on the workers automatically, but the package must be installed on every worker.

Post-processing per imputation

The argument post contains R code that is run after each imputation of a variable. The code can use the variable j, the iteration k, the column i of the imputations in imp[[j]], and the imputation number chain. A common use is to keep imputations within a range:

post <- make.post(nhanes)
post["chl"] <- "imp[[j]][, i] <- squeeze(imp[[j]][, i], c(120, 300))"

Use i only to index imp[[j]]. With parallel = TRUE, each worker holds a single chain, so i is always 1 there. For anything that should differ between the imputations, use chain. For example, in a sensitivity analysis that shifts the imputed values by an amount that differs per imputation:

delta <- c(0, 5, 10, 15, 20)
post["chl"] <- "imp[[j]][, i] <- imp[[j]][, i] + delta[chain]"
imp <- mice(nhanes, post = post, seed = 1, parallel = TRUE, n.core = 2,
            print = FALSE)

With parallel = FALSE, i and chain are equal, so code that uses chain works in both cases.

When an imputation fails

If an imputation method or a post statement fails, mice() stops with an error that tells where the sampler was. This works in the same way with parallel = TRUE, where the failing chain stops all chains:

post <- make.post(nhanes)
post["bmi"] <- "if (k == 3 && chain == 4) stop('implausible values')"
imp <- mice(nhanes, post = post, seed = 1, parallel = TRUE, n.core = 2,
            print = FALSE)
#> Warning: Caught mice_sampler_error. Canceling all iterations ...
#> Error:
#> ! mice() stopped at iteration 3, imputation 4, variable 'bmi' (post-processing):
#>   implausible values

The error is a condition of class mice_sampler_error. Its fields contain the details, and the field parent holds the original error:

e <- tryCatch(
  mice(nhanes, post = post, seed = 1, parallel = TRUE, n.core = 2,
       print = FALSE),
  error = identity
)
#> Warning: Caught mice_sampler_error. Canceling all iterations ...
e[c("iteration", "imputation", "variable", "method", "post")]
#> $iteration
#> [1] 3
#> 
#> $imputation
#> [1] 4
#> 
#> $variable
#> [1] "bmi"
#> 
#> $method
#> [1] "pmm"
#> 
#> $post
#> [1] TRUE
e$parent
#> <simpleError in eval(parse(text = cmd)): implausible values>

Things to watch out for

  • The workers are separate R sessions. They do not see the objects, packages and options of your session, except for what mice() sends them. Pass settings for an imputation method as arguments, for example through ... or blots, rather than through options().

  • Workers load the installed mice. If you develop mice and load it with devtools::load_all(), the workers still use the version in your library. Install the package to test changes in parallel.

  • Workers run your startup files. Each new worker reads ~/.Rprofile. Code that is only meant for interactive use, such as loading development tools, slows down every worker and may produce warnings. Wrap such code in if (interactive()) { ... }.

  • Avoid parallelism within parallelism. Some imputation methods use several threads themselves, such as method = "rf" with the ranger package. Running such methods on many workers at once can oversubscribe the cores. Use fewer workers, or limit the threads per worker, e.g. mice(..., method = "rf", num.threads = 1).

  • Memory. Each worker receives its own copy of the data and of the imputations of its chain. For large data, the memory use grows with the number of workers.

  • Number of cores. availableCores() respects the limits set by schedulers such as Slurm and by containers, and is a safer choice than parallel::detectCores().

References

Bengtsson, Henrik. 2021. “A Unifying Framework for Parallel and Distributed Processing in R Using Futures.” The R Journal 13 (2): 208–27. https://doi.org/10.32614/RJ-2021-048.
Volker, Thom Benjamin, and Gerko Vink. 2022. futuremice: The Future Starts Today. https://www.gerkovink.com/miceVignettes/futuremice/Vignette_futuremice.html.