| name | real-world-evidence |
| description | Real-world evidence analysis in R, including target trial emulation, propensity scores, external controls, and bias analysis. |
Real-World Evidence Analysis in R
Overview
Methods for analyzing real-world data (RWD) to generate real-world evidence (RWE). Covers target trial emulation, comparative effectiveness research, propensity score methods for observational data, external control arms, bias quantification, and sensitivity analysis for unmeasured confounding.
Target Trial Emulation
Conceptual Framework
Using TrialEmulation Package
library(TrialEmulation)
trial_data <- initiators(
data = rwd,
id = "patient_id",
period = "period",
treatment = "treatment",
outcome = "outcome",
eligible = "eligible",
outcome_cov = ~ age + sex + comorbidity,
model_var = "assigned_treatment",
switch_d_cov = ~ time_since_start + lag_outcome,
first_period = 1,
last_period = 52,
use_censor_weights = TRUE
)
fit <- trial_msm(
trial_data,
outcome_cov = ~ assigned_treatment * poly(follow_up, 2),
model_var = "assigned_treatment",
include_followup_time = TRUE,
include_trial_period = TRUE,
use_sample_weights = TRUE,
analysis_weights = "asis"
)
summary(fit)
Sequential Trial Emulation
rwd_expanded <- rwd |>
group_by(patient_id) |>
mutate(
eligible = check_eligibility(age, lab_values, prior_treatment),
trial_start = if_else(eligible & treatment_initiated, visit_date, NA)
) |>
ungroup()
Propensity Score Methods for RWE
Propensity Score Estimation
library(MatchIt)
library(WeightIt)
library(cobalt)
ps_model <- glm(
treatment ~ age + sex + bmi + smoking + diabetes +
prior_hosp + baseline_egfr + acei_use,
family = binomial,
data = rwd
)
rwd$ps <- predict(ps_model, type = "response")
ggplot(rwd, aes(x = ps, fill = factor(treatment))) +
geom_density(alpha = 0.5) +
labs(x = "Propensity Score", fill = "Treatment") +
theme_bw()
Inverse Probability Weighting (IPW)
library(WeightIt)
weights_att <- weightit(
treatment ~ age + sex + bmi + smoking + diabetes +
prior_hosp + baseline_egfr,
data = rwd,
method = "ps",
estimand = "ATT"
)
weights_ate <- weightit(
treatment ~ age + sex + bmi + smoking + diabetes,
data = rwd,
method = "ps",
estimand = "ATE"
)
library(cobalt)
bal.tab(weights_att, stats = c("m", "v", "ks"))
love.plot(weights_att, threshold = 0.1)
library(survey)
design <- svydesign(ids = ~1, weights = ~weights_att$weights, data = rwd)
fit_weighted <- svyglm(outcome ~ treatment, design = design, family = binomial)
summary(fit_weighted)
Overlap Weighting
library(PSweight)
ow_result <- PSweight(
ps.formula = treatment ~ age + sex + bmi + smoking + diabetes,
data = rwd,
yname = "outcome",
weight = "overlap"
)
summary(ow_result)
Propensity Score Matching
library(MatchIt)
match_nn <- matchit(
treatment ~ age + sex + bmi + smoking + diabetes + baseline_egfr,
data = rwd,
method = "nearest",
distance = "glm",
caliper = 0.2,
ratio = 1,
replace = FALSE
)
summary(match_nn)
plot(match_nn, type = "jitter")
matched_data <- match.data(match_nn)
fit_matched <- glm(outcome ~ treatment,
family = binomial,
data = matched_data,
weights = weights)
Doubly Robust Estimation
library(AIPW)
aipw_result <- AIPW$new(
Y = rwd$outcome,
A = rwd$treatment,
W = rwd |> select(age, sex, bmi, smoking, diabetes, baseline_egfr),
Q.SL.library = c("SL.glm", "SL.ranger", "SL.xgboost"),
g.SL.library = c("SL.glm", "SL.ranger"),
k_split = 5,
verbose = FALSE
)
aipw_result$fit()$summary()
aipw_result$summary()
External Control Arms
Historical Controls Integration
external_controls <- rwd |>
filter(
age >= 18 & age <= 75,
egfr >= 30,
no_prior_treatment == TRUE
)
library(MatchIt)
combined <- bind_rows(
trial_data |> mutate(source = "trial", treatment = treatment),
external_controls |> mutate(source = "external", treatment = 0)
)
match_ext <- matchit(
(source == "trial" & treatment == 0) ~ age + sex + stage + biomarker,
data = combined |> filter(treatment == 0 | source == "external"),
method = "nearest",
caliper = 0.2
)
matched_combined <- match.data(match_ext)
Propensity Score Integration
library(WeightIt)
weights_ext <- weightit(
source == "trial" ~ age + sex + stage + biomarker + region,
data = combined,
method = "ebal",
estimand = "ATT"
)
bal.tab(weights_ext)
library(survival)
fit_ext <- coxph(
Surv(time, event) ~ treatment + source,
data = combined,
weights = weights_ext$weights
)
Inverse Probability of Censoring Weighting
library(ipw)
cens_model <- glm(
censored ~ time_period + treatment + age + prior_event,
family = binomial,
data = rwd_long
)
rwd_long <- rwd_long |>
group_by(patient_id) |>
mutate(
p_uncensored = 1 - predict(cens_model, type = "response"),
ipcw = cumprod(p_uncensored)
) |>
ungroup()
rwd_long <- rwd_long |>
mutate(
sw = ps_weight * ipcw / mean(ps_weight * ipcw, na.rm = TRUE)
)
library(survey)
design_ipcw <- svydesign(ids = ~patient_id, weights = ~sw, data = rwd_long)
Time-Varying Confounding and MSMs
library(ipw)
temp_weights <- ipwtm(
exposure = treatment,
family = "binomial",
link = "logit",
numerator = ~ 1,
denominator = ~ age + cd4 + viral_load + prior_oi,
id = patient_id,
timevar = visit,
type = "first",
data = rwd_long
)
temp_cens <- ipwtm(
exposure = censored,
family = "binomial",
link = "logit",
numerator = ~ 1,
denominator = ~ age + cd4 + viral_load + treatment,
id = patient_id,
timevar = visit,
type = "first",
data = rwd_long
)
rwd_long$msm_weight <- temp_weights$ipw.weights * temp_cens$ipw.weights
rwd_long$msm_weight_trunc <- pmin(rwd_long$msm_weight,
quantile(rwd_long$msm_weight, 0.99))
library(geepack)
msm_fit <- geeglm(
outcome ~ treatment + time,
id = patient_id,
weights = msm_weight_trunc,
corstr = "independence",
family = binomial,
data = rwd_long
)
Adjusted Survival Curves
library(adjustedCurves)
adj_surv <- adjustedsurv(
data = rwd,
variable = "treatment",
ev_time = "time",
event = "event",
method = "direct",
outcome_model = coxph(Surv(time, event) ~ treatment + age + sex + stage,
data = rwd, x = TRUE),
conf_int = TRUE,
bootstrap = TRUE,
n_boot = 500
)
plot(adj_surv) +
labs(x = "Time (months)", y = "Survival Probability",
title = "Adjusted Survival Curves")
adj_surv_iptw <- adjustedsurv(
data = rwd,
variable = "treatment",
ev_time = "time",
event = "event",
method = "iptw_km",
treatment_model = glm(treatment ~ age + sex + stage, family = binomial,
data = rwd),
conf_int = TRUE
)
Sensitivity Analysis for Unmeasured Confounding
E-values
library(EValue)
evalue_result <- evalues.RR(
est = 1.8,
lo = 1.3,
hi = 2.5
)
print(evalue_result)
bias_plot(evalue_result)
Tip Analysis
library(tipr)
tip_result <- tip_coef(
effect_observed = log(1.8),
exposure_confounder_effect = seq(1, 3, 0.1),
confounder_outcome_effect = seq(1, 3, 0.1),
verbose = TRUE
)
plot(tip_result)
tip_result |>
filter(adjusted_effect < 0)
Quantitative Bias Analysis
library(episensr)
set.seed(123)
obs_table <- matrix(c(100, 50, 80, 120), nrow = 2)
pba_result <- probsens.conf(
obs_table,
reps = 10000,
seca.parms = list("trapezoidal", c(0.7, 0.8, 0.9, 0.95)),
spca.parms = list("trapezoidal", c(0.8, 0.85, 0.95, 1.0))
)
summary(pba_result)
Data Quality Assessment
completeness <- rwd |>
summarise(across(everything(), ~mean(!is.na(.)) * 100)) |>
pivot_longer(everything(), names_to = "variable", values_to = "pct_complete")
plausibility_checks <- rwd |>
summarise(
age_range_ok = all(age >= 0 & age <= 120),
dates_consistent = all(discharge_date >= admission_date, na.rm = TRUE),
lab_in_range = all(egfr >= 0 & egfr <= 200, na.rm = TRUE)
)
rwd |>
count(year = year(index_date)) |>
ggplot(aes(year, n)) +
geom_col() +
labs(title = "Patient Enrollment Over Time")
rwd |>
group_by(treatment) |>
summarise(
n = n(),
mean_age = mean(age),
pct_male = mean(sex == "male") * 100,
mean_followup = mean(followup_time)
)
Reporting RWE Studies
library(tableone)
vars <- c("age", "sex", "bmi", "smoking", "diabetes", "egfr", "stage")
cat_vars <- c("sex", "smoking", "diabetes", "stage")
tab1_unweighted <- CreateTableOne(
vars = vars,
strata = "treatment",
data = rwd,
factorVars = cat_vars,
test = FALSE
)
tab1_weighted <- svyCreateTableOne(
vars = vars,
strata = "treatment",
data = design_weighted,
factorVars = cat_vars,
test = FALSE
)
print(tab1_unweighted, smd = TRUE)
print(tab1_weighted, smd = TRUE)
Key Packages Summary
| Package | Purpose |
|---|
| TrialEmulation | Target trial emulation |
| WeightIt | Propensity score weighting |
| MatchIt | Propensity score matching |
| cobalt | Balance diagnostics |
| AIPW | Doubly robust estimation |
| adjustedCurves | Adjusted survival curves |
| tipr | Tipping point analysis |
| EValue | E-values for unmeasured confounding |
| ipw | Inverse probability weighting |
| episensr | Quantitative bias analysis |
Best Practices
- Protocol first: Define target trial before touching data
- Eligibility: Apply strict, transparent eligibility criteria
- Time zero: Align treatment initiation with follow-up start
- Immortal time: Avoid immortal time bias in study design
- Balance: Check and report covariate balance (SMD < 0.1)
- Positivity: Assess overlap and address violations
- Sensitivity: Quantify impact of unmeasured confounding
- Reporting: Follow RECORD/STROBE-RWE guidelines