Update repo
This commit is contained in:
@@ -3,6 +3,7 @@ title: "Target Trial Basics: Early Vasopressors in Septic Shock"
|
||||
format:
|
||||
html:
|
||||
embed-resources: true
|
||||
docx: default
|
||||
execute:
|
||||
echo: true
|
||||
warning: false
|
||||
|
||||
@@ -3,6 +3,7 @@ title: "Explore Simulated ICU Data"
|
||||
format:
|
||||
html:
|
||||
embed-resources: true
|
||||
docx: default
|
||||
execute:
|
||||
echo: true
|
||||
warning: false
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
---
|
||||
title: "Why The Naive Analysis Is Biased"
|
||||
format:
|
||||
html:
|
||||
embed-resources: true
|
||||
docx: default
|
||||
execute:
|
||||
echo: true
|
||||
warning: false
|
||||
message: false
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
This notebook shows why the naive mortality comparison can be biased.
|
||||
|
||||
The key idea is **confounding by indication**: sicker ICU patients are more likely to receive early vasopressors, and sicker ICU patients are also more likely to die.
|
||||
|
||||
## Setup
|
||||
|
||||
```{r}
|
||||
suppressPackageStartupMessages({
|
||||
library(dplyr)
|
||||
library(gt)
|
||||
library(gtsummary)
|
||||
library(tibble)
|
||||
})
|
||||
|
||||
source("../R/simulate_icu_cohort.R")
|
||||
source("../R/estimate_naive_vasopressor_mortality_effect.R")
|
||||
source("../R/estimate_standardized_vasopressor_mortality_effect.R")
|
||||
```
|
||||
|
||||
## Simulate Data And Estimate Effects
|
||||
|
||||
```{r}
|
||||
icu_data <- simulate_icu_cohort(n_patients = 1000, seed = 20260531)
|
||||
|
||||
naive_mortality_analysis <- estimate_naive_vasopressor_mortality_effect(icu_data)
|
||||
standardized_mortality_analysis <- estimate_standardized_vasopressor_mortality_effect(icu_data)
|
||||
|
||||
eligible_icu_patients <- naive_mortality_analysis$eligible_icu_patients
|
||||
```
|
||||
|
||||
Both analyses use the same eligible ICU patients.
|
||||
|
||||
The naive analysis compares observed treatment groups directly.
|
||||
|
||||
The standardized analysis uses an outcome model to compare two treatment strategies in the same eligible cohort.
|
||||
|
||||
## Baseline Imbalance
|
||||
|
||||
```{r}
|
||||
eligible_icu_patients |>
|
||||
mutate(
|
||||
early_vasopressor = factor(
|
||||
early_vasopressor,
|
||||
levels = c(0, 1),
|
||||
labels = c("No early vasopressor", "Early vasopressor")
|
||||
)
|
||||
) |>
|
||||
select(early_vasopressor, age, sex, sofa_score, lactate, map, death_28d) |>
|
||||
tbl_summary(
|
||||
by = early_vasopressor,
|
||||
statistic = list(
|
||||
all_continuous() ~ "{mean} ({sd})",
|
||||
all_categorical() ~ "{n} ({p}%)"
|
||||
),
|
||||
missing = "no"
|
||||
) |>
|
||||
add_overall()
|
||||
```
|
||||
|
||||
The early vasopressor group is generally older and sicker.
|
||||
|
||||
That matters because age, SOFA score, lactate, and MAP are also predictors of 28-day mortality.
|
||||
|
||||
## Naive Mortality Effect Estimates
|
||||
|
||||
```{r}
|
||||
naive_mortality_analysis$mortality_effect_estimates |>
|
||||
gt() |>
|
||||
tab_header(title = "Naive Mortality Effect Estimates") |>
|
||||
cols_label(
|
||||
estimate = "Estimate",
|
||||
value = "Value"
|
||||
) |>
|
||||
fmt_number(columns = value, decimals = 3)
|
||||
```
|
||||
|
||||
The naive estimate does not compare like with like.
|
||||
|
||||
It compares patients who actually received early vasopressors with patients who did not, even though those groups differ in baseline severity.
|
||||
|
||||
## Outcome Model Used For Standardization
|
||||
|
||||
```{r}
|
||||
mortality_model_coefficients <- summary(
|
||||
standardized_mortality_analysis$mortality_outcome_model
|
||||
)$coefficients |>
|
||||
as.data.frame() |>
|
||||
rownames_to_column("model_term") |>
|
||||
as_tibble()
|
||||
|
||||
names(mortality_model_coefficients) <- c(
|
||||
"model_term",
|
||||
"log_odds_estimate",
|
||||
"standard_error",
|
||||
"z_statistic",
|
||||
"p_value"
|
||||
)
|
||||
|
||||
mortality_model_coefficients |>
|
||||
mutate(odds_ratio = exp(log_odds_estimate)) |>
|
||||
select(model_term, log_odds_estimate, odds_ratio, standard_error, p_value) |>
|
||||
gt() |>
|
||||
tab_header(title = "Mortality Outcome Model") |>
|
||||
cols_label(
|
||||
model_term = "Model term",
|
||||
log_odds_estimate = "Log-odds estimate",
|
||||
odds_ratio = "Odds ratio",
|
||||
standard_error = "Standard error",
|
||||
p_value = "P-value"
|
||||
) |>
|
||||
fmt_number(
|
||||
columns = c(log_odds_estimate, odds_ratio, standard_error, p_value),
|
||||
decimals = 3
|
||||
)
|
||||
```
|
||||
|
||||
This logistic regression models 28-day mortality using observed treatment and baseline severity variables.
|
||||
|
||||
The model is not the target trial by itself. It is a tool for predicting mortality risk under each treatment strategy while holding the eligible patient population fixed.
|
||||
|
||||
## Standardized Mortality Risks
|
||||
|
||||
```{r}
|
||||
standardized_mortality_analysis$standardized_mortality_risks |>
|
||||
gt() |>
|
||||
tab_header(title = "Standardized 28-Day Mortality Risks") |>
|
||||
cols_label(
|
||||
treatment_strategy = "Treatment strategy",
|
||||
patient_count = "Eligible patients",
|
||||
standardized_mortality_risk_28d = "Standardized mortality risk"
|
||||
) |>
|
||||
fmt_integer(columns = patient_count) |>
|
||||
fmt_number(columns = standardized_mortality_risk_28d, decimals = 3)
|
||||
```
|
||||
|
||||
These risks answer a target-trial-style question:
|
||||
|
||||
What would the average mortality risk be if the same eligible patients all followed one strategy versus the other?
|
||||
|
||||
## Standardized Mortality Effect Estimates
|
||||
|
||||
```{r}
|
||||
standardized_mortality_analysis$standardized_mortality_effect_estimates |>
|
||||
gt() |>
|
||||
tab_header(title = "Standardized Mortality Effect Estimates") |>
|
||||
cols_label(
|
||||
estimate = "Estimate",
|
||||
value = "Value"
|
||||
) |>
|
||||
fmt_number(columns = value, decimals = 3)
|
||||
```
|
||||
|
||||
The standardized risk difference is less distorted by baseline severity imbalance than the naive risk difference.
|
||||
|
||||
This does not make the estimate automatically correct, but it is closer to the target trial question than a direct treated-versus-untreated comparison.
|
||||
|
||||
## Naive Versus Standardized Estimates
|
||||
|
||||
```{r}
|
||||
naive_risk_difference <- naive_mortality_analysis$mortality_effect_estimates |>
|
||||
filter(estimate == "Naive risk difference") |>
|
||||
pull(value)
|
||||
|
||||
naive_risk_ratio <- naive_mortality_analysis$mortality_effect_estimates |>
|
||||
filter(estimate == "Naive risk ratio") |>
|
||||
pull(value)
|
||||
|
||||
standardized_risk_difference <- standardized_mortality_analysis$standardized_mortality_effect_estimates |>
|
||||
filter(estimate == "Standardized risk difference") |>
|
||||
pull(value)
|
||||
|
||||
standardized_risk_ratio <- standardized_mortality_analysis$standardized_mortality_effect_estimates |>
|
||||
filter(estimate == "Standardized risk ratio") |>
|
||||
pull(value)
|
||||
|
||||
effect_estimate_comparison <- tibble(
|
||||
method = c("Naive observed comparison", "Outcome regression standardization"),
|
||||
risk_difference = c(naive_risk_difference, standardized_risk_difference),
|
||||
risk_ratio = c(naive_risk_ratio, standardized_risk_ratio)
|
||||
)
|
||||
|
||||
effect_estimate_comparison |>
|
||||
gt() |>
|
||||
tab_header(title = "Naive Versus Standardized Mortality Effect Estimates") |>
|
||||
cols_label(
|
||||
method = "Method",
|
||||
risk_difference = "Risk difference",
|
||||
risk_ratio = "Risk ratio"
|
||||
) |>
|
||||
fmt_number(columns = c(risk_difference, risk_ratio), decimals = 3)
|
||||
```
|
||||
|
||||
In the simulated data-generating process, early vasopressors have a modest protective effect.
|
||||
|
||||
The naive comparison can still make early vasopressors look harmful because early-treated patients are more severely ill at baseline.
|
||||
|
||||
Standardization partially addresses that problem by comparing treatment strategies in the same eligible patient population.
|
||||
|
||||
## Next Step
|
||||
|
||||
The next tutorial step is to connect this back to target trial emulation mechanics: time zero, eligibility, and treatment assignment windows.
|
||||
|
||||
After that, we can introduce inverse probability weighting from first principles.
|
||||
@@ -0,0 +1,275 @@
|
||||
---
|
||||
title: "Inverse Probability Weighting"
|
||||
format:
|
||||
html:
|
||||
embed-resources: true
|
||||
docx: default
|
||||
execute:
|
||||
echo: true
|
||||
warning: false
|
||||
message: false
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
This notebook introduces inverse probability of treatment weighting (IPTW) from first principles.
|
||||
|
||||
The key idea is to create a **pseudo-population** in which treatment assignment is independent of the measured baseline confounders.
|
||||
|
||||
If that pseudo-population behaves like a randomized experiment, a simple weighted average of outcomes by treatment group gives an unbiased effect estimate.
|
||||
|
||||
## Setup
|
||||
|
||||
```{r}
|
||||
suppressPackageStartupMessages({
|
||||
library(dplyr)
|
||||
library(gt)
|
||||
library(tibble)
|
||||
})
|
||||
|
||||
source("../R/simulate_icu_cohort.R")
|
||||
source("../R/estimate_naive_vasopressor_mortality_effect.R")
|
||||
source("../R/estimate_standardized_vasopressor_mortality_effect.R")
|
||||
source("../R/estimate_iptw_vasopressor_mortality_effect.R")
|
||||
```
|
||||
|
||||
## Simulate Data And Estimate Effects
|
||||
|
||||
```{r}
|
||||
icu_data <- simulate_icu_cohort(n_patients = 1000, seed = 20260531)
|
||||
|
||||
naive_mortality_analysis <- estimate_naive_vasopressor_mortality_effect(icu_data)
|
||||
standardized_mortality_analysis <- estimate_standardized_vasopressor_mortality_effect(icu_data)
|
||||
iptw_mortality_analysis <- estimate_iptw_vasopressor_mortality_effect(icu_data)
|
||||
|
||||
eligible_icu_patients <- iptw_mortality_analysis$eligible_icu_patients
|
||||
```
|
||||
|
||||
All three analyses use the same eligible ICU patients and the same confounder set.
|
||||
|
||||
- **Naive**: direct comparison of observed treatment groups.
|
||||
- **Standardized**: outcome regression and g-computation.
|
||||
- **IPTW**: reweight the sample so confounders are balanced.
|
||||
|
||||
## Propensity Score Model
|
||||
|
||||
IPTW starts with a model that predicts the probability of receiving early vasopressors given baseline characteristics.
|
||||
|
||||
This probability is called the **propensity score**.
|
||||
|
||||
```{r}
|
||||
propensity_model_coefficients <- summary(
|
||||
iptw_mortality_analysis$propensity_score_model
|
||||
)$coefficients |>
|
||||
as.data.frame() |>
|
||||
rownames_to_column("model_term") |>
|
||||
as_tibble()
|
||||
|
||||
names(propensity_model_coefficients) <- c(
|
||||
"model_term",
|
||||
"log_odds_estimate",
|
||||
"standard_error",
|
||||
"z_statistic",
|
||||
"p_value"
|
||||
)
|
||||
|
||||
propensity_model_coefficients |>
|
||||
mutate(odds_ratio = exp(log_odds_estimate)) |>
|
||||
select(model_term, log_odds_estimate, odds_ratio, standard_error, p_value) |>
|
||||
gt() |>
|
||||
tab_header(title = "Propensity Score Model") |>
|
||||
cols_label(
|
||||
model_term = "Model term",
|
||||
log_odds_estimate = "Log-odds estimate",
|
||||
odds_ratio = "Odds ratio",
|
||||
standard_error = "Standard error",
|
||||
p_value = "P-value"
|
||||
) |>
|
||||
fmt_number(
|
||||
columns = c(log_odds_estimate, odds_ratio, standard_error, p_value),
|
||||
decimals = 3
|
||||
)
|
||||
```
|
||||
|
||||
Higher SOFA score and higher lactate are associated with a higher probability of receiving early vasopressors.
|
||||
|
||||
That is exactly the confounding pattern we want to adjust for.
|
||||
|
||||
## Weight Diagnostics
|
||||
|
||||
Unstabilized IPTW weights are:
|
||||
|
||||
- `1 / propensity_score` for patients who received early vasopressors.
|
||||
- `1 / (1 - propensity_score)` for patients who did not.
|
||||
|
||||
Very large weights can signal a **positivity problem**: some patients have an extremely low or high probability of receiving the treatment they actually received.
|
||||
|
||||
```{r}
|
||||
iptw_mortality_analysis$weight_summary |>
|
||||
gt() |>
|
||||
tab_header(title = "IPTW Weight Distribution by Treatment Group") |>
|
||||
cols_label(
|
||||
treatment_group = "Treatment group",
|
||||
patient_count = "Patients",
|
||||
min_weight = "Minimum weight",
|
||||
max_weight = "Maximum weight",
|
||||
mean_weight = "Mean weight",
|
||||
median_weight = "Median weight"
|
||||
) |>
|
||||
fmt_integer(columns = patient_count) |>
|
||||
fmt_number(columns = c(min_weight, max_weight, mean_weight, median_weight), decimals = 3)
|
||||
```
|
||||
|
||||
If the maximum weights are extremely large, we would consider trimming or stabilizing them.
|
||||
|
||||
For this teaching example the weights are moderate, so we proceed with unstabilized weights.
|
||||
|
||||
## Baseline Balance
|
||||
|
||||
IPTW should rebalance the measured confounders across treatment groups.
|
||||
|
||||
The table below shows unweighted and weighted means side by side so you can see the rebalancing directly.
|
||||
|
||||
```{r}
|
||||
iptw_mortality_analysis$baseline_balance |>
|
||||
gt() |>
|
||||
tab_header(title = "Baseline Characteristics: Unweighted and Weighted Means") |>
|
||||
cols_label(
|
||||
characteristic = "Characteristic",
|
||||
no_early_unweighted = "No early vasopressor (unweighted)",
|
||||
no_early_weighted = "No early vasopressor (weighted)",
|
||||
early_unweighted = "Early vasopressor (unweighted)",
|
||||
early_weighted = "Early vasopressor (weighted)"
|
||||
) |>
|
||||
fmt_number(decimals = 2)
|
||||
```
|
||||
|
||||
After weighting, the weighted means in the two treatment groups are much closer for age, SOFA score, lactate, and MAP.
|
||||
|
||||
That is the goal of IPTW: to make the two groups comparable on measured confounders in the pseudo-population.
|
||||
|
||||
## Weighted Mortality Risks
|
||||
|
||||
Once the confounders are balanced by weighting, a simple weighted average of 28-day mortality in each group estimates the risk under each treatment strategy.
|
||||
|
||||
```{r}
|
||||
iptw_mortality_analysis$weighted_mortality_risks |>
|
||||
gt() |>
|
||||
tab_header(title = "IPTW 28-Day Mortality Risks") |>
|
||||
cols_label(
|
||||
treatment_strategy = "Treatment strategy",
|
||||
patient_count = "Eligible patients",
|
||||
weighted_mortality_risk_28d = "Weighted mortality risk"
|
||||
) |>
|
||||
fmt_integer(columns = patient_count) |>
|
||||
fmt_number(columns = weighted_mortality_risk_28d, decimals = 3)
|
||||
```
|
||||
|
||||
These risks answer the same target-trial-style question as standardization:
|
||||
|
||||
What would the average mortality risk be if the same eligible patients all followed one strategy versus the other?
|
||||
|
||||
## IPTW Mortality Effect Estimates
|
||||
|
||||
```{r}
|
||||
iptw_mortality_analysis$iptw_mortality_effect_estimates |>
|
||||
gt() |>
|
||||
tab_header(title = "IPTW Mortality Effect Estimates") |>
|
||||
cols_label(
|
||||
estimate = "Estimate",
|
||||
value = "Value"
|
||||
) |>
|
||||
fmt_number(columns = value, decimals = 3)
|
||||
```
|
||||
|
||||
The IPTW risk difference and risk ratio use the pseudo-population to reduce confounding by indication.
|
||||
|
||||
## Three-Way Comparison
|
||||
|
||||
The table below puts naive, standardized, and IPTW estimates side by side.
|
||||
|
||||
This makes it easy to see how each method addresses the same confounding problem in a different way.
|
||||
|
||||
```{r}
|
||||
naive_risk_difference <- naive_mortality_analysis$mortality_effect_estimates |>
|
||||
filter(estimate == "Naive risk difference") |>
|
||||
pull(value)
|
||||
|
||||
naive_risk_ratio <- naive_mortality_analysis$mortality_effect_estimates |>
|
||||
filter(estimate == "Naive risk ratio") |>
|
||||
pull(value)
|
||||
|
||||
standardized_risk_difference <- standardized_mortality_analysis$standardized_mortality_effect_estimates |>
|
||||
filter(estimate == "Standardized risk difference") |>
|
||||
pull(value)
|
||||
|
||||
standardized_risk_ratio <- standardized_mortality_analysis$standardized_mortality_effect_estimates |>
|
||||
filter(estimate == "Standardized risk ratio") |>
|
||||
pull(value)
|
||||
|
||||
iptw_risk_difference <- iptw_mortality_analysis$iptw_mortality_effect_estimates |>
|
||||
filter(estimate == "IPTW risk difference") |>
|
||||
pull(value)
|
||||
|
||||
iptw_risk_ratio <- iptw_mortality_analysis$iptw_mortality_effect_estimates |>
|
||||
filter(estimate == "IPTW risk ratio") |>
|
||||
pull(value)
|
||||
|
||||
three_way_comparison <- tibble(
|
||||
method = c(
|
||||
"Naive observed comparison",
|
||||
"Outcome regression standardization",
|
||||
"Inverse probability weighting (IPTW)"
|
||||
),
|
||||
risk_difference = c(
|
||||
naive_risk_difference,
|
||||
standardized_risk_difference,
|
||||
iptw_risk_difference
|
||||
),
|
||||
risk_ratio = c(
|
||||
naive_risk_ratio,
|
||||
standardized_risk_ratio,
|
||||
iptw_risk_ratio
|
||||
)
|
||||
)
|
||||
|
||||
three_way_comparison |>
|
||||
gt() |>
|
||||
tab_header(title = "Naive, Standardized, and IPTW Mortality Effect Estimates") |>
|
||||
cols_label(
|
||||
method = "Method",
|
||||
risk_difference = "Risk difference",
|
||||
risk_ratio = "Risk ratio"
|
||||
) |>
|
||||
fmt_number(columns = c(risk_difference, risk_ratio), decimals = 3)
|
||||
```
|
||||
|
||||
### What to notice
|
||||
|
||||
1. **Naive comparison**: early vasopressors appear harmful. The risk difference is positive and the risk ratio is greater than 1.
|
||||
|
||||
2. **Standardized comparison**: after adjusting for measured confounders with an outcome model, the risk difference is smaller and the risk ratio moves toward 1.
|
||||
|
||||
3. **IPTW comparison**: after reweighting to balance confounders, the estimate is similar to the standardized estimate.
|
||||
|
||||
Both adjusted methods point in the same direction: the apparent harm in the naive comparison is largely due to confounding by indication.
|
||||
|
||||
In the simulated data-generating process, early vasopressors actually have a modest protective effect. Neither method perfectly recovers the true effect in a single finite sample, but both are closer to the target trial question than the naive comparison.
|
||||
|
||||
## What IPTW Does Conceptually
|
||||
|
||||
- **Observed data**: treatment is assigned based on severity. Sicker patients get vasopressors and are also more likely to die.
|
||||
|
||||
- **Pseudo-population**: each patient is duplicated according to their IPTW weight. Patients who received an unexpected treatment (given their characteristics) receive more weight. After reweighting, the treatment groups look similar on measured confounders.
|
||||
|
||||
- **Effect estimation**: a simple weighted average of outcomes in each group now compares like with like.
|
||||
|
||||
## Next Step
|
||||
|
||||
The next tutorial step is to connect these ideas back to target trial emulation mechanics:
|
||||
|
||||
- Time zero and eligibility criteria alignment.
|
||||
- Treatment assignment windows.
|
||||
- Censoring logic and inverse probability of censoring weights.
|
||||
|
||||
These topics move us from a cross-sectional baseline adjustment to a longitudinal design that more closely emulates a randomized trial.
|
||||
@@ -0,0 +1,206 @@
|
||||
---
|
||||
title: "Treatment Assignment Windows and Cloning"
|
||||
format:
|
||||
html:
|
||||
embed-resources: true
|
||||
docx: default
|
||||
execute:
|
||||
echo: true
|
||||
warning: false
|
||||
message: false
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
This notebook introduces two core target trial emulation mechanics:
|
||||
|
||||
1. **Treatment assignment windows** (the grace period): the time during which a treatment decision is made.
|
||||
2. **Cloning**: at time zero, each eligible patient is duplicated into every treatment arm under study.
|
||||
|
||||
These ideas move us from a cross-sectional baseline comparison to a longitudinal design that more closely emulates a randomized trial.
|
||||
|
||||
## Setup
|
||||
|
||||
```{r}
|
||||
suppressPackageStartupMessages({
|
||||
library(dplyr)
|
||||
library(gt)
|
||||
library(tibble)
|
||||
library(tidyr)
|
||||
})
|
||||
|
||||
source("../R/simulate_icu_cohort.R")
|
||||
source("../R/simulate_icu_cohort_longitudinal.R")
|
||||
source("../R/clone_trial_arms.R")
|
||||
```
|
||||
|
||||
## Simulate Longitudinal Data
|
||||
|
||||
We now generate data with multiple rows per patient: one row for each time point at which we could observe a measurement or event.
|
||||
|
||||
Time points:
|
||||
|
||||
- `0h` — ICU admission (baseline)
|
||||
- `2h` — end of the treatment decision window
|
||||
- `6h, 12h, 24h` — early ICU course
|
||||
- `day7, day14, day21, day28` — outcome assessment
|
||||
|
||||
```{r}
|
||||
longitudinal_data <- simulate_icu_cohort_longitudinal(n_patients = 1000, seed = 20260531)
|
||||
|
||||
longitudinal_data |>
|
||||
filter(patient_id == 1)
|
||||
```
|
||||
|
||||
Each row shows the patient's state at one time point.
|
||||
|
||||
Notice:
|
||||
|
||||
- `vasopressor_started` is `0` before the treatment time and `1` after.
|
||||
- `map_current` and `lactate_current` evolve slightly once vasopressors start.
|
||||
- `alive` becomes `0` after death (if the patient died before day 28).
|
||||
|
||||
## Three Example Patient Trajectories
|
||||
|
||||
To see the structure more clearly, here are three patients side by side.
|
||||
|
||||
```{r}
|
||||
example_patients <- longitudinal_data |>
|
||||
filter(patient_id %in% c(1, 5, 10))
|
||||
|
||||
example_patients |>
|
||||
select(
|
||||
patient_id, time_label, vasopressor_started,
|
||||
map, map_current, lactate, lactate_current,
|
||||
alive, death_28d
|
||||
) |>
|
||||
gt() |>
|
||||
tab_header(title = "Example Patient Trajectories") |>
|
||||
cols_label(
|
||||
patient_id = "Patient ID",
|
||||
time_label = "Time",
|
||||
vasopressor_started = "Vasopressors started",
|
||||
map = "Baseline MAP",
|
||||
map_current = "Current MAP",
|
||||
lactate = "Baseline lactate",
|
||||
lactate_current = "Current lactate",
|
||||
alive = "Alive",
|
||||
death_28d = "Death by day 28"
|
||||
)
|
||||
```
|
||||
|
||||
These trajectories show that treatment is not assigned instantaneously at time zero.
|
||||
|
||||
Instead, there is a **grace period** (0 to 2 hours) during which clinicians decide whether to start vasopressors.
|
||||
|
||||
## What Is a Grace Period?
|
||||
|
||||
In the target trial we want to emulate:
|
||||
|
||||
- **Strategy A**: start vasopressors within 2 hours of ICU admission.
|
||||
- **Strategy B**: do not start vasopressors within 2 hours of ICU admission.
|
||||
|
||||
The **2-hour window** is the treatment assignment window, also called the grace period.
|
||||
|
||||
During this window, patients may receive monitoring, fluid resuscitation, and other care. The treatment decision is made based on the patient's response.
|
||||
|
||||
In the observational data, some patients start vasopressors at hour 0, some at hour 1.5, and some never start them within 2 hours.
|
||||
|
||||
The grace period is what makes this a longitudinal problem rather than a simple baseline cross-section.
|
||||
|
||||
## Clone the Eligible Patients
|
||||
|
||||
To emulate a randomized trial, we duplicate each eligible patient at time zero into two **clones**:
|
||||
|
||||
- **Clone "early"**: this patient is assigned to the early vasopressor strategy.
|
||||
- **Clone "no_early"**: this patient is assigned to the no early vasopressor strategy.
|
||||
|
||||
Both clones then follow the patient's actual observed trajectory over time.
|
||||
|
||||
```{r}
|
||||
clones <- clone_trial_arms(longitudinal_data)
|
||||
|
||||
clones$clone_baseline |>
|
||||
count(clone_strategy)
|
||||
```
|
||||
|
||||
Every eligible patient now has two rows in `clone_baseline`, one for each strategy.
|
||||
|
||||
## Censoring Rules
|
||||
|
||||
A clone is **censored** if the patient deviated from the assigned strategy or died before the treatment window closed.
|
||||
|
||||
### Rule 1: Protocol deviation
|
||||
|
||||
- The "early" clone is censored if the patient did NOT actually start vasopressors within 2 hours.
|
||||
- The "no_early" clone is censored if the patient DID actually start vasopressors within 2 hours.
|
||||
|
||||
### Rule 2: Death before treatment
|
||||
|
||||
- Both clones are censored if the patient died before the 2-hour window closed.
|
||||
|
||||
```{r}
|
||||
clones$clone_baseline |>
|
||||
count(clone_strategy, clone_censored, clone_censored_reason) |>
|
||||
gt() |>
|
||||
tab_header(title = "Censoring Summary by Clone Strategy") |>
|
||||
cols_label(
|
||||
clone_strategy = "Clone strategy",
|
||||
clone_censored = "Censored",
|
||||
clone_censored_reason = "Censoring reason",
|
||||
n = "Count"
|
||||
)
|
||||
```
|
||||
|
||||
This table shows how many clones are censored and why.
|
||||
|
||||
Protocol deviation is the most common reason because the observational data does not perfectly align with either strategy.
|
||||
|
||||
## One Patient, Two Clones
|
||||
|
||||
To make the cloning concrete, here is patient 1 at time zero, before any censoring is applied.
|
||||
|
||||
```{r}
|
||||
clones$clone_baseline |>
|
||||
filter(patient_id == 1) |>
|
||||
select(
|
||||
patient_id, clone_strategy, age, sex, sofa_score,
|
||||
lactate, map, early_vasopressor, clone_censored, clone_censored_reason
|
||||
) |>
|
||||
gt() |>
|
||||
tab_header(title = "Patient 1: Two Clones at Time Zero") |>
|
||||
cols_label(
|
||||
patient_id = "Patient ID",
|
||||
clone_strategy = "Clone strategy",
|
||||
age = "Age",
|
||||
sex = "Sex",
|
||||
sofa_score = "SOFA",
|
||||
lactate = "Lactate",
|
||||
map = "MAP",
|
||||
early_vasopressor = "Observed early treatment",
|
||||
clone_censored = "Clone censored?",
|
||||
clone_censored_reason = "Reason"
|
||||
)
|
||||
```
|
||||
|
||||
Both clones share the same baseline characteristics because they come from the same patient.
|
||||
|
||||
They differ only in the strategy they are assigned to.
|
||||
|
||||
## What We Have So Far
|
||||
|
||||
At this point in the target trial emulation:
|
||||
|
||||
1. We identified eligible patients at ICU admission (time zero).
|
||||
2. We cloned each eligible patient into two treatment arms.
|
||||
3. We applied censoring rules at the 2-hour mark.
|
||||
4. Uncensored clones continue to follow the patient's trajectory.
|
||||
5. Censored clones drop out and do not contribute to later time points.
|
||||
|
||||
The next step is to account for the fact that censoring is not random.
|
||||
|
||||
Some patients are more likely to be censored because of their baseline severity. We handle that with **inverse probability of censoring weighting (IPCW)** in the next notebook.
|
||||
|
||||
## Next Step
|
||||
|
||||
The next tutorial step is to compute inverse probability of treatment and censoring weights on the cloned dataset, and estimate a per-protocol effect that accounts for both baseline confounding and informative censoring.
|
||||
@@ -0,0 +1,265 @@
|
||||
---
|
||||
title: "Censoring and Inverse Probability of Censoring Weighting"
|
||||
format:
|
||||
html:
|
||||
embed-resources: true
|
||||
docx: default
|
||||
execute:
|
||||
echo: true
|
||||
warning: false
|
||||
message: false
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
This notebook adds **inverse probability of censoring weighting (IPCW)** to the cloned dataset from notebook 05.
|
||||
|
||||
The key idea is that clones are censored for a reason: sicker patients might die before the 2-hour window closes, or clinicians might deviate from the protocol based on severity.
|
||||
|
||||
IPCW weights the uncensored clones so they represent the full population that started in each trial arm.
|
||||
|
||||
## Setup
|
||||
|
||||
```{r}
|
||||
suppressPackageStartupMessages({
|
||||
library(dplyr)
|
||||
library(gt)
|
||||
library(tibble)
|
||||
library(tidyr)
|
||||
})
|
||||
|
||||
source("../R/simulate_icu_cohort.R")
|
||||
source("../R/simulate_icu_cohort_longitudinal.R")
|
||||
source("../R/clone_trial_arms.R")
|
||||
source("../R/estimate_iptw_and_ipcw_effect.R")
|
||||
source("../R/estimate_iptw_vasopressor_mortality_effect.R")
|
||||
```
|
||||
|
||||
## Simulate, Clone, and Weight
|
||||
|
||||
```{r}
|
||||
longitudinal_data <- simulate_icu_cohort_longitudinal(n_patients = 1000, seed = 20260531)
|
||||
clones <- clone_trial_arms(longitudinal_data)
|
||||
weighted_effects <- estimate_iptw_and_ipcw_effect(clones$clone_baseline)
|
||||
```
|
||||
|
||||
## Propensity Score Model (IPTW)
|
||||
|
||||
The IPTW model predicts the probability of receiving early vasopressors given baseline confounders.
|
||||
|
||||
It is identical in purpose to the model in notebook 04, but it is now applied to the **cloned** dataset.
|
||||
|
||||
Because both clones of the same patient share the same observed treatment, we fit the model once per patient, then assign the propensity score to both clones.
|
||||
|
||||
```{r}
|
||||
propensity_coefficients <- summary(
|
||||
weighted_effects$propensity_score_model
|
||||
)$coefficients |>
|
||||
as.data.frame() |>
|
||||
rownames_to_column("model_term") |>
|
||||
as_tibble()
|
||||
|
||||
names(propensity_coefficients) <- c(
|
||||
"model_term",
|
||||
"log_odds_estimate",
|
||||
"standard_error",
|
||||
"z_statistic",
|
||||
"p_value"
|
||||
)
|
||||
|
||||
propensity_coefficients |>
|
||||
mutate(odds_ratio = exp(log_odds_estimate)) |>
|
||||
select(model_term, log_odds_estimate, odds_ratio, standard_error, p_value) |>
|
||||
gt() |>
|
||||
tab_header(title = "Propensity Score Model (Treatment Assignment)") |>
|
||||
cols_label(
|
||||
model_term = "Model term",
|
||||
log_odds_estimate = "Log-odds estimate",
|
||||
odds_ratio = "Odds ratio",
|
||||
standard_error = "Standard error",
|
||||
p_value = "P-value"
|
||||
) |>
|
||||
fmt_number(
|
||||
columns = c(log_odds_estimate, odds_ratio, standard_error, p_value),
|
||||
decimals = 3
|
||||
)
|
||||
```
|
||||
|
||||
Higher SOFA score and lactate are associated with a higher probability of early vasopressors.
|
||||
|
||||
This is the same confounding pattern we adjusted for in notebook 04.
|
||||
|
||||
## Censoring Model (IPCW)
|
||||
|
||||
The IPCW model predicts the probability that a clone is censored at the 2-hour mark.
|
||||
|
||||
Censoring can happen for two reasons:
|
||||
|
||||
1. **Protocol deviation**: the patient did not follow the assigned clone strategy.
|
||||
2. **Death before treatment**: the patient died before the 2-hour window closed.
|
||||
|
||||
Both reasons may depend on baseline severity, so we model censoring using the same confounders plus the clone strategy.
|
||||
|
||||
```{r}
|
||||
censoring_coefficients <- summary(
|
||||
weighted_effects$censoring_model
|
||||
)$coefficients |>
|
||||
as.data.frame() |>
|
||||
rownames_to_column("model_term") |>
|
||||
as_tibble()
|
||||
|
||||
names(censoring_coefficients) <- c(
|
||||
"model_term",
|
||||
"log_odds_estimate",
|
||||
"standard_error",
|
||||
"z_statistic",
|
||||
"p_value"
|
||||
)
|
||||
|
||||
censoring_coefficients |>
|
||||
mutate(odds_ratio = exp(log_odds_estimate)) |>
|
||||
select(model_term, log_odds_estimate, odds_ratio, standard_error, p_value) |>
|
||||
gt() |>
|
||||
tab_header(title = "Censoring Model (Probability of Being Censored at 2 Hours)") |>
|
||||
cols_label(
|
||||
model_term = "Model term",
|
||||
log_odds_estimate = "Log-odds estimate",
|
||||
odds_ratio = "Odds ratio",
|
||||
standard_error = "Standard error",
|
||||
p_value = "P-value"
|
||||
) |>
|
||||
fmt_number(
|
||||
columns = c(log_odds_estimate, odds_ratio, standard_error, p_value),
|
||||
decimals = 3
|
||||
)
|
||||
```
|
||||
|
||||
A positive coefficient means the factor is associated with a **higher** probability of being censored.
|
||||
|
||||
If sicker patients (higher SOFA, higher lactate) are more likely to be censored, the IPCW weight will give more weight to the uncensored sicker patients so the analysis represents the full population.
|
||||
|
||||
## Combined Weight Diagnostics
|
||||
|
||||
The combined weight for each clone is:
|
||||
|
||||
`combined_weight = IPTW_weight × IPCW_weight`
|
||||
|
||||
- IPTW balances baseline confounders across treatment arms.
|
||||
- IPCW accounts for informative censoring.
|
||||
|
||||
Extreme weights indicate either a positivity problem (some patients have near-zero probability of treatment or near-zero probability of remaining uncensored) or model misspecification.
|
||||
|
||||
```{r}
|
||||
weighted_effects$weight_diagnostics |>
|
||||
gt() |>
|
||||
tab_header(title = "Combined IPTW + IPCW Weight Diagnostics") |>
|
||||
cols_label(
|
||||
clone_strategy = "Clone strategy",
|
||||
n_clones = "Clones",
|
||||
min_weight = "Minimum weight",
|
||||
max_weight = "Maximum weight",
|
||||
mean_weight = "Mean weight",
|
||||
median_weight = "Median weight"
|
||||
) |>
|
||||
fmt_integer(columns = n_clones) |>
|
||||
fmt_number(columns = c(min_weight, max_weight, mean_weight, median_weight), decimals = 3)
|
||||
```
|
||||
|
||||
If the maximum weights are very large, we would consider trimming them (capping at the 99th percentile) or stabilizing them.
|
||||
|
||||
For this teaching example the weights are moderate, so we proceed.
|
||||
|
||||
## Weighted Mortality Risks
|
||||
|
||||
Using the combined weights, we compute the weighted 28-day mortality risk in each clone arm.
|
||||
|
||||
Only uncensored clones are included in the numerator, but their weights account for the censored clones who would have had similar outcomes.
|
||||
|
||||
```{r}
|
||||
weighted_effects$weighted_effect_estimates |>
|
||||
head(2) |>
|
||||
gt() |>
|
||||
tab_header(title = "IPTW + IPCW 28-Day Mortality Risks") |>
|
||||
cols_label(
|
||||
estimate = "Estimate",
|
||||
value = "Value"
|
||||
) |>
|
||||
fmt_number(columns = value, decimals = 3)
|
||||
```
|
||||
|
||||
These are **per-protocol** risks: the risk under each strategy for patients who were able to follow the strategy through the 2-hour window.
|
||||
|
||||
## IPTW + IPCW Mortality Effect Estimates
|
||||
|
||||
```{r}
|
||||
weighted_effects$weighted_effect_estimates |>
|
||||
tail(2) |>
|
||||
gt() |>
|
||||
tab_header(title = "IPTW + IPCW Mortality Effect Estimates") |>
|
||||
cols_label(
|
||||
estimate = "Estimate",
|
||||
value = "Value"
|
||||
) |>
|
||||
fmt_number(columns = value, decimals = 3)
|
||||
```
|
||||
|
||||
## Comparison with Cross-Sectional IPTW
|
||||
|
||||
How does the longitudinal IPTW + IPCW estimate compare with the cross-sectional IPTW estimate from notebook 04?
|
||||
|
||||
```{r}
|
||||
# Pull the cross-sectional IPTW estimate from notebook 04
|
||||
icu_data <- simulate_icu_cohort(n_patients = 1000, seed = 20260531)
|
||||
iptw_analysis <- estimate_iptw_vasopressor_mortality_effect(icu_data)
|
||||
|
||||
cross_sectional_rd <- iptw_analysis$iptw_mortality_effect_estimates |>
|
||||
filter(estimate == "IPTW risk difference") |>
|
||||
pull(value)
|
||||
|
||||
cross_sectional_rr <- iptw_analysis$iptw_mortality_effect_estimates |>
|
||||
filter(estimate == "IPTW risk ratio") |>
|
||||
pull(value)
|
||||
|
||||
longitudinal_rd <- weighted_effects$weighted_effect_estimates |>
|
||||
filter(estimate == "IPTW + IPCW risk difference") |>
|
||||
pull(value)
|
||||
|
||||
longitudinal_rr <- weighted_effects$weighted_effect_estimates |>
|
||||
filter(estimate == "IPTW + IPCW risk ratio") |>
|
||||
pull(value)
|
||||
|
||||
comparison <- tibble(
|
||||
method = c("Cross-sectional IPTW", "Longitudinal IPTW + IPCW"),
|
||||
risk_difference = c(cross_sectional_rd, longitudinal_rd),
|
||||
risk_ratio = c(cross_sectional_rr, longitudinal_rr)
|
||||
)
|
||||
|
||||
comparison |>
|
||||
gt() |>
|
||||
tab_header(title = "Cross-Sectional Versus Longitudinal IPTW Estimates") |>
|
||||
cols_label(
|
||||
method = "Method",
|
||||
risk_difference = "Risk difference",
|
||||
risk_ratio = "Risk ratio"
|
||||
) |>
|
||||
fmt_number(columns = c(risk_difference, risk_ratio), decimals = 3)
|
||||
```
|
||||
|
||||
### What to notice
|
||||
|
||||
- The **cross-sectional IPTW** estimate treats the treatment as assigned at baseline, with no treatment window.
|
||||
- The **longitudinal IPTW + IPCW** estimate respects the grace period, censors protocol deviations, and reweights for informative censoring.
|
||||
- The two estimates may differ because the longitudinal design removes patients who could not adhere to the assigned strategy, and the IPCW adjustment accounts for the severity of those who were censored.
|
||||
|
||||
Both are valid approaches, but they answer slightly different questions:
|
||||
|
||||
- Cross-sectional IPTW answers: "What is the effect of receiving early vasopressors versus not, among all eligible patients?"
|
||||
- Longitudinal IPTW + IPCW answers: "What is the effect of the early vasopressor strategy versus the no early vasopressor strategy, among patients who can adhere to the strategy through the 2-hour window?"
|
||||
|
||||
The second is closer to the **per-protocol effect** of the target trial.
|
||||
|
||||
## Next Step
|
||||
|
||||
The next tutorial step is to put the full pipeline together in one place: simulate, clone, weight, and estimate.
|
||||
|
||||
We will also compare all four methods we have learned so far: naive, standardized, cross-sectional IPTW, and longitudinal IPTW + IPCW.
|
||||
@@ -0,0 +1,202 @@
|
||||
---
|
||||
title: "Full Emulation Pipeline"
|
||||
format:
|
||||
html:
|
||||
embed-resources: true
|
||||
docx: default
|
||||
execute:
|
||||
echo: true
|
||||
warning: false
|
||||
message: false
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
This notebook puts the entire target trial emulation pipeline together in one place:
|
||||
|
||||
1. Simulate the observational data.
|
||||
2. Define eligibility at time zero.
|
||||
3. Clone eligible patients into treatment arms.
|
||||
4. Apply censoring rules at the 2-hour mark.
|
||||
5. Compute IPTW + IPCW weights.
|
||||
6. Estimate the per-protocol effect.
|
||||
|
||||
Then we compare all four methods we have learned so far.
|
||||
|
||||
## Setup
|
||||
|
||||
```{r}
|
||||
suppressPackageStartupMessages({
|
||||
library(dplyr)
|
||||
library(gt)
|
||||
library(tibble)
|
||||
library(tidyr)
|
||||
})
|
||||
|
||||
source("../R/simulate_icu_cohort.R")
|
||||
source("../R/simulate_icu_cohort_longitudinal.R")
|
||||
source("../R/estimate_naive_vasopressor_mortality_effect.R")
|
||||
source("../R/estimate_standardized_vasopressor_mortality_effect.R")
|
||||
source("../R/estimate_iptw_vasopressor_mortality_effect.R")
|
||||
source("../R/clone_trial_arms.R")
|
||||
source("../R/estimate_iptw_and_ipcw_effect.R")
|
||||
```
|
||||
|
||||
## Step 1: Simulate Data
|
||||
|
||||
We generate both the cross-sectional and longitudinal datasets using the same seed.
|
||||
|
||||
```{r}
|
||||
icu_data <- simulate_icu_cohort(n_patients = 1000, seed = 20260531)
|
||||
longitudinal_data <- simulate_icu_cohort_longitudinal(n_patients = 1000, seed = 20260531)
|
||||
```
|
||||
|
||||
## Step 2: Cross-Sectional Estimates
|
||||
|
||||
### Naive comparison
|
||||
|
||||
```{r}
|
||||
naive_analysis <- estimate_naive_vasopressor_mortality_effect(icu_data)
|
||||
```
|
||||
|
||||
### Outcome regression standardization
|
||||
|
||||
```{r}
|
||||
standardized_analysis <- estimate_standardized_vasopressor_mortality_effect(icu_data)
|
||||
```
|
||||
|
||||
### Cross-sectional IPTW
|
||||
|
||||
```{r}
|
||||
iptw_analysis <- estimate_iptw_vasopressor_mortality_effect(icu_data)
|
||||
```
|
||||
|
||||
## Step 3: Longitudinal Emulation
|
||||
|
||||
### Clone and censor
|
||||
|
||||
```{r}
|
||||
clones <- clone_trial_arms(longitudinal_data)
|
||||
```
|
||||
|
||||
### Compute IPTW + IPCW weights and estimate effect
|
||||
|
||||
```{r}
|
||||
longitudinal_analysis <- estimate_iptw_and_ipcw_effect(clones$clone_baseline)
|
||||
```
|
||||
|
||||
## Four-Way Comparison
|
||||
|
||||
We now extract the risk difference and risk ratio from each method and place them in one table.
|
||||
|
||||
```{r}
|
||||
# Naive
|
||||
naive_rd <- naive_analysis$mortality_effect_estimates |>
|
||||
filter(estimate == "Naive risk difference") |>
|
||||
pull(value)
|
||||
|
||||
naive_rr <- naive_analysis$mortality_effect_estimates |>
|
||||
filter(estimate == "Naive risk ratio") |>
|
||||
pull(value)
|
||||
|
||||
# Standardized
|
||||
std_rd <- standardized_analysis$standardized_mortality_effect_estimates |>
|
||||
filter(estimate == "Standardized risk difference") |>
|
||||
pull(value)
|
||||
|
||||
std_rr <- standardized_analysis$standardized_mortality_effect_estimates |>
|
||||
filter(estimate == "Standardized risk ratio") |>
|
||||
pull(value)
|
||||
|
||||
# Cross-sectional IPTW
|
||||
iptw_rd <- iptw_analysis$iptw_mortality_effect_estimates |>
|
||||
filter(estimate == "IPTW risk difference") |>
|
||||
pull(value)
|
||||
|
||||
iptw_rr <- iptw_analysis$iptw_mortality_effect_estimates |>
|
||||
filter(estimate == "IPTW risk ratio") |>
|
||||
pull(value)
|
||||
|
||||
# Longitudinal IPTW + IPCW
|
||||
long_rd <- longitudinal_analysis$weighted_effect_estimates |>
|
||||
filter(estimate == "IPTW + IPCW risk difference") |>
|
||||
pull(value)
|
||||
|
||||
long_rr <- longitudinal_analysis$weighted_effect_estimates |>
|
||||
filter(estimate == "IPTW + IPCW risk ratio") |>
|
||||
pull(value)
|
||||
|
||||
four_way <- tibble(
|
||||
method = c(
|
||||
"1. Naive observed comparison",
|
||||
"2. Outcome regression standardization",
|
||||
"3. Cross-sectional IPTW",
|
||||
"4. Longitudinal IPTW + IPCW (per-protocol)"
|
||||
),
|
||||
risk_difference = c(naive_rd, std_rd, iptw_rd, long_rd),
|
||||
risk_ratio = c(naive_rr, std_rr, iptw_rr, long_rr)
|
||||
)
|
||||
|
||||
four_way |>
|
||||
gt() |>
|
||||
tab_header(title = "Four-Way Comparison of Effect Estimation Methods") |>
|
||||
cols_label(
|
||||
method = "Method",
|
||||
risk_difference = "Risk difference",
|
||||
risk_ratio = "Risk ratio"
|
||||
) |>
|
||||
fmt_number(columns = c(risk_difference, risk_ratio), decimals = 3)
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
### What each method answers
|
||||
|
||||
| Method | Question it answers |
|
||||
|--------|-------------------|
|
||||
| **Naive** | "What was the mortality difference between patients who did and did not receive early vasopressors?" |
|
||||
| **Standardization** | "What would the mortality risk be if everyone followed each strategy, holding the eligible population fixed?" |
|
||||
| **Cross-sectional IPTW** | "What is the effect of receiving early vasopressors versus not, reweighted to balance confounders?" |
|
||||
| **Longitudinal IPTW + IPCW** | "What is the per-protocol effect of the early vasopressor strategy versus the no early strategy, respecting the grace period and accounting for censoring?" |
|
||||
|
||||
### What to notice in the simulated data
|
||||
|
||||
In the data-generating process, early vasopressors have a modest **protective** effect.
|
||||
|
||||
1. The **naive** estimate shows **harm** (positive risk difference, risk ratio > 1). This is because sicker patients are more likely to receive early vasopressors and more likely to die — confounding by indication.
|
||||
|
||||
2. **Standardization** and **cross-sectional IPTW** both move the estimate toward a **protective** direction, correcting for baseline confounding.
|
||||
|
||||
3. The **longitudinal IPTW + IPCW** estimate may differ slightly from cross-sectional IPTW because it:
|
||||
- Restricts to the per-protocol population (patients who could adhere through the 2-hour window)
|
||||
- Re-weights for informative censoring (patients censored due to protocol deviation or early death)
|
||||
- Respects the grace period as a longitudinal treatment assignment window
|
||||
|
||||
None of the methods perfectly recover the true effect in a single finite sample, but the longitudinal method most closely emulates the target trial design.
|
||||
|
||||
## Assumption Checklist
|
||||
|
||||
Every method above relies on assumptions. A quick checklist:
|
||||
|
||||
| Assumption | Naive | Standardized | IPTW | IPTW + IPCW |
|
||||
|------------|-------|-------------|------|-------------|
|
||||
| No unmeasured confounding | ❌ | ✅ | ✅ | ✅ |
|
||||
| Positivity (every patient has some probability of each treatment) | ❌ | ✅ | ✅ | ✅ |
|
||||
| Correct model specification | N/A | ✅ | ✅ | ✅ |
|
||||
| No informative censoring | N/A | N/A | N/A | ✅ |
|
||||
|
||||
- **No unmeasured confounding**: We must have measured all variables that affect both treatment assignment and mortality. In a real study, this is never fully testable.
|
||||
- **Positivity**: Every patient must have a non-zero probability of receiving each treatment. Extreme propensity scores cause unstable weights.
|
||||
- **Correct model specification**: The logistic regression models must capture the true relationships between confounders, treatment, and outcome.
|
||||
- **No informative censoring (for IPCW)**: Censoring must be independent of the outcome given measured confounders. In practice, we use baseline-only IPCW here; a full analysis would include time-varying covariates.
|
||||
|
||||
## Next Steps
|
||||
|
||||
From here, the tutorial can extend into several directions:
|
||||
|
||||
1. **Time-varying confounders**: Include `map_current` and `lactate_current` in the censoring model to handle post-baseline severity changes.
|
||||
2. **Survival analysis**: Model time-to-death rather than 28-day binary mortality, using Kaplan-Meier or Cox models with IPCW.
|
||||
3. **Sensitivity analysis**: Test how sensitive the results are to unmeasured confounding (e.g., E-values).
|
||||
4. **`targets` pipeline**: Convert the analysis into a reproducible pipeline with dependency tracking.
|
||||
|
||||
These topics move the project from a teaching scaffold toward a production-ready target trial emulation workflow.
|
||||
Reference in New Issue
Block a user