1. Objective

This example demonstrates:

  1. Primary analysis under MAR using MMRM / MI.
  2. Sensitivity analysis under MNAR using a pattern-mixture model with delta adjustment.

Clinical example:

2. Packages

library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(tidyr)
library(nlme)
## 
## Attaching package: 'nlme'
## The following object is masked from 'package:dplyr':
## 
##     collapse
library(mice)
## Warning: package 'mice' was built under R version 4.4.3
## 
## Attaching package: 'mice'
## The following object is masked from 'package:stats':
## 
##     filter
## The following objects are masked from 'package:base':
## 
##     cbind, rbind
library(emmeans)
## Welcome to emmeans.
## Caution: You lose important information if you filter this package's results.
## See '? untidy'
set.seed(123)

3. Simulate Clinical Trial Data

n <- 300

dat <- data.frame(
  ID = 1:n,
  TRT = sample(c("Placebo", "Drug"), n, replace = TRUE),
  Baseline = rnorm(n, 8.5, 0.8)
)

dat <- dat %>%
  mutate(
    Y4 = Baseline +
      ifelse(TRT == "Drug", -0.4, -0.1) +
      rnorm(n, 0, 0.35),

    Y8 = Baseline +
      ifelse(TRT == "Drug", -0.8, -0.2) +
      rnorm(n, 0, 0.35),

    Y12_true = Baseline +
      ifelse(TRT == "Drug", -1.1, -0.3) +
      rnorm(n, 0, 0.35),

    AE = rbinom(
      n,
      1,
      ifelse(TRT == "Drug", 0.30, 0.12)
    )
  )

4. Generate MAR Dropout

Missingness depends on observed information:

Therefore, this can still satisfy MAR.

# Target approximately 10% Week 12 missingness.
# Calibrate the intercept while preserving the MAR mechanism.

target_missing <- 0.10

lp_without_intercept <-
  1.2 * dat$AE +
  0.35 * (dat$Y8 - 8) +
  0.3 * (dat$TRT == "Drug")

calibrate_fun <- function(intercept) {
  mean(plogis(intercept + lp_without_intercept)) - target_missing
}

dropout_intercept <- uniroot(
  calibrate_fun,
  interval = c(-10, 2)
)$root

drop_prob <- plogis(
  dropout_intercept + lp_without_intercept
)

dat$Dropout <- rbinom(n, 1, drop_prob)

dat$Y12 <- dat$Y12_true
dat$Y12[dat$Dropout == 1] <- NA

# Overall and treatment-specific missing rates
overall_missing_rate <- mean(is.na(dat$Y12))

missing_by_trt <- dat %>%
  group_by(TRT) %>%
  summarise(
    MissingRate = mean(is.na(Y12)),
    .groups = "drop"
  )

overall_missing_rate
## [1] 0.1133333
missing_by_trt
## # A tibble: 2 × 2
##   TRT     MissingRate
##   <chr>         <dbl>
## 1 Drug         0.130 
## 2 Placebo      0.0974

5. Primary Analysis: MMRM Under MAR

longdat <- dat %>%
  select(ID, TRT, Baseline, Y4, Y8, Y12) %>%
  pivot_longer(
    cols = c(Y4, Y8, Y12),
    names_to = "Visit",
    values_to = "HbA1c"
  )

longdat$Visit <- factor(
  longdat$Visit,
  levels = c("Y4", "Y8", "Y12")
)

mmrm_fit <- lme(
  HbA1c ~ TRT * Visit + Baseline,
  random = ~1 | ID,
  data = longdat,
  na.action = na.exclude,
  method = "REML"
)

summary(mmrm_fit)
## Linear mixed-effects model fit by REML
##   Data: longdat 
##        AIC      BIC    logLik
##   695.5062 738.3082 -338.7531
## 
## Random effects:
##  Formula: ~1 | ID
##         (Intercept)  Residual
## StdDev:  0.07478946 0.3437904
## 
## Fixed effects:  HbA1c ~ TRT * Visit + Baseline 
##                          Value  Std.Error  DF   t-value p-value
## (Intercept)         -0.5278772 0.13700433 562  -3.85300   1e-04
## TRTPlacebo           0.3280141 0.04064364 297   8.07049   0e+00
## VisitY8             -0.3711867 0.04023763 562  -9.22486   0e+00
## VisitY12            -0.6905802 0.04177966 562 -16.52910   0e+00
## Baseline             1.0140474 0.01568428 297  64.65373   0e+00
## TRTPlacebo:VisitY8   0.2681477 0.05616072 562   4.77465   0e+00
## TRTPlacebo:VisitY12  0.4720353 0.05802480 562   8.13506   0e+00
##  Correlation: 
##                     (Intr) TRTPlc VistY8 VstY12 Baseln TRTP:VY8
## TRTPlacebo          -0.165                                     
## VisitY8             -0.147  0.495                              
## VisitY12            -0.150  0.477  0.482                       
## Baseline            -0.977  0.013  0.000  0.008                
## TRTPlacebo:VisitY8   0.105 -0.691 -0.716 -0.345  0.000         
## TRTPlacebo:VisitY12  0.107 -0.669 -0.347 -0.720 -0.005  0.484  
## 
## Standardized Within-Group Residuals:
##          Min           Q1          Med           Q3          Max 
## -2.693948186 -0.664785935  0.001773989  0.645792024  3.297561182 
## 
## Number of Observations: 866
## Number of Groups: 300
# Week 12 Drug - Placebo contrast
mmrm_emm <- emmeans(
  mmrm_fit,
  ~ TRT | Visit
)

mmrm_contrasts <- contrast(
  mmrm_emm,
  method = "revpairwise"
)

# Request confidence intervals explicitly.
# Depending on the model, emmeans may name CI columns
# lower.CL/upper.CL or asymp.LCL/asymp.UCL.
mmrm_contrast_summary <- as.data.frame(
  summary(
    mmrm_contrasts,
    infer = c(TRUE, TRUE)
  )
)

mmrm_week12 <- mmrm_contrast_summary %>%
  filter(Visit == "Y12")

mmrm_week12
## Visit = Y12:
##  contrast        estimate         SE  df  lower.CL  upper.CL t.ratio p.value
##  Placebo - Drug 0.8000494 0.04317906 297 0.7150738 0.8850251  18.529  <.0001
## 
## Degrees-of-freedom method: containment 
## Confidence level used: 0.95

The coefficient TRTDrug in an MMRM with a TRT * Visit interaction is the treatment difference at the reference visit, not necessarily the Week 12 effect.

Therefore, the clinically relevant Week 12 effect should be obtained from a visit-specific contrast.

6. Primary Analysis: Multiple Imputation Under MAR

mi_dat <- dat %>%
  select(TRT, Baseline, Y4, Y8, AE, Y12)

mi_dat$TRT <- factor(
  mi_dat$TRT,
  levels = c("Placebo", "Drug")
)

imp <- mice(
  mi_dat,
  m = 20,
  method = "pmm",
  seed = 123,
  printFlag = FALSE
)

fit_mi <- with(
  imp,
  lm(Y12 ~ TRT + Baseline)
)

mi_pooled <- summary(
  pool(fit_mi),
  conf.int = TRUE
)

mi_pooled
##          term   estimate  std.error  statistic       df       p.value
## 1 (Intercept) -0.6302126 0.22022253  -2.861708 254.8366  4.563488e-03
## 2     TRTDrug -0.7989755 0.04241025 -18.839208 192.4937  1.465626e-45
## 3    Baseline  1.0385687 0.02554325  40.659228 262.3539 2.921101e-115
##        2.5 %     97.5 %   conf.low  conf.high
## 1 -1.0639005 -0.1965248 -1.0639005 -0.1965248
## 2 -0.8826240 -0.7153271 -0.8826240 -0.7153271
## 3  0.9882729  1.0888646  0.9882729  1.0888646
mi_week12 <- mi_pooled %>%
  filter(term == "TRTDrug") %>%
  transmute(
    Method = "MI under MAR",
    Estimate = estimate,
    SE = std.error,
    Lower95 = `2.5 %`,
    Upper95 = `97.5 %`
  )

mi_week12
##         Method   Estimate         SE   Lower95    Upper95
## 2 MI under MAR -0.7989755 0.04241025 -0.882624 -0.7153271

The MI analysis is a Week 12 ANCOVA. Because Placebo is the reference group, TRTDrug directly estimates the adjusted Drug - Placebo difference at Week 12.

7. Compare MMRM and MI Week 12 Treatment Effects

The two methods should be compared using the same Week 12 contrast, rather than comparing the raw TRTDrug coefficient from the MMRM output.

# Identify the confidence-interval column names returned by emmeans
lower_col <- intersect(
  c("lower.CL", "asymp.LCL"),
  names(mmrm_week12)
)[1]

upper_col <- intersect(
  c("upper.CL", "asymp.UCL"),
  names(mmrm_week12)
)[1]

if (is.na(lower_col) || is.na(upper_col)) {
  stop("Confidence interval columns were not found in the emmeans output.")
}

mmrm_compare <- data.frame(
  Method = "MMRM under MAR",
  Estimate = mmrm_week12$estimate,
  SE = mmrm_week12$SE,
  Lower95 = mmrm_week12[[lower_col]],
  Upper95 = mmrm_week12[[upper_col]]
)

comparison_table <- bind_rows(
  mmrm_compare,
  mi_week12
)

comparison_table
##              Method   Estimate         SE    Lower95    Upper95
## ...1 MMRM under MAR  0.8000494 0.04317906  0.7150738  0.8850251
## 2      MI under MAR -0.7989755 0.04241025 -0.8826240 -0.7153271

Interpretation:

8. MNAR Sensitivity Analysis: Delta Adjustment

Under MNAR, assume that patients with missing Week 12 HbA1c have outcomes worse than predicted under MAR.

For example:

\[ Y_{MNAR} = Y_{MAR} + \delta \]

where a positive delta means higher, and therefore worse, HbA1c.

run_delta <- function(delta, imp_object, original_data) {

  missing_y12 <- is.na(original_data$Y12)

  estimates <- numeric(imp_object$m)
  variances <- numeric(imp_object$m)

  for (i in 1:imp_object$m) {

    temp <- complete(imp_object, i)

    # Keep Placebo as the reference group
    temp$TRT <- factor(
      temp$TRT,
      levels = c("Placebo", "Drug")
    )

    # MNAR delta adjustment:
    # higher HbA1c is worse, so positive delta is unfavorable
    # to patients with originally missing Week 12 outcomes
    temp$Y12[missing_y12] <-
      temp$Y12[missing_y12] + delta

    fit <- lm(
      Y12 ~ TRT + Baseline,
      data = temp
    )

    estimates[i] <- unname(coef(fit)["TRTDrug"])

    vc <- vcov(fit)

    if (!"TRTDrug" %in% rownames(vc)) {
      stop(
        "TRTDrug coefficient was not found. ",
        "Check treatment factor levels."
      )
    }

    variances[i] <- vc["TRTDrug", "TRTDrug"]
  }

  # Rubin's rules
  Qbar <- mean(estimates)
  Ubar <- mean(variances)
  B <- var(estimates)

  total_var <-
    Ubar + (1 + 1 / imp_object$m) * B

  SE <- sqrt(total_var)

  data.frame(
    Delta = delta,
    Estimate = Qbar,
    SE = SE,
    Lower95 = Qbar - 1.96 * SE,
    Upper95 = Qbar + 1.96 * SE
  )
}

9. Test Several MNAR Assumptions

delta_values <- c(
  0,
  0.2,
  0.4,
  0.6,
  0.8,
  1.0
)

results <- bind_rows(
  lapply(
    delta_values,
    run_delta,
    imp_object = imp,
    original_data = mi_dat
  )
)

results
##   Delta   Estimate         SE    Lower95    Upper95
## 1   0.0 -0.7989755 0.04241025 -0.8820996 -0.7158514
## 2   0.2 -0.7925499 0.04292054 -0.8766742 -0.7084257
## 3   0.4 -0.7861243 0.04464960 -0.8736376 -0.6986111
## 4   0.6 -0.7796988 0.04746444 -0.8727290 -0.6866685
## 5   0.8 -0.7732732 0.05118622 -0.8735982 -0.6729482
## 6   1.0 -0.7668476 0.05563324 -0.8758887 -0.6578064

Interpretation:

The treatment coefficient is Drug - Placebo because Placebo is explicitly set as the reference group. For HbA1c, a negative estimate favors Drug.

10. Interpretation

The overall strategy is:

\[ \text{Primary Analysis} \rightarrow \text{MMRM or MI under MAR} \]

followed by:

\[ \text{Sensitivity Analysis} \rightarrow \text{Pattern-Mixture Model + Delta Adjustment} \]

If the treatment conclusion remains similar across clinically plausible delta values, the result is considered robust to departures from MAR.

If the conclusion changes only after a sufficiently large delta, that value can be interpreted as a tipping point.

11. Key Takeaway

MAR allows missingness to depend on observed covariates and prior outcomes.

For the primary analysis, MMRM or MI can therefore be used under a reasonable MAR assumption.

Because MAR cannot be verified from the observed data alone, MNAR sensitivity analyses such as pattern-mixture models with delta adjustment are used to assess robustness.