flowchart TB
rct("Randomised controlled trial")
Epidemiology as driver of personalised care
Causal aim:
flowchart TB
rct("Randomised controlled trial")
Causal aim:
flowchart TB
rct("Randomised controlled trial") --> ma["Main analysis"]
Causal aim:
flowchart TB
rct("Randomised controlled trial") --> ma["Main analysis"]
ma --> ate(["Average treatment effect (ATE)"])
Causal aim:
flowchart TB
rct("Randomised controlled trial") --> ma["Main analysis"]
ma --> ate(["Average treatment effect (ATE)"])
ate --> pop("Population")
Causal aim:
flowchart TB
rct("Randomised controlled trial") --> ma["Main analysis"]
ma --> ate(["Average treatment effect (ATE)"])
ate --> pop("Population")
rct --> sa["Subgroup analysis"]
Causal aim:
flowchart TB
rct("Randomised controlled trial") --> ma["Main analysis"]
ma --> ate(["Average treatment effect (ATE)"])
ate --> pop("Population")
rct --> sa["Subgroup analysis"]
sa --> cate(["Conditional average <br> treatment effect (CATE)"])
Causal aim:
flowchart TB
rct("Randomised controlled trial") --> ma["Main analysis"]
ma --> ate(["Average treatment effect (ATE)"])
ate --> pop("Population")
rct --> sa["Subgroup analysis"]
sa --> cate(["Conditional average <br> treatment effect (CATE)"])
cate --> subpop("Subgroups in population")
Causal aim:
flowchart TB
rct("Randomised controlled trial") --> ma["Main analysis"]
ma --> ate(["Average treatment effect (ATE)"])
ate --> pop("Population")
rct --> sa["Subgroup analysis"]
sa --> cate(["Conditional average <br> treatment effect (CATE)"])
cate --> subpop("Subgroups in population")
rct --> path["Risk/effect modelling"]
Causal aim:
flowchart TB
rct("Randomised controlled trial") --> ma["Main analysis"]
ma --> ate(["Average treatment effect (ATE)"])
ate --> pop("Population")
rct --> sa["Subgroup analysis"]
sa --> cate(["Conditional average <br> treatment effect (CATE)"])
cate --> subpop("Subgroups in population")
rct --> path["Risk/effect modelling"]
path --> ite(["Individualised treatment <br> effect (ITE)"])
Causal aim:
flowchart TB
rct("Randomised controlled trial") --> ma["Main analysis"]
ma --> ate(["Average treatment effect (ATE)"])
ate --> pop("Population")
rct --> sa["Subgroup analysis"]
sa --> cate(["Conditional average <br> treatment effect (CATE)"])
cate --> subpop("Subgroups in population")
rct --> path["Risk/effect modelling"]
path --> ite(["Individualised treatment <br> effect (ITE)"])
ite --> ind("Individual people")
Average treatment effect: \[\mathbb{E}[Y^{a=1}] - \mathbb{E}[Y^{a=0}]\]
Useful for:
RCTs are powered to estimate the ATE.
Conditional average treatment effect: \[\mathbb{E}[Y^{a=1}|X] - \mathbb{E}[Y^{a=0}|X]\]
Useful for:
RCTs are rarely powered to estimate a CATE.
Individualised treatment effect:
(really just a CATE) \[\mathbb{E}[Y^{a=1}|X_1,X_2...X_p] - \mathbb{E}[Y^{a=0}|X_1,X_2...X_p]\]
Useful for:
RCTs are not powered to estimate an ITE
flowchart TB
rct("Randomised controlled trial <br> (RCT)") --> ma["Main analysis"]
ma --> ate(["Average treatment effect (ATE)"])
ate --> pop("Population")
rct --> sa["Subgroup analysis"]
sa --> cate(["Conditional average <br> treatment effect (CATE)"])
cate --> subpop("Subgroups in population")
rct --> path["Risk/effect modelling"]
path --> ite(["Individualised treatment <br> effect (ITE)"])
ite --> ind("Individual people")
flowchart TB
rct("Randomised controlled trial <br> (RCT)") --> ma["Main analysis"]
ma --> ate(["Average treatment effect (ATE)"])
ate --> pop("Population")
rct --> sa["Subgroup analysis"]
sa --> cate(["Conditional average <br> treatment effect (CATE)"])
cate --> subpop("Subgroups in population")
rct --> path["Risk/effect modelling"]
path --> ite(["Individualised treatment <br> effect (ITE)"])
ite --> ind("Individual people")
%% Custom style for node CATE and ITE
style cate fill:#F9A03F,stroke:#C36F09
style ite fill:#E79E9C,stroke:#6F1D1B
flowchart TB
rct("Pooled RCTs/ <br> Observational study") --> ma["Main analysis"]
ma --> ate(["Average treatment effect (ATE)"])
ate --> pop("Population")
rct --> sa["Subgroup analysis"]
sa --> cate(["Conditional average <br> treatment effect (CATE)"])
cate --> subpop("Subgroups in population")
rct --> path["Risk/effect modelling"]
path --> ite(["Individualised treatment <br> effect (ITE)"])
ite --> ind("Individual people")
We should note the scale of HTE: relative homogeneity may still give absolute heterogeneity:


(and more)
General concept: Incorporate individual baseline risk (\(E\)) into analyses
Important
When performing subgroup analyses, ensure that exchangeability holds
How to estimate the individual baseline risk?
Their baseline risk is their conditional probability of the outcome (\(Pr(Y|X_1,X_2,...X_p)\)) and can be estimated using an:
Important
Internal models should be developed in the population not receiving the treatment of interest
External models are available in abundance:
Regardless of the model used:
General concept: Directly estimate individualised treatment effect
In case exchangeability holds:
Several techniques for effect modelling,exist:
S-learners are the simplest method, as they rely on only a single model:
Note
\(M\) can be any model architecture we desire
An example using data from one of the first succesful trials of adjuvant chemotherapy in colon cancer (Levamisole + 5-FU vs. placebo) 1
# A tibble: 619 × 11
id trt female age obstruct perfor adhere nodes differ time death
<dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <fct> <dbl> <dbl>
1 1 1 0 43 0 0 0 5 2 1521 1
2 2 1 0 63 0 0 0 1 2 3087 0
3 3 0 1 71 0 0 1 7 2 963 1
4 4 1 1 66 1 0 0 6 2 293 1
5 5 0 0 69 0 0 0 22 2 659 1
6 6 1 1 57 0 0 0 9 2 1767 1
7 8 0 0 54 0 0 0 1 2 3192 0
8 10 1 1 68 0 0 0 1 2 3308 0
9 12 1 0 52 0 0 0 2 3 3309 0
10 13 0 0 64 0 0 0 1 2 2085 1
# ℹ 609 more rows
We also need a small function to get the outcome risk:
ar <- function(.data, # Data with predictors
trt_status, # Hypothetical treatment status
fit # Model fit
){
# Get linear predictor with hypothetical new data
lp <- predict(fit,
type = "lp",
newdata = mutate(.data, trt = trt_status))
# Get AR
ar <- 1 - (exp(-last(basehaz(fit))[["hazard"]]) ^ exp(lp))
# Return output
return(ar)
}
T-learners instead use a single model per treatment arm. In our example, that would be two (T) models.
# Subset data
dat_a1 <- filter(dat, trt == 1) # Treated population
dat_a0 <- filter(dat, trt == 0) # Untreated population
# Fit treated model
fit_tlearner_a1 <- coxph(Surv(time, death) ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat_a1)
# Fit untreated model
fit_tlearner_a0 <- coxph(Surv(time, death) ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat_a0)# Subset data
dat_a1 <- filter(dat, trt == 1) # Treated population
dat_a0 <- filter(dat, trt == 0) # Untreated population
# Fit treated model
fit_tlearner_a1 <- coxph(Surv(time, death) ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat_a1)
# Fit untreated model
fit_tlearner_a0 <- coxph(Surv(time, death) ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat_a0)
# Estimate ARD
vec_ard <- ar(dat, NULL, fit_tlearner_a1) - ar(dat, NULL, fit_tlearner_a0)An X-learner similarly fits two models, like the T-learner, but uses them in a cross-over manner (hence X):
# Subset data
dat_a1 <- filter(dat, trt == 1) # Treated population
dat_a0 <- filter(dat, trt == 0) # Untreated population
# Fit treated model
fit_xlearner_a1 <- coxph(Surv(time, death) ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat_a1)
# Fit untreated model
fit_xlearner_a0 <- coxph(Surv(time, death) ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat_a0)# Subset data
dat_a1 <- filter(dat, trt == 1) # Treated population
dat_a0 <- filter(dat, trt == 0) # Untreated population
# Fit treated model
fit_xlearner_a1 <- coxph(Surv(time, death) ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat_a1)
# Fit untreated model
fit_xlearner_a0 <- coxph(Surv(time, death) ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat_a0)
# Get counterfactual predictions
vec_cf_pred_a1 <- ar(dat_a1, NULL, fit_xlearner_a0) # Treated population
vec_cf_pred_a0 <- ar(dat_a0, NULL, fit_xlearner_a1) # Untreated population# Subset data
dat_a1 <- filter(dat, trt == 1) # Treated population
dat_a0 <- filter(dat, trt == 0) # Untreated population
# Fit treated model
fit_xlearner_a1 <- coxph(Surv(time, death) ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat_a1)
# Fit untreated model
fit_xlearner_a0 <- coxph(Surv(time, death) ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat_a0)
# Get counterfactual predictions
vec_cf_pred_a1 <- ar(dat_a1, NULL, fit_xlearner_a0) # Treated population
vec_cf_pred_a0 <- ar(dat_a0, NULL, fit_xlearner_a1) # Untreated population
# Impute treatment effects
dat_a1[["d"]] <- dat_a1[["death"]] - vec_cf_pred_a1
dat_a0[["d"]] <- vec_cf_pred_a0 - dat_a0[["death"]]# Subset data
dat_a1 <- filter(dat, trt == 1) # Treated population
dat_a0 <- filter(dat, trt == 0) # Untreated population
# Fit treated model
fit_xlearner_a1 <- coxph(Surv(time, death) ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat_a1)
# Fit untreated model
fit_xlearner_a0 <- coxph(Surv(time, death) ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat_a0)
# Get counterfactual predictions
vec_cf_pred_a1 <- ar(dat_a1, NULL, fit_xlearner_a0) # Treated population
vec_cf_pred_a0 <- ar(dat_a0, NULL, fit_xlearner_a1) # Untreated population
# Impute treatment effects
dat_a1[["d"]] <- dat_a1[["death"]] - vec_cf_pred_a1
dat_a0[["d"]] <- vec_cf_pred_a0 - dat_a0[["death"]]
# Fit treated model on imputed treatment effect
fit_xlearner_d_a1 <- lm(d ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat_a1)
# Fit untreated model on imputed treatment effect
fit_xlearner_d_a0 <- lm(d ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat_a0)# Subset data
dat_a1 <- filter(dat, trt == 1) # Treated population
dat_a0 <- filter(dat, trt == 0) # Untreated population
# Fit treated model
fit_xlearner_a1 <- coxph(Surv(time, death) ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat_a1)
# Fit untreated model
fit_xlearner_a0 <- coxph(Surv(time, death) ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat_a0)
# Get counterfactual predictions
vec_cf_pred_a1 <- ar(dat_a1, NULL, fit_xlearner_a0) # Treated population
vec_cf_pred_a0 <- ar(dat_a0, NULL, fit_xlearner_a1) # Untreated population
# Impute treatment effects
dat_a1[["d"]] <- dat_a1[["death"]] - vec_cf_pred_a1
dat_a0[["d"]] <- vec_cf_pred_a0 - dat_a0[["death"]]
# Fit treated model on imputed treatment effect
fit_xlearner_d_a1 <- lm(d ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat_a1)
# Fit untreated model on imputed treatment effect
fit_xlearner_d_a0 <- lm(d ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat_a0)
# Estimate propensity score
dat[["ps"]] <- predict(
# Propensity score model
glm(trt ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat,
family = binomial),
# Return PS
type = "response")# Subset data
dat_a1 <- filter(dat, trt == 1) # Treated population
dat_a0 <- filter(dat, trt == 0) # Untreated population
# Fit treated model
fit_xlearner_a1 <- coxph(Surv(time, death) ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat_a1)
# Fit untreated model
fit_xlearner_a0 <- coxph(Surv(time, death) ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat_a0)
# Get counterfactual predictions
vec_cf_pred_a1 <- ar(dat_a1, NULL, fit_xlearner_a0) # Treated population
vec_cf_pred_a0 <- ar(dat_a0, NULL, fit_xlearner_a1) # Untreated population
# Impute treatment effects
dat_a1[["d"]] <- dat_a1[["death"]] - vec_cf_pred_a1
dat_a0[["d"]] <- vec_cf_pred_a0 - dat_a0[["death"]]
# Fit treated model on imputed treatment effect
fit_xlearner_d_a1 <- lm(d ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat_a1)
# Fit untreated model on imputed treatment effect
fit_xlearner_d_a0 <- lm(d ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat_a0)
# Estimate propensity score
dat[["ps"]] <- predict(
# Propensity score model
glm(trt ~ age + female + obstruct + perfor + adhere + nodes + differ,
data = dat,
family = binomial),
# Return PS
type = "response")
# Final estimate
vec_ard <-
# No treatment
dat[["ps"]] * predict(fit_xlearner_d_a0, newdata = dat) +
# Treatment
(1 - dat[["ps"]]) * predict(fit_xlearner_d_a1, newdata = dat)Note
Despite all our best efforts to individualise estimates, model performance metrics generally reflect the average population or a coarse subgroup: a model that performs adequate on average does not reflect individual adequacy of the model
Approach 1: Grouping
Approach 2: Matching (calibration-for-benefit)
Simulated data (so that we know true benefit)
Our S-learner
Approach 1: C-for-benefit
Approach 2: concentration of benefit (\(C_b\))
Warning
Warning
Warning
Warning
“A prediction model that is not used has as much clinical impact as a prediction model that was never developed” - Roemer J. Janse, 2026
We can put as much effort, data, and complex statistics into our model as we want, but if it is not used, it did not matter. After development, there is a long road to walk from validation to market approval to implementation. Do not walk that road alone: involve your stakeholders.