--- title: "brood(): Vaccine Coverage Data Structures for All Study Designs" subtitle: "Pre-aggregated and cohort models including time-series for interrupted time series analysis" output: rmarkdown::html_vignette: toc: true toc_depth: 3 vignette: > %\VignetteIndexEntry{brood(): Vaccine Coverage Data Structures} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = TRUE ) ``` ## Overview ```{r load} library(mudnester) ``` `brood()` constructs a `brood_df` object for vaccine coverage analysis. Unlike `roost()` which counts events over time, vaccine coverage requires an explicitly documented eligible-population denominator and a declared assessment window. `brood()` makes both explicit in every object it produces. The output `brood_df` is designed for `bowerbird::brood_plot()`. ### Why "brood"? > *A brood is the full clutch under a parent bird's care -- every egg counted, > every hatchling tracked, none overlooked. The gap between the clutch and the > hatchlings is not an absence; it is information.* ### Two population models | Model | Use case | Input | |---|---|---| | `"pre_aggregated"` | Already-computed counts per stratum | Summary data frame | | `"cohort"` | Record-level, one row per person | Wide or long dose data | The `"cohort"` model covers single time-point snapshots, birth cohort designs with person-time, and time-series sweeps for interrupted time series analysis. --- ## Model 1: Pre-aggregated denominator Supply a data frame with `n_vaccinated` and `n_eligible` already computed. No per-person dose logic is performed. ```{r pre-aggregated} cov_static <- brood( data.frame( stratum = c("0-17", "18-49", "50-64", "65+"), n_vaccinated = c(480L, 3550L, 2370L, 1760L), n_eligible = c(1000L, 5000L, 3000L, 2000L) ), denominator_notes = "ABS ERP 2024, Sunshine Coast LGA." ) print(cov_static) ``` ### Multi-vaccine product comparison ```{r multi-vaccine} cov_multi <- brood( data.frame( stratum = rep(c("18-49", "50-64", "65+"), each = 2), vaccine_type = rep(c("Moderna XBB.1.5", "Pfizer XBB.1.5"), 3), n_vaccinated = c(1800L, 1750L, 1100L, 1270L, 800L, 960L), n_eligible = c(5000L, 5000L, 3000L, 3000L, 2000L, 2000L) ), vaccine_type_col = "vaccine_type", denominator_notes = "AIR enrolled persons, SC HHS 2024." ) print(cov_multi) ``` --- ## Model 2: Cohort -- single time point One row per person in `data`. Use `entry_date_col` and one of the five assessment windows. Inputs come from `starling::murmuration()` in wide format. ```{r cohort-data} set.seed(99) n <- 120L dialysis_cohort <- data.frame( patient_id = paste0("D", seq_len(n)), dialysis_start_date = seq(as.Date("2022-01-01"), by = "week", length.out = n), dialysis_end_date = seq(as.Date("2022-01-01"), by = "week", length.out = n) + sample(90:900, n, replace = TRUE), age_group = sample(c("45-64","65-74","75+"), n, replace = TRUE, prob = c(0.25, 0.45, 0.30)), vax_date_1 = as.Date(ifelse( sample(c(TRUE, FALSE), n, replace = TRUE, prob = c(0.65, 0.35)), as.character(seq(as.Date("2022-01-01"), by = "week", length.out = n) + sample(-400:400, n, replace = TRUE)), NA_character_)), vax_type_1 = sample(c("Influenza","COVID-19",NA), n, replace = TRUE, prob = c(0.5, 0.3, 0.2)), stringsAsFactors = FALSE ) ``` ### Window 2a: Vaccination status at exit ```{r cohort-at-exit} cov_exit <- brood( dialysis_cohort, data_format = "wide", population_model = "cohort", vax_date_cols = "vax_date_1", entry_date_col = "dialysis_start_date", exit_date_col = "dialysis_end_date", window = "at_exit", stratum_col = "age_group", denominator_notes = "Dialysis cohort SC HHS. At-exit = vaccinated by dialysis end date." ) print(cov_exit) ``` ### Window 2b: Baseline (pre-entry) vaccination status ```{r cohort-pre-entry} cov_pre <- brood( dialysis_cohort, data_format = "wide", population_model = "cohort", vax_date_cols = "vax_date_1", vax_type_cols = "vax_type_1", vax_target = "Influenza", entry_date_col = "dialysis_start_date", window = "pre_entry", stratum_col = "age_group", denominator_notes = "Dialysis cohort. Pre-entry = vaccinated before dialysis start." ) print(cov_pre) ``` ### Window 2c: Vaccination within 1 year of cohort entry ```{r cohort-post-entry} cov_post <- brood( dialysis_cohort, data_format = "wide", population_model = "cohort", vax_date_cols = "vax_date_1", vax_type_cols = "vax_type_1", vax_target = "Influenza", entry_date_col = "dialysis_start_date", window = "post_entry_days", window_days = 365L, stratum_col = "age_group", denominator_notes = "Dialysis cohort. Post-entry window = 365 days from dialysis start." ) print(cov_post) ``` ### Window 2d: Post-intervention vaccination ```{r cohort-post-intervention} cov_intervention <- brood( dialysis_cohort, data_format = "wide", population_model = "cohort", vax_date_cols = "vax_date_1", vax_type_cols = "vax_type_1", vax_target = "Influenza", entry_date_col = "dialysis_start_date", exit_date_col = "dialysis_end_date", window = "post_intervention", intervention_date = as.Date("2023-06-01"), stratum_col = "age_group", denominator_notes = paste0( "Dialysis cohort. Post-intervention = vaccinated on or after 2023-06-01." ) ) print(cov_intervention) ``` --- ## Model 3: Cohort -- birth cohort with person-time Birth cohort is a special case of the cohort model. Set `entry_date_col = "dob"` and supply `eligibility_days` to define the age-based eligibility window. Person-time (days at risk from birth until vaccinated or window expired) is computed automatically. ```{r birth-cohort-data} set.seed(42) n <- 200L birth_records <- data.frame( baby_id = paste0("B", seq_len(n)), dob = seq(as.Date("2024-01-01"), by = "day", length.out = n), gestation = sample(c("Term","Preterm"), n, replace = TRUE, prob = c(0.85, 0.15)), vax_date_1 = as.Date(ifelse( sample(c(TRUE, FALSE), n, replace = TRUE, prob = c(0.55, 0.45)), as.character(seq(as.Date("2024-01-01"), by = "day", length.out = n) + sample(30:200, n, replace = TRUE)), NA_character_)), vax_type_1 = "nirsevimab", stringsAsFactors = FALSE ) ``` ```{r birth-cohort-brood} cov_birth <- brood( birth_records, data_format = "wide", population_model = "cohort", vax_date_cols = "vax_date_1", vax_type_cols = "vax_type_1", vax_target = "nirsevimab", entry_date_col = "dob", # DOB is the cohort entry date eligibility_days = 180L, # eligible from birth until 6 months censor_date = as.Date("2024-12-31"), stratum_col = "gestation", denominator_notes = paste0( "SCPHU birth cohort 2024-01-01 to 2024-12-31. ", "Eligible = live births in SC LGA. ", "Eligibility window = 180 days from birth. ", "Administrative censor date = 2024-12-31." ) ) print(cov_birth) ``` The `person_time` column shows days at risk. `coverage` = proportion vaccinated among those who had at least one eligible day. --- ## Model 4: Cohort -- time series (interrupted time series analysis) This is the backbone of `brood()`. Set `time_series = TRUE` and supply `ts_start`, `ts_end`, and `ts_by` to sweep `window = "current_at_date"` across monthly (or other) intervals automatically. Returns one row per stratum per time point -- the right shape for `bowerbird::brood_plot()`. When `intervention_date` is supplied, an `intervention_period` column is automatically added, labelling each row as `"Pre-intervention"` or `"Post-intervention"`. ```{r time-series-data} set.seed(7) n <- 120L jak_cohort <- data.frame( patient_id = paste0("P", seq_len(n)), entry_date_nominal = as.Date("2000-01-01"), # placeholder entry date age_group = sample(c("18-49","50-64","65+"), n, replace = TRUE), vax_date_1 = as.Date(ifelse( sample(c(TRUE, FALSE), n, replace = TRUE, prob = c(0.45, 0.55)), as.character(as.Date("2024-01-01") + sample(0:730, n, replace = TRUE)), NA_character_)), vax_type_1 = "Shingrix", vax_date_2 = as.Date(ifelse( sample(c(TRUE, FALSE), n, replace = TRUE, prob = c(0.25, 0.75)), as.character(as.Date("2024-06-01") + sample(0:365, n, replace = TRUE)), NA_character_)), vax_type_2 = "Shingrix", stringsAsFactors = FALSE ) ``` ```{r time-series-brood} cov_ts <- brood( jak_cohort, data_format = "wide", population_model = "cohort", vax_date_cols = c("vax_date_1", "vax_date_2"), vax_type_cols = c("vax_type_1", "vax_type_2"), vax_target = "Shingrix", validity_days = Inf, entry_date_col = "entry_date_nominal", time_series = TRUE, ts_start = as.Date("2024-12-01"), ts_end = as.Date("2026-06-01"), ts_by = "month", intervention_date = as.Date("2025-12-01"), denominator_notes = "JAK-inhibitor cohort. Shingrix only, no expiry." ) print(cov_ts) ``` The result is `r nrow(cov_ts)` rows: one per month ( `r attr(cov_ts, "n_time_points")` time points) across `r attr(cov_ts, "n_strata")` stratum ("Overall"). With `stratum_col` supplied, each stratum gets its own row at every time point. ### Stratified time series ```{r time-series-stratified} cov_ts_strat <- brood( jak_cohort, data_format = "wide", population_model = "cohort", vax_date_cols = c("vax_date_1", "vax_date_2"), vax_type_cols = c("vax_type_1", "vax_type_2"), vax_target = "Shingrix", validity_days = Inf, entry_date_col = "entry_date_nominal", stratum_col = "age_group", time_series = TRUE, ts_start = as.Date("2024-12-01"), ts_end = as.Date("2026-06-01"), ts_by = "month", intervention_date = as.Date("2025-12-01"), denominator_notes = "JAK-inhibitor cohort by age group. Shingrix only." ) cat("Rows:", nrow(cov_ts_strat), "= ", attr(cov_ts_strat, "n_time_points"), "months x", attr(cov_ts_strat, "n_strata"), "age groups\n") print(head(cov_ts_strat[, c("stratum","reference_date","intervention_period", "n_vaccinated","n_eligible","coverage")], 9)) ``` --- ## Passing to bowerbird::brood_plot() All models return `brood_df` objects accepted by `bowerbird::brood_plot()`. The time-series output auto-detects as a line chart: ```{r bowerbird, eval = FALSE} # Pre-aggregated: bar chart with target line bowerbird::brood_plot(cov_static, target_line = 0.80, title = "COVID-19 booster coverage by age group") # Time series: line chart with vertical intervention line (auto-detected) bowerbird::brood_plot(cov_ts, title = "Monthly Shingrix coverage, JAK-inhibitor cohort") # Stratified time series: one line per stratum bowerbird::brood_plot(cov_ts_strat, title = "Shingrix coverage by age group") ``` --- ## The denominator_notes requirement Every `brood()` call should include `denominator_notes` documenting: - Who counts as eligible (age, disease, program criteria) - The source of the denominator (ABS ERP, ESKD registry, AIR cohort) - The observation window (dates, censor date) - What intervention or event defines the window anchor `brood()` stores these notes as a metadata attribute. `print.brood_df()` displays them and `bowerbird::brood_plot()` can use them as a plot caption. --- ## Session info ```{r session} sessionInfo() ```