update project design

This commit is contained in:
2026-06-03 22:06:23 -08:00
parent 49409a7d1b
commit 73e5d46c30
27 changed files with 490 additions and 8513 deletions
+4
View File
@@ -0,0 +1,4 @@
*.csv
*.html
@@ -0,0 +1,98 @@
suppressPackageStartupMessages({
library(dplyr)
library(tidyr)
})
# estimate_naive_vasopressor_mortality_effect() computes the first simple
# mortality comparison for the early vasopressor teaching example.
#
# This function deliberately does NOT adjust for confounding.
# It only compares observed 28-day mortality between eligible patients who did
# and did not receive early vasopressors.
#
# Arguments:
# - icu_data: a data frame or tibble with the columns created by
# simulate_icu_cohort().
#
# Returns:
# - A named list with four pieces:
# - eligible_icu_patients: only the target-trial eligible patients.
# - mortality_risks_by_early_vasopressor: observed 28-day mortality risk in
# each treatment group.
# - mortality_effect_estimates: naive risk difference and risk ratio.
# - baseline_characteristics_by_early_vasopressor: compact baseline means by
# observed treatment group.
#
# Example REPL use:
#
# source("R/simulate_icu_cohort.R")
# icu_data <- simulate_icu_cohort(n_patients = 1000, seed = 20260531)
# mortality_analysis <- estimate_naive_vasopressor_mortality_effect(icu_data)
# mortality_analysis$mortality_effect_estimates
#
estimate_naive_vasopressor_mortality_effect <- function(icu_data) {
eligible_icu_patients <- icu_data |>
filter(eligible == 1)
mortality_risks_by_early_vasopressor <- eligible_icu_patients |>
group_by(early_vasopressor) |>
summarize(
patient_count = n(),
mortality_risk_28d = mean(death_28d),
.groups = "drop"
) |>
mutate(
observed_treatment_group = factor(
early_vasopressor,
levels = c(0, 1),
labels = c("No early vasopressor", "Early vasopressor")
)
) |>
select(observed_treatment_group, patient_count, mortality_risk_28d)
mortality_risk_early_vasopressor <- mortality_risks_by_early_vasopressor |>
filter(observed_treatment_group == "Early vasopressor") |>
pull(mortality_risk_28d)
mortality_risk_no_early_vasopressor <- mortality_risks_by_early_vasopressor |>
filter(observed_treatment_group == "No early vasopressor") |>
pull(mortality_risk_28d)
mortality_effect_estimates <- tibble(
estimate = c(
"Eligible patients",
"Early vasopressor patients",
"No early vasopressor patients",
"28-day mortality risk, early vasopressor",
"28-day mortality risk, no early vasopressor",
"Naive risk difference",
"Naive risk ratio"
),
value = c(
nrow(eligible_icu_patients),
sum(eligible_icu_patients$early_vasopressor == 1),
sum(eligible_icu_patients$early_vasopressor == 0),
mortality_risk_early_vasopressor,
mortality_risk_no_early_vasopressor,
mortality_risk_early_vasopressor - mortality_risk_no_early_vasopressor,
mortality_risk_early_vasopressor / mortality_risk_no_early_vasopressor
)
)
baseline_characteristics_by_early_vasopressor <- eligible_icu_patients |>
group_by(early_vasopressor) |>
summarize(
mean_age = mean(age),
mean_sofa_score = mean(sofa_score),
mean_lactate = mean(lactate),
mean_map = mean(map),
.groups = "drop"
)
list(
eligible_icu_patients = eligible_icu_patients,
mortality_risks_by_early_vasopressor = mortality_risks_by_early_vasopressor,
mortality_effect_estimates = mortality_effect_estimates,
baseline_characteristics_by_early_vasopressor = baseline_characteristics_by_early_vasopressor
)
}
+85 -2
View File
@@ -1,58 +1,141 @@
suppressPackageStartupMessages(library(dplyr))
suppressPackageStartupMessages({
library(dplyr)
})
# simulate_icu_cohort() is our first reusable project primitive.
#
# A "primitive" is a small function that does one useful job for the project.
# Here, the job is to create one simulated ICU observational cohort that we can
# reuse in scripts, notebooks, and later targets pipelines.
#
# Arguments:
# - n_patients: how many ICU patients to simulate.
# - seed: a number that makes the random simulation reproducible.
#
# Returns:
# - A tibble with one row per ICU patient.
# - The columns include baseline variables, eligibility indicators, observed
# early vasopressor treatment, treatment timing, and 28-day mortality.
#
# Example REPL use:
#
# source("R/simulate_icu_cohort.R")
# icu_data <- simulate_icu_cohort(n_patients = 5, seed = 1)
# icu_data
#
# Example output shape:
#
# # A tibble: 5 x 13
# patient_id age sex sofa_score lactate map suspected_sepsis ...
# <int> <dbl> <chr> <dbl> <dbl> <dbl> <int> ...
# 1 1 56 female 8 2.9 59 1 ...
#
simulate_icu_cohort <- function(n_patients = 1000, seed = 20260531) {
# set.seed() fixes the random-number stream.
# That means the same inputs produce the same simulated dataset each time.
set.seed(seed)
# tibble() creates a modern data frame.
# Each argument below becomes a column.
# Each column must have either one value or n_patients values.
tibble(
# seq_len(n_patients) creates patient IDs 1, 2, ..., n_patients.
patient_id = seq_len(n_patients),
# rnorm() draws from a normal distribution.
# round() makes age whole-number-like.
# pmin() and pmax() cap ages to a plausible ICU range.
age = round(rnorm(n_patients, mean = 65, sd = 14)) |>
pmin(95) |>
pmax(18),
sex = sample(c("female", "male"), size = n_patients, replace = TRUE, prob = c(0.45, 0.55)),
# sample() draws categorical values.
# replace = TRUE means each patient gets an independent draw.
sex = sample(
c("female", "male"),
size = n_patients,
replace = TRUE,
prob = c(0.45, 0.55)
),
# rpois() draws count-like SOFA scores from a Poisson distribution.
# pmin(20) keeps the simulated score within a plausible upper range.
sofa_score = rpois(n_patients, lambda = 7) |>
pmin(20),
# rlnorm() creates right-skewed lactate values.
# Clinical lab values often have this kind of skew.
lactate = round(rlnorm(n_patients, meanlog = log(3), sdlog = 0.5), 1) |>
pmin(15),
# MAP is mean arterial pressure.
# Lower MAP means more hypotension and greater shock severity.
map = round(rnorm(n_patients, mean = 62, sd = 10)) |>
pmin(95) |>
pmax(35),
# rbinom(..., size = 1) creates 0/1 indicators.
# Here 1 means suspected sepsis is present at ICU admission.
suspected_sepsis = rbinom(n_patients, size = 1, prob = 0.90)
) |>
# mutate() adds new columns or changes existing columns.
# These columns depend on the baseline variables created above.
mutate(
# as.integer(TRUE) is 1 and as.integer(FALSE) is 0.
hypotension_at_baseline = as.integer(map < 65),
elevated_lactate_at_baseline = as.integer(lactate >= 2),
# This is the simulated eligibility definition for the target trial.
# A patient is eligible only if all three baseline criteria are true.
eligible = as.integer(
suspected_sepsis == 1 &
hypotension_at_baseline == 1 &
elevated_lactate_at_baseline == 1
),
# This is not a real clinical score.
# It is a simulation device that makes sicker patients more likely to
# receive early vasopressors and more likely to die.
severity_score = 0.04 * (age - 65) +
0.18 * (sofa_score - 7) +
0.25 * (lactate - 3) -
0.04 * (map - 62),
# plogis() converts any real number into a probability between 0 and 1.
# Higher severity_score gives a higher chance of early vasopressors.
prob_early_vasopressor = plogis(-0.2 + severity_score),
# This is the observed treatment group in the observational data.
# It is not randomized; it depends on severity through the probability above.
early_vasopressor = rbinom(n(), size = 1, prob = prob_early_vasopressor),
# ifelse() chooses one value when the condition is TRUE and another when FALSE.
# Early-treated patients get a time between 0 and 2 hours.
# Not-early-treated patients get a time between 2 and 24 hours.
time_to_vasopressor_hours = ifelse(
early_vasopressor == 1,
runif(n(), min = 0, max = 2),
runif(n(), min = 2, max = 24)
) |>
round(2),
# This linear predictor controls each patient's mortality risk.
# Sicker patients have higher risk; early vasopressor has a modest
# protective effect in the data-generating process.
mortality_linear_predictor = -1.4 +
0.03 * (age - 65) +
0.20 * (sofa_score - 7) +
0.28 * (lactate - 3) -
0.05 * (map - 62) -
0.25 * early_vasopressor,
# Convert the mortality linear predictor into a probability, then draw
# the observed 28-day death indicator.
prob_death_28d = plogis(mortality_linear_predictor),
death_28d = rbinom(n(), size = 1, prob = prob_death_28d)
) |>
# select() keeps the columns learners should analyze.
# We intentionally drop helper columns like severity_score and probabilities.
select(
patient_id,
age,
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
/.quarto/
**/*.quarto_ipynb
File diff suppressed because it is too large Load Diff
+91 -117
View File
@@ -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.
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,236 +0,0 @@
/* quarto syntax highlight colors */
:root {
--quarto-hl-ot-color: #003B4F;
--quarto-hl-at-color: #657422;
--quarto-hl-ss-color: #20794D;
--quarto-hl-an-color: #5E5E5E;
--quarto-hl-fu-color: #4758AB;
--quarto-hl-st-color: #20794D;
--quarto-hl-cf-color: #003B4F;
--quarto-hl-op-color: #5E5E5E;
--quarto-hl-er-color: #AD0000;
--quarto-hl-bn-color: #AD0000;
--quarto-hl-al-color: #AD0000;
--quarto-hl-va-color: #111111;
--quarto-hl-bu-color: inherit;
--quarto-hl-ex-color: inherit;
--quarto-hl-pp-color: #AD0000;
--quarto-hl-in-color: #5E5E5E;
--quarto-hl-vs-color: #20794D;
--quarto-hl-wa-color: #5E5E5E;
--quarto-hl-do-color: #5E5E5E;
--quarto-hl-im-color: #00769E;
--quarto-hl-ch-color: #20794D;
--quarto-hl-dt-color: #AD0000;
--quarto-hl-fl-color: #AD0000;
--quarto-hl-co-color: #5E5E5E;
--quarto-hl-cv-color: #5E5E5E;
--quarto-hl-cn-color: #8f5902;
--quarto-hl-sc-color: #5E5E5E;
--quarto-hl-dv-color: #AD0000;
--quarto-hl-kw-color: #003B4F;
}
/* other quarto variables */
:root {
--quarto-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
/* syntax highlight based on Pandoc's rules */
pre > code.sourceCode > span {
color: #003B4F;
}
code.sourceCode > span {
color: #003B4F;
}
div.sourceCode,
div.sourceCode pre.sourceCode {
color: #003B4F;
}
/* Normal */
code span {
color: #003B4F;
}
/* Alert */
code span.al {
color: #AD0000;
font-style: inherit;
}
/* Annotation */
code span.an {
color: #5E5E5E;
font-style: inherit;
}
/* Attribute */
code span.at {
color: #657422;
font-style: inherit;
}
/* BaseN */
code span.bn {
color: #AD0000;
font-style: inherit;
}
/* BuiltIn */
code span.bu {
font-style: inherit;
}
/* ControlFlow */
code span.cf {
color: #003B4F;
font-weight: bold;
font-style: inherit;
}
/* Char */
code span.ch {
color: #20794D;
font-style: inherit;
}
/* Constant */
code span.cn {
color: #8f5902;
font-style: inherit;
}
/* Comment */
code span.co {
color: #5E5E5E;
font-style: inherit;
}
/* CommentVar */
code span.cv {
color: #5E5E5E;
font-style: italic;
}
/* Documentation */
code span.do {
color: #5E5E5E;
font-style: italic;
}
/* DataType */
code span.dt {
color: #AD0000;
font-style: inherit;
}
/* DecVal */
code span.dv {
color: #AD0000;
font-style: inherit;
}
/* Error */
code span.er {
color: #AD0000;
font-style: inherit;
}
/* Extension */
code span.ex {
font-style: inherit;
}
/* Float */
code span.fl {
color: #AD0000;
font-style: inherit;
}
/* Function */
code span.fu {
color: #4758AB;
font-style: inherit;
}
/* Import */
code span.im {
color: #00769E;
font-style: inherit;
}
/* Information */
code span.in {
color: #5E5E5E;
font-style: inherit;
}
/* Keyword */
code span.kw {
color: #003B4F;
font-weight: bold;
font-style: inherit;
}
/* Operator */
code span.op {
color: #5E5E5E;
font-style: inherit;
}
/* Other */
code span.ot {
color: #003B4F;
font-style: inherit;
}
/* Preprocessor */
code span.pp {
color: #AD0000;
font-style: inherit;
}
/* SpecialChar */
code span.sc {
color: #5E5E5E;
font-style: inherit;
}
/* SpecialString */
code span.ss {
color: #20794D;
font-style: inherit;
}
/* String */
code span.st {
color: #20794D;
font-style: inherit;
}
/* Variable */
code span.va {
color: #111111;
font-style: inherit;
}
/* VerbatimString */
code span.vs {
color: #20794D;
font-style: inherit;
}
/* Warning */
code span.wa {
color: #5E5E5E;
font-style: italic;
}
.prevent-inlining {
content: "</";
}
/*# sourceMappingURL=789ad8cd35626ba772517505b42a116e.css.map */
@@ -1,845 +0,0 @@
import * as tabsets from "./tabsets/tabsets.js";
const sectionChanged = new CustomEvent("quarto-sectionChanged", {
detail: {},
bubbles: true,
cancelable: false,
composed: false,
});
const layoutMarginEls = () => {
// Find any conflicting margin elements and add margins to the
// top to prevent overlap
const marginChildren = window.document.querySelectorAll(
".column-margin.column-container > *, .margin-caption, .aside"
);
let lastBottom = 0;
for (const marginChild of marginChildren) {
if (marginChild.offsetParent !== null) {
// clear the top margin so we recompute it
marginChild.style.marginTop = null;
const top = marginChild.getBoundingClientRect().top + window.scrollY;
if (top < lastBottom) {
const marginChildStyle = window.getComputedStyle(marginChild);
const marginBottom = parseFloat(marginChildStyle["marginBottom"]);
const margin = lastBottom - top + marginBottom;
marginChild.style.marginTop = `${margin}px`;
}
const styles = window.getComputedStyle(marginChild);
const marginTop = parseFloat(styles["marginTop"]);
lastBottom = top + marginChild.getBoundingClientRect().height + marginTop;
}
}
};
window.document.addEventListener("DOMContentLoaded", function (_event) {
// Recompute the position of margin elements anytime the body size changes
if (window.ResizeObserver) {
const resizeObserver = new window.ResizeObserver(
throttle(() => {
layoutMarginEls();
if (
window.document.body.getBoundingClientRect().width < 990 &&
isReaderMode()
) {
quartoToggleReader();
}
}, 50)
);
resizeObserver.observe(window.document.body);
}
const tocEl = window.document.querySelector('nav.toc-active[role="doc-toc"]');
const sidebarEl = window.document.getElementById("quarto-sidebar");
const leftTocEl = window.document.getElementById("quarto-sidebar-toc-left");
const marginSidebarEl = window.document.getElementById(
"quarto-margin-sidebar"
);
// function to determine whether the element has a previous sibling that is active
const prevSiblingIsActiveLink = (el) => {
const sibling = el.previousElementSibling;
if (sibling && sibling.tagName === "A") {
return sibling.classList.contains("active");
} else {
return false;
}
};
// dispatch for htmlwidgets
// they use slideenter event to trigger resize
function fireSlideEnter() {
const event = window.document.createEvent("Event");
event.initEvent("slideenter", true, true);
window.document.dispatchEvent(event);
}
const tabs = window.document.querySelectorAll('a[data-bs-toggle="tab"]');
tabs.forEach((tab) => {
tab.addEventListener("shown.bs.tab", fireSlideEnter);
});
// dispatch for shiny
// they use BS shown and hidden events to trigger rendering
function distpatchShinyEvents(previous, current) {
if (window.jQuery) {
if (previous) {
window.jQuery(previous).trigger("hidden");
}
if (current) {
window.jQuery(current).trigger("shown");
}
}
}
// tabby.js listener: Trigger event for htmlwidget and shiny
document.addEventListener(
"tabby",
function (event) {
fireSlideEnter();
distpatchShinyEvents(event.detail.previousTab, event.detail.tab);
},
false
);
// Track scrolling and mark TOC links as active
// get table of contents and sidebar (bail if we don't have at least one)
const tocLinks = tocEl
? [...tocEl.querySelectorAll("a[data-scroll-target]")]
: [];
const makeActive = (link) => tocLinks[link].classList.add("active");
const removeActive = (link) => tocLinks[link].classList.remove("active");
const removeAllActive = () =>
[...Array(tocLinks.length).keys()].forEach((link) => removeActive(link));
// activate the anchor for a section associated with this TOC entry
tocLinks.forEach((link) => {
link.addEventListener("click", () => {
if (link.href.indexOf("#") !== -1) {
const anchor = link.href.split("#")[1];
const heading = window.document.querySelector(
`[data-anchor-id="${anchor}"]`
);
if (heading) {
// Add the class
heading.classList.add("reveal-anchorjs-link");
// function to show the anchor
const handleMouseout = () => {
heading.classList.remove("reveal-anchorjs-link");
heading.removeEventListener("mouseout", handleMouseout);
};
// add a function to clear the anchor when the user mouses out of it
heading.addEventListener("mouseout", handleMouseout);
}
}
});
});
const sections = tocLinks.map((link) => {
const target = link.getAttribute("data-scroll-target");
if (target.startsWith("#")) {
return window.document.getElementById(decodeURI(`${target.slice(1)}`));
} else {
return window.document.querySelector(decodeURI(`${target}`));
}
});
const sectionMargin = 200;
let currentActive = 0;
// track whether we've initialized state the first time
let init = false;
const updateActiveLink = () => {
// The index from bottom to top (e.g. reversed list)
let sectionIndex = -1;
if (
window.innerHeight + window.pageYOffset >=
window.document.body.offsetHeight
) {
// This is the no-scroll case where last section should be the active one
sectionIndex = 0;
} else {
// This finds the last section visible on screen that should be made active
sectionIndex = [...sections].reverse().findIndex((section) => {
if (section) {
return window.pageYOffset >= section.offsetTop - sectionMargin;
} else {
return false;
}
});
}
if (sectionIndex > -1) {
const current = sections.length - sectionIndex - 1;
if (current !== currentActive) {
removeAllActive();
currentActive = current;
makeActive(current);
if (init) {
window.dispatchEvent(sectionChanged);
}
init = true;
}
}
};
const inHiddenRegion = (top, bottom, hiddenRegions) => {
for (const region of hiddenRegions) {
if (top <= region.bottom && bottom >= region.top) {
return true;
}
}
return false;
};
const categorySelector = "header.quarto-title-block .quarto-category";
const activateCategories = (href) => {
// Find any categories
// Surround them with a link pointing back to:
// #category=Authoring
try {
const categoryEls = window.document.querySelectorAll(categorySelector);
for (const categoryEl of categoryEls) {
const categoryText = categoryEl.textContent;
if (categoryText) {
const link = `${href}#category=${encodeURIComponent(categoryText)}`;
const linkEl = window.document.createElement("a");
linkEl.setAttribute("href", link);
for (const child of categoryEl.childNodes) {
linkEl.append(child);
}
categoryEl.appendChild(linkEl);
}
}
} catch {
// Ignore errors
}
};
function hasTitleCategories() {
return window.document.querySelector(categorySelector) !== null;
}
function offsetRelativeUrl(url) {
const offset = getMeta("quarto:offset");
return offset ? offset + url : url;
}
function offsetAbsoluteUrl(url) {
const offset = getMeta("quarto:offset");
const baseUrl = new URL(offset, window.location);
const projRelativeUrl = url.replace(baseUrl, "");
if (projRelativeUrl.startsWith("/")) {
return projRelativeUrl;
} else {
return "/" + projRelativeUrl;
}
}
// read a meta tag value
function getMeta(metaName) {
const metas = window.document.getElementsByTagName("meta");
for (let i = 0; i < metas.length; i++) {
if (metas[i].getAttribute("name") === metaName) {
return metas[i].getAttribute("content");
}
}
return "";
}
async function findAndActivateCategories() {
// Categories search with listing only use path without query
const currentPagePath = offsetAbsoluteUrl(
window.location.origin + window.location.pathname
);
const response = await fetch(offsetRelativeUrl("listings.json"));
if (response.status == 200) {
return response.json().then(function (listingPaths) {
const listingHrefs = [];
for (const listingPath of listingPaths) {
const pathWithoutLeadingSlash = listingPath.listing.substring(1);
for (const item of listingPath.items) {
const encodedItem = encodeURI(item);
if (
encodedItem === currentPagePath ||
encodedItem === currentPagePath + "index.html"
) {
// Resolve this path against the offset to be sure
// we already are using the correct path to the listing
// (this adjusts the listing urls to be rooted against
// whatever root the page is actually running against)
const relative = offsetRelativeUrl(pathWithoutLeadingSlash);
const baseUrl = window.location;
const resolvedPath = new URL(relative, baseUrl);
listingHrefs.push(resolvedPath.pathname);
break;
}
}
}
// Look up the tree for a nearby linting and use that if we find one
const nearestListing = findNearestParentListing(
offsetAbsoluteUrl(window.location.pathname),
listingHrefs
);
if (nearestListing) {
activateCategories(nearestListing);
} else {
// See if the referrer is a listing page for this item
const referredRelativePath = offsetAbsoluteUrl(document.referrer);
const referrerListing = listingHrefs.find((listingHref) => {
const isListingReferrer =
listingHref === referredRelativePath ||
listingHref === referredRelativePath + "index.html";
return isListingReferrer;
});
if (referrerListing) {
// Try to use the referrer if possible
activateCategories(referrerListing);
} else if (listingHrefs.length > 0) {
// Otherwise, just fall back to the first listing
activateCategories(listingHrefs[0]);
}
}
});
}
}
if (hasTitleCategories()) {
findAndActivateCategories();
}
const findNearestParentListing = (href, listingHrefs) => {
if (!href || !listingHrefs) {
return undefined;
}
// Look up the tree for a nearby linting and use that if we find one
const relativeParts = href.substring(1).split("/");
while (relativeParts.length > 0) {
const path = relativeParts.join("/");
for (const listingHref of listingHrefs) {
if (listingHref.startsWith(path)) {
return listingHref;
}
}
relativeParts.pop();
}
return undefined;
};
const manageSidebarVisiblity = (el, placeholderDescriptor) => {
let isVisible = true;
let elRect;
return (hiddenRegions) => {
if (el === null) {
return;
}
// Find the last element of the TOC
const lastChildEl = el.lastElementChild;
if (lastChildEl) {
// Converts the sidebar to a menu
const convertToMenu = () => {
for (const child of el.children) {
child.style.opacity = 0;
child.style.overflow = "hidden";
child.style.pointerEvents = "none";
}
nexttick(() => {
const toggleContainer = window.document.createElement("div");
toggleContainer.style.width = "100%";
toggleContainer.classList.add("zindex-over-content");
toggleContainer.classList.add("quarto-sidebar-toggle");
toggleContainer.classList.add("headroom-target"); // Marks this to be managed by headeroom
toggleContainer.id = placeholderDescriptor.id;
toggleContainer.style.position = "fixed";
const toggleIcon = window.document.createElement("i");
toggleIcon.classList.add("quarto-sidebar-toggle-icon");
toggleIcon.classList.add("bi");
toggleIcon.classList.add("bi-caret-down-fill");
const toggleTitle = window.document.createElement("div");
const titleEl = window.document.body.querySelector(
placeholderDescriptor.titleSelector
);
if (titleEl) {
toggleTitle.append(
titleEl.textContent || titleEl.innerText,
toggleIcon
);
}
toggleTitle.classList.add("zindex-over-content");
toggleTitle.classList.add("quarto-sidebar-toggle-title");
toggleContainer.append(toggleTitle);
const toggleContents = window.document.createElement("div");
toggleContents.classList = el.classList;
toggleContents.classList.add("zindex-over-content");
toggleContents.classList.add("quarto-sidebar-toggle-contents");
for (const child of el.children) {
if (child.id === "toc-title") {
continue;
}
const clone = child.cloneNode(true);
clone.style.opacity = 1;
clone.style.pointerEvents = null;
clone.style.display = null;
toggleContents.append(clone);
}
toggleContents.style.height = "0px";
const positionToggle = () => {
// position the element (top left of parent, same width as parent)
if (!elRect) {
elRect = el.getBoundingClientRect();
}
toggleContainer.style.left = `${elRect.left}px`;
toggleContainer.style.top = `${elRect.top}px`;
toggleContainer.style.width = `${elRect.width}px`;
};
positionToggle();
toggleContainer.append(toggleContents);
el.parentElement.prepend(toggleContainer);
// Process clicks
let tocShowing = false;
// Allow the caller to control whether this is dismissed
// when it is clicked (e.g. sidebar navigation supports
// opening and closing the nav tree, so don't dismiss on click)
const clickEl = placeholderDescriptor.dismissOnClick
? toggleContainer
: toggleTitle;
const closeToggle = () => {
if (tocShowing) {
toggleContainer.classList.remove("expanded");
toggleContents.style.height = "0px";
tocShowing = false;
}
};
// Get rid of any expanded toggle if the user scrolls
window.document.addEventListener(
"scroll",
throttle(() => {
closeToggle();
}, 50)
);
// Handle positioning of the toggle
window.addEventListener(
"resize",
throttle(() => {
elRect = undefined;
positionToggle();
}, 50)
);
window.addEventListener("quarto-hrChanged", () => {
elRect = undefined;
});
// Process the click
clickEl.onclick = () => {
if (!tocShowing) {
toggleContainer.classList.add("expanded");
toggleContents.style.height = null;
tocShowing = true;
} else {
closeToggle();
}
};
});
};
// Converts a sidebar from a menu back to a sidebar
const convertToSidebar = () => {
for (const child of el.children) {
child.style.opacity = 1;
child.style.overflow = null;
child.style.pointerEvents = null;
}
const placeholderEl = window.document.getElementById(
placeholderDescriptor.id
);
if (placeholderEl) {
placeholderEl.remove();
}
el.classList.remove("rollup");
};
if (isReaderMode()) {
convertToMenu();
isVisible = false;
} else {
// Find the top and bottom o the element that is being managed
const elTop = el.offsetTop;
const elBottom =
elTop + lastChildEl.offsetTop + lastChildEl.offsetHeight;
if (!isVisible) {
// If the element is current not visible reveal if there are
// no conflicts with overlay regions
if (!inHiddenRegion(elTop, elBottom, hiddenRegions)) {
convertToSidebar();
isVisible = true;
}
} else {
// If the element is visible, hide it if it conflicts with overlay regions
// and insert a placeholder toggle (or if we're in reader mode)
if (inHiddenRegion(elTop, elBottom, hiddenRegions)) {
convertToMenu();
isVisible = false;
}
}
}
}
};
};
const tabEls = document.querySelectorAll('a[data-bs-toggle="tab"]');
for (const tabEl of tabEls) {
const id = tabEl.getAttribute("data-bs-target");
if (id) {
const columnEl = document.querySelector(
`${id} .column-margin, .tabset-margin-content`
);
if (columnEl)
tabEl.addEventListener("shown.bs.tab", function (event) {
const el = event.srcElement;
if (el) {
const visibleCls = `${el.id}-margin-content`;
// walk up until we find a parent tabset
let panelTabsetEl = el.parentElement;
while (panelTabsetEl) {
if (panelTabsetEl.classList.contains("panel-tabset")) {
break;
}
panelTabsetEl = panelTabsetEl.parentElement;
}
if (panelTabsetEl) {
const prevSib = panelTabsetEl.previousElementSibling;
if (
prevSib &&
prevSib.classList.contains("tabset-margin-container")
) {
const childNodes = prevSib.querySelectorAll(
".tabset-margin-content"
);
for (const childEl of childNodes) {
if (childEl.classList.contains(visibleCls)) {
childEl.classList.remove("collapse");
} else {
childEl.classList.add("collapse");
}
}
}
}
}
layoutMarginEls();
});
}
}
// Manage the visibility of the toc and the sidebar
const marginScrollVisibility = manageSidebarVisiblity(marginSidebarEl, {
id: "quarto-toc-toggle",
titleSelector: "#toc-title",
dismissOnClick: true,
});
const sidebarScrollVisiblity = manageSidebarVisiblity(sidebarEl, {
id: "quarto-sidebarnav-toggle",
titleSelector: ".title",
dismissOnClick: false,
});
let tocLeftScrollVisibility;
if (leftTocEl) {
tocLeftScrollVisibility = manageSidebarVisiblity(leftTocEl, {
id: "quarto-lefttoc-toggle",
titleSelector: "#toc-title",
dismissOnClick: true,
});
}
// Find the first element that uses formatting in special columns
const conflictingEls = window.document.body.querySelectorAll(
'[class^="column-"], [class*=" column-"], aside, [class*="margin-caption"], [class*=" margin-caption"], [class*="margin-ref"], [class*=" margin-ref"]'
);
// Filter all the possibly conflicting elements into ones
// the do conflict on the left or ride side
const arrConflictingEls = Array.from(conflictingEls);
const leftSideConflictEls = arrConflictingEls.filter((el) => {
if (el.tagName === "ASIDE") {
return false;
}
return Array.from(el.classList).find((className) => {
return (
className !== "column-body" &&
className.startsWith("column-") &&
!className.endsWith("right") &&
!className.endsWith("container") &&
className !== "column-margin"
);
});
});
const rightSideConflictEls = arrConflictingEls.filter((el) => {
if (el.tagName === "ASIDE") {
return true;
}
const hasMarginCaption = Array.from(el.classList).find((className) => {
return className == "margin-caption";
});
if (hasMarginCaption) {
return true;
}
return Array.from(el.classList).find((className) => {
return (
className !== "column-body" &&
!className.endsWith("container") &&
className.startsWith("column-") &&
!className.endsWith("left")
);
});
});
const kOverlapPaddingSize = 10;
function toRegions(els) {
return els.map((el) => {
const boundRect = el.getBoundingClientRect();
const top =
boundRect.top +
document.documentElement.scrollTop -
kOverlapPaddingSize;
return {
top,
bottom: top + el.scrollHeight + 2 * kOverlapPaddingSize,
};
});
}
let hasObserved = false;
const visibleItemObserver = (els) => {
let visibleElements = [...els];
const intersectionObserver = new IntersectionObserver(
(entries, _observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
if (visibleElements.indexOf(entry.target) === -1) {
visibleElements.push(entry.target);
}
} else {
visibleElements = visibleElements.filter((visibleEntry) => {
return visibleEntry !== entry;
});
}
});
if (!hasObserved) {
hideOverlappedSidebars();
}
hasObserved = true;
},
{}
);
els.forEach((el) => {
intersectionObserver.observe(el);
});
return {
getVisibleEntries: () => {
return visibleElements;
},
};
};
const rightElementObserver = visibleItemObserver(rightSideConflictEls);
const leftElementObserver = visibleItemObserver(leftSideConflictEls);
const hideOverlappedSidebars = () => {
marginScrollVisibility(toRegions(rightElementObserver.getVisibleEntries()));
sidebarScrollVisiblity(toRegions(leftElementObserver.getVisibleEntries()));
if (tocLeftScrollVisibility) {
tocLeftScrollVisibility(
toRegions(leftElementObserver.getVisibleEntries())
);
}
};
window.quartoToggleReader = () => {
// Applies a slow class (or removes it)
// to update the transition speed
const slowTransition = (slow) => {
const manageTransition = (id, slow) => {
const el = document.getElementById(id);
if (el) {
if (slow) {
el.classList.add("slow");
} else {
el.classList.remove("slow");
}
}
};
manageTransition("TOC", slow);
manageTransition("quarto-sidebar", slow);
};
const readerMode = !isReaderMode();
setReaderModeValue(readerMode);
// If we're entering reader mode, slow the transition
if (readerMode) {
slowTransition(readerMode);
}
highlightReaderToggle(readerMode);
hideOverlappedSidebars();
// If we're exiting reader mode, restore the non-slow transition
if (!readerMode) {
slowTransition(!readerMode);
}
};
const highlightReaderToggle = (readerMode) => {
const els = document.querySelectorAll(".quarto-reader-toggle");
if (els) {
els.forEach((el) => {
if (readerMode) {
el.classList.add("reader");
} else {
el.classList.remove("reader");
}
});
}
};
const setReaderModeValue = (val) => {
if (window.location.protocol !== "file:") {
window.localStorage.setItem("quarto-reader-mode", val);
} else {
localReaderMode = val;
}
};
const isReaderMode = () => {
if (window.location.protocol !== "file:") {
return window.localStorage.getItem("quarto-reader-mode") === "true";
} else {
return localReaderMode;
}
};
let localReaderMode = null;
const tocOpenDepthStr = tocEl?.getAttribute("data-toc-expanded");
const tocOpenDepth = tocOpenDepthStr ? Number(tocOpenDepthStr) : 1;
// Walk the TOC and collapse/expand nodes
// Nodes are expanded if:
// - they are top level
// - they have children that are 'active' links
// - they are directly below an link that is 'active'
const walk = (el, depth) => {
// Tick depth when we enter a UL
if (el.tagName === "UL") {
depth = depth + 1;
}
// It this is active link
let isActiveNode = false;
if (el.tagName === "A" && el.classList.contains("active")) {
isActiveNode = true;
}
// See if there is an active child to this element
let hasActiveChild = false;
for (const child of el.children) {
hasActiveChild = walk(child, depth) || hasActiveChild;
}
// Process the collapse state if this is an UL
if (el.tagName === "UL") {
if (tocOpenDepth === -1 && depth > 1) {
// toc-expand: false
el.classList.add("collapse");
} else if (
depth <= tocOpenDepth ||
hasActiveChild ||
prevSiblingIsActiveLink(el)
) {
el.classList.remove("collapse");
} else {
el.classList.add("collapse");
}
// untick depth when we leave a UL
depth = depth - 1;
}
return hasActiveChild || isActiveNode;
};
// walk the TOC and expand / collapse any items that should be shown
if (tocEl) {
updateActiveLink();
walk(tocEl, 0);
}
// Throttle the scroll event and walk peridiocally
window.document.addEventListener(
"scroll",
throttle(() => {
if (tocEl) {
updateActiveLink();
walk(tocEl, 0);
}
if (!isReaderMode()) {
hideOverlappedSidebars();
}
}, 5)
);
window.addEventListener(
"resize",
throttle(() => {
if (tocEl) {
updateActiveLink();
walk(tocEl, 0);
}
if (!isReaderMode()) {
hideOverlappedSidebars();
}
}, 10)
);
hideOverlappedSidebars();
highlightReaderToggle(isReaderMode());
});
tabsets.init();
function throttle(func, wait) {
let waiting = false;
return function () {
if (!waiting) {
func.apply(this, arguments);
waiting = true;
setTimeout(function () {
waiting = false;
}, wait);
}
};
}
function nexttick(func) {
return setTimeout(func, 0);
}
@@ -1,95 +0,0 @@
// grouped tabsets
export function init() {
window.addEventListener("pageshow", (_event) => {
function getTabSettings() {
const data = localStorage.getItem("quarto-persistent-tabsets-data");
if (!data) {
localStorage.setItem("quarto-persistent-tabsets-data", "{}");
return {};
}
if (data) {
return JSON.parse(data);
}
}
function setTabSettings(data) {
localStorage.setItem(
"quarto-persistent-tabsets-data",
JSON.stringify(data)
);
}
function setTabState(groupName, groupValue) {
const data = getTabSettings();
data[groupName] = groupValue;
setTabSettings(data);
}
function toggleTab(tab, active) {
const tabPanelId = tab.getAttribute("aria-controls");
const tabPanel = document.getElementById(tabPanelId);
if (active) {
tab.classList.add("active");
tabPanel.classList.add("active");
} else {
tab.classList.remove("active");
tabPanel.classList.remove("active");
}
}
function toggleAll(selectedGroup, selectorsToSync) {
for (const [thisGroup, tabs] of Object.entries(selectorsToSync)) {
const active = selectedGroup === thisGroup;
for (const tab of tabs) {
toggleTab(tab, active);
}
}
}
function findSelectorsToSyncByLanguage() {
const result = {};
const tabs = Array.from(
document.querySelectorAll(`div[data-group] a[id^='tabset-']`)
);
for (const item of tabs) {
const div = item.parentElement.parentElement.parentElement;
const group = div.getAttribute("data-group");
if (!result[group]) {
result[group] = {};
}
const selectorsToSync = result[group];
const value = item.innerHTML;
if (!selectorsToSync[value]) {
selectorsToSync[value] = [];
}
selectorsToSync[value].push(item);
}
return result;
}
function setupSelectorSync() {
const selectorsToSync = findSelectorsToSyncByLanguage();
Object.entries(selectorsToSync).forEach(([group, tabSetsByValue]) => {
Object.entries(tabSetsByValue).forEach(([value, items]) => {
items.forEach((item) => {
item.addEventListener("click", (_event) => {
setTabState(group, value);
toggleAll(value, selectorsToSync[group]);
});
});
});
});
return selectorsToSync;
}
const selectorsToSync = setupSelectorSync();
for (const [group, selectedName] of Object.entries(getTabSettings())) {
const selectors = selectorsToSync[group];
// it's possible that stale state gives us empty selections, so we explicitly check here.
if (selectors) {
toggleAll(selectedName, selectors);
}
}
});
}
@@ -1 +0,0 @@
.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1}
File diff suppressed because one or more lines are too long
+164
View File
@@ -0,0 +1,164 @@
---
title: "Explore Simulated ICU Data"
format:
html:
embed-resources: true
execute:
echo: true
warning: false
message: false
---
## Goal
This notebook is for exploring the simulated ICU cohort.
The report notebook focuses on target trial components and core results. This exploratory notebook focuses on visualizing the data-generating process and understanding why the naive comparison can be biased.
## Setup
```{r}
suppressPackageStartupMessages({
library(dplyr)
library(ggplot2)
library(gt)
library(gtsummary)
library(tibble)
})
source("../R/simulate_icu_cohort.R")
source("../R/estimate_naive_vasopressor_mortality_effect.R")
```
## Simulate Data
```{r}
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
```
## Eligibility Overview
```{r}
eligibility_overview <- icu_data |>
summarize(
simulated_patients = n(),
suspected_sepsis = sum(suspected_sepsis == 1),
hypotension_at_baseline = sum(hypotension_at_baseline == 1),
elevated_lactate_at_baseline = sum(elevated_lactate_at_baseline == 1),
eligible = sum(eligible == 1)
) |>
tidyr::pivot_longer(
cols = everything(),
names_to = "measure",
values_to = "patients"
)
eligibility_overview |>
gt() |>
tab_header(title = "Eligibility Overview") |>
cols_label(
measure = "Measure",
patients = "Patients"
) |>
fmt_integer(columns = patients)
```
## Baseline Distributions
```{r}
eligible_icu_patients |>
select(age, sofa_score, lactate, map) |>
tbl_summary(
statistic = all_continuous() ~ "{mean} ({sd}); {median} [{p25}, {p75}]",
missing = "no"
)
```
## Severity By Observed Treatment
```{r}
eligible_icu_patients |>
mutate(
observed_treatment = factor(
early_vasopressor,
levels = c(0, 1),
labels = c("No early vasopressor", "Early vasopressor")
)
) |>
ggplot(aes(x = observed_treatment, y = sofa_score, fill = observed_treatment)) +
geom_boxplot(alpha = 0.75, width = 0.65, show.legend = FALSE) +
labs(
title = "SOFA Score Is Higher In Early-Treated Patients",
x = NULL,
y = "SOFA score"
) +
theme_minimal()
```
Early vasopressor patients tend to have higher SOFA scores in this simulated cohort. That happens because the treatment assignment mechanism makes sicker patients more likely to receive early vasopressors.
## Lactate And MAP By Treatment
```{r}
eligible_icu_patients |>
mutate(
observed_treatment = factor(
early_vasopressor,
levels = c(0, 1),
labels = c("No early vasopressor", "Early vasopressor")
)
) |>
ggplot(aes(x = map, y = lactate, color = observed_treatment)) +
geom_point(alpha = 0.55) +
labs(
title = "Shock Severity Markers Differ By Observed Treatment",
x = "Mean arterial pressure",
y = "Lactate",
color = "Observed treatment"
) +
theme_minimal()
```
Higher lactate and lower MAP are both markers of greater shock severity. If treatment groups differ on these variables, a simple treated-versus-untreated comparison is not yet a causal estimate.
## Mortality By SOFA Score
```{r}
eligible_icu_patients |>
mutate(
observed_treatment = factor(
early_vasopressor,
levels = c(0, 1),
labels = c("No early vasopressor", "Early vasopressor")
)
) |>
ggplot(aes(x = sofa_score, y = death_28d, color = observed_treatment)) +
geom_jitter(height = 0.04, width = 0.15, alpha = 0.35) +
geom_smooth(method = "glm", method.args = list(family = "binomial"), se = FALSE) +
labs(
title = "Mortality Risk Rises With SOFA Score",
x = "SOFA score",
y = "Observed 28-day death",
color = "Observed treatment"
) +
theme_minimal()
```
This plot shows why baseline severity matters. If SOFA score predicts death and also affects treatment assignment, then SOFA score is a confounder for the naive treatment comparison.
## Naive Mortality Effect Estimates
```{r}
naive_mortality_analysis$mortality_effect_estimates |>
gt() |>
tab_header(title = "Naive 28-Day Mortality Effect Estimates") |>
cols_label(
estimate = "Estimate",
value = "Value"
) |>
fmt_number(columns = value, decimals = 3)
```
The next methodological step is to adjust for baseline severity rather than comparing observed treatment groups directly.
-513
View File
@@ -1,513 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<style>body{background-color:white;}</style>
</head>
<body>
<div id="aphfsmpepd" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>#aphfsmpepd table {
font-family: system-ui, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#aphfsmpepd thead, #aphfsmpepd tbody, #aphfsmpepd tfoot, #aphfsmpepd tr, #aphfsmpepd td, #aphfsmpepd th {
border-style: none;
}
#aphfsmpepd p {
margin: 0;
padding: 0;
}
#aphfsmpepd .gt_table {
display: table;
border-collapse: collapse;
line-height: normal;
margin-left: auto;
margin-right: auto;
color: #333333;
font-size: 16px;
font-weight: normal;
font-style: normal;
background-color: #FFFFFF;
width: auto;
border-top-style: solid;
border-top-width: 2px;
border-top-color: #A8A8A8;
border-right-style: none;
border-right-width: 2px;
border-right-color: #D3D3D3;
border-bottom-style: solid;
border-bottom-width: 2px;
border-bottom-color: #A8A8A8;
border-left-style: none;
border-left-width: 2px;
border-left-color: #D3D3D3;
}
#aphfsmpepd .gt_caption {
padding-top: 4px;
padding-bottom: 4px;
}
#aphfsmpepd .gt_title {
color: #333333;
font-size: 125%;
font-weight: initial;
padding-top: 4px;
padding-bottom: 4px;
padding-left: 5px;
padding-right: 5px;
border-bottom-color: #FFFFFF;
border-bottom-width: 0;
}
#aphfsmpepd .gt_subtitle {
color: #333333;
font-size: 85%;
font-weight: initial;
padding-top: 3px;
padding-bottom: 5px;
padding-left: 5px;
padding-right: 5px;
border-top-color: #FFFFFF;
border-top-width: 0;
}
#aphfsmpepd .gt_heading {
background-color: #FFFFFF;
text-align: center;
border-bottom-color: #FFFFFF;
border-left-style: none;
border-left-width: 1px;
border-left-color: #D3D3D3;
border-right-style: none;
border-right-width: 1px;
border-right-color: #D3D3D3;
}
#aphfsmpepd .gt_bottom_border {
border-bottom-style: solid;
border-bottom-width: 2px;
border-bottom-color: #D3D3D3;
}
#aphfsmpepd .gt_col_headings {
border-top-style: solid;
border-top-width: 2px;
border-top-color: #D3D3D3;
border-bottom-style: solid;
border-bottom-width: 2px;
border-bottom-color: #D3D3D3;
border-left-style: none;
border-left-width: 1px;
border-left-color: #D3D3D3;
border-right-style: none;
border-right-width: 1px;
border-right-color: #D3D3D3;
}
#aphfsmpepd .gt_col_heading {
color: #333333;
background-color: #FFFFFF;
font-size: 100%;
font-weight: normal;
text-transform: inherit;
border-left-style: none;
border-left-width: 1px;
border-left-color: #D3D3D3;
border-right-style: none;
border-right-width: 1px;
border-right-color: #D3D3D3;
vertical-align: bottom;
padding-top: 5px;
padding-bottom: 6px;
padding-left: 5px;
padding-right: 5px;
overflow-x: hidden;
}
#aphfsmpepd .gt_column_spanner_outer {
color: #333333;
background-color: #FFFFFF;
font-size: 100%;
font-weight: normal;
text-transform: inherit;
padding-top: 0;
padding-bottom: 0;
padding-left: 4px;
padding-right: 4px;
}
#aphfsmpepd .gt_column_spanner_outer:first-child {
padding-left: 0;
}
#aphfsmpepd .gt_column_spanner_outer:last-child {
padding-right: 0;
}
#aphfsmpepd .gt_column_spanner {
border-bottom-style: solid;
border-bottom-width: 2px;
border-bottom-color: #D3D3D3;
vertical-align: bottom;
padding-top: 5px;
padding-bottom: 5px;
overflow-x: hidden;
display: inline-block;
width: 100%;
}
#aphfsmpepd .gt_spanner_row {
border-bottom-style: hidden;
}
#aphfsmpepd .gt_group_heading {
padding-top: 8px;
padding-bottom: 8px;
padding-left: 5px;
padding-right: 5px;
color: #333333;
background-color: #FFFFFF;
font-size: 100%;
font-weight: initial;
text-transform: inherit;
border-top-style: solid;
border-top-width: 2px;
border-top-color: #D3D3D3;
border-bottom-style: solid;
border-bottom-width: 2px;
border-bottom-color: #D3D3D3;
border-left-style: none;
border-left-width: 1px;
border-left-color: #D3D3D3;
border-right-style: none;
border-right-width: 1px;
border-right-color: #D3D3D3;
vertical-align: middle;
text-align: left;
}
#aphfsmpepd .gt_empty_group_heading {
padding: 0.5px;
color: #333333;
background-color: #FFFFFF;
font-size: 100%;
font-weight: initial;
border-top-style: solid;
border-top-width: 2px;
border-top-color: #D3D3D3;
border-bottom-style: solid;
border-bottom-width: 2px;
border-bottom-color: #D3D3D3;
vertical-align: middle;
}
#aphfsmpepd .gt_from_md > :first-child {
margin-top: 0;
}
#aphfsmpepd .gt_from_md > :last-child {
margin-bottom: 0;
}
#aphfsmpepd .gt_row {
padding-top: 8px;
padding-bottom: 8px;
padding-left: 5px;
padding-right: 5px;
margin: 10px;
border-top-style: solid;
border-top-width: 1px;
border-top-color: #D3D3D3;
border-left-style: none;
border-left-width: 1px;
border-left-color: #D3D3D3;
border-right-style: none;
border-right-width: 1px;
border-right-color: #D3D3D3;
vertical-align: middle;
overflow-x: hidden;
}
#aphfsmpepd .gt_stub {
color: #333333;
background-color: #FFFFFF;
font-size: 100%;
font-weight: initial;
text-transform: inherit;
border-right-style: solid;
border-right-width: 2px;
border-right-color: #D3D3D3;
padding-left: 5px;
padding-right: 5px;
}
#aphfsmpepd .gt_stub_row_group {
color: #333333;
background-color: #FFFFFF;
font-size: 100%;
font-weight: initial;
text-transform: inherit;
border-right-style: solid;
border-right-width: 2px;
border-right-color: #D3D3D3;
padding-left: 5px;
padding-right: 5px;
vertical-align: top;
}
#aphfsmpepd .gt_row_group_first td {
border-top-width: 2px;
}
#aphfsmpepd .gt_row_group_first th {
border-top-width: 2px;
}
#aphfsmpepd .gt_summary_row {
color: #333333;
background-color: #FFFFFF;
text-transform: inherit;
padding-top: 8px;
padding-bottom: 8px;
padding-left: 5px;
padding-right: 5px;
}
#aphfsmpepd .gt_first_summary_row {
border-top-style: solid;
border-top-color: #D3D3D3;
}
#aphfsmpepd .gt_first_summary_row.thick {
border-top-width: 2px;
}
#aphfsmpepd .gt_last_summary_row {
padding-top: 8px;
padding-bottom: 8px;
padding-left: 5px;
padding-right: 5px;
border-bottom-style: solid;
border-bottom-width: 2px;
border-bottom-color: #D3D3D3;
}
#aphfsmpepd .gt_grand_summary_row {
color: #333333;
background-color: #FFFFFF;
text-transform: inherit;
padding-top: 8px;
padding-bottom: 8px;
padding-left: 5px;
padding-right: 5px;
}
#aphfsmpepd .gt_first_grand_summary_row {
padding-top: 8px;
padding-bottom: 8px;
padding-left: 5px;
padding-right: 5px;
border-top-style: double;
border-top-width: 6px;
border-top-color: #D3D3D3;
}
#aphfsmpepd .gt_last_grand_summary_row_top {
padding-top: 8px;
padding-bottom: 8px;
padding-left: 5px;
padding-right: 5px;
border-bottom-style: double;
border-bottom-width: 6px;
border-bottom-color: #D3D3D3;
}
#aphfsmpepd .gt_striped {
background-color: rgba(128, 128, 128, 0.05);
}
#aphfsmpepd .gt_table_body {
border-top-style: solid;
border-top-width: 2px;
border-top-color: #D3D3D3;
border-bottom-style: solid;
border-bottom-width: 2px;
border-bottom-color: #D3D3D3;
}
#aphfsmpepd .gt_footnotes {
color: #333333;
background-color: #FFFFFF;
border-bottom-style: none;
border-bottom-width: 2px;
border-bottom-color: #D3D3D3;
border-left-style: none;
border-left-width: 2px;
border-left-color: #D3D3D3;
border-right-style: none;
border-right-width: 2px;
border-right-color: #D3D3D3;
}
#aphfsmpepd .gt_footnote {
margin: 0px;
font-size: 90%;
padding-top: 4px;
padding-bottom: 4px;
padding-left: 5px;
padding-right: 5px;
}
#aphfsmpepd .gt_sourcenotes {
color: #333333;
background-color: #FFFFFF;
border-bottom-style: none;
border-bottom-width: 2px;
border-bottom-color: #D3D3D3;
border-left-style: none;
border-left-width: 2px;
border-left-color: #D3D3D3;
border-right-style: none;
border-right-width: 2px;
border-right-color: #D3D3D3;
}
#aphfsmpepd .gt_sourcenote {
font-size: 90%;
padding-top: 4px;
padding-bottom: 4px;
padding-left: 5px;
padding-right: 5px;
}
#aphfsmpepd .gt_left {
text-align: left;
}
#aphfsmpepd .gt_center {
text-align: center;
}
#aphfsmpepd .gt_right {
text-align: right;
font-variant-numeric: tabular-nums;
}
#aphfsmpepd .gt_font_normal {
font-weight: normal;
}
#aphfsmpepd .gt_font_bold {
font-weight: bold;
}
#aphfsmpepd .gt_font_italic {
font-style: italic;
}
#aphfsmpepd .gt_super {
font-size: 65%;
}
#aphfsmpepd .gt_footnote_marks {
font-size: 75%;
vertical-align: 0.4em;
position: initial;
}
#aphfsmpepd .gt_asterisk {
font-size: 100%;
vertical-align: 0;
}
#aphfsmpepd .gt_indent_1 {
text-indent: 5px;
}
#aphfsmpepd .gt_indent_2 {
text-indent: 10px;
}
#aphfsmpepd .gt_indent_3 {
text-indent: 15px;
}
#aphfsmpepd .gt_indent_4 {
text-indent: 20px;
}
#aphfsmpepd .gt_indent_5 {
text-indent: 25px;
}
#aphfsmpepd .katex-display {
display: inline-flex !important;
margin-bottom: 0.75em !important;
}
#aphfsmpepd div.Reactable > div.rt-table > div.rt-thead > div.rt-tr.rt-tr-group-header > div.rt-th-group:after {
height: 0px !important;
}
</style>
<table class="gt_table" data-quarto-disable-processing="false" data-quarto-bootstrap="false">
<thead>
<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_left" rowspan="1" colspan="1" scope="col" id="label"><span class='gt_from_md'><strong>Characteristic</strong></span></th>
<th class="gt_col_heading gt_columns_bottom_border gt_center" rowspan="1" colspan="1" scope="col" id="stat_0"><span class='gt_from_md'><strong>Overall</strong><br />
N = 416</span><span class="gt_footnote_marks" style="white-space:nowrap;font-style:italic;font-weight:normal;line-height:0;"><sup>1</sup></span></th>
<th class="gt_col_heading gt_columns_bottom_border gt_center" rowspan="1" colspan="1" scope="col" id="stat_1"><span class='gt_from_md'><strong>No early vasopressor</strong><br />
N = 181</span><span class="gt_footnote_marks" style="white-space:nowrap;font-style:italic;font-weight:normal;line-height:0;"><sup>1</sup></span></th>
<th class="gt_col_heading gt_columns_bottom_border gt_center" rowspan="1" colspan="1" scope="col" id="stat_2"><span class='gt_from_md'><strong>Early vasopressor</strong><br />
N = 235</span><span class="gt_footnote_marks" style="white-space:nowrap;font-style:italic;font-weight:normal;line-height:0;"><sup>1</sup></span></th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr><td headers="label" class="gt_row gt_left">age</td>
<td headers="stat_0" class="gt_row gt_center">66 (14)</td>
<td headers="stat_1" class="gt_row gt_center">63 (14)</td>
<td headers="stat_2" class="gt_row gt_center">67 (14)</td></tr>
<tr><td headers="label" class="gt_row gt_left">sex</td>
<td headers="stat_0" class="gt_row gt_center"><br /></td>
<td headers="stat_1" class="gt_row gt_center"><br /></td>
<td headers="stat_2" class="gt_row gt_center"><br /></td></tr>
<tr><td headers="label" class="gt_row gt_left">    female</td>
<td headers="stat_0" class="gt_row gt_center">201 (48%)</td>
<td headers="stat_1" class="gt_row gt_center">91 (50%)</td>
<td headers="stat_2" class="gt_row gt_center">110 (47%)</td></tr>
<tr><td headers="label" class="gt_row gt_left">    male</td>
<td headers="stat_0" class="gt_row gt_center">215 (52%)</td>
<td headers="stat_1" class="gt_row gt_center">90 (50%)</td>
<td headers="stat_2" class="gt_row gt_center">125 (53%)</td></tr>
<tr><td headers="label" class="gt_row gt_left">sofa_score</td>
<td headers="stat_0" class="gt_row gt_center">7.00 (2.60)</td>
<td headers="stat_1" class="gt_row gt_center">6.29 (2.50)</td>
<td headers="stat_2" class="gt_row gt_center">7.55 (2.55)</td></tr>
<tr><td headers="label" class="gt_row gt_left">lactate</td>
<td headers="stat_0" class="gt_row gt_center">3.98 (1.86)</td>
<td headers="stat_1" class="gt_row gt_center">3.68 (1.60)</td>
<td headers="stat_2" class="gt_row gt_center">4.22 (2.01)</td></tr>
<tr><td headers="label" class="gt_row gt_left">map</td>
<td headers="stat_0" class="gt_row gt_center">55.4 (6.3)</td>
<td headers="stat_1" class="gt_row gt_center">56.4 (6.0)</td>
<td headers="stat_2" class="gt_row gt_center">54.6 (6.4)</td></tr>
<tr><td headers="label" class="gt_row gt_left">death_28d</td>
<td headers="stat_0" class="gt_row gt_center">131 (31%)</td>
<td headers="stat_1" class="gt_row gt_center">47 (26%)</td>
<td headers="stat_2" class="gt_row gt_center">84 (36%)</td></tr>
</tbody>
<tfoot>
<tr class="gt_footnotes">
<td class="gt_footnote" colspan="4"><span class="gt_footnote_marks" style="white-space:nowrap;font-style:italic;font-weight:normal;line-height:0;"><sup>1</sup></span> <span class='gt_from_md'>Mean (SD); n (%)</span></td>
</tr>
</tfoot>
</table>
</div>
</body>
</html>
-490
View File
@@ -1,490 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<style>body{background-color:white;}</style>
</head>
<body>
<div id="nplmwnpuqt" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>#nplmwnpuqt table {
font-family: system-ui, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#nplmwnpuqt thead, #nplmwnpuqt tbody, #nplmwnpuqt tfoot, #nplmwnpuqt tr, #nplmwnpuqt td, #nplmwnpuqt th {
border-style: none;
}
#nplmwnpuqt p {
margin: 0;
padding: 0;
}
#nplmwnpuqt .gt_table {
display: table;
border-collapse: collapse;
line-height: normal;
margin-left: auto;
margin-right: auto;
color: #333333;
font-size: 16px;
font-weight: normal;
font-style: normal;
background-color: #FFFFFF;
width: auto;
border-top-style: solid;
border-top-width: 2px;
border-top-color: #A8A8A8;
border-right-style: none;
border-right-width: 2px;
border-right-color: #D3D3D3;
border-bottom-style: solid;
border-bottom-width: 2px;
border-bottom-color: #A8A8A8;
border-left-style: none;
border-left-width: 2px;
border-left-color: #D3D3D3;
}
#nplmwnpuqt .gt_caption {
padding-top: 4px;
padding-bottom: 4px;
}
#nplmwnpuqt .gt_title {
color: #333333;
font-size: 125%;
font-weight: initial;
padding-top: 4px;
padding-bottom: 4px;
padding-left: 5px;
padding-right: 5px;
border-bottom-color: #FFFFFF;
border-bottom-width: 0;
}
#nplmwnpuqt .gt_subtitle {
color: #333333;
font-size: 85%;
font-weight: initial;
padding-top: 3px;
padding-bottom: 5px;
padding-left: 5px;
padding-right: 5px;
border-top-color: #FFFFFF;
border-top-width: 0;
}
#nplmwnpuqt .gt_heading {
background-color: #FFFFFF;
text-align: center;
border-bottom-color: #FFFFFF;
border-left-style: none;
border-left-width: 1px;
border-left-color: #D3D3D3;
border-right-style: none;
border-right-width: 1px;
border-right-color: #D3D3D3;
}
#nplmwnpuqt .gt_bottom_border {
border-bottom-style: solid;
border-bottom-width: 2px;
border-bottom-color: #D3D3D3;
}
#nplmwnpuqt .gt_col_headings {
border-top-style: solid;
border-top-width: 2px;
border-top-color: #D3D3D3;
border-bottom-style: solid;
border-bottom-width: 2px;
border-bottom-color: #D3D3D3;
border-left-style: none;
border-left-width: 1px;
border-left-color: #D3D3D3;
border-right-style: none;
border-right-width: 1px;
border-right-color: #D3D3D3;
}
#nplmwnpuqt .gt_col_heading {
color: #333333;
background-color: #FFFFFF;
font-size: 100%;
font-weight: normal;
text-transform: inherit;
border-left-style: none;
border-left-width: 1px;
border-left-color: #D3D3D3;
border-right-style: none;
border-right-width: 1px;
border-right-color: #D3D3D3;
vertical-align: bottom;
padding-top: 5px;
padding-bottom: 6px;
padding-left: 5px;
padding-right: 5px;
overflow-x: hidden;
}
#nplmwnpuqt .gt_column_spanner_outer {
color: #333333;
background-color: #FFFFFF;
font-size: 100%;
font-weight: normal;
text-transform: inherit;
padding-top: 0;
padding-bottom: 0;
padding-left: 4px;
padding-right: 4px;
}
#nplmwnpuqt .gt_column_spanner_outer:first-child {
padding-left: 0;
}
#nplmwnpuqt .gt_column_spanner_outer:last-child {
padding-right: 0;
}
#nplmwnpuqt .gt_column_spanner {
border-bottom-style: solid;
border-bottom-width: 2px;
border-bottom-color: #D3D3D3;
vertical-align: bottom;
padding-top: 5px;
padding-bottom: 5px;
overflow-x: hidden;
display: inline-block;
width: 100%;
}
#nplmwnpuqt .gt_spanner_row {
border-bottom-style: hidden;
}
#nplmwnpuqt .gt_group_heading {
padding-top: 8px;
padding-bottom: 8px;
padding-left: 5px;
padding-right: 5px;
color: #333333;
background-color: #FFFFFF;
font-size: 100%;
font-weight: initial;
text-transform: inherit;
border-top-style: solid;
border-top-width: 2px;
border-top-color: #D3D3D3;
border-bottom-style: solid;
border-bottom-width: 2px;
border-bottom-color: #D3D3D3;
border-left-style: none;
border-left-width: 1px;
border-left-color: #D3D3D3;
border-right-style: none;
border-right-width: 1px;
border-right-color: #D3D3D3;
vertical-align: middle;
text-align: left;
}
#nplmwnpuqt .gt_empty_group_heading {
padding: 0.5px;
color: #333333;
background-color: #FFFFFF;
font-size: 100%;
font-weight: initial;
border-top-style: solid;
border-top-width: 2px;
border-top-color: #D3D3D3;
border-bottom-style: solid;
border-bottom-width: 2px;
border-bottom-color: #D3D3D3;
vertical-align: middle;
}
#nplmwnpuqt .gt_from_md > :first-child {
margin-top: 0;
}
#nplmwnpuqt .gt_from_md > :last-child {
margin-bottom: 0;
}
#nplmwnpuqt .gt_row {
padding-top: 8px;
padding-bottom: 8px;
padding-left: 5px;
padding-right: 5px;
margin: 10px;
border-top-style: solid;
border-top-width: 1px;
border-top-color: #D3D3D3;
border-left-style: none;
border-left-width: 1px;
border-left-color: #D3D3D3;
border-right-style: none;
border-right-width: 1px;
border-right-color: #D3D3D3;
vertical-align: middle;
overflow-x: hidden;
}
#nplmwnpuqt .gt_stub {
color: #333333;
background-color: #FFFFFF;
font-size: 100%;
font-weight: initial;
text-transform: inherit;
border-right-style: solid;
border-right-width: 2px;
border-right-color: #D3D3D3;
padding-left: 5px;
padding-right: 5px;
}
#nplmwnpuqt .gt_stub_row_group {
color: #333333;
background-color: #FFFFFF;
font-size: 100%;
font-weight: initial;
text-transform: inherit;
border-right-style: solid;
border-right-width: 2px;
border-right-color: #D3D3D3;
padding-left: 5px;
padding-right: 5px;
vertical-align: top;
}
#nplmwnpuqt .gt_row_group_first td {
border-top-width: 2px;
}
#nplmwnpuqt .gt_row_group_first th {
border-top-width: 2px;
}
#nplmwnpuqt .gt_summary_row {
color: #333333;
background-color: #FFFFFF;
text-transform: inherit;
padding-top: 8px;
padding-bottom: 8px;
padding-left: 5px;
padding-right: 5px;
}
#nplmwnpuqt .gt_first_summary_row {
border-top-style: solid;
border-top-color: #D3D3D3;
}
#nplmwnpuqt .gt_first_summary_row.thick {
border-top-width: 2px;
}
#nplmwnpuqt .gt_last_summary_row {
padding-top: 8px;
padding-bottom: 8px;
padding-left: 5px;
padding-right: 5px;
border-bottom-style: solid;
border-bottom-width: 2px;
border-bottom-color: #D3D3D3;
}
#nplmwnpuqt .gt_grand_summary_row {
color: #333333;
background-color: #FFFFFF;
text-transform: inherit;
padding-top: 8px;
padding-bottom: 8px;
padding-left: 5px;
padding-right: 5px;
}
#nplmwnpuqt .gt_first_grand_summary_row {
padding-top: 8px;
padding-bottom: 8px;
padding-left: 5px;
padding-right: 5px;
border-top-style: double;
border-top-width: 6px;
border-top-color: #D3D3D3;
}
#nplmwnpuqt .gt_last_grand_summary_row_top {
padding-top: 8px;
padding-bottom: 8px;
padding-left: 5px;
padding-right: 5px;
border-bottom-style: double;
border-bottom-width: 6px;
border-bottom-color: #D3D3D3;
}
#nplmwnpuqt .gt_striped {
background-color: rgba(128, 128, 128, 0.05);
}
#nplmwnpuqt .gt_table_body {
border-top-style: solid;
border-top-width: 2px;
border-top-color: #D3D3D3;
border-bottom-style: solid;
border-bottom-width: 2px;
border-bottom-color: #D3D3D3;
}
#nplmwnpuqt .gt_footnotes {
color: #333333;
background-color: #FFFFFF;
border-bottom-style: none;
border-bottom-width: 2px;
border-bottom-color: #D3D3D3;
border-left-style: none;
border-left-width: 2px;
border-left-color: #D3D3D3;
border-right-style: none;
border-right-width: 2px;
border-right-color: #D3D3D3;
}
#nplmwnpuqt .gt_footnote {
margin: 0px;
font-size: 90%;
padding-top: 4px;
padding-bottom: 4px;
padding-left: 5px;
padding-right: 5px;
}
#nplmwnpuqt .gt_sourcenotes {
color: #333333;
background-color: #FFFFFF;
border-bottom-style: none;
border-bottom-width: 2px;
border-bottom-color: #D3D3D3;
border-left-style: none;
border-left-width: 2px;
border-left-color: #D3D3D3;
border-right-style: none;
border-right-width: 2px;
border-right-color: #D3D3D3;
}
#nplmwnpuqt .gt_sourcenote {
font-size: 90%;
padding-top: 4px;
padding-bottom: 4px;
padding-left: 5px;
padding-right: 5px;
}
#nplmwnpuqt .gt_left {
text-align: left;
}
#nplmwnpuqt .gt_center {
text-align: center;
}
#nplmwnpuqt .gt_right {
text-align: right;
font-variant-numeric: tabular-nums;
}
#nplmwnpuqt .gt_font_normal {
font-weight: normal;
}
#nplmwnpuqt .gt_font_bold {
font-weight: bold;
}
#nplmwnpuqt .gt_font_italic {
font-style: italic;
}
#nplmwnpuqt .gt_super {
font-size: 65%;
}
#nplmwnpuqt .gt_footnote_marks {
font-size: 75%;
vertical-align: 0.4em;
position: initial;
}
#nplmwnpuqt .gt_asterisk {
font-size: 100%;
vertical-align: 0;
}
#nplmwnpuqt .gt_indent_1 {
text-indent: 5px;
}
#nplmwnpuqt .gt_indent_2 {
text-indent: 10px;
}
#nplmwnpuqt .gt_indent_3 {
text-indent: 15px;
}
#nplmwnpuqt .gt_indent_4 {
text-indent: 20px;
}
#nplmwnpuqt .gt_indent_5 {
text-indent: 25px;
}
#nplmwnpuqt .katex-display {
display: inline-flex !important;
margin-bottom: 0.75em !important;
}
#nplmwnpuqt div.Reactable > div.rt-table > div.rt-thead > div.rt-tr.rt-tr-group-header > div.rt-th-group:after {
height: 0px !important;
}
</style>
<table class="gt_table" data-quarto-disable-processing="false" data-quarto-bootstrap="false">
<thead>
<tr class="gt_heading">
<td colspan="2" class="gt_heading gt_title gt_font_normal gt_bottom_border" style>Naive 28-Day Mortality Comparison</td>
</tr>
<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_left" rowspan="1" colspan="1" scope="col" id="measure">Measure</th>
<th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="value">Value</th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr><td headers="measure" class="gt_row gt_left">n_eligible</td>
<td headers="value" class="gt_row gt_right">416.000</td></tr>
<tr><td headers="measure" class="gt_row gt_left">n_early</td>
<td headers="value" class="gt_row gt_right">235.000</td></tr>
<tr><td headers="measure" class="gt_row gt_left">n_not_early</td>
<td headers="value" class="gt_row gt_right">181.000</td></tr>
<tr><td headers="measure" class="gt_row gt_left">risk_early</td>
<td headers="value" class="gt_row gt_right">0.357</td></tr>
<tr><td headers="measure" class="gt_row gt_left">risk_not_early</td>
<td headers="value" class="gt_row gt_right">0.260</td></tr>
<tr><td headers="measure" class="gt_row gt_left">risk_difference</td>
<td headers="value" class="gt_row gt_right">0.098</td></tr>
<tr><td headers="measure" class="gt_row gt_left">risk_ratio</td>
<td headers="value" class="gt_row gt_right">1.377</td></tr>
</tbody>
</table>
</div>
</body>
</html>
Executable
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Run the current learning project workflow end to end.
#
# This is intentionally simpler than a targets pipeline.
# It gives us one command that runs the simulation smoke check and renders the
# Quarto notebooks while the project is still small.
set -euo pipefail
# Make the script work even if it is called from another directory.
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$script_dir"
mkdir -p "outputs/reports"
Rscript "scripts/01_simulate_icu_data.R"
quarto render "notebooks/01_target_trial_basics.qmd" --output-dir "../outputs/reports"
quarto render "notebooks/02_explore_simulated_data.qmd" --output-dir "../outputs/reports"
printf '\nWorkflow complete. Reports are in outputs/reports/.\n'
+24
View File
@@ -0,0 +1,24 @@
# Simulate an ICU cohort for learning target trial emulation.
# The simulation logic lives in R/simulate_icu_cohort.R.
# suppressPackageStartupMessages() keeps package startup text out of the console.
# This makes the important output easier to read when learning.
suppressPackageStartupMessages({
library(skimr)
})
# source() runs another R file.
# After this line runs, simulate_icu_cohort() is available in this script.
source("R/simulate_icu_cohort.R")
# This calls our reusable simulation function.
# n_patients controls the number of rows.
# seed controls reproducibility.
icu_data <- simulate_icu_cohort(n_patients = 1000, seed = 20260531)
# cli::cli_alert_success() prints a nicely formatted success message.
cli::cli_alert_success("Simulated an in-memory ICU cohort with {nrow(icu_data)} patients.")
# skim() gives a quick learner-friendly overview of the dataset.
# It shows variable types, missingness, summary statistics, and small histograms.
skim(icu_data)
-18
View File
@@ -1,18 +0,0 @@
# Simulate an ICU cohort for learning target trial emulation.
# The simulation logic lives in R/simulate_icu_cohort.R.
suppressPackageStartupMessages(library(readr))
suppressPackageStartupMessages(library(skimr))
source("R/simulate_icu_cohort.R")
icu_data <- simulate_icu_cohort(n_patients = 1000, seed = 20260531)
write_csv(
icu_data,
file = "data/icu_septic_shock_base_r.csv"
)
cli::cli_alert_success("Wrote simulated cohort to {.file data/icu_septic_shock_base_r.csv}.")
skim(icu_data)
-91
View File
@@ -1,91 +0,0 @@
suppressPackageStartupMessages(library(dplyr))
suppressPackageStartupMessages(library(gt))
suppressPackageStartupMessages(library(gtsummary))
suppressPackageStartupMessages(library(readr))
suppressPackageStartupMessages(library(tidyr))
cli::cli_h1("Naive analysis of the simulated ICU cohort")
icu_data <- read_csv("data/icu_septic_shock_base_r.csv", show_col_types = FALSE)
if (!dir.exists("outputs")) {
dir.create("outputs")
}
eligible_data <- icu_data |>
filter(eligible == 1)
analysis_summary <- eligible_data |>
summarize(
n_eligible = n(),
n_early = sum(early_vasopressor == 1),
n_not_early = sum(early_vasopressor == 0),
risk_early = mean(death_28d[early_vasopressor == 1]),
risk_not_early = mean(death_28d[early_vasopressor == 0]),
risk_difference = risk_early - risk_not_early,
risk_ratio = risk_early / risk_not_early
)
analysis_results <- analysis_summary |>
pivot_longer(
cols = everything(),
names_to = "measure",
values_to = "value"
)
analysis_table <- analysis_results |>
gt() |>
tab_header(title = "Naive 28-Day Mortality Comparison") |>
cols_label(
measure = "Measure",
value = "Value"
) |>
fmt_number(columns = value, decimals = 3)
baseline_table <- eligible_data |>
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()
baseline_console <- eligible_data |>
group_by(early_vasopressor) |>
summarize(
mean_age = mean(age),
mean_sofa_score = mean(sofa_score),
mean_lactate = mean(lactate),
mean_map = mean(map),
.groups = "drop"
)
cli::cli_h2("Core causal contrast")
print(analysis_results)
gtsave(
data = analysis_table,
filename = "outputs/naive_mortality_comparison.html"
)
cli::cli_alert_success("Wrote {.file outputs/naive_mortality_comparison.html}.")
cli::cli_h2("Baseline comparison by observed treatment")
print(baseline_console)
baseline_table |>
as_gt() |>
gtsave(filename = "outputs/baseline_by_treatment.html")
cli::cli_alert_success("Wrote {.file outputs/baseline_by_treatment.html}.")