| name | ipd-meta-analysis |
| description | Individual participant data meta-analysis in R, including one-stage, two-stage, survival, and IPD with aggregate data. |
Individual Participant Data Meta-Analysis in R
Overview
Individual participant data (IPD) meta-analysis methods for synthesizing patient-level data across studies. Covers one-stage and two-stage approaches, mixed-effects models, combining IPD with aggregate data, treatment-covariate interactions, and handling missing data in multi-study settings.
Two-Stage IPD Meta-Analysis
Stage 1: Study-Level Analysis
library(dplyr)
library(purrr)
library(broom)
ipd_data <- data.frame(
study = rep(paste0("Study", 1:5), each = 100),
patient_id = 1:500,
treatment = rbinom(500, 1, 0.5),
age = rnorm(500, 60, 10),
outcome = rnorm(500, 50, 15)
)
study_results <- ipd_data |>
group_by(study) |>
nest() |>
mutate(
model = map(data, ~lm(outcome ~ treatment + age, data = .x)),
tidy_model = map(model, tidy, conf.int = TRUE)
) |>
unnest(tidy_model) |>
filter(term == "treatment") |>
select(study, estimate, std.error, conf.low, conf.high)
print(study_results)
Stage 2: Meta-Analysis of Study Effects
library(metafor)
ma_result <- rma(
yi = study_results$estimate,
sei = study_results$std.error,
method = "REML",
slab = study_results$study
)
summary(ma_result)
forest(ma_result, header = TRUE)
cat("I-squared:", round(ma_result$I2, 1), "%\n")
cat("Tau-squared:", round(ma_result$tau2, 4), "\n")
One-Stage IPD Meta-Analysis
Linear Mixed-Effects Model
library(lme4)
fit_ri <- lmer(
outcome ~ treatment + age + (1 | study),
data = ipd_data
)
summary(fit_ri)
confint(fit_ri)
fit_rt <- lmer(
outcome ~ treatment + age + (1 + treatment | study),
data = ipd_data
)
summary(fit_rt)
anova(fit_ri, fit_rt)
library(broom.mixed)
tidy(fit_rt, effects = "fixed", conf.int = TRUE) |>
filter(term == "treatment")
Binary Outcomes
library(lme4)
fit_logistic <- glmer(
outcome_binary ~ treatment + age + (1 + treatment | study),
family = binomial(link = "logit"),
data = ipd_data,
control = glmerControl(optimizer = "bobyqa", optCtrl = list(maxfun = 100000))
)
summary(fit_logistic)
exp(fixef(fit_logistic)["treatment"])
exp(confint(fit_logistic, parm = "treatment", method = "Wald"))
Survival Outcomes
library(survival)
library(coxme)
fit_strat <- coxph(
Surv(time, event) ~ treatment + age + strata(study),
data = ipd_data
)
summary(fit_strat)
fit_frailty <- coxph(
Surv(time, event) ~ treatment + age + frailty(study, distribution = "gamma"),
data = ipd_data
)
summary(fit_frailty)
library(coxme)
fit_coxme <- coxme(
Surv(time, event) ~ treatment + age + (1 | study),
data = ipd_data
)
summary(fit_coxme)
Combining IPD with Aggregate Data
Two-Stage with Combined Data
library(metafor)
ipd_studies <- ipd_data |>
group_by(study) |>
nest() |>
mutate(
model = map(data, ~lm(outcome ~ treatment, data = .x)),
results = map(model, ~tibble(
yi = coef(.x)["treatment"],
vi = vcov(.x)["treatment", "treatment"],
source = "IPD"
))
) |>
unnest(results) |>
select(study, yi, vi, source)
agd_studies <- tibble(
study = c("Study6", "Study7", "Study8"),
yi = c(2.5, 3.1, 1.8),
vi = c(0.5, 0.6, 0.4)^2,
source = "AgD"
)
all_studies <- bind_rows(ipd_studies, agd_studies)
ma_combined <- rma(yi, vi, data = all_studies, method = "REML")
summary(ma_combined)
ma_combined_sub <- rma(yi, vi, mods = ~ source, data = all_studies)
summary(ma_combined_sub)
One-Stage with Pseudo-IPD
library(multinma)
network <- combine_network(
set_ipd(ipd_studies_data, study, trt, y = outcome),
set_agd_arm(agd_studies_data, study, trt, y = mean_outcome, se = se_outcome)
)
fit_combined <- nma(
network,
trt_effects = "random",
regression = ~age
)
Treatment-Covariate Interactions
Within-Study vs Between-Study Effects
library(lme4)
ipd_data <- ipd_data |>
group_by(study) |>
mutate(
age_mean = mean(age),
age_centered = age - age_mean
) |>
ungroup()
fit_interaction <- lmer(
outcome ~ treatment + age_centered + age_mean +
treatment:age_centered + treatment:age_mean +
(1 + treatment | study),
data = ipd_data
)
summary(fit_interaction)
Testing Treatment-Effect Modification
library(lme4)
library(lmerTest)
fit_mod <- lmer(
outcome ~ treatment * age + (1 + treatment | study),
data = ipd_data
)
summary(fit_mod)
ipd_data$age_group <- cut(ipd_data$age, breaks = c(0, 50, 65, 100),
labels = c("Young", "Middle", "Old"))
results_by_age <- ipd_data |>
group_by(age_group) |>
nest() |>
mutate(
model = map(data, ~lmer(outcome ~ treatment + (1 | study), data = .x)),
effect = map(model, ~fixef(.x)["treatment"])
) |>
unnest(effect)
Handling Missing Data
Multiple Imputation for IPD-MA
library(mice)
library(mitml)
imp <- mice(
ipd_data,
method = c(
study = "",
treatment = "",
age = "2l.norm",
outcome = "2l.norm"
),
m = 20,
maxit = 10
)
fit_list <- with(imp, lmer(outcome ~ treatment + age + (1 | study)))
library(mitml)
pool_fit <- testEstimates(fit_list)
print(pool_fit)
Pattern-Mixture Models
delta <- c(0, 1, 2, 5)
results_sensitivity <- map_dfr(delta, function(d) {
imp_adjusted <- complete(imp, "long") |>
mutate(outcome = if_else(.imp > 0 & is_missing, outcome + d, outcome))
fit <- lmer(outcome ~ treatment + age + (1 | study),
data = imp_adjusted)
tibble(
delta = d,
estimate = fixef(fit)["treatment"],
se = sqrt(vcov(fit)["treatment", "treatment"])
)
})
IPD Network Meta-Analysis
library(multinma)
ipd_network <- set_ipd(
data = ipd_nma_data,
study = study,
trt = treatment,
y = outcome
)
ipd_network_bin <- set_ipd(
data = ipd_nma_data,
study = study,
trt = treatment,
r = events
)
nma_ipd <- nma(
ipd_network,
trt_effects = "random",
prior_intercept = normal(scale = 10),
prior_trt = normal(scale = 10),
prior_het = half_normal(scale = 1)
)
summary(nma_ipd)
relative_effects(nma_ipd)
IPD-NMA with Covariate Adjustment
library(multinma)
nma_adj <- nma(
ipd_network,
trt_effects = "random",
regression = ~age + sex,
class_interactions = "common"
)
predict(nma_adj, newdata = data.frame(age = 65, sex = 1))
Diagnostics and Model Checking
Residual Analysis
library(lme4)
library(DHARMa)
fit <- lmer(outcome ~ treatment + age + (1 + treatment | study), data = ipd_data)
resid_l1 <- residuals(fit, type = "pearson")
ranef_fit <- ranef(fit)$study
sim_res <- simulateResiduals(fit)
plot(sim_res)
plot(fitted(fit), resid_l1)
abline(h = 0, col = "red")
Influence Diagnostics
library(influence.ME)
infl <- influence(fit, group = "study")
cooks.distance(infl)
dfbetas(infl)
plot(infl, which = "cook")
Reporting IPD-MA Results
create_ipd_ma_summary <- function(fit_one_stage, fit_two_stage) {
summary_table <- tibble(
Method = c("One-stage (mixed effects)", "Two-stage (meta-analysis)"),
Estimate = c(
fixef(fit_one_stage)["treatment"],
fit_two_stage$beta
),
SE = c(
sqrt(vcov(fit_one_stage)["treatment", "treatment"]),
fit_two_stage$se
),
CI_Lower = Estimate - 1.96 * SE,
CI_Upper = Estimate + 1.96 * SE,
Heterogeneity = c(
VarCorr(fit_one_stage)$study["treatment", "treatment"],
fit_two_stage$tau2
)
)
return(summary_table)
}
Key Packages Summary
| Package | Purpose |
|---|
| lme4 | Linear/generalized mixed-effects models |
| metafor | Two-stage meta-analysis |
| coxme | Mixed-effects Cox models |
| survival | Stratified/frailty survival models |
| mice | Multiple imputation |
| mitml | MI pooling for multilevel |
| multinma | IPD network meta-analysis |
| ipdmeta | IPD-MA utilities |
| joineR | Joint models for IPD |
| DHARMa | Residual diagnostics |
Best Practices
- Data sharing: Establish data governance before IPD collection
- Harmonization: Standardize variable definitions across studies
- One vs two-stage: One-stage preferred for treatment-covariate interactions
- Random effects: Include random treatment effects to allow for heterogeneity
- Missing data: Use multilevel MI methods; conduct sensitivity analyses
- Confounding: Separate within vs between-study covariate effects
- Reporting: Follow PRISMA-IPD guidelines
- Sensitivity: Compare one-stage and two-stage results