Doubly Robust MAIC for HTA: A Complete Worked Example in Advanced NSCLC

drMAIC Package Authors

2026-03-25

1 Introduction

1.1 Background

In health technology appraisal (HTA), regulators and payers frequently require indirect treatment comparisons (ITC) when no head-to-head randomised controlled trial (RCT) exists between treatments of interest. A common scenario — particularly in oncology — is the unanchored indirect comparison: two single-arm trials, each testing a different treatment, with no common comparator arm.

Matching-Adjusted Indirect Comparison (MAIC) [@signorovitch2010] addresses this by reweighting the individual patient data (IPD) from one trial to match the aggregate baseline characteristics of the comparator. However, standard MAIC depends entirely on the weighting model being correctly specified.

The Doubly Robust MAIC (DR-MAIC) implemented in this package combines: 1. Inverse probability weighting (standard MAIC) 2. Outcome regression (Standardised Treatment Comparison, STC / g-computation)

…into a single estimator that is consistent if either component is correctly specified [@remiroazocar2022; @lunceford2004; @tan2010].

1.2 Package scope

The drMAIC package is aligned with:


2 Statistical Background

2.1 Standard MAIC

Given IPD from study A with covariates \(X_i\) and outcomes \(Y_i\), and target aggregate statistics \(\bar{X}_B\) from study B, MAIC weights are:

\[w_i = \exp(X_i^\top \hat\lambda)\]

where \(\hat\lambda\) solves: \[\sum_{i=1}^n w_i X_i = n \bar{X}_B \quad (\text{moment-matching conditions})\]

The Effective Sample Size (ESS) quantifies information loss from reweighting: \[ESS = \frac{(\sum_i w_i)^2}{\sum_i w_i^2}\]

Low ESS (< 30% of \(n\)) indicates limited population overlap and is a key validity concern per NICE TSD 18.

2.2 Doubly Robust Estimator

The DR-MAIC estimator:

\[\hat\theta_{DR} = \underbrace{\sum_i \omega_i \hat m(X_i)}_{\text{STC (g-computation)}} + \underbrace{\sum_i \omega_i \left(Y_i - \hat m(X_i)\right)}_{\text{IPW bias correction}}\]

where \(\omega_i = w_i / \sum w_i\) are normalized weights and \(\hat m(X_i)\) is the predicted outcome from an outcome regression model.

Double robustness: The estimator is consistent if either: - The weights correctly balance \(X\) between populations (even if \(\hat m\) is wrong), or - The outcome model \(\hat m\) is correctly specified (even if weights are imperfect)


3 Worked Example: Advanced NSCLC

3.1 Data

library(drMAIC)

data(nsclc_ipd)
data(nsclc_agd)

# IPD from Study A (index trial — immunotherapy)
cat("=== Study A: IPD Summary ===\n")
#> === Study A: IPD Summary ===
cat(sprintf("n = %d patients\n", nrow(nsclc_ipd)))
#> n = 200 patients
cat(sprintf("Response rate: %.1f%%\n", 100 * mean(nsclc_ipd$response)))
#> Response rate: 38.0%
cat(sprintf("Mean age: %.1f years\n", mean(nsclc_ipd$age)))
#> Mean age: 62.2 years
cat(sprintf("%% ECOG 1/2: %.1f%%\n", 100 * mean(nsclc_ipd$ecog)))
#> % ECOG 1/2: 44.0%
cat(sprintf("%% Ever-smoker: %.1f%%\n", 100 * mean(nsclc_ipd$smoker)))
#> % Ever-smoker: 74.0%

# AgD from Study B (comparator trial)
cat("\n=== Study B: AgD ===\n")
#> 
#> === Study B: AgD ===
cat(sprintf("n = %d patients\n", nsclc_agd$n_agd))
#> n = 185 patients
cat(sprintf("Response rate: %.1f%%\n", 100 * nsclc_agd$response_rate))
#> Response rate: 34.0%
cat(sprintf("Mean age: %.1f years\n", nsclc_agd$mean_age))
#> Mean age: 66.8 years
cat(sprintf("%% ECOG 1/2: %.1f%%\n", 100 * nsclc_agd$prop_ecog1))
#> % ECOG 1/2: 55.0%
cat(sprintf("%% Ever-smoker: %.1f%%\n", 100 * nsclc_agd$prop_smoker))
#> % Ever-smoker: 80.0%

Notice that Study B has an older, sicker population — this population imbalance is exactly what MAIC corrects for.

3.2 Step 1: Compute MAIC Weights

# Define target moments from Study B
target_moments <- c(
  age    = nsclc_agd$mean_age,
  ecog   = nsclc_agd$prop_ecog1,
  smoker = nsclc_agd$prop_smoker
)

# Compute entropy-balancing weights
w <- compute_weights(
  ipd            = nsclc_ipd,
  target_moments = target_moments,
  match_vars     = c("age", "ecog", "smoker"),
  verbose        = TRUE
)
#> 
#> [drMAIC] Weight computation complete
#>   n_original = 200 | ESS = 149.4 (74.7%) | Convergence: 0

3.3 Step 2: Covariate Balance Diagnostics

diag <- maic_diagnostics(w, plot_type = "all")
#> 
#> === Covariate Balance Diagnostics ===
#>   n_original     : 200
#>   ESS            : 149.4 (74.7% of n)
#>   SMD threshold  : 0.10
#>   Vars balanced  : 3 / 3
#> 
#>   Balance table (|SMD|):
#>  Variable Abs_SMD_before Abs_SMD_after Balanced
#>       age         0.4961             0     TRUE
#>      ecog         0.2210             0     TRUE
#>    smoker         0.1364             0     TRUE
#> 
#>   Weight statistics:
#>     Min      : 0.1040
#>     Median   : 0.7301
#>     Max      : 2.8846
#>     CV       : 58.36%
#>     >5x mean : 0 patients (0.0%)
diag$love_plot
Love Plot — covariate balance
Love Plot — covariate balance
diag$weight_plot
Weight distribution
Weight distribution

The Love plot shows that all covariates achieve |SMD| < 0.10 after weighting (the NICE TSD 18 recommended threshold), confirming successful covariate balance.

3.4 Step 3: Check Assumptions

check_assumptions(w, ess_threshold = 30, smd_threshold = 0.10)
#> 
#> === DR-MAIC Assumption Checklist ===
#> (NICE DSU TSD 18 | Cochrane Handbook Ch.23 | ISPOR)
#> 
#>   [PASS]  A1. Adequate ESS (positivity / overlap)
#>          Detail   : ESS = 149.4 (74.7%), threshold = 30%
#>          Reference: NICE TSD 18 Section 3.2
#> 
#>   [PASS]  A2. Covariate balance achieved
#>          Detail   : Worst |SMD| = 0.0000 for 'ecog', threshold = 0.10
#>          Reference: NICE TSD 18 Section 3.4 | Austin and Stuart (2015)
#> 
#>   [PASS]  A3. Weight optimisation converged
#>          Detail   : Convergence code: 0
#>          Reference: NICE TSD 18 Section 3.1
#> 
#>   Summary: 3 PASS | 0 WARN | 0 FAIL

3.5 Step 4: DR-MAIC Estimation

result <- dr_maic(
  maic_weights        = w,
  outcome_var         = "response",
  outcome_type        = "binary",
  comparator_estimate = nsclc_agd$response_rate,
  comparator_se       = nsclc_agd$response_se,
  effect_measure      = "OR"
)

print(result)
#> 
#> === Doubly Robust MAIC (drMAIC) ===
#>   Outcome type      : binary
#>   Effect measure    : OR
#>   ESS               : 149.4 / 200
#> 
#>   Estimates in target (B) population:
#>     MAIC (IPW)      : 0.3565
#>     STC (g-comp)    : 0.3602
#>     DR-MAIC         : 0.3565
#>     DR correction   : -0.0038
#>     Comparator (B)  : 0.3400
#> 
#>   Indirect Treatment Comparison (A vs B):
#>     MAIC ITC (OR)    : 0.0726
#>     STC  ITC (OR)    : 0.0890
#>     DR   ITC (OR)    : 0.0726  [SE=0.0349]
#>     95% CI (exp scale): [1.0041, 1.1515]

3.5.1 Interpreting the three estimators

Estimator Description Robust to
MAIC (IPW) Re-weighted outcome mean Outcome model misspecification
STC (g-comp) Outcome model prediction Weight misspecification
DR-MAIC Augmented combination Misspecification of either component

The DR augmentation term (the difference between DR and STC) quantifies the residual imbalance not captured by the outcome model — ideally close to zero.

3.6 Step 5: Bootstrap Confidence Intervals

# Run 1000 bootstrap replicates (BCa method recommended by NICE TSD 18)
boot_res <- bootstrap_ci(
  dr_maic_result = result,
  R              = 1000,
  ci_type        = "bca",
  seed           = 2024
)
print(boot_res)
boot_res$boot_plot
#> 
#> === Bootstrap CIs (perc, 95%, R=200) ===
#>   MAIC ITC (OR): [-0.2562, 0.3690]
#>   STC  ITC (OR): [-0.2167, 0.3895]
#>   DR   ITC (OR): [-0.2562, 0.3690]  (SE=0.1578)
#>   DR ITC (exp scale): [0.7740, 1.4463]

3.7 Step 6: Sensitivity Analysis

sa <- sensitivity_analysis(
  dr_maic_result   = result,
  trim_percentiles = c(0.90, 0.95, 0.99),
  lovo             = TRUE
)
#> 
#> === Sensitivity Analysis: E-value (Unmeasured Confounding) ===
#>   Effect measure    : OR
#>   ITC estimate      : 0.0726
#>   E-value (estimate): 1.233
#>   E-value (CI bound): 1.048
#>   Interpretation: An unmeasured confounder would need a >=1.2-fold association
#>   with BOTH treatment and outcome to fully explain the observed effect.
#> 
#> === Sensitivity Analysis: Weight Trimming ===
#>  Trim_pct n_trimmed   ESS theta_dr ITC_dr
#>        90        17  96.2   0.3916 0.2227
#>        95        10 116.4   0.3437 0.0165
#>        99         2 142.1   0.3470 0.0309
#>       100         0 149.4       NA     NA
#> 
#> === Sensitivity Analysis: Leave-One-Variable-Out (LOVO) ===
#>  Excluded_var   ESS theta_dr ITC_dr
#>           age 185.9   0.3860 0.1992
#>          ecog 154.9   0.3573 0.0760
#>        smoker 151.0   0.3514 0.0503
if (!is.null(sa$trim_plot)) sa$trim_plot
Weight trimming sensitivity
Weight trimming sensitivity
if (!is.null(sa$lovo_plot)) sa$lovo_plot
Leave-one-variable-out sensitivity
Leave-one-variable-out sensitivity

E-value interpretation: An unmeasured confounder would need at least a 1.23-fold association with both treatment and outcome to fully explain away the observed treatment effect. Values > 2 generally indicate a robust finding.

3.8 Step 7: NICE Report

nice_report(
  dr_maic_result   = result,
  bootstrap_result = boot_res,
  sensitivity_result = sa,
  study_a_name     = "KEYNOTE-024 (simulated)",
  study_b_name     = "IMpower150 (simulated)",
  indication       = "Advanced / Metastatic NSCLC",
  treatment_a      = "Pembrolizumab (simulated)",
  treatment_b      = "Atezo + Bev + Chemo (simulated)"
)
#> ======================================================================
#>   DOUBLY ROBUST MAIC (DR-MAIC) - ANALYSIS REPORT
#>   Aligned with NICE DSU TSD 18 | Cochrane Handbook Ch.23 | ISPOR
#> ======================================================================
#>   Date            : 25 March 2026
#>   Indication      : Advanced / Metastatic NSCLC
#>   Index trial     : KEYNOTE-024 (simulated) (Pembrolizumab (simulated))
#>   Comparator      : IMpower150 (simulated) (Atezo + Bev + Chemo (simulated))
#>   Effect measure  : OR
#>   Package         : drMAIC v0.1.0
#> 
#> ======================================================================
#>   1. POPULATION CHARACTERISTICS
#> ======================================================================
#>   IPD (Study A)   : n = 200 patients
#>   AgD (Study B)   : aggregate data
#> 
#>   Matching variables and target moments (Study B):
#>   Variable              Target value (Study B)
#>   --------------------  --------------------
#>   age                   66.8000
#>   ecog                  0.5500
#>   smoker                0.8000
#> 
#> ======================================================================
#>   2. WEIGHT ESTIMATION
#> ======================================================================
#>   Method          : Entropy balancing (exponential tilting)
#>   Convergence     : SUCCESS (code=0)
#>   n (original)    : 200
#>   ESS             : 149.4 (74.7% of n)
#>   Weight range    : [0.1040, 2.8846]
#>   Weight CV       : 58.4%
#> 
#> ======================================================================
#>   3. COVARIATE BALANCE (NICE TSD 18 FORMAT)
#> ======================================================================
#>   Variable              |SMD| pre  |SMD| post  Balanced?
#>   --------------------  --------  --------  ---------
#>   age                     0.4961    0.0000  [YES]
#>   ecog                    0.2210    0.0000  [YES]
#>   smoker                  0.1364    0.0000  [YES]
#> 
#>   Reference threshold: |SMD| < 0.10 per Austin & Stuart (2015)
#> 
#> ======================================================================
#>   4. TREATMENT EFFECT ESTIMATES
#> ======================================================================
#>   Estimator                               OR
#>   ------------------------------  ----------
#>   Standard MAIC (IPW)                 0.0726
#>   STC (g-computation)                 0.0890
#>   DR-MAIC (doubly robust)             0.0726  <-- PRIMARY ESTIMATE
#>   DR augmentation term               -0.0038
#> 
#> ======================================================================
#>   5. UNCERTAINTY QUANTIFICATION (BOOTSTRAP)
#> ======================================================================
#>   Method          : Bootstrap (R=200, PERC)
#>   SE (bootstrap)  : 0.1578
#>   95% CI (OR)  : [-0.2562, 0.3690]
#>   95% CI (exp)    : [0.7740, 1.4463]
#> 
#> ======================================================================
#>   6. SENSITIVITY ANALYSIS
#> ======================================================================
#>   E-value (main estimate) : 1.233
#>   E-value (CI bound)      : 1.048
#>   Interpretation: Unmeasured confounders with >=1.2-fold association
#>   with treatment and outcome would be needed to fully explain the result.
#> 
#>   Weight trimming range (ITC_DR): [0.0165, 0.2227]
#> 
#> ======================================================================
#>   7. ASSUMPTIONS AND LIMITATIONS (per NICE TSD 18 / Cochrane)
#> ======================================================================
#>   1. Transportability: The IPD-weighted population is exchangeable with
#>      the target population of Study B on all effect-modifying covariates.
#>      Unverifiable from data alone; clinical expert review required.
#>      [Cochrane Handbook 23.1.3 / NICE TSD 18 Section 4]
#>   
#>   2. No unmeasured effect modifiers: All prognostic factors and effect
#>      modifiers that differ between populations are included in the
#>      matching set. E-value quantifies robustness to violations.
#>      [NICE TSD 18 Assumption A2]
#>   
#>   3. Positivity (overlap): Sufficient covariate overlap exists between
#>      the IPD and AgD target populations. Assessed via ESS and weight
#>      distribution; extreme ESS loss indicates potential violation.
#>      [NICE TSD 18 Section 3.2]
#>   
#>   4. Consistency (SUTVA): No interference between subjects; treatment
#>      effect is well-defined. Assumed throughout.
#>   
#>   5. Correct model specification (double robustness): DR-MAIC is
#>      consistent if EITHER the weight model OR the outcome regression
#>      model is correctly specified. Sensitivity to specification
#>      assessed via the DR augmentation term and LOVO analysis.
#>      [Remiro-Azocar et al., 2022 Stat Med] 
#> 
#> ======================================================================
#>   8. METHODS PARAGRAPH (for submission)
#> ======================================================================
#>   A doubly robust matching-adjusted indirect comparison (DR-MAIC) was
#>   conducted to estimate the comparative effectiveness of Pembrolizumab (simulated) (KEYNOTE-024 (simulated)) versus
#>   Atezo + Bev + Chemo (simulated) (IMpower150 (simulated)) in patients with Advanced / Metastatic NSCLC. Individual patient data (IPD; n=200) from
#>   KEYNOTE-024 (simulated) were reweighted to match the aggregate baseline characteristics of IMpower150 (simulated)
#>   by estimating entropy balancing weights (Signorovitch et al., 2010) for
#>   the following effect-modifying covariates: age, ecog, smoker. The effective sample size
#>   (ESS) after reweighting was 149.4 (74.7% of original n). Covariate balance
#>   was assessed using absolute standardised mean differences (|SMD|), with
#>   a threshold of 0.10 per Austin and Stuart (2015).
#> 
#>   A doubly robust (DR/augmented MAIC) estimator was applied, combining
#>   inverse probability weighting (standard MAIC) with outcome regression
#>   (standardised treatment comparison, STC) in a single estimator that is
#>   consistent under misspecification of either component (Lunceford &
#>   Davidian, 2004; Tan, 2010; Remiro-Azocar et al., 2022). The indirect
#>   treatment comparison was expressed as the OR. Uncertainty was quantified
#>   using bias-corrected and accelerated (BCa) bootstrap confidence intervals
#>   (Efron & Tibshirani, 1993). Sensitivity analyses included E-value
#>   computation (VanderWeele & Ding, 2017), weight trimming at the 90th,
#>   95th and 99th percentile, and leave-one-variable-out analysis.
#>   All analyses were conducted in R using the drMAIC package v0.1.0,
#>   in accordance with NICE DSU Technical Support Document 18 (Phillippo
#>   et al., 2016) and Cochrane Handbook Chapter 23 guidance.
#> ======================================================================

4 Advanced Usage

4.1 Adding second-moment matching (mean + SD)

For continuous variables, you can match on both mean and standard deviation:

w2 <- compute_weights(
  ipd            = nsclc_ipd,
  target_moments = c(age    = nsclc_agd$mean_age,
                     age_sd = nsclc_agd$sd_age,
                     ecog   = nsclc_agd$prop_ecog1,
                     smoker = nsclc_agd$prop_smoker),
  match_vars      = c("age", "ecog", "smoker"),
  match_var_types = c(age = "mean_sd", ecog = "proportion", smoker = "proportion")
)

4.2 Additional prognostic covariates in outcome model

Including additional prognostic variables in the outcome model can improve efficiency of the DR estimator (even without including them in matching):

result2 <- dr_maic(
  maic_weights          = w,
  outcome_var           = "response",
  outcome_type          = "binary",
  comparator_estimate   = nsclc_agd$response_rate,
  comparator_se         = nsclc_agd$response_se,
  additional_covariates = c("pdl1_high", "prior_lines"),  # efficiency gain
  effect_measure        = "OR"
)

4.3 Time-to-event outcomes

result_os <- dr_maic(
  maic_weights        = w,
  outcome_var         = "os_event",
  outcome_type        = "tte",
  time_var            = "os_time",
  comparator_estimate = log(0.78),  # log-HR from comparator
  comparator_se       = 0.12,
  effect_measure      = "HR"
)

5 Reporting Checklist

Per NICE DSU TSD 18 and ISPOR guidance, a complete DR-MAIC submission should include:


6 References