Note
This article is translated from the Cole et al. (2023): Fusion Designs example in the documentation of delicatessen, deli’s Python counterpart.
The following is a replication of the cases described in Cole et al. (2023). Fusion designs combine information from multiple data sources to estimate quantities that cannot be identified from any single source alone. These designs are useful when the target population lacks the outcome of interest but shares covariates with a study population where the outcome is measured.
The three cases below illustrate: (1) transporting a proportion to a target population using inverse odds of sampling weights, (2) correcting for outcome misclassification using the Rogan-Gladen estimator, and (3) combining transport and misclassification correction in a single analysis.
Case 1: Transporting the proportion
In this scenario, we have two populations: a study sample (S=0) where the outcome Y is observed, and a target population (S=1) where Y is missing. Both populations share a covariate W. The goal is to transport the estimated proportion from the study to the target population using inverse odds of sampling weights derived from a logistic model for S \mid W.
# Data from Cole et al. (2023), Table 1
d1 <- data.frame(
Y = c(0, 1, 0, 1, 0, 0),
W = c(0, 0, 1, 1, 0, 1),
n = c(266, 67, 400, 267, 333, 167),
S = c(0, 0, 0, 0, 1, 1)
)
# Expand rows by count
d1 <- d1[rep(seq_len(nrow(d1)), d1$n), ]
d1$n <- NULL
rownames(d1) <- NULL
# Replace Y with 0 where S=1 (target population has no outcome)
d1$Y_fill <- ifelse(d1$S == 1, 0, d1$Y)
# Design matrix for sampling model: intercept + W
d1$intercept <- 1
# Extract arrays
y <- d1$Y_fill
s <- d1$S
W <- as.matrix(d1[, c("intercept", "W")])
estr <- m_estimate(
stacked_equations = function(theta) {
mu_w <- theta[1] # Weighted (transported) proportion
mu_n <- theta[2] # Naive (study-only) proportion
beta <- theta[3:4] # Sampling model coefficients
# Sampling model: logistic regression of S on W
ee_sm <- ee_regression(beta, X = W, y = s, model = "logistic")
# Predicted probability of being in sample
pi_s <- inverse_logit(as.numeric(W %*% beta))
# Inverse odds of sampling weights
iosw <- (1 - s) * pi_s / (1 - pi_s)
# Weighted proportion (transported to target)
ee_wprop <- matrix(iosw * (y - mu_w), nrow = 1)
# Naive proportion (study sample only)
ee_nprop <- matrix((1 - s) * (y - mu_n), nrow = 1)
rbind(ee_wprop, ee_nprop, ee_sm)
},
init = c(0.5, 0.5, 0., 0.)
)
data.frame(
Param = c("mu_weighted", "mu_naive", "beta_0", "beta_1"),
Coef = round(estr@theta, 4),
LCL = round(confint(estr)[, 1], 4),
UCL = round(confint(estr)[, 2], 4)
)
#> Param Coef LCL UCL
#> theta_1 mu_weighted 0.2677 0.2354 0.3000
#> theta_2 mu_naive 0.3340 0.3048 0.3632
#> theta_3 beta_0 0.0000 -0.1519 0.1519
#> theta_4 beta_1 -1.3848 -1.6125 -1.1571The weighted (transported) proportion adjusts for the difference in the distribution of W between the study and target populations. The naive proportion reflects the unadjusted estimate from the study sample alone.
Case 2: Misclassified proportion (Rogan-Gladen)
When the outcome is subject to misclassification, the Rogan-Gladen estimator uses external validation data to estimate sensitivity and specificity, then corrects the observed proportion. Here, R=1 denotes the main study (where only the mismeasured Y^* is observed), and R=0 denotes the external validation sample (where both Y and Y^* are observed).
estr <- m_estimate(
stacked_equations = function(theta) {
ee_rogan_gladen(theta, y = d2$Y, y_star = d2$Y_star, r = d2$R)
},
init = c(0.5, 0.5, 0.75, 0.75)
)
data.frame(
Param = c("Corrected", "Mismeasured", "Sensitivity", "Specificity"),
Coef = round(estr@theta, 4),
LCL = round(confint(estr)[, 1], 4),
UCL = round(confint(estr)[, 2], 4)
)
#> Param Coef LCL UCL
#> corrected_proportion Corrected 0.8015 0.7243 0.8787
#> naive_proportion Mismeasured 0.7158 0.6871 0.7445
#> sensitivity Sensitivity 0.8430 0.7971 0.8888
#> specificity Specificity 0.7978 0.7143 0.8812The corrected proportion adjusts the mismeasured proportion using the estimated sensitivity and specificity from the validation sample. This demonstrates the Rogan-Gladen identity: \mu = (\mu^* + \text{specificity} - 1) / (\text{sensitivity} + \text{specificity} - 1).
Case 3: Combined transport and misclassification
In this case, we combine both ideas: the outcome is misclassified in the study population and must be transported to a target population. We have three data sources:
- S=1: Main study with mismeasured outcome Y^*
- S=2: Target population with no outcome
- S=3: Validation sample with both Y and Y^*
The stacked estimating equations include: (1) the Rogan-Gladen correction applied with inverse odds of sampling weights to transport the corrected proportion, and (2) a logistic sampling model to estimate the weights. The main study (S=1) contributes the mismeasured outcome and the covariate W, the target population (S=2) contributes only W, and the validation sample (S=3) contributes both Y and Y^*. Missing entries in each source are filled with a placeholder value and excluded from the relevant estimating functions by construction.
# Combined fusion data from Cole et al. (2023) rejoinder
d3 <- data.frame(
Y_star = c(0, 1, 0, 1, NA, NA, 1, 0, 1, 0),
Y = c(NA, NA, NA, NA, NA, NA, 1, 1, 0, 0),
W = c(0, 0, 1, 1, 0, 1, NA, NA, NA, NA),
n = c(266, 67, 400, 267, 333, 167, 180, 20, 60, 240),
S = c(1, 1, 1, 1, 2, 2, 3, 3, 3, 3)
)
# Expand rows by count
d3 <- d3[rep(seq_len(nrow(d3)), d3$n), ]
d3$n <- NULL
rownames(d3) <- NULL
d3$intercept <- 1
# Replace missing values with a placeholder; these rows are zeroed out of the
# estimating functions where they do not apply
y_no_nan <- ifelse(is.na(d3$Y), -1, d3$Y)
ystar_no_nan <- ifelse(is.na(d3$Y_star), -1, d3$Y_star)
W_no_nan <- as.matrix(
data.frame(intercept = d3$intercept, W = ifelse(is.na(d3$W), -1, d3$W))
)
# Population indicators
s1 <- as.numeric(d3$S == 1) # Main study (mismeasured outcome)
s2 <- as.numeric(d3$S == 2) # Target population
s3 <- as.numeric(d3$S == 3) # Validation sample
estr <- m_estimate(
stacked_equations = function(theta) {
param <- theta[1:4] # Measurement error parameters
beta <- theta[5:6] # Sampling model coefficients
# Sampling model: logistic regression of the target indicator on W,
# restricted to the main study and target populations (S=1 or S=2)
ee_sm <- ee_regression(beta, X = W_no_nan, y = s2, model = "logistic")
ee_sm <- ee_sm * matrix(1 - s3, nrow = nrow(ee_sm),
ncol = ncol(ee_sm), byrow = TRUE)
# Inverse odds of sampling weights: odds weight for the main study,
# one for the validation sample, zero for the target population
pi_s <- inverse_logit(as.numeric(W_no_nan %*% beta))
iosw <- s1 * pi_s / (1 - pi_s) + s3
# Weighted Rogan-Gladen correction, restricted to S=1 or S=3
ee_rg <- ee_rogan_gladen(param, y = y_no_nan, y_star = ystar_no_nan,
r = s1, weights = iosw)
ee_rg <- ee_rg * matrix(1 - s2, nrow = nrow(ee_rg),
ncol = ncol(ee_rg), byrow = TRUE)
rbind(ee_rg, ee_sm)
},
init = c(0.5, 0.5, 0.75, 0.75, 0., 0.),
solver = "lm"
)
se <- sqrt(diag(vcov(estr)))
data.frame(
Param = c("Corrected_transported", "Mismeasured_weighted",
"Sensitivity", "Specificity", "beta_0", "beta_1"),
Coef = round(estr@theta, 4),
SE = round(se, 4),
LCL = round(confint(estr)[, 1], 4),
UCL = round(confint(estr)[, 2], 4)
)
#> Param Coef SE LCL UCL
#> theta_1 Corrected_transported 0.0967 0.0381 0.0220 0.1714
#> theta_2 Mismeasured_weighted 0.2677 0.0165 0.2354 0.3000
#> theta_3 Sensitivity 0.9000 0.0212 0.8584 0.9416
#> theta_4 Specificity 0.8000 0.0231 0.7547 0.8453
#> theta_5 beta_0 0.0000 0.0775 -0.1519 0.1519
#> theta_6 beta_1 -1.3848 0.1162 -1.6125 -1.1571This combined estimator simultaneously corrects for misclassification and transports the result to the target population. The sandwich variance estimator properly accounts for the uncertainty in all nuisance parameters (sensitivity, specificity, and sampling weights), yielding valid confidence intervals for the transported, corrected proportion.
References
Cole SR, Edwards JK, Breskin A, Rosin S, Zivich PN, Shook-Sa BE, & Hudgens MG. (2023). Illustration of two fusion designs and estimators. American Journal of Epidemiology, 192(3), 467-474.