diff --git a/.claude/skills/ingest-source.md b/.claude/skills/ingest-source.md new file mode 100644 index 0000000..ed7a9dd --- /dev/null +++ b/.claude/skills/ingest-source.md @@ -0,0 +1,364 @@ +--- +name: ingest-source +description: Ingest a new Epic Cosmos data source into this repo (epic_preprocessing) — creates the folder structure via the dcf R package (required), writes an ingest.R script that parses a SlicerDicer export and standardizes it into wide format, and creates a measure_info.json documenting every output column. Use when the user wants to add a new cosmos_* data source, mentions "ingest", "new data source", or provides a SlicerDicer export to onboard. +--- + +# ingest-source + +Ingest a new Epic Cosmos data source: create the folder structure, write the `ingest.R` script to +standardize a SlicerDicer export, and create `measure_info.json`. + +Adapted from the `ingest-source` skill in the `PopHIVE/Ingest` repository. This repo only ever +ingests Epic Cosmos SlicerDicer exports (every source directory is `cosmos_*`, feeding +`data/bundle_cosmos`) — it has no website data-sources catalog build step (no +`scripts/build_docs.R`, no `docs/data_sources_index.json`), so this version drops that build +phase. It **keeps** the `_catalog` block itself, since its `files` field is a good place to +document the population/stratification each SlicerDicer pull actually represents (e.g. "ZIP-level, +general population" vs. "county-level, base-population-eligible patients only") even though +nothing currently builds a website index from it. + +## Usage + +``` +/ingest-source [description of data source and where to get it] +``` + +## Description + +End-to-end skill for adding and ingesting a new Epic Cosmos data source into this repository: + +1. Creates the folder structure **exclusively via `dcf::dcf_add_source()`** — never by hand +2. Examines the raw SlicerDicer export to understand its structure +3. Writes an `ingest.R` script that transforms the raw export into the standard wide format +4. Creates a `measure_info.json` documenting every output column, including the `_sources` block + +## Instructions + +When the user invokes this skill: + +### Phase 1: Create Folder Structure + +**CRITICAL — this phase is non-negotiable.** The directory and **every file inside it** MUST be +created by the `dcf` R package via `dcf::dcf_add_source()`. This is the only supported way to +initialize a source. + +**You MUST NOT:** +- Create `data//` (or any subdirectory) with `mkdir`, `New-Item`, or the Write tool +- Hand-write `process.json`, or copy one from another source or from `bundle_cosmos` +- Scaffold empty `ingest.R` / `measure_info.json` files before running `dcf_add_source()` +- Work around a missing/broken `dcf` installation by writing the structure manually + +**If `dcf_add_source()` fails** (package not installed, R not found, permissions), STOP and report +the error to the user. Do not proceed to Phase 4/5 and do not fabricate the structure — fix the +`dcf` installation first (`install.packages("dcf")` or `remotes::install_github("dissc-yale/dcf")`). + +Steps: + +1. **Validate the source name**: lowercase with underscores, prefixed `cosmos_` to match the + convention of every existing source in this repo (`cosmos_immunization`, `cosmos_chronic`, + `cosmos_birth_vaccines`, …). No spaces or special characters. + +2. **Detect the R installation** (Windows only): + ```powershell + Get-ChildItem 'C:\Program Files\R' | Select-Object Name + ``` + Use the most recent version found. + +3. **Run the dcf command** from the project root: + ```powershell + & 'C:\Program Files\R\\bin\Rscript.exe' -e "dcf::dcf_add_source('')" + ``` + +4. **Verify the created structure**: + ``` + data// + ├── raw/ # For downloaded/staged source files + ├── standard/ # For standardized output files + ├── ingest.R # Transformation script (filled in below) + ├── measure_info.json # Variable metadata (filled in below) + └── process.json # Processing state (dcf-generated — DO NOT hand-edit or create) + ``` + +5. **Confirm `process.json` is correct** before continuing: `"name"` matches the directory + (dcf sometimes writes `"."` — that's expected) and `"type"` is `"source"`. If it looks wrong, + re-run `dcf::dcf_add_source()` rather than patching the file by hand. + +From this point on, only `ingest.R` and `measure_info.json` are edited by you. `process.json` is +owned by `dcf` and is updated at runtime through `dcf::dcf_process_record()` inside `ingest.R`. + +### Phase 2: Gather Information + +Ask the user (if not already provided): +- **What does the SlicerDicer session measure?**: the outcome(s)/variable(s) — e.g. "Percentage + with Any Encounter preceded by Immunizations", filtered to specific vaccines +- **Geographic level(s)**: state, county, ZIP, or multiple — each level in this repo typically + gets its own staging folder and its own `standard/data_*.csv.gz` file (see + `cosmos_immunization` for an example with `staging/` → ZIP, `staging_county/` → county, + `staging_county_base_pt/` → county with a base-population toggle) +- **Time resolution**: SlicerDicer exports here are typically annual or monthly +- **Demographic breakdowns**: age at encounter, sex, race/ethnicity, other filters used in the + session +- **Whether the export is password-protected**: most raw `.xlsx` exports from SlicerDicer are + encrypted and require the `EPIC_XLSX_PASSWORD` environment variable plus the + `msoffcrypto-tool` Python package to decrypt (see `cosmos_immunization/ingest.R` for the + pattern) — confirm this dependency is available before writing decrypt logic + +If the user has already placed raw files in `raw/` (or a `raw/staging*` subfolder matching the +existing convention), examine them directly. + +### Phase 3: Examine Raw Data + +Before writing any code, understand the raw SlicerDicer export structure. See the +**"Epic Cosmos SlicerDicer Exports"** section of `CLAUDE.md` for the full parsing reference; the +key points: + +- Multi-row header block before the actual data — skip 12 rows and assign column names manually + (the true column names split across rows 11–12, so `header = TRUE` is unreliable) +- **Merged cells**: grouping columns (year, month/age group, state/county/ZIP, etc.) are only + filled in on the first row of each group — convert blanks to `NA` then + `tidyr::fill(..., .direction = "down")` +- **Suppression**: counts ≤ 10 appear as `"10 or fewer"`; percentages for suppressed rows appear + as `"-"` — treat `"-"` as `NA` +- **Non-US / catch-all rows**: filter out foreign states/provinces, `"None of the above"`, and + decide whether `"Total"` should map to geography `"00"` or be dropped, depending on what the + session's population base represents +- **Non-UTF-8 characters**: apply `iconv(column, to = "UTF-8", sub = "")` **before** + `trimws()`/`na_if()` on `month`/`state_name`/`county_name` columns +- **County name mismatches**: Epic Cosmos county names need normalization before joining to + `resources/all_fips.csv.gz` (SAINT → ST., missing apostrophes, LA SALLE → LASALLE, Alaska + boroughs/census areas, Virginia independent cities, Connecticut planning regions) — see the + "County names not matching FIPS lookup" and "Connecticut county FIPS codes" sections of + `CLAUDE.md` for the full normalization table and fallback `stringdist` fuzzy-match pattern +- **ZIP-level exports**: no FIPS join needed — ZIP is used directly as the geography-like + identifier (see `cosmos_immunization`'s `data_zip.csv.gz`, which keeps ZIP rather than + converting to FIPS) + +Identify: +- Which columns map to `geography`, `time`, and demographic dimensions (`age`, `sex`, + `race_ethnicity`) +- Which columns are outcome values vs. the patient-count denominator +- Any filtering needed (record types, aggregate rows to drop) +- Geographic format (state name, county name, ZIP, FIPS) +- Date format in the raw data (year/month columns, or year only) + +### Phase 4: Write ingest.R + +Write the `ingest.R` script at `data//ingest.R`. + +#### Script Structure + +```r +# ============================================================================= +# {SOURCE_NAME} Data Ingestion +# Source: Epic SlicerDicer export ("{session title}") +# Geography: +# raw/staging/ -> standard/data.csv.gz ({geography level}) +# Dimensions: {age/sex/etc.} x geography +# ============================================================================= + +library(dplyr) +library(tidyr) + +# Initialize process record (creates process.json if it doesn't exist) +if (!file.exists("process.json")) { + process <- list(raw_state = NULL) +} else { + process <- dcf::dcf_process_record() +} + +# --- 1. Decrypt/read raw SlicerDicer export(s) --- +# --- 2. Check for changes (hash of raw file(s)) --- +if (!identical(process$raw_state, raw_state)) { + + # --- 3. Parse (skip 12 rows, assign names, tidyr::fill merged cells) --- + # --- 4. Clean (iconv, suppression handling, geography normalization + FIPS join) --- + # --- 5. Transform to standard wide format --- + # --- 6. Write standardized output --- + + process$raw_state <- raw_state + dcf::dcf_process_record(updated = process) +} +``` + +#### Output Format: Standard Wide Format + +The standardized output MUST be in **wide format** with: + +- **Index columns**: `geography`, `time`, and whichever of `age`, `sex`, `race_ethnicity` the + session was stratified by (one row per unique combination) +- **Value columns**: each output measure gets its own column, e.g. `pct_rsv`, `pct_pcv`, + `pct_zoster`, `pct_flu`, `n_patients` (see `cosmos_immunization/measure_info.json` for a live + example) +- **Per-column suppression flags**: because a wide row can mix suppressed and non-suppressed + measures, flag suppression **per value column** as `suppressed_flag_{column_name}`, not with a + single shared `suppressed_flag` column — e.g. `suppressed_flag_pct_rsv`, + `suppressed_flag_n_patients` + +#### Column Naming Convention + +Existing sources in this repo use short, descriptive, lowercase-with-underscores names for value +columns **without a source prefix** (`pct_rsv`, `pct_pcv`, `n_patients` — not `epic_pct_rsv`), +since every source here is already Epic Cosmos. Keep new columns consistent with that: + +- All lowercase with underscores +- Short but unambiguous (`pct_{vaccine}`, `n_patients`, `rate_{condition}`) +- Avoid redundancy (don't repeat "percentage" if `pct_` prefix already implies it) + +#### Geography Handling + +- County/state exports: convert names to FIPS codes using `resources/all_fips.csv.gz` (see + Phase 3 for the county-name-normalization caveats specific to Epic Cosmos exports) +- ZIP-level exports: keep ZIP as-is, do not force a FIPS conversion +- National level = `"00"`; state = 2-digit FIPS string; county = 5-digit FIPS string + +#### Time Handling + +- **Format**: `MM-DD-YYYY` (this repo's standard — see `CLAUDE.md`; note the Ingest repo has + since moved to `YYYY-mm-dd`, but files in this repo currently write `MM-DD-YYYY`, so stay + consistent with existing sources unless the user says otherwise) +- Always call `format()` explicitly on `Date` objects before writing — do not rely on vroom's + implicit `Date` serialization — and verify the written output matches the expected format +- Weekly data: Saturday at end of epiweek. Monthly data: last day of month. Annual data: + `12-31-YYYY` + +#### Data Quality + +- Handle suppressed values: `"10 or fewer"` → impute as `5`, flag with the matching + `suppressed_flag_{column}` column; suppressed percentages (`"-"`) → `NA` +- Remove aggregate/catch-all rows that would double-count (`"None of the above"`, and decide + case-by-case whether `"Total"` becomes geography `"00"` or is dropped) +- Ensure no duplicate rows per (geography, time, demographic) combination + +#### Output Writing + +```r +vroom::vroom_write(data_standard, "standard/data.csv.gz", ",") +``` + +If multiple geography levels are exported (state/county/ZIP, or a county export with a different +population base), write each to its own file — `standard/data_county.csv.gz`, +`standard/data_zip.csv.gz`, `standard/data_county_base_pt.csv.gz`, etc. — following the +`cosmos_immunization` pattern. + +### Phase 5: Write measure_info.json + +Create `data//measure_info.json` with an entry for every value column, following the +schema in `CLAUDE.md`. + +#### For Each Value Column + +```json +{ + "column_name": { + "id": "column_name", + "short_name": "Human-readable short name (< 100 chars)", + "long_name": "Full descriptive name", + "category": "respiratory|immunization|chronic|injury", + "short_description": "One sentence description.", + "long_description": "Detailed description with methodology notes — mention the SlicerDicer stratification, suppression of counts ≤ 10 imputed as 5, and suppression of percentages when the denominator is too small.", + "statement": "Template: 'In {location}, {value}% of patients ...'", + "measure_type": "Incidence|Prevalence|Rate|Percent|Count", + "unit": "Cases per 100,000|Percent|Count", + "time_resolution": "Week|Month|Year", + "sources": [{ "id": "epic_cosmos" }], + "citations": [] + } +} +``` + +Do **not** put inline `restrictions`, `name`, or `url` in each measure's `sources` array — only +`{ "id": "epic_cosmos" }`. + +#### Always Include `_sources` + +Use the standard `epic_cosmos` `_sources` block from `CLAUDE.md`'s "Epic Cosmos Source Metadata" +section verbatim (name, url, organization, description, restrictions) — copy it from an existing +`measure_info.json` in this repo (e.g. `data/cosmos_immunization/measure_info.json`) rather than +retyping it, so the wording stays identical across sources. + +#### Include `_catalog` — Document the Population of Each Pull + +Write `_catalog` as the last top-level key, after `_sources`. No script in this repo reads it +yet, but it's the natural place to record, in one spot, what population each SlicerDicer pull in +this source actually covers — useful the next time someone (human or Claude) needs to remember +why `data_county.csv.gz` and `data_county_base_pt.csv.gz` give different denominators. + +```json +{ + "_catalog": { + "summary": "One to two plain-English sentences describing the dataset and, if it matters, which patient population it was pulled from.", + "search_terms": ["Immunizations", "rsv", "flu"], + "bucket": [], + "files": { + "data_zip.csv.gz": "ZIP-level, all patients with any encounter, by age", + "data_county.csv.gz": "County-level, all patients with any encounter, by age", + "data_county_base_pt.csv.gz": "County-level, restricted to base-population-eligible patients only, by age" + } + } +} +``` + +- **`summary`** — ~1-2 sentences, derived from the `_sources` description; call out the + population base only when it isn't the default "any patient with an encounter" (e.g. a pull + restricted to a specific age range, insurance type, or the "base population" cohort). +- **`search_terms`** — 3-4 terms: topic label(s) first (`Immunizations`, `Respiratory`, + `Chronic diseases`, …), then lowercase keywords a person would actually type (`rsv`, `flu`, + `pcv`, `zoster`). +- **`bucket`** — leave `[]` unless the user names a specific grouping. +- **`files`** — one entry per `standard/data*.csv.gz`, a short noun phrase (~5-10 words) naming + the geography level, **which population the pull was drawn from**, and any stratification + beyond time/geography. This is the field that matters most here: when a source has multiple + staging pulls with different population bases (like `cosmos_immunization`'s `staging` vs. + `staging_county_base_pt`), spell out the difference explicitly rather than writing the same + generic blurb for both files. + +### Phase 6: Validate and Report + +After writing all files: + +1. **Check file structure**: verify `ingest.R`, `measure_info.json`, and the dcf-generated + `process.json` all exist. +2. **If raw data is available**: offer to run the ingest.R script to test it (from the source + directory, or via `dcf::dcf_process("")` from the project root). +3. **Manually inspect the output**: read the first rows of each `standard/data*.csv.gz` produced + (e.g. `vroom::vroom("standard/data.csv.gz", n_max = 10)`) and check: + - `time` values match `MM-DD-YYYY` + - `geography` values are valid FIPS codes (or ZIP codes, for ZIP-level files) with no + unexpected `NA`s from a failed county-name join + - Suppressed rows show `suppressed_flag_{column} == 1` and an imputed value of `5` (counts) or + `NA` (percentages) + - No duplicate rows per (geography, time, demographic) combination + + This repo does not currently have an automated `validate_standard.R` / `validate_dataset.Rmd` + report — do this inspection manually, or propose adding one if the user wants a repeatable + check. +4. **Report what was created**: + - Source directory path + - List of standardized output columns (name + what it measures) + - Geographic level(s) covered + - Time resolution + - Next steps (run ingest, add to `bundle_cosmos`, etc.) + +### Phase 7: Add to bundle_cosmos + +Once the source's `standard/data*.csv.gz` files exist, wire it into +`data/bundle_cosmos/build.R` (or the relevant bundle) and re-run +`dcf::dcf_process("bundle_cosmos")` (or `dcf::dcf_process('./data/bundle_cosmos')` per this +repo's `README.md`) to regenerate the combined parquet output. + +## Example + +User: `/ingest-source cosmos_asthma Epic SlicerDicer export of "Percentage with Asthma Diagnosis", state and county level, annual, staged in raw/staging_county` + +The skill would: +1. Run `dcf::dcf_add_source("cosmos_asthma")` to create the directory and all of its contents, + then verify the generated `process.json` +2. Examine the staged SlicerDicer export (skip 12 header rows, check for merged cells and + suppression markers) +3. Write `ingest.R` that decrypts/reads the export, normalizes county names, joins to + `resources/all_fips.csv.gz`, and writes wide-format columns like `pct_asthma` and `n_patients` + with matching `suppressed_flag_pct_asthma` / `suppressed_flag_n_patients` columns +4. Write `measure_info.json` with an entry per output column plus the standard `epic_cosmos` + `_sources` block +5. Manually inspect the standardized output, then report the created structure and suggest + wiring it into `bundle_cosmos` diff --git a/data/cosmos_birth_vaccines/README.md b/data/cosmos_birth_vaccines/README.md new file mode 100644 index 0000000..633fb4f --- /dev/null +++ b/data/cosmos_birth_vaccines/README.md @@ -0,0 +1,69 @@ +# cosmos_birth_vaccines + +Epic Cosmos vaccination coverage among patients with birthing parent information: Vitamin +K, RSV immunization (nirsevimab), and hepatitis B, by year and state of residence. Two +population bases are produced -- see below. + +This is a dcf data source project, initialized with `dcf::dcf_add_source`. + +## Updating + +The source is two Epic Cosmos SlicerDicer sessions using the "Patients with Birthing +Parent Information" data model, each exported as a crosstab xlsx: + +1. Re-run each session in Epic Cosmos and export the crosstab as xlsx. + - All patients (`raw/staging/`): Population Base = "All Patients with Birthing Parent + Information", no additional population filter. + - CPT birth cohort (`raw/staging_cpt_birth/`): same data model, additionally filtered + to a Billed Procedures criterion indicating birth (CPT 99460-99465, 99468, 99477 -- + "1st hosp/birthing center care", "normal newborn care", etc.). + - Both sessions share the same layout: rows = Year x State of Residence; measures = + Vit K (%), RSV (%), Hep B (%), Percentage of Population (%), Number of Patients. + "Percentage of Population" is read but not carried into the standardized output. +2. Drop each export into its raw folder, replacing the previous file. Each folder should + contain exactly one xlsx file. +3. Run the ingest — `ingest.R` reprocesses each population base only when its staging + file hash changes, writing `standard/data.csv.gz` (all patients) and + `standard/data_cpt_birth.csv.gz` (CPT birth cohort). + +Requires two things in the environment: + +- `EPIC_XLSX_PASSWORD` in `.Renviron` (see `usethis::edit_r_environ()`) — SlicerDicer + exports are password protected. +- `msoffcrypto-tool` on the Python used by R (`python -m pip install msoffcrypto-tool`), + used to decrypt the xlsx. + +## Notes + +- **Two population bases, same measures.** `standard/data.csv.gz` covers all Epic Cosmos + patients with birthing parent information; `standard/data_cpt_birth.csv.gz` is the + subset with a billed procedure indicating birth. The CPT-filtered population runs + roughly half the patient count of the all-patients population nationally. The two are + not meant to be summed or compared row-for-row -- they are alternative denominators for + the same measures. +- **RSV is missing, not suppressed, before 2023.** `epic_pct_rsv` is `NA` for 2018-2022 in + both population bases because nirsevimab (the RSV immunization this measure tracks) was + not FDA-approved until 2023 -- the measure did not exist yet, so + `epic_pct_rsv_suppressed_flag` is 0 for those rows even though the value is missing. +- **Suppression.** Epic Cosmos suppresses patient counts of 10 or fewer as `"10 or + fewer"` and percentages as `"-"`. Both are imputed rather than left missing: counts as + 5, percentages as 5 divided by the row's `epic_n_patients`. `suppressed_flag` stays 1 + either way, to mark the value as imputed. As of the 2026-09-08 export neither session + has any suppressed cells at this year x state granularity, but both markers are still + handled defensively. +- **Coverage.** Both exports cover 49 states (no Rhode Island or DC in either session, + across all years 2018-2025) plus national (`"00"`). State-level `epic_n_patients` + reflects Epic's footprint among birthing facilities, not the underlying birth count. +- Time resolution is annual; `time` is `12-31-`. + +You can use the `dcf` package to check the project: + +```R +dcf_check() +``` + +And process it: + +```R +dcf_process() +``` diff --git a/data/cosmos_birth_vaccines/check_map.Rmd b/data/cosmos_birth_vaccines/check_map.Rmd new file mode 100644 index 0000000..eb9681c --- /dev/null +++ b/data/cosmos_birth_vaccines/check_map.Rmd @@ -0,0 +1,258 @@ +--- +title: "Cosmos Birth Vaccines" +date: "`r format(Sys.Date(), '%Y-%m-%d')`" +output: + html_document: + toc: true + toc_float: true + code_folding: hide +--- + +```{r setup, include=FALSE} +knitr::opts_chunk$set(echo = FALSE, warning = FALSE, message = FALSE) + +library(dplyr) +library(ggplot2) +library(tigris) +library(sf) +library(patchwork) +library(vroom) +library(ggrepel) + +options(tigris_use_cache = TRUE) +if (!dir.exists("maps")) dir.create("maps") +``` + +Comparing two Epic Cosmos population bases: + +- **All patients** — `standard/data.csv.gz`: all patients with birthing parent + information, no additional filter. +- **CPT birth cohort** — `standard/data_cpt_birth.csv.gz`: subset filtered to a billed + procedure indicating birth (CPT 99460-99465, 99468, 99477). + + + +```{r load-data} +all_patients <- vroom::vroom("standard/data.csv.gz", show_col_types = FALSE) %>% + filter(geography != "00") %>% + mutate(date = as.Date(time, "%m-%d-%Y")) +cpt_birth <- vroom::vroom("standard/data_cpt_birth.csv.gz", show_col_types = FALSE) %>% + filter(geography != "00") %>% + mutate(date = as.Date(time, "%m-%d-%Y")) + +# Snapshot at the latest year both exports cover +common_date <- max(intersect(all_patients$date, cpt_birth$date)) %>% + as.Date(origin = "1970-01-01") +time_label <- format(common_date, "%Y") + +all_snap <- all_patients %>% filter(date == common_date) +cpt_snap <- cpt_birth %>% filter(date == common_date) + +states_sf <- tigris::states(cb = TRUE, year = 2020) %>% + filter(!STATEFP %in% c("60", "66", "69", "72", "78")) %>% # drop territories + select(geography = STATEFP, state_name = NAME, geometry) %>% + tigris::shift_geometry() # inset AK/HI below the continental US + +map_all <- states_sf %>% left_join(all_snap, by = "geography") +map_cpt <- states_sf %>% left_join(cpt_snap, by = "geography") + +pct_measures <- list( + list(col = "epic_pct_vitamin_k", label = "Vitamin K", slug = "vitamin_k"), + list(col = "epic_pct_rsv", label = "RSV immunization", slug = "rsv"), + list(col = "epic_pct_hepb", label = "HepB vaccination", slug = "hepb") +) + +diff_data <- all_snap %>% + select(geography, all_of(vapply(pct_measures, `[[`, character(1), "col"))) %>% + rename_with(~ paste0(., "_all"), -geography) %>% + full_join( + cpt_snap %>% + select(geography, all_of(vapply(pct_measures, `[[`, character(1), "col")), epic_n_patients) %>% + rename_with(~ paste0(., "_cpt"), -geography), + by = "geography" + ) %>% + left_join( + all_snap %>% select(geography, epic_n_patients_all = epic_n_patients), + by = "geography" + ) %>% + left_join(states_sf %>% st_drop_geometry() %>% select(geography, state_name), by = "geography") %>% + mutate(state_abb = state.abb[match(state_name, state.name)]) + +for (m in pct_measures) { + diff_data[[paste0("diff_", m$slug)]] <- + diff_data[[paste0(m$col, "_cpt")]] - diff_data[[paste0(m$col, "_all")]] +} +diff_data$ratio_n <- diff_data$epic_n_patients_cpt / diff_data$epic_n_patients_all + +map_diff <- states_sf %>% left_join(diff_data, by = "geography") +``` + +```{r helpers} +state_panel <- function(map_df, fill_var, title, legend_name = "", diverging = FALSE) { + if (diverging) { + lim <- max(abs(map_df[[fill_var]]), na.rm = TRUE) + scale <- scale_fill_distiller( + name = legend_name, palette = "RdBu", direction = 1, + limits = c(-lim, lim), na.value = "grey85" + ) + } else { + scale <- scale_fill_viridis_c( + name = legend_name, option = "plasma", na.value = "grey85", + breaks = scales::breaks_pretty(n = 4) + ) + } + + ggplot(map_df) + + geom_sf(aes(fill = .data[[fill_var]]), color = "white", linewidth = 0.1) + + scale + + labs(title = title) + + theme_void(base_size = 10) + + theme( + plot.title = element_text(face = "bold", size = 11, hjust = 0.5), + legend.position = "bottom", + legend.key.width = unit(1.2, "cm"), + legend.title = element_text(size = 8), + legend.text = element_text(size = 7) + ) +} + +# Builds and saves the triptych (All | CPT birth | Difference) for one measure, +# returning the combined patchwork so the calling chunk can print it inline. +render_triptych <- function(m) { + p1 <- state_panel(map_all, m$col, "All patients", "%") + p2 <- state_panel(map_cpt, m$col, "CPT birth cohort", "%") + p3 <- state_panel(map_diff, paste0("diff_", m$slug), "Difference (CPT birth − All)", "pp", diverging = TRUE) + + combined <- (p1 | p2 | p3) + + plot_annotation( + title = paste0(m$label, " (%) by State — ", time_label), + subtitle = "Epic Cosmos | All patients with birthing parent information vs. CPT birth cohort subset", + caption = "Grey = no data.", + theme = theme( + plot.title = element_text(face = "bold", size = 14), + plot.subtitle = element_text(color = "grey40"), + plot.caption = element_text(color = "grey50", size = 8) + ) + ) + + ggsave(file.path("maps", paste0("compare_", m$slug, ".png")), combined, width = 18, height = 6, dpi = 150) + combined +} + +# Builds and saves the All-vs-CPT-birth scatter plot for one measure, returning +# the ggplot so the calling chunk can print it inline. +render_scatter <- function(m) { + x_col <- paste0(m$col, "_all") + y_col <- paste0(m$col, "_cpt") + + scatter_data <- diff_data %>% + filter(!is.na(.data[[x_col]]), !is.na(.data[[y_col]])) + + lim <- range(c(scatter_data[[x_col]], scatter_data[[y_col]]), na.rm = TRUE) + + cor_test <- cor.test(scatter_data[[x_col]], scatter_data[[y_col]]) + cor_label <- sprintf( + "r = %.3f, p = %.3f", round(cor_test$estimate, 3), round(cor_test$p.value, 3) + ) + + p_scatter <- ggplot(scatter_data, aes(x = .data[[x_col]], y = .data[[y_col]])) + + geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "grey60") + + geom_point(color = "#1b6ca8", size = 2) + + ggrepel::geom_text_repel(aes(label = state_abb), size = 2.8, max.overlaps = 30) + + coord_equal(xlim = lim, ylim = lim) + + labs( + title = paste0(m$label, " (%) — All vs. CPT Birth Cohort by State, ", time_label), + subtitle = paste0(cor_label), + x = "All patients (%)", + y = "CPT birth cohort (%)" + ) + + theme_minimal(base_size = 11) + + theme(plot.title = element_text(face = "bold", size = 12)) + + ggsave(file.path("maps", paste0("scatter_", m$slug, ".png")), p_scatter, width = 7, height = 7, dpi = 150) + p_scatter +} +``` + +## Vitamin K + +```{r vitamin-k-map, fig.width=18, fig.height=6} +render_triptych(pct_measures[[1]]) +``` + +```{r vitamin-k-scatter, fig.width=7, fig.height=7} +render_scatter(pct_measures[[1]]) +``` + +## RSV Immunization + +```{r rsv-map, fig.width=18, fig.height=6} +render_triptych(pct_measures[[2]]) +``` + +```{r rsv-scatter, fig.width=7, fig.height=7} +render_scatter(pct_measures[[2]]) +``` + +## HepB Vaccination + +```{r hepb-map, fig.width=18, fig.height=6} +render_triptych(pct_measures[[3]]) +``` + +```{r hepb-scatter, fig.width=7, fig.height=7} +render_scatter(pct_measures[[3]]) +``` + +## Patient Count QC + +Share of the "all patients" population that also has a CPT code indicating birth. + +```{r ratio-map, fig.width=10, fig.height=8.5} +p_ratio <- ggplot(map_diff) + + geom_sf(aes(fill = ratio_n), color = "white", linewidth = 0.1) + + scale_fill_viridis_c( + name = "CPT birth / All", option = "magma", na.value = "grey85", + breaks = scales::breaks_pretty(n = 5) + ) + + labs( + title = paste0("Patient Count Ratio (CPT Birth Cohort / All) by State — ", time_label), + subtitle = "Share of the 'all patients' population that also has a CPT code indicating birth" + ) + + theme_void(base_size = 11) + + theme( + plot.title = element_text(face = "bold", size = 13, hjust = 0.5), + plot.subtitle = element_text(color = "grey40", hjust = 0.5, size = 9), + legend.position = "bottom", + legend.key.width = unit(1.5, "cm") + ) + +ggsave(file.path("maps", "compare_n_patients_ratio.png"), p_ratio, width = 10, height = 8.5, dpi = 150) +p_ratio +``` + +```{r timeseries, fig.width=9, fig.height=5} +national <- bind_rows( + vroom::vroom("standard/data.csv.gz", show_col_types = FALSE) %>% + filter(geography == "00") %>% mutate(source = "All patients"), + vroom::vroom("standard/data_cpt_birth.csv.gz", show_col_types = FALSE) %>% + filter(geography == "00") %>% mutate(source = "CPT birth cohort") +) %>% + mutate(date = as.Date(time, "%m-%d-%Y")) + +p_ts <- ggplot(national, aes(x = date, y = epic_n_patients, color = source)) + + geom_line(linewidth = 0.9) + + geom_point(size = 1.5) + + scale_y_continuous(labels = scales::label_comma()) + + scale_color_manual(values = c("All patients" = "#1b6ca8", "CPT birth cohort" = "#d1495b")) + + labs( + title = "National Patient Count Over Time", + x = NULL, y = "Patients", color = NULL + ) + + theme_minimal(base_size = 11) + + theme(legend.position = "bottom") + +ggsave(file.path("maps", "compare_n_patients_timeseries.png"), p_ts, width = 9, height = 5, dpi = 150) +p_ts +``` + diff --git a/data/cosmos_birth_vaccines/check_map.html b/data/cosmos_birth_vaccines/check_map.html new file mode 100644 index 0000000..f782ed3 --- /dev/null +++ b/data/cosmos_birth_vaccines/check_map.html @@ -0,0 +1,1689 @@ + + + + + + + + + + + + + + +Cosmos Birth Vaccines + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+
+
+
+ +
+ + + + + + + +

Comparing two Epic Cosmos population bases:

+
    +
  • All patientsstandard/data.csv.gz: +all patients with birthing parent information, no additional +filter.
  • +
  • CPT birth cohort — +standard/data_cpt_birth.csv.gz: subset filtered to a billed +procedure indicating birth (CPT 99460-99465, 99468, 99477).
  • +
+
+

Vitamin K

+

+

+
+
+

RSV Immunization

+

+

+
+
+

HepB Vaccination

+

+

+
+
+

Patient Count QC

+

Share of the “all patients” population that also has a CPT code +indicating birth.

+

+

+
+ + + +
+
+ +
+ + + + + + + + + + + + + + + + + diff --git a/data/cosmos_birth_vaccines/ingest.R b/data/cosmos_birth_vaccines/ingest.R new file mode 100644 index 0000000..5ca34ac --- /dev/null +++ b/data/cosmos_birth_vaccines/ingest.R @@ -0,0 +1,289 @@ +# ============================================================================= +# Epic Cosmos Birthing Parent Vaccination Data Ingestion +# Source: Epic Cosmos SlicerDicer export, "Patients with Birthing Parent +# Information" data model (Session IDs 2855051 / 2855069) +# +# Two population bases, exported as separate sessions with identical layout: +# raw/staging/ -> standard/data.csv.gz (all patients +# with birthing parent information -- no +# additional population filter) +# raw/staging_cpt_birth/ -> standard/data_cpt_birth.csv.gz (subset filtered +# to a billed procedure indicating birth: CPT +# 99460-99465, 99468, 99477 -- "1st hosp/birthing +# center care", "normal newborn care", etc.) +# +# Both sessions share the same layout: rows = Year x State of Residence; +# measures = Vit K (%), RSV (%), Hep B (%), Percentage of Population (%), +# Number of Patients with Birthing Parent Information. "Percentage of +# Population" is read but dropped from the standardized output -- it's not a +# coverage/vaccination measure, just each state's share of that session's +# total patient count for the year (not useful without also carrying the +# session total, and out of scope for this source). +# +# RSV (%) is blank for 2018-2022 in both sessions -- nirsevimab (the RSV +# monoclonal antibody this measure tracks) was not approved until 2023, so +# this is a genuine "measure not yet available" gap, not suppression. +# +# Suppressed cells ("10 or fewer" for counts, "-" for percentages) are +# imputed rather than left missing: counts as 5, percentages as 5 divided by +# the row's patient count. The suppressed_flag stays 1 either way, to mark +# the value as imputed. Neither session's raw export contains any suppressed +# cells as of the 2026-09-08 export, but both markers are still handled +# defensively since they are standard Epic Cosmos suppression tokens and may +# appear in future re-exports. +# ============================================================================= + +library(dplyr) +library(tidyr) +library(openxlsx2) +library(vroom) + +# Check that msoffcrypto-tool is available (required for decrypting +# password-protected xlsx files) +msoffcrypto_check <- system( + paste(Sys.which("python"), "-c \"import msoffcrypto\""), + ignore.stdout = TRUE, ignore.stderr = TRUE +) +if (msoffcrypto_check != 0) { + stop( + "Python package 'msoffcrypto-tool' is required but not installed.\n", + "Install it with: python -m pip install msoffcrypto-tool" + ) +} + +# Initialize process record +if (!file.exists("process.json")) { + process <- list(raw_state = NULL, raw_state_cpt_birth = NULL) +} else { + process <- dcf::dcf_process_record() +} + +if (!dir.exists("standard")) dir.create("standard") + +xlsx_password <- Sys.getenv("EPIC_XLSX_PASSWORD") +if (!nzchar(xlsx_password)) { + stop( + "EPIC_XLSX_PASSWORD not set. ", + "Add it to ~/.Renviron via usethis::edit_r_environ()" + ) +} + +# Load FIPS lookup (state-level) +all_fips <- vroom::vroom("../../resources/all_fips.csv.gz", show_col_types = FALSE) +state_fips_lookup <- all_fips %>% + filter(nchar(geography) == 2) %>% + select(geography, geography_name) + +# --------------------------------------------------------------------------- +# Helper: decrypt one password-protected xlsx and return the sheet as an +# all-character grid with no header interpretation +# --------------------------------------------------------------------------- +read_slicerdicer_grid <- function(file, password) { + temp_input <- tempfile(fileext = ".xlsx") + decrypted_file <- tempfile(fileext = ".xlsx") + file.copy(file, temp_input, overwrite = TRUE) + + cmd <- sprintf( + 'python -m msoffcrypto -p "%s" "%s" "%s"', + password, + normalizePath(temp_input, winslash = "/"), + decrypted_file + ) + status <- system(cmd) + unlink(temp_input) + if (status != 0) stop("Failed to decrypt: ", file) + + wb <- openxlsx2::wb_load(decrypted_file) + grid <- openxlsx2::wb_to_df( + wb, sheet = 1, col_names = FALSE, + skip_empty_rows = FALSE, skip_empty_cols = FALSE + ) + unlink(decrypted_file) + + as.data.frame( + lapply(grid, function(x) { x <- as.character(x); x[is.na(x)] <- ""; x }), + stringsAsFactors = FALSE + ) +} + +# --------------------------------------------------------------------------- +# Helper: parse one measure column. "10 or fewer" (Epic's suppression marker +# for counts) is imputed as 5 and flagged; "-" (Epic's suppression marker for +# percentages) is left NA here and flagged -- the percentage measures impute +# it afterward as 5 / n_patients (see impute_suppressed_pct()), since that +# requires the row's patient count, which isn't available inside this +# function. A genuinely blank cell (e.g. RSV before 2023, when nirsevimab did +# not exist) is left NA, unflagged. +# --------------------------------------------------------------------------- +parse_measure <- function(raw) { + raw <- trimws(as.character(raw)) + suppressed_count <- raw == "10 or fewer" + suppressed_dash <- !is.na(raw) & raw == "-" + value <- suppressWarnings(as.numeric(gsub("[%,]", "", raw))) + value[suppressed_count] <- 5 + list( + value = value, + suppressed_flag = as.integer(suppressed_count | suppressed_dash) + ) +} + +# --------------------------------------------------------------------------- +# Helper: impute a suppressed percentage cell as the standard imputed +# numerator (5) over the row's patient count, rather than leaving it NA. +# The suppressed flag stays 1 -- it still marks the value as imputed, just +# imputed to a number instead of to missing. +# --------------------------------------------------------------------------- +impute_suppressed_pct <- function(measure, n_patients) { + needs_impute <- measure$suppressed_flag == 1 & is.na(measure$value) + measure$value[needs_impute] <- 5 / n_patients[needs_impute] * 100 + measure +} + +# --------------------------------------------------------------------------- +# Parse and standardize one SlicerDicer export: rows are Year x State of +# Residence, followed by 5 measure columns in a fixed order (Vit K %, RSV %, +# Hep B %, Percentage of Population %, Number of Patients). "Percentage of +# Population" is read (to keep column positions aligned) but intentionally +# not carried into the standardized output -- it's not a coverage measure, +# just each state's share of that session's total patient count. +# --------------------------------------------------------------------------- +standardize_export <- function(file, password, label) { + grid <- read_slicerdicer_grid(file, password) + + header_row <- which(trimws(grid[[1]]) == "Year")[1] + if (is.na(header_row)) { + stop("[", label, "] Could not locate the 'Year' header row in: ", basename(file)) + } + + data_raw <- grid[(header_row + 1):nrow(grid), 1:7, drop = FALSE] + rownames(data_raw) <- NULL + colnames(data_raw) <- c( + "year", "state_name", "raw_vitk", "raw_rsv", "raw_hepb", + "raw_pctpop", "raw_npatients" + ) + + # Year is a merged cell, filled only on the first row of each year's states + data_raw$year[data_raw$year == ""] <- NA + data_raw <- tidyr::fill(data_raw, year, .direction = "down") + + # Drop "None of the above" (unknown state of residence) + data_raw <- data_raw %>% filter(trimws(state_name) != "None of the above") + + vitk <- parse_measure(data_raw$raw_vitk) + rsv <- parse_measure(data_raw$raw_rsv) + hepb <- parse_measure(data_raw$raw_hepb) + npt <- parse_measure(data_raw$raw_npatients) + + # Nirsevimab (the RSV monoclonal antibody this measure tracks) was not + # approved until 2023, so the measure did not exist before then. The raw + # export currently reports these years as blank (already NA), but force it + # explicitly rather than relying on that -- a future re-export could report + # 0% instead of blank, which would be misleading, not a true zero. + pre_2023 <- as.integer(data_raw$year) < 2023 + rsv$value[pre_2023] <- NA + rsv$suppressed_flag[pre_2023] <- 0 + + # Impute suppressed percentages as 5 / n_patients, since each is computed + # by Epic Cosmos over the row's patient count. Done after the pre-2023 + # override above, so a suppressed-looking pre-2023 RSV cell (there + # shouldn't be one, but just in case) stays NA rather than being imputed. + vitk <- impute_suppressed_pct(vitk, npt$value) + rsv <- impute_suppressed_pct(rsv, npt$value) + hepb <- impute_suppressed_pct(hepb, npt$value) + + data_standard <- data_raw %>% + mutate( + geography_name = if_else( + trimws(state_name) == "Total", "United States", trimws(state_name) + ), + time = format(as.Date(paste0(year, "-12-31")), "%m-%d-%Y"), + epic_pct_vitamin_k = vitk$value, + epic_pct_vitamin_k_suppressed_flag = vitk$suppressed_flag, + epic_pct_rsv = rsv$value, + epic_pct_rsv_suppressed_flag = rsv$suppressed_flag, + epic_pct_hepb = hepb$value, + epic_pct_hepb_suppressed_flag = hepb$suppressed_flag, + epic_n_patients = npt$value, + epic_n_patients_suppressed_flag = npt$suppressed_flag + ) %>% + left_join(state_fips_lookup, by = "geography_name") + + n_unmatched <- sum(is.na(data_standard$geography)) + if (n_unmatched > 0) { + unmatched <- unique(data_standard$state_name[is.na(data_standard$geography)]) + warning( + "[", label, "] ", n_unmatched, " rows could not be matched to a FIPS code. ", + "Unmatched: ", paste(unmatched, collapse = ", ") + ) + } + + data_standard <- data_standard %>% + filter(!is.na(geography)) %>% + select( + geography, time, + epic_pct_vitamin_k, epic_pct_vitamin_k_suppressed_flag, + epic_pct_rsv, epic_pct_rsv_suppressed_flag, + epic_pct_hepb, epic_pct_hepb_suppressed_flag, + epic_n_patients, epic_n_patients_suppressed_flag + ) %>% + arrange(geography, time) + + dupes <- data_standard %>% count(geography, time) %>% filter(n > 1) + if (nrow(dupes) > 0) { + stop("[", label, "] Duplicate geography/time combinations found: ", nrow(dupes)) + } + + message( + "[", label, "] Standardized ", nrow(data_standard), " rows | ", + length(unique(data_standard$geography)), " geographies | ", + min(data_standard$time), " to ", max(data_standard$time) + ) + + data_standard +} + +# ============================================================================= +# BLOCK 1: All patients with birthing parent information +# raw/staging/ -> standard/data.csv.gz +# ============================================================================= +raw_files_all <- list.files("raw/staging", pattern = "\\.xlsx$", full.names = TRUE) +if (length(raw_files_all) == 0) { + stop("No xlsx files found in raw/staging/") +} +if (length(raw_files_all) > 1) { + stop("Expected exactly one xlsx file in raw/staging/, found ", length(raw_files_all)) +} + +raw_state_all <- as.list(tools::md5sum(raw_files_all)) +names(raw_state_all) <- basename(raw_files_all) + +if (!identical(process$raw_state, raw_state_all)) { + data_all <- standardize_export(raw_files_all[1], xlsx_password, "all") + vroom::vroom_write(data_all, "standard/data.csv.gz", delim = ",") + + process$raw_state <- raw_state_all + dcf::dcf_process_record(updated = process) +} + +# ============================================================================= +# BLOCK 2: Patients with a CPT code indicating birth (billed procedure filter) +# raw/staging_cpt_birth/ -> standard/data_cpt_birth.csv.gz +# ============================================================================= +raw_files_cpt <- list.files("raw/staging_cpt_birth", pattern = "\\.xlsx$", full.names = TRUE) +if (length(raw_files_cpt) == 0) { + stop("No xlsx files found in raw/staging_cpt_birth/") +} +if (length(raw_files_cpt) > 1) { + stop("Expected exactly one xlsx file in raw/staging_cpt_birth/, found ", length(raw_files_cpt)) +} + +raw_state_cpt <- as.list(tools::md5sum(raw_files_cpt)) +names(raw_state_cpt) <- basename(raw_files_cpt) + +if (!identical(process$raw_state_cpt_birth, raw_state_cpt)) { + data_cpt <- standardize_export(raw_files_cpt[1], xlsx_password, "cpt_birth") + vroom::vroom_write(data_cpt, "standard/data_cpt_birth.csv.gz", delim = ",") + + process$raw_state_cpt_birth <- raw_state_cpt + dcf::dcf_process_record(updated = process) +} diff --git a/data/cosmos_birth_vaccines/maps/compare_hepb.png b/data/cosmos_birth_vaccines/maps/compare_hepb.png new file mode 100644 index 0000000..62442ed Binary files /dev/null and b/data/cosmos_birth_vaccines/maps/compare_hepb.png differ diff --git a/data/cosmos_birth_vaccines/maps/compare_n_patients_ratio.png b/data/cosmos_birth_vaccines/maps/compare_n_patients_ratio.png new file mode 100644 index 0000000..2281284 Binary files /dev/null and b/data/cosmos_birth_vaccines/maps/compare_n_patients_ratio.png differ diff --git a/data/cosmos_birth_vaccines/maps/compare_n_patients_timeseries.png b/data/cosmos_birth_vaccines/maps/compare_n_patients_timeseries.png new file mode 100644 index 0000000..1903026 Binary files /dev/null and b/data/cosmos_birth_vaccines/maps/compare_n_patients_timeseries.png differ diff --git a/data/cosmos_birth_vaccines/maps/compare_rsv.png b/data/cosmos_birth_vaccines/maps/compare_rsv.png new file mode 100644 index 0000000..e9ebfb8 Binary files /dev/null and b/data/cosmos_birth_vaccines/maps/compare_rsv.png differ diff --git a/data/cosmos_birth_vaccines/maps/compare_vitamin_k.png b/data/cosmos_birth_vaccines/maps/compare_vitamin_k.png new file mode 100644 index 0000000..f756e46 Binary files /dev/null and b/data/cosmos_birth_vaccines/maps/compare_vitamin_k.png differ diff --git a/data/cosmos_birth_vaccines/maps/scatter_hepb.png b/data/cosmos_birth_vaccines/maps/scatter_hepb.png new file mode 100644 index 0000000..b6175dd Binary files /dev/null and b/data/cosmos_birth_vaccines/maps/scatter_hepb.png differ diff --git a/data/cosmos_birth_vaccines/maps/scatter_rsv.png b/data/cosmos_birth_vaccines/maps/scatter_rsv.png new file mode 100644 index 0000000..54dff07 Binary files /dev/null and b/data/cosmos_birth_vaccines/maps/scatter_rsv.png differ diff --git a/data/cosmos_birth_vaccines/maps/scatter_vitamin_k.png b/data/cosmos_birth_vaccines/maps/scatter_vitamin_k.png new file mode 100644 index 0000000..2a32fdb Binary files /dev/null and b/data/cosmos_birth_vaccines/maps/scatter_vitamin_k.png differ diff --git a/data/cosmos_birth_vaccines/measure_info.json b/data/cosmos_birth_vaccines/measure_info.json new file mode 100644 index 0000000..d067789 --- /dev/null +++ b/data/cosmos_birth_vaccines/measure_info.json @@ -0,0 +1,131 @@ +{ + "epic_pct_vitamin_k": { + "id": "epic_pct_vitamin_k", + "short_name": "Vitamin K prophylaxis", + "long_name": "Percentage of patients with birthing parent with a recorded Vitamin K prophylaxis", + "category": "immunization", + "short_description": "Percent of patients with birthing parent information who received Vitamin K prophylaxis.", + "long_description": "Among Epic Cosmos patients with birthing parent information, the percentage with a recorded Vitamin K (phytonadione) prophylaxis administration. Percentages are computed by Epic Cosmos over the patient count in each cell (epic_n_patients). ", + "statement": "In {location}, {value}% of patients with birthing parent received Vitamin K prophylaxis.", + "measure_type": "Percent", + "unit": "Percent", + "time_resolution": "Year", + "sources": [{ "id": "epic_cosmos" }], + "citations": [] + }, + "epic_pct_vitamin_k_suppressed_flag": { + "id": "epic_pct_vitamin_k_suppressed_flag", + "short_name": "Suppressed flag: Vitamin K prophylaxis", + "long_name": "Suppression flag for the Vitamin K prophylaxis percentage", + "category": "immunization", + "short_description": "Indicates whether the Vitamin K prophylaxis percentage was suppressed by Epic Cosmos.", + "long_description": "This flag is 1 when Epic Cosmos suppressed the Vitamin K percentage (reported as '-'), and 0 otherwise. Suppressed percentages are imputed as 5 / epic_n_patients (the standard imputed numerator of 5, over the row's patient count) rather than left missing; the flag stays 1 to mark the value as imputed.", + "measure_type": "Binary", + "unit": "0/1", + "time_resolution": "Year", + "sources": [{ "id": "epic_cosmos" }], + "citations": [] + }, + "epic_pct_rsv": { + "id": "epic_pct_rsv", + "short_name": "RSV immunization", + "long_name": "Percentage of patients with birthing parent with a recorded RSV immunization", + "category": "immunization", + "short_description": "Percent of patients with birthing parent information who received an RSV immunization (nirsevimab).", + "long_description": "Among Epic Cosmos patients with birthing parent information, the percentage with a recorded RSV immunization (nirsevimab, a monoclonal antibody). Percentages are computed by Epic Cosmos over the patient count in each cell (epic_n_patients). Note that this metric is a known underestimate, as RSV immunizations are often administered in primary care offices, which are underrepresented in Epic Cosmos. Because nirsevimab was not approved until 2023, this measure did not exist and is marked as missing (NA), rather than suppressed, for the years 2018–2022 in both population bases.", + "statement": "In {location}, {value}% of patients with birthing parent received an RSV immunization.", + "measure_type": "Percent", + "unit": "Percent", + "time_resolution": "Year", + "sources": [{ "id": "epic_cosmos" }], + "citations": [] + }, + "epic_pct_rsv_suppressed_flag": { + "id": "epic_pct_rsv_suppressed_flag", + "short_name": "Suppressed flag: RSV immunization", + "long_name": "Suppression flag for the RSV immunization percentage", + "category": "immunization", + "short_description": "Indicates whether the RSV immunization percentage was suppressed by Epic Cosmos.", + "long_description": "This flag is 1 when Epic Cosmos suppressed the RSV immunization percentage (reported as '-'), and 0 otherwise. Suppressed percentages are imputed as 5 / epic_n_patients (the standard imputed numerator of 5, over the row's patient count) rather than left missing; the flag stays 1 to mark the value as imputed. This flag is 0 for the 2018-2022 rows where epic_pct_rsv is NA. That gap reflects the measure not existing yet (nirsevimab was not approved until 2023), not suppression.", + "measure_type": "Binary", + "unit": "0/1", + "time_resolution": "Year", + "sources": [{ "id": "epic_cosmos" }], + "citations": [] + }, + "epic_pct_hepb": { + "id": "epic_pct_hepb", + "short_name": "HepB vaccination", + "long_name": "Percentage of patients with birthing parent with a recorded hepatitis B vaccination", + "category": "immunization", + "short_description": "Percent of patients with birthing parent information with at least one recorded hepatitis B vaccine dose.", + "long_description": "Among Epic Cosmos patients with birthing parent information, the percentage with at least one recorded hepatitis B (HepB) vaccine dose. Percentages are computed by Epic Cosmos over the patient count in each cell (epic_n_patients).", + "statement": "In {location}, {value}% of patients with birthing parent had a recorded HepB vaccine dose.", + "measure_type": "Percent", + "unit": "Percent", + "time_resolution": "Year", + "sources": [{ "id": "epic_cosmos" }], + "citations": [] + }, + "epic_pct_hepb_suppressed_flag": { + "id": "epic_pct_hepb_suppressed_flag", + "short_name": "Suppressed flag: HepB vaccination", + "long_name": "Suppression flag for the hepatitis B vaccination percentage", + "category": "immunization", + "short_description": "Indicates whether the HepB vaccination percentage was suppressed by Epic Cosmos.", + "long_description": "This flag is 1 when Epic Cosmos suppressed the HepB vaccination percentage (reported as '-'), and 0 otherwise. Suppressed percentages are imputed as 5 / epic_n_patients (the standard imputed numerator of 5, over the row's patient count) rather than left missing; the flag stays 1 to mark the value as imputed.", + "measure_type": "Binary", + "unit": "0/1", + "time_resolution": "Year", + "sources": [{ "id": "epic_cosmos" }], + "citations": [] + }, + "epic_n_patients": { + "id": "epic_n_patients", + "short_name": "patients with birthing parent", + "long_name": "Number of patients with birthing parent information at Epic Cosmos network sites", + "category": "immunization", + "short_description": "Count of patients with birthing parent information, used as the denominator for the vaccination percentages.", + "long_description": "Annual count of Epic Cosmos patients with birthing parent information, stratified by state of residence. Counts of 10 or fewer are suppressed and imputed as 5 (see suppressed_flag). This source produces two population bases: standard/data.csv.gz (all patients with birthing parent information, larger) and standard/data_cpt_birth.csv.gz (subset with a billed procedure indicating birth: CPT 99460-99465, 99468, 99477, smaller). State-level counts reflect Epic's footprint among birthing facilities, not the underlying birth count.", + "statement": "In {location}, {value} patients with birthing parent information were recorded at Epic Cosmos network sites.", + "measure_type": "Count", + "unit": "Count", + "time_resolution": "Year", + "sources": [{ "id": "epic_cosmos" }], + "citations": [] + }, + "epic_n_patients_suppressed_flag": { + "id": "epic_n_patients_suppressed_flag", + "short_name": "Suppressed flag: patient count", + "long_name": "Suppression flag for the birthing parent patient count", + "category": "immunization", + "short_description": "Indicates whether the patient count was suppressed and imputed.", + "long_description": "Due to privacy protections, Epic Cosmos suppresses counts of 10 or fewer patients. This flag is 1 when epic_n_patients was suppressed and imputed as 5, and 0 otherwise.", + "measure_type": "Binary", + "unit": "0/1", + "time_resolution": "Year", + "sources": [{ "id": "epic_cosmos" }], + "citations": [] + }, + + "_sources": { + "epic_cosmos": { + "name": "Epic Cosmos", + "url": "https://cosmos.epic.com/", + "date_accessed": 2026, + "organization": "Epic Systems", + "organization_url": "https://www.epic.com/", +"description": "Epic Cosmos is a collaborative research platform containing de-identified patient data from hospitals and health systems using Epic electronic health record systems. Data is accessed via SlicerDicer, a self-service analytics tool. This source produces two population datasets: standard/data.csv.gz and standard/data_cpt_birth.csv.gz. The first includes all patients with linked birthing parent information and captures records from the child's first year of life, which may include vaccines administered at follow-up appointments after birth. The second is a subset restricted to the specific encounter in which a birth-related billing code was recorded (CPT 99460-99465, 99468, 99477). Because this second dataset is limited to a single encounter, it may exclude vaccines administered at birth under certain circumstances—for example, if an infant was delivered at a non-Epic facility and immediately transferred to an Epic-using site. Due to privacy protections, counts fewer than 10 are suppressed and imputed. Note that county-level and city-level stratifications could differ markedly in total sample size due to high levels of missingness of county data in some states.", "restrictions": "The data can be re-used with appropriate attribution. A suggested citation relating to this data is 'Results of research performed with Epic Cosmos were obtained from the PopHIVE platform (https://github.com/PopHIVE/Ingest).'" + } + }, + + "_catalog": { + "summary": "Epic Cosmos data on patients with birthing parent' rates of Vitamin K prophylaxis, RSV immunization (nirsevimab), and hepatitis B vaccination, plus patient counts, by state and year.", + "search_terms": ["Immunizations", "Maternal health", "vitamin k", "rsv", "hepatitis b", "nirsevimab"], + "bucket": [], + "files": { + "data.csv.gz": "All patients with birthing parent information (broadest population base), by state and year", + "data_cpt_birth.csv.gz": "Subset restricted to patients with a billed procedure indicating birth (CPT 99460-99465, 99468, 99477), by state and year" + } + } +} diff --git a/data/cosmos_birth_vaccines/process.json b/data/cosmos_birth_vaccines/process.json new file mode 100644 index 0000000..389b5ca --- /dev/null +++ b/data/cosmos_birth_vaccines/process.json @@ -0,0 +1,15 @@ +{ + "raw_state": { + "Patients with Birthing Parent Information_Crosstab_2026-09-08T15_18_31.xlsx": "71c2852e09d5e81bafe0e31729d79c45" + }, + "raw_state_cpt_birth": { + "Patients with Birthing Parent Information_Crosstab_2026-09-08T15_29_27.xlsx": "d535783660cb1477464bd3066e4bcb85" + }, + "type": "source", + "name": "cosmos_birth_vaccines", + "standard_state": { + "./data/cosmos_birth_vaccines/measure_info.json": "f0c01ee178c6c90e39acecdafe1ec6d9", + "./data/cosmos_birth_vaccines/standard/data.csv.gz": "dd3cbf3b5dfe032616ad4c3809c2d0fc", + "./data/cosmos_birth_vaccines/standard/data_cpt_birth.csv.gz": "e1adf7a8142df4757f2753a24ee4fe22" + } +} diff --git a/data/cosmos_birth_vaccines/raw/staging/Patients with Birthing Parent Information_Crosstab_2026-09-08T15_18_31.xlsx b/data/cosmos_birth_vaccines/raw/staging/Patients with Birthing Parent Information_Crosstab_2026-09-08T15_18_31.xlsx new file mode 100644 index 0000000..47cd476 Binary files /dev/null and b/data/cosmos_birth_vaccines/raw/staging/Patients with Birthing Parent Information_Crosstab_2026-09-08T15_18_31.xlsx differ diff --git a/data/cosmos_birth_vaccines/raw/staging_cpt_birth/Patients with Birthing Parent Information_Crosstab_2026-09-08T15_29_27.xlsx b/data/cosmos_birth_vaccines/raw/staging_cpt_birth/Patients with Birthing Parent Information_Crosstab_2026-09-08T15_29_27.xlsx new file mode 100644 index 0000000..7dc4989 Binary files /dev/null and b/data/cosmos_birth_vaccines/raw/staging_cpt_birth/Patients with Birthing Parent Information_Crosstab_2026-09-08T15_29_27.xlsx differ diff --git a/data/cosmos_birth_vaccines/standard/data.csv.gz b/data/cosmos_birth_vaccines/standard/data.csv.gz new file mode 100644 index 0000000..5e2ef01 Binary files /dev/null and b/data/cosmos_birth_vaccines/standard/data.csv.gz differ diff --git a/data/cosmos_birth_vaccines/standard/data_cpt_birth.csv.gz b/data/cosmos_birth_vaccines/standard/data_cpt_birth.csv.gz new file mode 100644 index 0000000..1ae9845 Binary files /dev/null and b/data/cosmos_birth_vaccines/standard/data_cpt_birth.csv.gz differ diff --git a/data/cosmos_birth_vaccines/standard/datapackage.json b/data/cosmos_birth_vaccines/standard/datapackage.json new file mode 100644 index 0000000..51e8ac5 --- /dev/null +++ b/data/cosmos_birth_vaccines/standard/datapackage.json @@ -0,0 +1,750 @@ +{ + "name": "cosmos_birth_vaccines", + "title": "Cosmos Birth Vaccines", + "licenses": [], + "resources": [ + { + "bytes": 6078, + "encoding": "windows-1252", + "md5": "dd3cbf3b5dfe032616ad4c3809c2d0fc", + "format": "csv", + "name": "data.csv", + "filename": "data.csv.gz", + "versions": {}, + "source": [], + "data_format": "wide", + "ids": [ + { + "variable": "geography" + } + ], + "id_length": 2, + "time": "time", + "profile": "data-resource", + "created": "2026-09-09 09:50:42.445438", + "last_modified": "2026-09-09 09:50:42.506454", + "vintage": {}, + "row_count": 400, + "entity_count": 50, + "schema": { + "fields": [ + { + "name": "geography", + "duplicates": 350, + "info": [], + "time_range": [0, 7], + "type": "string", + "missing": 0, + "table": { + "00": 8, + "01": 8, + "02": 8, + "04": 8, + "05": 8, + "06": 8, + "08": 8, + "09": 8, + "10": 8, + "12": 8, + "13": 8, + "15": 8, + "16": 8, + "17": 8, + "18": 8, + "19": 8, + "20": 8, + "21": 8, + "22": 8, + "23": 8, + "24": 8, + "25": 8, + "26": 8, + "27": 8, + "28": 8, + "29": 8, + "30": 8, + "31": 8, + "32": 8, + "33": 8, + "34": 8, + "35": 8, + "36": 8, + "37": 8, + "38": 8, + "39": 8, + "40": 8, + "41": 8, + "42": 8, + "45": 8, + "46": 8, + "47": 8, + "48": 8, + "49": 8, + "50": 8, + "51": 8, + "53": 8, + "54": 8, + "55": 8, + "56": 8 + } + }, + { + "name": "time", + "duplicates": 392, + "info": [], + "time_range": [0, 7], + "type": "string", + "missing": 0, + "table": { + "12-31-2018": 50, + "12-31-2019": 50, + "12-31-2020": 50, + "12-31-2021": 50, + "12-31-2022": 50, + "12-31-2023": 50, + "12-31-2024": 50, + "12-31-2025": 50 + } + }, + { + "name": "epic_pct_vitamin_k", + "duplicates": 87, + "info": [], + "time_range": [0, 7], + "type": "float", + "missing": 0, + "mean": 94.1625, + "sd": 7.823103, + "min": 21.18, + "max": 99.02 + }, + { + "name": "epic_pct_vitamin_k_suppressed_flag", + "duplicates": 399, + "info": [], + "time_range": [0, 7], + "type": "integer", + "missing": 0, + "mean": 0, + "sd": 0, + "min": 0, + "max": 0 + }, + { + "name": "epic_pct_rsv", + "duplicates": 252, + "info": [], + "time_range": [5, 7], + "type": "float", + "missing": 250, + "mean": 20.146267, + "sd": 10.928836, + "min": 0.98, + "max": 40.6 + }, + { + "name": "epic_pct_rsv_suppressed_flag", + "duplicates": 399, + "info": [], + "time_range": [0, 7], + "type": "integer", + "missing": 0, + "mean": 0, + "sd": 0, + "min": 0, + "max": 0 + }, + { + "name": "epic_pct_hepb", + "duplicates": 42, + "info": [], + "time_range": [0, 7], + "type": "float", + "missing": 0, + "mean": 88.1831, + "sd": 7.27683, + "min": 41.51, + "max": 96.19 + }, + { + "name": "epic_pct_hepb_suppressed_flag", + "duplicates": 399, + "info": [], + "time_range": [0, 7], + "type": "integer", + "missing": 0, + "mean": 0, + "sd": 0, + "min": 0, + "max": 0 + }, + { + "name": "epic_pct_of_population", + "duplicates": 162, + "info": [], + "time_range": [0, 7], + "type": "float", + "missing": 0, + "mean": 3.97875, + "sd": 13.8717, + "min": 0.12, + "max": 100 + }, + { + "name": "epic_pct_of_population_suppressed_flag", + "duplicates": 399, + "info": [], + "time_range": [0, 7], + "type": "integer", + "missing": 0, + "mean": 0, + "sd": 0, + "min": 0, + "max": 0 + }, + { + "name": "epic_n_patients", + "duplicates": 0, + "info": { + "id": "epic_n_patients", + "short_name": "Newborn cohort patients", + "long_name": "Number of newborn cohort patients at Epic Cosmos network sites", + "category": "immunization", + "short_description": "Count of patients in the Epic Cosmos newborn birth cohort, used as the denominator for the vaccination percentages.", + "long_description": "Monthly count of Epic Cosmos patients with a newborn or birth-hospitalization billed procedure (CPT 99460-99465, 99468, 99477), stratified by state of residence. Counts of 10 or fewer are suppressed and imputed as 5 (see suppressed_flag). Coverage varies substantially by state, reflecting Epic's footprint among birthing facilities rather than the underlying birth count; state-level counts should not be read as birth counts.", + "statement": "In {location}, {value} newborn cohort patients were recorded at Epic Cosmos network sites.", + "measure_type": "Count", + "unit": "Count", + "time_resolution": "Month", + "sources": [ + { + "id": "epic_cosmos" + } + ] + }, + "time_range": [0, 7], + "type": "integer", + "missing": 0, + "mean": 53506.9425, + "sd": 189397.553566, + "min": 1414, + "max": 1702776 + }, + { + "name": "epic_n_patients_suppressed_flag", + "duplicates": 399, + "info": { + "id": "epic_n_patients_suppressed_flag", + "short_name": "Suppressed flag: patient count", + "long_name": "Suppression flag for the newborn cohort patient count", + "category": "immunization", + "short_description": "Indicates whether the patient count was suppressed and imputed.", + "long_description": "Due to privacy protections, Epic Cosmos suppresses counts of 10 or fewer patients, reporting them as '10 or fewer'. This flag is 1 when epic_n_patients was suppressed and imputed as 5, and 0 otherwise. Where this flag is 1, all three percentage measures are also suppressed and are left missing rather than imputed, because the imputed denominator of 5 would force a meaningless 100%.", + "measure_type": "Binary", + "unit": "0/1", + "time_resolution": "Month", + "sources": [ + { + "id": "epic_cosmos" + } + ] + }, + "time_range": [0, 7], + "type": "integer", + "missing": 0, + "mean": 0, + "sd": 0, + "min": 0, + "max": 0 + } + ] + }, + "sha512": "e9e287e620a4fc0cccf24b167b2cf08173fd549b75a52ae4098a32f8a07618c6a224800b17a0c9d0fb7496f96434746250d3ff76fa7aa759b89242a256133248" + }, + { + "bytes": 5408, + "encoding": "windows-1252", + "md5": "e1adf7a8142df4757f2753a24ee4fe22", + "format": "csv", + "name": "data_cpt_birth.csv", + "filename": "data_cpt_birth.csv.gz", + "versions": {}, + "source": [], + "data_format": "wide", + "ids": [ + { + "variable": "geography" + } + ], + "id_length": 2, + "time": "time", + "profile": "data-resource", + "created": "2026-09-09 09:50:42.851065", + "last_modified": "2026-09-09 09:50:42.990429", + "vintage": {}, + "row_count": 360, + "entity_count": 45, + "schema": { + "fields": [ + { + "name": "geography", + "duplicates": 315, + "info": [], + "time_range": [0, 7], + "type": "string", + "missing": 0, + "table": { + "00": 8, + "01": 8, + "05": 8, + "06": 8, + "08": 8, + "09": 8, + "10": 8, + "12": 8, + "13": 8, + "15": 8, + "16": 8, + "17": 8, + "18": 8, + "19": 8, + "21": 8, + "22": 8, + "24": 8, + "25": 8, + "26": 8, + "27": 8, + "28": 8, + "30": 8, + "31": 8, + "32": 8, + "33": 8, + "34": 8, + "35": 8, + "36": 8, + "37": 8, + "38": 8, + "39": 8, + "40": 8, + "41": 8, + "42": 8, + "45": 8, + "46": 8, + "47": 8, + "48": 8, + "49": 8, + "50": 8, + "51": 8, + "53": 8, + "54": 8, + "55": 8, + "56": 8 + } + }, + { + "name": "time", + "duplicates": 352, + "info": [], + "time_range": [0, 7], + "type": "string", + "missing": 0, + "table": { + "12-31-2018": 45, + "12-31-2019": 45, + "12-31-2020": 45, + "12-31-2021": 45, + "12-31-2022": 45, + "12-31-2023": 45, + "12-31-2024": 45, + "12-31-2025": 45 + } + }, + { + "name": "epic_pct_vitamin_k", + "duplicates": 144, + "info": [], + "time_range": [0, 7], + "type": "float", + "missing": 0, + "mean": 97.706222, + "sd": 3.040294, + "min": 69.44, + "max": 99.53 + }, + { + "name": "epic_pct_vitamin_k_suppressed_flag", + "duplicates": 359, + "info": [], + "time_range": [0, 7], + "type": "integer", + "missing": 0, + "mean": 0, + "sd": 0, + "min": 0, + "max": 0 + }, + { + "name": "epic_pct_rsv", + "duplicates": 226, + "info": [], + "time_range": [5, 7], + "type": "float", + "missing": 225, + "mean": 22.961778, + "sd": 11.989571, + "min": 1.16, + "max": 43.77 + }, + { + "name": "epic_pct_rsv_suppressed_flag", + "duplicates": 359, + "info": [], + "time_range": [0, 7], + "type": "integer", + "missing": 0, + "mean": 0, + "sd": 0, + "min": 0, + "max": 0 + }, + { + "name": "epic_pct_hepb", + "duplicates": 21, + "info": [], + "time_range": [0, 7], + "type": "float", + "missing": 0, + "mean": 47.129028, + "sd": 10.007885, + "min": 6.5, + "max": 72.98 + }, + { + "name": "epic_pct_hepb_suppressed_flag", + "duplicates": 359, + "info": [], + "time_range": [0, 7], + "type": "integer", + "missing": 0, + "mean": 0, + "sd": 0, + "min": 0, + "max": 0 + }, + { + "name": "epic_pct_of_population", + "duplicates": 115, + "info": [], + "time_range": [0, 7], + "type": "float", + "missing": 0, + "mean": 4.355417, + "sd": 14.565279, + "min": 0.08, + "max": 100 + }, + { + "name": "epic_pct_of_population_suppressed_flag", + "duplicates": 359, + "info": [], + "time_range": [0, 7], + "type": "integer", + "missing": 0, + "mean": 0, + "sd": 0, + "min": 0, + "max": 0 + }, + { + "name": "epic_n_patients", + "duplicates": 4, + "info": { + "id": "epic_n_patients", + "short_name": "Newborn cohort patients", + "long_name": "Number of newborn cohort patients at Epic Cosmos network sites", + "category": "immunization", + "short_description": "Count of patients in the Epic Cosmos newborn birth cohort, used as the denominator for the vaccination percentages.", + "long_description": "Monthly count of Epic Cosmos patients with a newborn or birth-hospitalization billed procedure (CPT 99460-99465, 99468, 99477), stratified by state of residence. Counts of 10 or fewer are suppressed and imputed as 5 (see suppressed_flag). Coverage varies substantially by state, reflecting Epic's footprint among birthing facilities rather than the underlying birth count; state-level counts should not be read as birth counts.", + "statement": "In {location}, {value} newborn cohort patients were recorded at Epic Cosmos network sites.", + "measure_type": "Count", + "unit": "Count", + "time_resolution": "Month", + "sources": [ + { + "id": "epic_cosmos" + } + ] + }, + "time_range": [0, 7], + "type": "integer", + "missing": 0, + "mean": 26601.569444, + "sd": 92124.090467, + "min": 288, + "max": 865483 + }, + { + "name": "epic_n_patients_suppressed_flag", + "duplicates": 359, + "info": { + "id": "epic_n_patients_suppressed_flag", + "short_name": "Suppressed flag: patient count", + "long_name": "Suppression flag for the newborn cohort patient count", + "category": "immunization", + "short_description": "Indicates whether the patient count was suppressed and imputed.", + "long_description": "Due to privacy protections, Epic Cosmos suppresses counts of 10 or fewer patients, reporting them as '10 or fewer'. This flag is 1 when epic_n_patients was suppressed and imputed as 5, and 0 otherwise. Where this flag is 1, all three percentage measures are also suppressed and are left missing rather than imputed, because the imputed denominator of 5 would force a meaningless 100%.", + "measure_type": "Binary", + "unit": "0/1", + "time_resolution": "Month", + "sources": [ + { + "id": "epic_cosmos" + } + ] + }, + "time_range": [0, 7], + "type": "integer", + "missing": 0, + "mean": 0, + "sd": 0, + "min": 0, + "max": 0 + } + ] + }, + "sha512": "e839a8428011e32e6ed71506923ad5aafd2a6c7ccba2c48fc3dd5e4f59a203f75998983efbd3b52a935ba1855b01ada3e9783ec82a34e01b5ba280658dd581d6" + } + ], + "measure_info": { + "epic_pct_rsv_immunization": { + "id": "epic_pct_rsv_immunization", + "short_name": "RSV immunization after birth", + "long_name": "Percentage of infants receiving RSV immunization after birth", + "category": "immunization", + "short_description": "Percent of patients in the newborn birth cohort who received an RSV immunization (nirsevimab) after birth.", + "long_description": "Among Epic Cosmos patients with a newborn or birth-hospitalization billed procedure (CPT 99460-99465, 99468, 99477), the percentage who received an RSV immunization (nirsevimab, a monoclonal antibody administered to infants for RSV prevention) after birth. Data are stratified by state of residence and calendar month of the birth encounter. Percentages are computed by Epic Cosmos over the patient count in each cell (epic_n_patients). Where the numerator is 10 or fewer, Epic suppresses the percentage; those cells are flagged in epic_pct_rsv_immunization_suppressed_flag and the numerator is imputed as 5, giving a value of 5 / epic_n_patients * 100. Bounded values reported as '<0.01%' are imputed at half the bound (0.005%). IMPORTANT: this measure is right-censored for recent birth months. Because nirsevimab is administered seasonally (roughly October through March), infants born in the most recent months have not yet reached their first RSV season, so the last two to three months of the series are systematically low and will rise as later data accrue. In the source export this measure is labeled 'Immunization after birth (%)'.", + "statement": "In {location}, {value}% of infants received an RSV immunization after birth.", + "measure_type": "Percent", + "unit": "Percent", + "time_resolution": "Month", + "sources": [ + { + "id": "epic_cosmos" + } + ], + "citations": [] + }, + "epic_pct_hepb_vaccination": { + "id": "epic_pct_hepb_vaccination", + "short_name": "HepB vaccination", + "long_name": "Percentage of infants with hepatitis B vaccination", + "category": "immunization", + "short_description": "Percent of patients in the newborn birth cohort with a recorded hepatitis B vaccine dose.", + "long_description": "Among Epic Cosmos patients with a newborn or birth-hospitalization billed procedure (CPT 99460-99465, 99468, 99477), the percentage with at least one recorded hepatitis B (HepB) vaccine dose. Data are stratified by state of residence and calendar month of the birth encounter. Percentages are computed by Epic Cosmos over the patient count in each cell (epic_n_patients). Where the numerator is 10 or fewer, Epic suppresses the percentage; those cells are flagged in epic_pct_hepb_vaccination_suppressed_flag and the numerator is imputed as 5, giving a value of 5 / epic_n_patients * 100. Unlike the RSV and PCV measures, this one is not materially right-censored, because the HepB birth dose is given during the birth hospitalization. This measure covers the same construct as the cosmos_hepb_vax source but uses a billed-procedure newborn population base rather than a liveborn-infant diagnosis (ICD-10-CM Z38.*) base, so values are not directly comparable between the two.", + "statement": "In {location}, {value}% of infants had a recorded HepB vaccine dose.", + "measure_type": "Percent", + "unit": "Percent", + "time_resolution": "Month", + "sources": [ + { + "id": "epic_cosmos" + } + ], + "citations": [] + }, + "epic_pct_vitamin_k_icd": { + "id": "epic_pct_vitamin_k_icd", + "short_name": "Vitamin K, ICD-coded (discontinued)", + "long_name": "Percentage of infants with an ICD-coded Vitamin K encounter after birth", + "category": "immunization", + "short_description": "Percent of patients in the newborn birth cohort with an encounter diagnosis of Z41.8 or Z29.8 after birth, used as an ICD-coded proxy for Vitamin K prophylaxis. DISCONTINUED: only ever produced by the now-replaced 2026-08-12T14:19:53 monthly export; not present in any current standard output.", + "long_description": "Among Epic Cosmos patients with a newborn or birth-hospitalization billed procedure (CPT 99460-99465, 99468, 99477), the percentage with an ICD-10 encounter diagnosis of Z41.8 (encounter for procedures for purposes other than remedial care) or Z29.8 (encounter for other specified prophylactic measures) after birth. This was a second, independent signal for Vitamin K prophylaxis alongside epic_n_vitamin_k, capturing it via encounter diagnosis coding rather than a standalone billed drug charge (J3430). In the source export this measure was mislabeled 'cpt coded vit k (%)'; its actual criteria were the ICD-10 codes above, not a CPT procedure code. This measure existed only in the monthly export's 2026-08-12T14:19:53 session (Session ID 2849153); the yearly export's session never carried it, and the 2026-08-13 monthly session (Session ID 2849283) replaced it with a medication-administration-based Vitamin K count (epic_n_vitamin_k_med). It is retained here for historical reference only and is not produced by any current ingest of this source.", + "statement": "In {location}, {value}% of infants had an ICD-coded Vitamin K encounter after birth.", + "measure_type": "Percent", + "unit": "Percent", + "time_resolution": "Month", + "sources": [ + { + "id": "epic_cosmos" + } + ], + "citations": [] + }, + "epic_n_patients": { + "id": "epic_n_patients", + "short_name": "Newborn cohort patients", + "long_name": "Number of newborn cohort patients at Epic Cosmos network sites", + "category": "immunization", + "short_description": "Count of patients in the Epic Cosmos newborn birth cohort, used as the denominator for the vaccination percentages.", + "long_description": "Monthly count of Epic Cosmos patients with a newborn or birth-hospitalization billed procedure (CPT 99460-99465, 99468, 99477), stratified by state of residence. Counts of 10 or fewer are suppressed and imputed as 5 (see suppressed_flag). Coverage varies substantially by state, reflecting Epic's footprint among birthing facilities rather than the underlying birth count; state-level counts should not be read as birth counts.", + "statement": "In {location}, {value} newborn cohort patients were recorded at Epic Cosmos network sites.", + "measure_type": "Count", + "unit": "Count", + "time_resolution": "Month", + "sources": [ + { + "id": "epic_cosmos" + } + ], + "citations": [] + }, + "epic_n_patients_suppressed_flag": { + "id": "epic_n_patients_suppressed_flag", + "short_name": "Suppressed flag: patient count", + "long_name": "Suppression flag for the newborn cohort patient count", + "category": "immunization", + "short_description": "Indicates whether the patient count was suppressed and imputed.", + "long_description": "Due to privacy protections, Epic Cosmos suppresses counts of 10 or fewer patients, reporting them as '10 or fewer'. This flag is 1 when epic_n_patients was suppressed and imputed as 5, and 0 otherwise. Where this flag is 1, all three percentage measures are also suppressed and are left missing rather than imputed, because the imputed denominator of 5 would force a meaningless 100%.", + "measure_type": "Binary", + "unit": "0/1", + "time_resolution": "Month", + "sources": [ + { + "id": "epic_cosmos" + } + ], + "citations": [] + }, + "epic_pct_rsv_immunization_suppressed_flag": { + "id": "epic_pct_rsv_immunization_suppressed_flag", + "short_name": "Suppressed flag: RSV immunization", + "long_name": "Suppression flag for the RSV immunization percentage", + "category": "immunization", + "short_description": "Indicates whether the RSV immunization percentage was suppressed and imputed.", + "long_description": "This flag is 1 when Epic Cosmos suppressed the RSV immunization percentage because its numerator was 10 or fewer, and 0 otherwise. Where the flag is 1 and epic_n_patients was observed, the numerator is imputed as 5 and the value is set to 5 / epic_n_patients * 100. Where epic_n_patients was itself suppressed, the value is left missing. Values with a flag of 1 are imputed rather than reported and should be excluded from precise estimates.", + "measure_type": "Binary", + "unit": "0/1", + "time_resolution": "Month", + "sources": [ + { + "id": "epic_cosmos" + } + ], + "citations": [] + }, + "epic_pct_hepb_vaccination_suppressed_flag": { + "id": "epic_pct_hepb_vaccination_suppressed_flag", + "short_name": "Suppressed flag: HepB vaccination", + "long_name": "Suppression flag for the hepatitis B vaccination percentage", + "category": "immunization", + "short_description": "Indicates whether the hepatitis B vaccination percentage was suppressed and imputed.", + "long_description": "This flag is 1 when Epic Cosmos suppressed the hepatitis B vaccination percentage because its numerator was 10 or fewer, and 0 otherwise. Where the flag is 1 and epic_n_patients was observed, the numerator is imputed as 5 and the value is set to 5 / epic_n_patients * 100. Where epic_n_patients was itself suppressed, the value is left missing. Values with a flag of 1 are imputed rather than reported and should be excluded from precise estimates.", + "measure_type": "Binary", + "unit": "0/1", + "time_resolution": "Month", + "sources": [ + { + "id": "epic_cosmos" + } + ], + "citations": [] + }, + "epic_pct_vitamin_k_icd_suppressed_flag": { + "id": "epic_pct_vitamin_k_icd_suppressed_flag", + "short_name": "Suppressed flag: Vitamin K, ICD-coded", + "long_name": "Suppression flag for the ICD-coded Vitamin K percentage", + "category": "immunization", + "short_description": "Indicates whether the ICD-coded Vitamin K percentage was suppressed and imputed.", + "long_description": "This flag is 1 when Epic Cosmos suppressed the ICD-coded Vitamin K percentage because its numerator was 10 or fewer, and 0 otherwise. Where the flag is 1 and epic_n_patients was observed, the numerator is imputed as 5 and the value is set to 5 / epic_n_patients * 100. Where epic_n_patients was itself suppressed, the value is left missing. Values with a flag of 1 are imputed rather than reported and should be excluded from precise estimates. DISCONTINUED alongside epic_pct_vitamin_k_icd -- see that entry.", + "measure_type": "Binary", + "unit": "0/1", + "time_resolution": "Month", + "sources": [ + { + "id": "epic_cosmos" + } + ], + "citations": [] + }, + "epic_n_vitamin_k": { + "id": "epic_n_vitamin_k", + "short_name": "Vitamin K billed procedure count (unreliable, yearly only)", + "long_name": "Number of newborn cohort patients with a standalone billed Vitamin K injection procedure", + "category": "immunization", + "short_description": "Count of patients in the newborn birth cohort with a standalone billed Vitamin K (phytonadione) injection procedure (J3430). NOT a reliable measure of Vitamin K prophylaxis coverage. Yearly export only (data_yearly.csv.gz); see epic_n_vitamin_k_med for the monthly export's medication-based signal.", + "long_description": "Among Epic Cosmos patients with a newborn or birth-hospitalization billed procedure (CPT 99460-99465, 99468, 99477), the count who also have a standalone billed Vitamin K phytonadione injection procedure (J3430). IMPORTANT: Vitamin K prophylaxis is administered to roughly 99% of US newborns, but it is almost always bundled into the birth-hospitalization charge rather than billed as a separate line item, so this count captures only the rare cases where it was itemized standalone. It is suppressed ('10 or fewer') in the large majority of state/period combinations and never exceeds 50 patients even at the national level. This measure is retained for transparency but should not be interpreted as a Vitamin K coverage rate. As of the 2026-08-13 monthly export (Session ID 2849283), the monthly session replaced its Billed Procedures dimension with an All Medications dimension carrying a medication-administration-based count instead (epic_n_vitamin_k_med, far less suppressed), so this measure now appears only in the yearly export (standard/data_yearly.csv.gz), whose session still carries the Billed Procedures dimension.", + "statement": "In {location}, {value} infants in the newborn cohort had a standalone billed Vitamin K injection procedure.", + "measure_type": "Count", + "unit": "Count", + "time_resolution": "Year", + "sources": [ + { + "id": "epic_cosmos" + } + ], + "citations": [] + }, + "epic_n_vitamin_k_suppressed_flag": { + "id": "epic_n_vitamin_k_suppressed_flag", + "short_name": "Suppressed flag: Vitamin K billed procedure count", + "long_name": "Suppression flag for the Vitamin K billed procedure count", + "category": "immunization", + "short_description": "Indicates whether the Vitamin K billed procedure count was suppressed and imputed.", + "long_description": "Due to privacy protections, Epic Cosmos suppresses counts of 10 or fewer patients, reporting them as '10 or fewer'. This flag is 1 when epic_n_vitamin_k was suppressed and imputed as 5, and 0 otherwise. Because a standalone Vitamin K billed procedure is rarely itemized, this flag is 1 for the large majority of state/period combinations.", + "measure_type": "Binary", + "unit": "0/1", + "time_resolution": "Year", + "sources": [ + { + "id": "epic_cosmos" + } + ], + "citations": [] + }, + "epic_n_vitamin_k_med": { + "id": "epic_n_vitamin_k_med", + "short_name": "Vitamin K medication count", + "long_name": "Number of newborn cohort patients with a recorded Vitamin K medication administration", + "category": "immunization", + "short_description": "Count of patients in the newborn birth cohort with a recorded Vitamin K medication administration. Monthly export only (standard/data.csv.gz); a more usable Vitamin K signal than the yearly export's billed-procedure count, though still a count, not a percentage.", + "long_description": "Among Epic Cosmos patients with a newborn or birth-hospitalization billed procedure (CPT 99460-99465, 99468, 99477), the count who also have a recorded Vitamin K medication administration, drawn from the session's All Medications dimension ('vitamin k medication' value). Introduced in the 2026-08-13 monthly export (Session ID 2849283), replacing the session's prior ICD-coded Vitamin K percentage (epic_pct_vitamin_k_icd). Unlike the yearly export's standalone-billed-procedure count (epic_n_vitamin_k), which is suppressed in the large majority of state/periods because Vitamin K is rarely itemized as a separate billed charge, this medication-administration-based count runs roughly 80-90% of epic_n_patients nationally and is far less suppressed. It remains a count with no independent percentage, since the All Medications dimension is not broken out for RSV/HepB. Data are stratified by state of residence, age at encounter, and calendar month of the birth encounter. Where the count is 10 or fewer, Epic suppresses it as '10 or fewer'; those cells are flagged in epic_n_vitamin_k_med_suppressed_flag and imputed as 5.", + "statement": "In {location}, {value} infants in the newborn cohort had a recorded Vitamin K medication administration.", + "measure_type": "Count", + "unit": "Count", + "time_resolution": "Month", + "sources": [ + { + "id": "epic_cosmos" + } + ], + "citations": [] + }, + "epic_n_vitamin_k_med_suppressed_flag": { + "id": "epic_n_vitamin_k_med_suppressed_flag", + "short_name": "Suppressed flag: Vitamin K medication count", + "long_name": "Suppression flag for the Vitamin K medication count", + "category": "immunization", + "short_description": "Indicates whether the Vitamin K medication count was suppressed and imputed.", + "long_description": "Due to privacy protections, Epic Cosmos suppresses counts of 10 or fewer patients, reporting them as '10 or fewer'. This flag is 1 when epic_n_vitamin_k_med was suppressed and imputed as 5, and 0 otherwise.", + "measure_type": "Binary", + "unit": "0/1", + "time_resolution": "Month", + "sources": [ + { + "id": "epic_cosmos" + } + ], + "citations": [] + }, + "_sources": { + "epic_cosmos": { + "name": "Epic Cosmos", + "url": "https://cosmos.epic.com/", + "date_accessed": 2026, + "organization": "Epic Systems", + "organization_url": "https://www.epic.com/", + "description": "Epic Cosmos is a collaborative research platform containing de-identified patient data from over 300 million patients across more than 1,600 hospitals and health systems using Epic electronic health record systems. Data is accessed via SlicerDicer, a self-service analytics tool. The dataset includes emergency department visits, diagnoses, immunizations, laboratory results, and other clinical data. Due to privacy protections, counts fewer than 10 are suppressed and imputed. Coverage extends across all U.S. states and territories. Note that county-level and city-level stratifications could differ markedly in total sample size due to high levels of missingness of county data in some states.", + "restrictions": "The data can be re-used with appropriate attribution. A suggested citation relating to this data is 'Results of research performed with Epic Cosmos were obtained from the PopHIVE platform (https://github.com/PopHIVE/Ingest).'" + }, + "id": "_sources" + } + }, + "change_report": { + "data.csv.gz": { + "state": "new file" + }, + "data_cpt_birth.csv.gz": { + "state": "new file" + } + } +} diff --git a/data/cosmos_immunization/measure_info.json b/data/cosmos_immunization/measure_info.json index 85b584b..05dc6d7 100644 --- a/data/cosmos_immunization/measure_info.json +++ b/data/cosmos_immunization/measure_info.json @@ -80,5 +80,16 @@ "description": "Epic Cosmos is a collaborative research platform containing de-identified patient data from over 300 million patients across more than 1,600 hospitals and health systems using Epic electronic health record systems. Data is accessed via SlicerDicer, a self-service analytics tool. The dataset includes emergency department visits, diagnoses, immunizations, laboratory results, and other clinical data. Due to privacy protections, counts fewer than 10 are suppressed and imputed. Coverage extends across all U.S. states and territories. Note that county-level and city-level stratifications could differ markedly in total sample size due to high levels of missingness of county data in some states.", "restrictions": "The data can be re-used with appropriate attribution. A suggested citation relating to this data is 'Results of research performed with Epic Cosmos were obtained from the PopHIVE platform (https://github.com/PopHIVE/Ingest).'" } + }, + + "_catalog": { + "summary": "Epic Cosmos immunization data reporting the percentage of patients with RSV, PCV, zoster, and influenza vaccinations, among those with any encounter at a participating health system.", + "search_terms": ["Immunizations", "vaccination", "rsv", "flu", "pcv", "zoster"], + "bucket": [], + "files": { + "data_zip.csv.gz": "ZIP-level, Georgia only, all patients with any encounter, by age", + "data_county.csv.gz": "County-level across 10 states (CA, CO, CT, GA, MD, MN, NM, NY, OR, TN), all patients with any encounter, by age", + "data_county_base_pt.csv.gz": "County-level across the same 10 states, restricted to Epic Cosmos' 'Base Patient' population (a stricter patient-attribution filter than 'any encounter'), by age" + } } }