mlumr implements Multilevel Unanchored Meta-Regression (ML-UMR), a population-adjusted indirect treatment comparison for disconnected evidence networks: individual patient data (IPD) for one treatment, aggregate data (AgD) for the comparator, and no common reference arm (e.g., comparing single-arm studies). It estimates treatment effects while adjusting for cross-trial differences in prognostic factors, extending Multilevel Network Meta-Regression (ML-NMR; Phillippo et al., 2020) to the unanchored setting.
It provides four estimators behind one data interface, two of them ML-UMR variants:
- ML-UMR (Bayesian): the shared prognostic factor assumption (SPFA) model and the relaxed SPFA model, fitted via Stan with quasi-Monte Carlo integration and a Gaussian copula for population adjustment
- STC (frequentist): one-arm simulated treatment comparison via comparator-population G-computation, with delta-method standard errors for binary, continuous, and count outcomes and a nonparametric bootstrap for time-to-event
- Naive (benchmark): unadjusted comparison of crude outcome summaries
Binary, continuous, count, and time-to-event outcomes are supported.
# From CRAN
install.packages("mlumr")
# Development version
# install.packages("remotes")
remotes::install_github("choxos/mlumr")mlumr fits models with Stan (default backend rstan, C++ generated by rstantools and compiled at install), so a C++ toolchain is required:
- macOS: Xcode Command Line Tools (
xcode-select --install) - Windows: Rtools
- Linux:
g++andmake(usually present by default)
A cmdstanr backend is optional; see Stan backend below.
library(mlumr)
set.seed(2026)
# --- Prepare IPD (index treatment) ---
n_A <- 500
ipd_df <- data.frame(
trt = "Drug_A",
outcome = rbinom(n_A, 1, 0.55),
age_group = rbinom(n_A, 1, 0.40),
sex = rbinom(n_A, 1, 0.55)
)
ipd <- set_ipd(ipd_df, treatment = "trt", outcome = "outcome",
covariates = c("age_group", "sex"))
# --- Prepare AgD (comparator) ---
agd_df <- data.frame(
trt = "Drug_B", n_total = 400, n_events = 148,
age_group_mean = 0.35, sex_mean = 0.50
)
agd <- set_agd(agd_df, treatment = "trt",
outcome_n = "n_total", outcome_r = "n_events",
cov_means = c("age_group_mean", "sex_mean"),
cov_types = c("binary", "binary"))
# --- Combine and add integration points ---
dat <- combine_data(ipd, agd)
dat <- add_integration(dat, n_int = 64,
age_group = distr(qbern, prob = age_group_mean),
sex = distr(qbern, prob = sex_mean)
)
# --- Run all three methods ---
naive_result <- naive(dat) # instant
stc_result <- stc(dat) # instant
fit <- mlumr(dat, model = "spfa", # Bayesian, a few minutes
prior_intercept = prior_normal(0, 10),
prior_beta = prior_normal(0, 2.5),
chains = 4, iter = 4000, warmup = 2000, seed = 42)
summary(fit)
marginal_effects(fit, effect = "lor")| Family | Outcome type | Link functions |
|---|---|---|
| Binomial | Binary (0/1) | logit (default), probit, cloglog |
| Normal | Continuous | identity (default), log |
| Poisson | Count + exposure | log (default) |
| Survival | Time-to-event | log (proportional hazards, or accelerated failure time) |
For survival outcomes the comparator arm is reconstructed pseudo-IPD
(event/censoring times digitized from a published Kaplan-Meier curve) supplied
via set_agd_surv(). The distribution argument selects one of nine
parametric forms (exponential, Weibull, Gompertz, and accelerated failure time
variants) or a flexible "mspline" / "pexp" baseline. predict() returns
survival, hazard, RMST, and median curves plus the time-varying marginal log
hazard ratio (type = "loghr"). marginal_effects() reports the hazard ratio
for proportional-hazards distributions on the natural scale (null 1, as for the
rate ratio). For accelerated failure time distributions it reports a time ratio
only when the two studies share one baseline shape and one coefficient vector;
under the default aux_by = ".study", or under the relaxed model, it reports
the exponentiated linear-predictor contrast (EXP_DELTA_ETA) instead. Both are
accompanied by the RMST difference and ratio, each reported with the
restriction horizon it was integrated to.
The baseline hazard is estimated separately for each study (aux_by = ".study", the default, as in multinma). The spelling matches multinma; the
meaning does not. In an anchored network a study-specific shape is a nuisance
parameter and within-study randomization still identifies the treatment effect.
Here each study contributes exactly one arm, so a study-specific shape and a
treatment-specific shape are perfectly aliased: nothing in the data separates
them. aux_by = "none" instead assumes one shared shape, which for
proportional-hazards distributions imposes proportional hazards across studies
and for accelerated failure time distributions imposes a common distributional
shape; that assumption is at least testable against the two observed
Kaplan-Meier curves. Neither choice is assumption-free, so fit both and report
which was used.
After marginalization the hazard ratio is generally time-varying in the SPFA
and relaxed models alike, because each arm weights the covariate distribution
by its own survival and the two risk sets diverge; study-specific shapes add
the ratio of the baselines on top of that. The scalar marginal hazard ratio is
therefore its value at one time, chosen with at_time, and the primary
reported estimand should be the loghr curve or the collapsible RMST effects.
See vignette("survival-outcomes").
# Index IPD with a Surv outcome; comparator from a digitized KM curve
ipd <- set_ipd(ipd_df, treatment = "trt", covariates = c("age", "sex"),
family = "survival", time = "time", status = "status")
agd <- set_agd_surv(km_df, treatment = "trt", time = "time", status = "status",
cov_means = c("age_mean", "sex_prop"),
cov_sds = c("age_sd", NA),
cov_types = c("continuous", "binary"))
dat <- add_integration(combine_data(ipd, agd), n_int = 64,
age = distr(qnorm, mean = age_mean, sd = age_sd),
sex = distr(qbern, prob = sex_mean))
fit <- mlumr(dat, model = "spfa", distribution = "weibull")
marginal_effects(fit, effect = "hr") # marginal hazard ratio, null 1
predict(fit, type = "rmst") # restricted mean survival time| Feature | ML-UMR SPFA | ML-UMR Relaxed | STC | Naive |
|---|---|---|---|---|
| Covariate adjustment | Joint model | Joint model | Outcome regression | None |
| Effect modification | Assumed absent | Estimated | Not captured | N/A |
| Uncertainty | Posterior | Posterior | Delta method (bootstrap for TTE) | Delta method |
| Population weighting | QMC integration | QMC integration | Comparator-population G-computation | Crude observed populations |
| Integration points required | Yes | Yes | Yes, except normal identity | No |
| Method | Data required | Type of ITC | Pairwise only | Type of treatment effect | Target population |
|---|---|---|---|---|---|
| MAIC | IPD + AgD | Anchored or unanchored | Yes | Marginal | Comparator |
| STC | IPD + AgD | Unanchored | Yes | Marginal | Comparator |
| ML-NMR | IPD + AgD | Anchored | No | Marginal or conditional | Any pre-specified target |
| ML-UMR | IPD + AgD | Unanchored | Yes | Marginal or conditional | Any pre-specified target |
The index population is the decision-relevant target in most health
technology assessment (HTA) settings:
cost-effectiveness models are built for the population a reimbursement decision
is about, which is normally the index trial's. MAIC and pairwise STC identify an
effect in the comparator population, so applying them to a decision problem
carries an extra, usually implicit, transport step. ML-UMR makes that step
explicit by standardizing each treatment model to the index population (or any
pre-specified covariate target via newdata). marginal_effects() reports both
populations; lead with the population relevant to the decision and show the
other as a sensitivity analysis. This standardization is itself a transport
step and relies on correct outcome models, the stated cross-treatment
assumptions, and adequate covariate overlap.
ML-UMR is most appropriate when:
- You have IPD for one treatment and AgD for the comparator
- No common reference arm connects the evidence (unanchored)
- Binary, continuous, count, or time-to-event outcomes are of interest
- Covariate distributions differ between trial populations
| Function | Purpose |
|---|---|
set_ipd(), set_agd() |
Prepare IPD and AgD with outcome and covariate specification |
set_agd_surv() |
Prepare comparator survival pseudo-IPD (reconstructed KM) |
combine_data() |
Combine IPD and AgD into a unified dataset |
add_integration() |
Generate QMC integration points with Gaussian copula |
make_knots() |
Choose M-spline knots for a flexible survival baseline |
mlumr() |
Fit ML-UMR SPFA or Relaxed model via Stan |
mlumr_engine() |
Get or set the Stan backend (rstan or cmdstanr) |
naive(), stc() |
Frequentist benchmark methods |
predict() |
Population-specific predicted outcomes |
marginal_effects() |
Posterior treatment effect summaries |
conditional_effects() |
Covariate-conditional treatment effects |
conditional_predict() |
Predictions at specific covariate values |
check_identification() |
Screen whether the aggregate rows can inform the relaxed model's comparator coefficients (binomial, normal, poisson) |
check_integration() |
Check that the integration points reproduce the declared AgD moments |
prior_sensitivity() |
Refit across a grid of prior scales, varying only the prior |
calculate_loo(), calculate_waic(), compare_models() |
Bayesian model comparison (LOO-CV, WAIC); survival_unit sets the pointwise unit for survival fits |
calculate_dic() |
Deviance information criterion, computed from the fit's own log likelihood |
plot(), mlumr_forest(), geom_km(), plot_prior_posterior() |
Forest, curve, and prior-versus-posterior figures |
The default backend is rstan. Users who prefer cmdstanr can switch after installation:
mlumr_engine("cmdstanr") # offers to install cmdstanr + CmdStan if needed
mlumr_engine("rstan") # switch back
mlumr_engine() # check current engineThe preference persists for the session. Set a permanent default in
.Rprofile with options(mlumr.stan_engine = "cmdstanr"), or override per fit:
mlumr(dat, model = "spfa", engine = "cmdstanr").
Some exported functions intentionally use names familiar from multinma
counterparts (set_ipd(), set_agd(), add_integration(),
unnest_integration(), distr(), marginal_effects(), make_knots(),
geom_km(), qbern()/pbern()/dbern(), qgamma()/pgamma()/dgamma(),
qlogitnorm()/plogitnorm()/dlogitnorm()).
Shared names signal related concepts, not drop-in compatibility. The data
objects, argument sets, supported evidence structures, and fitted models differ,
so translate a multinma specification explicitly and check each mlumr help page.
One difference is worth stating outright, because both packages spell it
"link": multinma::predict(type = "link") returns the average linear
predictor E[eta], and its marginal link-scale contrast is
marginal_effects(mtype = "link"). mlumr's predict(type = "link") is the
marginal one, the fitted link applied to the population-standardized response
mean, because every effect mlumr reports is standardized over a population and
there is no conditional estimand here for E[eta] to pair with.
For survival, aux_by = NULL resolves to ".study"; the mlumr-specific
aux_by = "none" shares one baseline shape across both studies.
The cleanest practice is still to use one package per session, matched to the network type (anchored connected → multinma; unanchored disconnected → mlumr). When both must be attached, disambiguate with the namespace prefix:
library(multinma)
library(mlumr)
# mlumr fits ML-UMR (disconnected, two-trial)
fit_umr <- mlumr::mlumr(dat, model = "spfa")
# multinma fits ML-NMR (connected network)
net_nmr <- multinma::set_ipd(pso_ipd, study = studyc, trt = trtc, r = pasi75)
fit_nmr <- multinma::nma(net_nmr, regression = ~ age:.trt)Detailed tutorials are available as package vignettes, in reading order:
Getting started
vignette("introduction"): overview, the unanchored problem, the four methods, and a reading roadmapvignette("data-preparation"): the four-step pipeline and the numerical integration that powers population adjustment
Outcome types (each a complete worked example)
vignette("binary-outcomes"): binary response (PASI 75, plaque psoriasis)vignette("continuous-outcomes"): continuous outcome (shoulder pain, ASD vs exercise therapy)vignette("count-outcomes"): count outcome (dmft dental caries, silver diamine fluoride vs nano-silver fluoride)vignette("survival-outcomes"): time-to-event (progression-free survival in multiple myeloma, lenalidomide vs thalidomide)
Methods
vignette("fitting-and-diagnostics"): sampler control, backends, priors, and MCMC diagnosticsvignette("choosing-a-method"): assumptions, model comparison, and a decision guide for ML-UMR vs STC vs naivevignette("subgroup-identification"): how many jointly defined aggregate subgroup rows the relaxed model needs, the geometry those rows must have, and how to readcheck_identification()
mlumr implements multilevel unanchored meta-regression (ML-UMR):
Chandler, C. & Ishak, J. (2026). "Anchors Away: Navigating Unanchored Indirect Comparisons with Multilevel Unanchored Meta-Regression (ML-UMR)." Preprint, arXiv:2606.20341 [stat.ME]. doi:10.48550/arXiv.2606.20341
Chandler, C. & Ishak, J. (2025). "Anchors Away: Navigating Unanchored Indirect Comparisons With Multilevel Unanchored Meta-Regression (ML-UMR)." ISPOR Europe 2025, MSR28. Value in Health, 28, S498. https://www.valueinhealthjournal.com/article/S1098-3015(25)05944-3/abstract
Chandler, C. & Ishak, J. (2026). "Surviving Unanchored Indirect Comparisons: An Extension of Multilevel Unanchored Meta-Regression (ML-UMR) for Survival Analyses." ISPOR 2026, MSR131. Value in Health, 29(S6). https://www.ispor.org/heor-resources/presentations-database/presentation-cti/ispor-2026/poster-session-3-3/surviving-unanchored-indirect-comparisons-an-extension-of-multilevel-unanchored-meta-regression-ml-umr-for-survival-analyses
ML-UMR is an adaptation of multilevel network meta-regression (ML-NMR) to the unanchored case, where no common comparator arm links the two studies:
Phillippo, D. M., Dias, S., Ades, A. E., Belger, M., Brnabic, A., Schacht, A., Saure, D., Kadziola, Z., & Welton, N. J. (2020). "Multilevel Network Meta-Regression for population-adjusted treatment comparisons." Journal of the Royal Statistical Society: Series A, 183(3), 1189--1210. doi:10.1111/rssa.12579
citation("mlumr")Each release is archived on Zenodo with a citable DOI (the DOI badge above appears after the first release). The repository ships machine-readable citation and software metadata for FAIR harvesters:
CITATION.cff: Citation File Format (GitHub "Cite this repository", Zenodo).codemeta.json: CodeMeta 2.0 software metadata crosswalk..zenodo.json: Zenodo deposit metadata (authors, ORCIDs, license, related identifiers).ro-crate-metadata.json: RO-Crate 1.1 packaging and provenance.inst/CITATION: the R-level citation (citation("mlumr")).
All carry the GPL-3.0 SPDX identifier, both authors' ORCIDs, and the upstream
ML-NMR / multinma provenance.
- Ahmad Sofi-Mahmudi (author, maintainer)
- Conor Chandler (author)
mlumr is an adaptation of the GPL-3 licensed
multinma package, reusing its
Sobol quasi-Monte Carlo and Gaussian-copula integration machinery and adding
Stan likelihoods for the unanchored two-treatment problem.
Portions of the package code, documentation, and Stan models were drafted and reviewed with the assistance of large language models: Anthropic's Claude Opus 4.6, 4.7, and 5 (via Claude Code) and OpenAI's ChatGPT 5.4, 5.5, and 5.6 Sol (via Codex). All methodological choices, design decisions, and the final review and validation were performed by the named authors, who take responsibility for the package's contents.
GPL-3. See https://www.gnu.org/licenses/gpl-3.0 for the full license text.
