Update repo

This commit is contained in:
2026-06-08 10:15:36 -07:00
parent 73e5d46c30
commit 867c37696f
15 changed files with 1899 additions and 5 deletions
@@ -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.