update project design
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
---
|
||||
title: "Target Trial Basics: Early Vasopressors in Septic Shock"
|
||||
format: html
|
||||
format:
|
||||
html:
|
||||
embed-resources: true
|
||||
execute:
|
||||
echo: true
|
||||
warning: false
|
||||
@@ -18,18 +20,20 @@ The clinical question is:
|
||||
## Setup
|
||||
|
||||
```{r}
|
||||
library(dplyr)
|
||||
library(gt)
|
||||
library(gtsummary)
|
||||
library(readr)
|
||||
library(skimr)
|
||||
library(tibble)
|
||||
# library() attaches a package so its functions are available by name.
|
||||
# suppressPackageStartupMessages() keeps package startup text out of the report.
|
||||
suppressPackageStartupMessages({
|
||||
library(dplyr)
|
||||
library(gt)
|
||||
library(gtsummary)
|
||||
library(tibble)
|
||||
})
|
||||
|
||||
# source() loads reusable project functions from the R/ folder.
|
||||
source("../R/simulate_icu_cohort.R")
|
||||
source("../R/estimate_naive_vasopressor_mortality_effect.R")
|
||||
```
|
||||
|
||||
These packages make the routine analysis code easier to read.
|
||||
|
||||
`dplyr` handles data manipulation, `readr` reads rectangular files, `skimr` gives quick data summaries, `gt` builds display tables, and `gtsummary` builds analytic summary tables.
|
||||
|
||||
## Why A Target Trial?
|
||||
|
||||
Observational ICU data are not randomized.
|
||||
@@ -41,14 +45,25 @@ The target trial framework asks us to describe the randomized trial we wish we h
|
||||
## Target Trial Protocol
|
||||
|
||||
```{r}
|
||||
target_trial_protocol <- tribble(
|
||||
~component, ~definition,
|
||||
"Eligibility criteria", "ICU admission, suspected sepsis, hypotension, elevated lactate",
|
||||
"Time zero", "ICU admission",
|
||||
"Treatment strategy 1", "Start vasopressors within 2 hours",
|
||||
"Treatment strategy 2", "Do not start vasopressors within 2 hours",
|
||||
"Outcome", "Death within 28 days",
|
||||
"Causal contrast", "Risk difference and risk ratio"
|
||||
# tibble() creates a small rectangular dataset.
|
||||
# Each named argument becomes one column.
|
||||
target_trial_protocol <- tibble(
|
||||
component = c(
|
||||
"Eligibility criteria",
|
||||
"Time zero",
|
||||
"Treatment strategy 1",
|
||||
"Treatment strategy 2",
|
||||
"Outcome",
|
||||
"Estimand"
|
||||
),
|
||||
definition = c(
|
||||
"ICU admission, suspected sepsis, hypotension, elevated lactate",
|
||||
"ICU admission",
|
||||
"Start vasopressors within 2 hours",
|
||||
"Do not start vasopressors within 2 hours",
|
||||
"Death within 28 days",
|
||||
"Risk difference and risk ratio"
|
||||
)
|
||||
)
|
||||
|
||||
target_trial_protocol |>
|
||||
@@ -60,112 +75,73 @@ target_trial_protocol |>
|
||||
)
|
||||
```
|
||||
|
||||
`tribble()` creates a small tibble by typing the rows directly.
|
||||
## Simulate The Cohort
|
||||
|
||||
`gt()` turns that tibble into a clearer presentation table.
|
||||
For this report, we call the reusable simulation function directly.
|
||||
|
||||
## Load The Simulated Cohort
|
||||
|
||||
This first version uses a CSV generated by `scripts/01_simulate_icu_data_base_r.R`.
|
||||
Using the same seed gives the same simulated cohort each time the notebook renders.
|
||||
|
||||
```{r}
|
||||
icu_data <- read_csv("../data/icu_septic_shock_base_r.csv", show_col_types = FALSE)
|
||||
icu_data <- simulate_icu_cohort(n_patients = 1000, seed = 20260531)
|
||||
naive_mortality_analysis <- estimate_naive_vasopressor_mortality_effect(icu_data)
|
||||
eligible_icu_patients <- naive_mortality_analysis$eligible_icu_patients
|
||||
```
|
||||
|
||||
`read_csv()` reads a rectangular CSV file into R.
|
||||
|
||||
The object `icu_data` is a tibble, where each row is one ICU patient.
|
||||
|
||||
## Inspect The Data
|
||||
## Cohort Overview
|
||||
|
||||
```{r}
|
||||
icu_data |>
|
||||
slice_head(n = 6)
|
||||
```
|
||||
|
||||
`slice_head()` prints the first few rows so we can inspect the structure before analyzing anything.
|
||||
|
||||
```{r}
|
||||
skim(icu_data)
|
||||
```
|
||||
|
||||
`skim()` gives a quick summary of variable types, missingness, and distributions.
|
||||
|
||||
## Apply Eligibility Criteria
|
||||
|
||||
```{r}
|
||||
eligible_data <- icu_data |>
|
||||
filter(eligible == 1)
|
||||
```
|
||||
|
||||
`filter()` keeps rows that satisfy a condition.
|
||||
|
||||
Here, we keep only patients who satisfy the simulated eligibility criteria.
|
||||
|
||||
## Count Treatment Groups
|
||||
|
||||
```{r}
|
||||
eligible_data |>
|
||||
count(early_vasopressor)
|
||||
```
|
||||
|
||||
`count()` counts how many eligible patients were observed under each treatment group.
|
||||
|
||||
In this first simplified dataset:
|
||||
|
||||
- `1` means vasopressors started within 2 hours.
|
||||
- `0` means vasopressors were not started within 2 hours.
|
||||
|
||||
## Estimate Naive Mortality Risks
|
||||
|
||||
```{r}
|
||||
naive_risks <- eligible_data |>
|
||||
group_by(early_vasopressor) |>
|
||||
summarize(
|
||||
n_patients = n(),
|
||||
risk_death_28d = mean(death_28d),
|
||||
.groups = "drop"
|
||||
)
|
||||
|
||||
naive_risks |>
|
||||
gt() |>
|
||||
tab_header(title = "Naive 28-Day Mortality Risk") |>
|
||||
cols_label(
|
||||
early_vasopressor = "Early vasopressor",
|
||||
n_patients = "Patients",
|
||||
risk_death_28d = "28-day mortality risk"
|
||||
) |>
|
||||
fmt_number(columns = risk_death_28d, decimals = 3)
|
||||
```
|
||||
|
||||
Because `death_28d` is coded as 0 or 1, its mean is the proportion who died.
|
||||
|
||||
This is a naive comparison because it does not yet adjust for the fact that treatment decisions depend on patient severity.
|
||||
|
||||
## Estimate Naive Contrasts
|
||||
|
||||
```{r}
|
||||
risk_early <- naive_risks |>
|
||||
filter(early_vasopressor == 1) |>
|
||||
pull(risk_death_28d)
|
||||
|
||||
risk_not_early <- naive_risks |>
|
||||
filter(early_vasopressor == 0) |>
|
||||
pull(risk_death_28d)
|
||||
|
||||
naive_contrasts <- tibble(
|
||||
measure = c("Risk difference", "Risk ratio"),
|
||||
# These summary counts describe how the full simulated cohort maps onto the
|
||||
# eligible target-trial cohort.
|
||||
cohort_overview <- tibble(
|
||||
measure = c(
|
||||
"Simulated ICU patients",
|
||||
"Eligible patients",
|
||||
"Early vasopressor patients",
|
||||
"Not early vasopressor patients"
|
||||
),
|
||||
value = c(
|
||||
risk_early - risk_not_early,
|
||||
risk_early / risk_not_early
|
||||
nrow(icu_data),
|
||||
nrow(eligible_icu_patients),
|
||||
sum(eligible_icu_patients$early_vasopressor == 1),
|
||||
sum(eligible_icu_patients$early_vasopressor == 0)
|
||||
)
|
||||
)
|
||||
|
||||
naive_contrasts |>
|
||||
cohort_overview |>
|
||||
gt() |>
|
||||
tab_header(title = "Naive Treatment Contrast") |>
|
||||
tab_header(title = "Cohort Overview") |>
|
||||
cols_label(
|
||||
measure = "Measure",
|
||||
value = "Patients"
|
||||
) |>
|
||||
fmt_integer(columns = value)
|
||||
```
|
||||
|
||||
## Naive Mortality Risk
|
||||
|
||||
```{r}
|
||||
naive_mortality_analysis$mortality_risks_by_early_vasopressor |>
|
||||
gt() |>
|
||||
tab_header(title = "Naive 28-Day Mortality Risk") |>
|
||||
cols_label(
|
||||
observed_treatment_group = "Observed treatment group",
|
||||
patient_count = "Patients",
|
||||
mortality_risk_28d = "28-day mortality risk"
|
||||
) |>
|
||||
fmt_integer(columns = patient_count) |>
|
||||
fmt_number(columns = mortality_risk_28d, decimals = 3)
|
||||
```
|
||||
|
||||
This is a naive comparison because it does not yet adjust for the fact that treatment decisions depend on patient severity.
|
||||
|
||||
## 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)
|
||||
@@ -175,10 +151,10 @@ The risk difference is an absolute difference in 28-day mortality risk.
|
||||
|
||||
The risk ratio is a relative comparison of 28-day mortality risk.
|
||||
|
||||
## Check Confounding By Severity
|
||||
## Baseline Severity By Treatment Group
|
||||
|
||||
```{r}
|
||||
eligible_data |>
|
||||
eligible_icu_patients |>
|
||||
mutate(
|
||||
early_vasopressor = factor(
|
||||
early_vasopressor,
|
||||
@@ -198,14 +174,12 @@ eligible_data |>
|
||||
add_overall()
|
||||
```
|
||||
|
||||
This compares baseline severity between the two treatment groups.
|
||||
Early vasopressor patients are generally sicker at baseline in this simulated observational cohort.
|
||||
|
||||
If early vasopressor patients have higher SOFA scores, higher lactate, or lower MAP, then the naive comparison mixes the treatment effect with baseline severity differences.
|
||||
|
||||
That problem is one reason we need target trial emulation methods rather than a simple treated-versus-untreated comparison.
|
||||
That means the naive comparison mixes the effect of treatment with baseline severity differences, which is why we need target trial emulation methods rather than a simple treated-versus-untreated comparison.
|
||||
|
||||
## Next Step
|
||||
|
||||
The next lesson should walk through `R/simulate_icu_cohort.R` line by line.
|
||||
|
||||
After that, we can use `scripts/02_naive_analysis_base_r.R` to compute the first naive association and then discuss why it is not yet a causal estimate.
|
||||
After that, we can extend the notebooks to show why the naive mortality effect estimates are biased and start building adjusted analyses in reusable `R/` functions.
|
||||
|
||||
Reference in New Issue
Block a user