207 lines
6.4 KiB
Plaintext
207 lines
6.4 KiB
Plaintext
---
|
|
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.
|