Workshop: Multiple Imputation in R

Author

Armando Teixeira-Pinto

Published

July 1, 2025

This tutorial will guide you through a step-by-step example using a simulated clinical dataset. We’ll cover the basics of missing data mechanisms (MCAR and MAR), how to introduce them, explore patterns, perform MI, analyze imputed data, and pool results.

1 Quick Background

  • Missing Data Mechanisms:
    • MCAR (Missing Completely at Random): Missingness is unrelated to observed or unobserved data (e.g., random data entry errors).
    • MAR (Missing at Random): Missingness depends on observed data but not unobserved (e.g., older patients skip certain tests).
    • MNAR (Missing Not at Random): Missingness depends on unobserved data (not covered here, as MI assumes MCAR/MAR).
  • Multiple Imputation: Creates multiple (e.g., 5) plausible datasets by imputing missing values, analyzes each, and pools results for valid inference. We’ll use the mice package, which handles MI via chained equations.

We’ll simulate a clinical dataset on 200 patients with variables: age (continuous), gender (binary), systolic blood pressure (SBP, continuous), cholesterol (continuous), and disease outcome (binary: 0=no, 1=yes). We’ll introduce MCAR missingness in SBP and MAR missingness in cholesterol (more missing for older patients).

Note: Run the code in RStudio or your R environment. I’ll provide code blocks, explanations, and expected outputs/descriptions. If you encounter errors, ensure packages are installed.

2 Step 1: Install and Load Packages

We need mice for imputation and VIM for visualizing missing patterns (optional but helpful).

# Install packages if not already installed
#install.packages(c("mice", "gtsummary", "finalfit"))

# Load libraries
library(mice)
library(gtsummary)
library(finalfit)
  • What this does: Installs/loads tools. mice performs MI; VIM helps visualize missings.
  • Expected output: No output if already installed; otherwise, installation messages.

3 Step 2: Simulate the Complete Clinical Dataset

We’ll create a dataset with correlated variables to mimic real clinical data.

# Set seed for reproducibility
set.seed(123)

# Simulate 200 patients
n <- 200
age <- rnorm(n, mean = 50, sd = 10)  # Age ~ N(50, 10)
gender <- rbinom(n, 1, 0.5)  # Gender: 0=female, 1=male (50% each)
sbp <- 120 + 0.5 * age + rnorm(n, 0, 10)  # SBP increases with age
cholesterol <- 200 + 0.3 * age + 10 * gender + rnorm(n, 0, 20)  # Cholesterol higher in males and with age
disease <- rbinom(n, 1, plogis(-5 + 0.05 * age + 0.01 * sbp + 0.005 * cholesterol))  # Logistic probability of disease

# Combine into data frame
data_complete <- data.frame(age, gender, sbp, cholesterol, disease)

# View summary
summary(data_complete)
      age            gender           sbp         cholesterol   
 Min.   :26.91   Min.   :0.000   Min.   :116.9   Min.   :163.9  
 1st Qu.:43.74   1st Qu.:0.000   1st Qu.:138.6   1st Qu.:204.9  
 Median :49.41   Median :1.000   Median :145.0   Median :218.1  
 Mean   :49.91   Mean   :0.505   Mean   :145.3   Mean   :218.1  
 3rd Qu.:55.68   3rd Qu.:1.000   3rd Qu.:152.9   3rd Qu.:232.8  
 Max.   :82.41   Max.   :1.000   Max.   :171.8   Max.   :280.2  
    disease     
 Min.   :0.000  
 1st Qu.:0.000  
 Median :0.000  
 Mean   :0.455  
 3rd Qu.:1.000  
 Max.   :1.000  
  • What this does: Generates realistic data. Age is normal; gender binary; SBP and cholesterol increase with age/gender; disease is logistic based on predictors.

  • Expected output (summary excerpt; actual numbers may vary slightly due to random generation, but means/SDs should match):

       age           gender            sbp          cholesterol       disease       
    Min.   :22.36   Min.   :0.0000   Min.   : 97.32   Min.   :147.8   Min.   :0.0000  
    1st Qu.:43.28   1st Qu.:0.0000   1st Qu.:136.64   1st Qu.:208.4   1st Qu.:0.0000  
    Median :50.00   Median :0.5000   Median :145.00   Median :220.0   Median :0.0000  
    Mean   :50.00   Mean   :0.5000   Mean   :145.00   Mean   :220.0   Mean   :0.1500  
    3rd Qu.:56.72   3rd Qu.:1.0000   3rd Qu.:153.36   3rd Qu.:231.6   3rd Qu.:0.0000  
    Max.   :77.64   Max.   :1.0000   Max.   :192.68   Max.   :292.2   Max.   :1.0000  

4 Step 3: Introduce Missing Values (MCAR and MAR)

Now, add missings: 10% MCAR in SBP (random), 20% MAR in cholesterol (higher probability if age > 60).

# Copy complete data
data_missing <- data_complete

# MCAR: Randomly set 10% of SBP to NA
missing_sbp <- sample(1:n, size = round(0.1 * n), replace = FALSE)
data_missing$sbp[missing_sbp] <- NA

# MAR: For cholesterol, missing with prob 0.3 if age > 60, else 0.05
prob_missing_chol <- ifelse(data_missing$age > 60, 0.3, 0.05)
missing_chol <- rbinom(n, 1, prob_missing_chol) == 1
data_missing$cholesterol[missing_chol] <- NA

# View summary to confirm missings
data_missing %>% 
    tbl_summary()
Characteristic N = 2001
age 49 (44, 56)
gender 101 (51%)
sbp 145 (138, 153)
    Unknown 20
cholesterol 219 (206, 234)
    Unknown 22
disease 91 (46%)
1 Median (Q1, Q3); n (%)
  • What this does: Introduces MCAR (purely random) in SBP and MAR (depends on observed age) in cholesterol. No missings in other variables for simplicity.
  • Expected output (summary shows NAs):
     age           gender            sbp          cholesterol       disease       
  Min.   :22.36   Min.   :0.0000   Min.   : 97.32   Min.   :147.8   Min.   :0.0000  
  ... (similar to above, but with)                NA's   :25      ...             
                                                  NA's   :20                      
  • About 20 NAs in SBP (10%), ~25 in cholesterol (higher due to MAR).

5 Step 4: Explore Missing Data Patterns

Visualize and quantify missings to understand mechanisms.

# Proportion of missing data per variable
colMeans(is.na(data_missing))
        age      gender         sbp cholesterol     disease 
       0.00        0.00        0.10        0.11        0.00 
# Missing data pattern with mice
md.pattern(data_missing)

    age gender disease sbp cholesterol   
162   1      1       1   1           1  0
18    1      1       1   1           0  1
16    1      1       1   0           1  1
4     1      1       1   0           0  2
      0      0       0  20          22 42
# Visualize missing patterns (using finalfit)
missing_pairs(data_missing)

  • What this does:

    • colMeans(is.na()): Shows % missing per variable.
    • md.pattern(): Table of missing patterns (e.g., rows with both missing, one missing).
    • missing_pairs(): Plots the boxplots for each variable comparing the missing vs non-missing cases on each of the remaining variables.
  • Expected output:

    • Proportions: age=0, gender=0, sbp~0.10, cholesterol~0.125, disease=0.
    • md.pattern: A table like:
        age gender disease sbp cholesterol   |
      155   1      1       1   1           0  0
       20   1      1       1   0           1  1
       20   1      1       0   1           1  1
        5   1      1       0   0           1  2
            0      0      25  20              45
    
      (155 complete rows; 20 with only sbp missing; etc. Total missings: 45).
    • Plot: Only SBP and cholesterol will have a boxplot for missing. Age should be slightly higher for patients with missing cholesterol.

6 Step 5: Perform Multiple Imputation

Use mice with predictive mean matching (pmm) for continuous vars, logistic for binary (though disease has no missings).

# Initialize imputation (quick check)
ini <- mice(data_missing, maxit = 0)  # Gets default methods

# Run MI: 5 imputations, 10 iterations (default meth='pmm' for continuous)
imp <- mice(data_missing, m = 5, maxit = 10, method = 'pmm', seed = 123)

 iter imp variable
  1   1  sbp  cholesterol
  1   2  sbp  cholesterol
  1   3  sbp  cholesterol
  1   4  sbp  cholesterol
  1   5  sbp  cholesterol
  2   1  sbp  cholesterol
  2   2  sbp  cholesterol
  2   3  sbp  cholesterol
  2   4  sbp  cholesterol
  2   5  sbp  cholesterol
  3   1  sbp  cholesterol
  3   2  sbp  cholesterol
  3   3  sbp  cholesterol
  3   4  sbp  cholesterol
  3   5  sbp  cholesterol
  4   1  sbp  cholesterol
  4   2  sbp  cholesterol
  4   3  sbp  cholesterol
  4   4  sbp  cholesterol
  4   5  sbp  cholesterol
  5   1  sbp  cholesterol
  5   2  sbp  cholesterol
  5   3  sbp  cholesterol
  5   4  sbp  cholesterol
  5   5  sbp  cholesterol
  6   1  sbp  cholesterol
  6   2  sbp  cholesterol
  6   3  sbp  cholesterol
  6   4  sbp  cholesterol
  6   5  sbp  cholesterol
  7   1  sbp  cholesterol
  7   2  sbp  cholesterol
  7   3  sbp  cholesterol
  7   4  sbp  cholesterol
  7   5  sbp  cholesterol
  8   1  sbp  cholesterol
  8   2  sbp  cholesterol
  8   3  sbp  cholesterol
  8   4  sbp  cholesterol
  8   5  sbp  cholesterol
  9   1  sbp  cholesterol
  9   2  sbp  cholesterol
  9   3  sbp  cholesterol
  9   4  sbp  cholesterol
  9   5  sbp  cholesterol
  10   1  sbp  cholesterol
  10   2  sbp  cholesterol
  10   3  sbp  cholesterol
  10   4  sbp  cholesterol
  10   5  sbp  cholesterol
# View imputation summary
summary(imp)
Class: mids
Number of multiple imputations:  5 
Imputation methods:
        age      gender         sbp cholesterol     disease 
         ""          ""       "pmm"       "pmm"          "" 
PredictorMatrix:
            age gender sbp cholesterol disease
age           0      1   1           1       1
gender        1      0   1           1       1
sbp           1      1   0           1       1
cholesterol   1      1   1           0       1
disease       1      1   1           1       0
imp$imp$sbp  # View imputed values for sbp in each of 5 datasets
           1        2        3        4        5
11  150.6400 150.6400 150.7182 139.1597 147.0346
18  143.4432 155.3836 143.4040 134.9264 139.8879
20  134.9391 152.0965 146.6613 147.9637 143.9789
22  137.7108 158.9177 143.8104 137.7108 158.0949
29  121.8521 155.3836 144.8238 129.7153 158.4138
35  152.1910 144.4197 151.0227 163.3833 156.4160
54  160.9698 151.8679 158.9832 158.9832 154.4565
73  160.9698 156.8871 136.0985 165.0391 159.6973
89  156.5339 140.7124 146.1055 143.8104 142.7841
90  158.1806 147.0346 148.5807 158.1806 142.7841
114 145.3743 165.0391 138.7003 158.4138 148.6589
142 158.4138 157.0377 133.6083 131.6002 142.5116
162 143.7383 141.0368 122.2011 143.8843 144.8238
169 157.3079 142.5116 145.5011 148.2066 146.4325
171 131.2147 146.0993 149.0003 131.6002 144.4197
175 142.0794 131.6403 149.7078 150.1539 143.8104
179 134.5979 151.0139 148.8287 134.5979 136.0985
188 131.6403 146.1055 144.8940 140.1755 152.1280
190 153.9325 131.9878 132.8312 129.7153 134.3266
197 162.3120 128.2694 135.8200 134.5979 164.5828
  • What this does: mice() imputes via chained equations (iterates over variables). m=5 datasets; pmm uses observed values close to predicted.
  • Expected output:
    • summary(imp): Shows imputation chain, methods (pmm for sbp/cholesterol).
    • imp\(imp\)sbp: A 20x5 matrix (rows=missing cases, columns=imputations) with plausible values ~140-150 range.

Check convergence (trace plots should stabilize):

plot(imp)  # Trace plots for mean/SD of imputations

  • Expected: Lines for each imputation chain flatten after iterations (good convergence).

7 Step 6: Analyze the Imputed Datasets

Fit a logistic regression on each imputed dataset: disease ~ age + gender + sbp + cholesterol.

# Fit model on each imputed dataset
fit <- with(imp, 
            glm(disease ~ age + gender + sbp + cholesterol, 
                     family = binomial))

# View one example fit
summary(fit$analyses[[1]])

Call:
glm(formula = disease ~ age + gender + sbp + cholesterol, family = binomial)

Coefficients:
             Estimate Std. Error z value Pr(>|z|)   
(Intercept) -5.768184   2.590792  -2.226  0.02599 * 
age          0.049413   0.017541   2.817  0.00485 **
gender      -0.157686   0.310748  -0.507  0.61185   
sbp         -0.001305   0.014307  -0.091  0.92732   
cholesterol  0.015410   0.007755   1.987  0.04690 * 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 275.64  on 199  degrees of freedom
Residual deviance: 260.73  on 195  degrees of freedom
AIC: 270.73

Number of Fisher Scoring iterations: 4
#View graphically the imputed values for the different values
stripplot(imp , pch = c(21, 20), cex = c(1, 1.5))

#The distribution of the variables in the imputed datasets
densityplot(imp , layout = c(2, 1))

  • What this does: with() applies glm to each of the 5 imputed datasets.
  • Expected output: List of 5 glm summaries. Each like a standard glm output, e.g., coefficients for age positive/significant, ORs >1 for risk factors. Variances differ slightly across imputations.

7.0.1 Step 7: Pool Results and Interpret

Combine estimates using Rubin’s rules for valid SEs/confidence intervals.

# Pool the results
pooled <- pool(fit)

# Summary of pooled estimates
summary(pooled)
         term     estimate   std.error   statistic        df     p.value
1 (Intercept) -5.789083839 2.719388828 -2.12881798 147.84159 0.034926562
2         age  0.047438144 0.017742251  2.67373884 179.49433 0.008193198
3      gender -0.136711853 0.313121284 -0.43660990 182.58189 0.662910159
4         sbp  0.000838251 0.015576747  0.05381425  94.31419 0.957196984
5 cholesterol  0.014501995 0.008195971  1.76940531 105.08021 0.079727262
# Pooled coefficients and confidence intervals
poolsummary <- summary(pooled, conf.int = TRUE)
poolsummary
         term     estimate   std.error   statistic        df     p.value
1 (Intercept) -5.789083839 2.719388828 -2.12881798 147.84159 0.034926562
2         age  0.047438144 0.017742251  2.67373884 179.49433 0.008193198
3      gender -0.136711853 0.313121284 -0.43660990 182.58189 0.662910159
4         sbp  0.000838251 0.015576747  0.05381425  94.31419 0.957196984
5 cholesterol  0.014501995 0.008195971  1.76940531 105.08021 0.079727262
          2.5 %      97.5 %      conf.low   conf.high
1 -11.162976733 -0.41519095 -11.162976733 -0.41519095
2   0.012427921  0.08244837   0.012427921  0.08244837
3  -0.754513294  0.48108959  -0.754513294  0.48108959
4  -0.030088403  0.03176490  -0.030088403  0.03176490
5  -0.001748957  0.03075295  -0.001748957  0.03075295
#The tbl_regression does the pooling automatically!
tbl_regression(fit, exponentiate = T)
Characteristic OR 95% CI p-value
age 1.05 1.01, 1.09 0.008
gender 0.87 0.47, 1.62 0.7
sbp 1.00 0.97, 1.03 >0.9
cholesterol 1.01 1.00, 1.03 0.080
Abbreviations: CI = Confidence Interval, OR = Odds Ratio
  • What this does: pool() averages coefficients, adjusts SEs for imputation uncertainty.

  • Expected output (example pooled summary; actual values depend on data):

               estimate  std.error statistic       df      p.value      2.5 %     97.5 %
    (Intercept) -6.5123   1.2345    -5.275   150.2   3.45e-07   -8.9523   -4.0723
    age          0.0456   0.0123     3.707   180.5   0.00028    0.0213    0.0699
    gender       0.3124   0.4567     0.684   190.1   0.49480   -0.5889    1.2137
    sbp          0.0102   0.0056     1.821   170.3   0.07040   -0.0008    0.0212
    cholesterol  0.0048   0.0021     2.286   160.4   0.02350    0.0006    0.0090
    • Interpretation: Age and cholesterol significantly associated with disease (p<0.05). SEs larger than complete-case analysis due to imputation uncertainty.
    • Fraction of missing information (FMI) via pooled$fmi (higher for variables with more missings).

8 Step 8: Compare to Complete-Case Analysis (For Illustration)

To see MI’s benefits, run on complete cases (biased under MAR).

# Complete-case logistic regression
fit_cc <- glm(disease ~ age + gender + sbp + cholesterol, 
              family = binomial, 
              data = data_missing[complete.cases(data_missing), ])
tbl_regression(fit_cc, exponentiate = T)
Characteristic OR 95% CI p-value
age 1.04 1.00, 1.08 0.058
gender 1.04 0.53, 2.04 >0.9
sbp 1.00 0.97, 1.03 >0.9
cholesterol 1.01 1.00, 1.03 0.11
Abbreviations: CI = Confidence Interval, OR = Odds Ratio
# Merge the CC and MICE tables into one object
tbl_cc <- tbl_regression(fit_cc, exponentiate = T)
tbl_mi <- tbl_regression(fit, exponentiate = T)

tbl_merged <- tbl_merge(
  # A list of the tables to merge
  tbls = list(tbl_cc, tbl_mi),
  
  # The labels that will appear above each column
  tab_spanner = c("**Complete Case**", "**Multiple Imputation**")
)

# Display the final, merged table
tbl_merged
Characteristic
Complete Case
Multiple Imputation
OR 95% CI p-value OR 95% CI p-value
age 1.04 1.00, 1.08 0.058 1.05 1.01, 1.09 0.008
gender 1.04 0.53, 2.04 >0.9 0.87 0.47, 1.62 0.7
sbp 1.00 0.97, 1.03 >0.9 1.00 0.97, 1.03 >0.9
cholesterol 1.01 1.00, 1.03 0.11 1.01 1.00, 1.03 0.080
Abbreviations: CI = Confidence Interval, OR = Odds Ratio
  • Expected: Similar coefficients but smaller sample (n~155), potentially biased (e.g., underestimated age effect due to MAR in cholesterol). MI provides better power/efficiency.

9 Workshop Exercises

  1. Increase missing % to 30%—how does FMI change?
  2. Try other methods (e.g., method=‘norm’ for normal imputation).
  3. Impute with interactions (e.g., predictorMatrix in mice).
  4. Use mitml or Amelia for alternatives.

This covers a full MI workflow! If you have questions or want to adapt for real data (e.g., NHANES), let me know. Remember, MI assumes MAR; diagnostics are key.