Add bundle_gas (Group A + Group B Strep) and the ABCs Strep sources - #189
Open
MadiBal wants to merge 3 commits into
Open
Add bundle_gas (Group A + Group B Strep) and the ABCs Strep sources#189MadiBal wants to merge 3 commits into
MadiBal wants to merge 3 commits into
Conversation
Brings the abcs_gas source (CDC dataset 9y49-tura, ABCs Group A Streptococcus, national, annual 1997-2024) onto main-line development from the stale `gas` branch, and rewrites the ingest because CDC changed the shape AND meaning of two topics between the 2025 and 2026 releases. Re-running the old parsing against current data produced an empty emm file and mislabelled rates. emm types: was one topic "emm types" whose viewby held the type or "Number of isolates". Now each type is its own topic (emm 1, emm 12, ..., Other), viewby holds that type's isolate count, and value is a PROPORTION of typed isolates despite units reading "Percent". Verified against the old release: 1997 emm 1 arrives as viewby=99 value=0.225, per-type counts sum to 440, and 99/440 = 0.225 = the 22.5 percent previously reported on 440 isolates. Now emits the percent, the per-type count, and the typed total; percents sum to 100.00 +/- 0.02 in every year. Syndromes: was a percent of cases from 1997, is now a rate per 100,000 from 2001. Columns renamed abcs_gas_pct_syndrome_* -> abcs_gas_rate_syndrome_*, since carrying rates under a "pct" name would mislead silently. More broadly every "Percent" value is now a proportion and is scaled by 100 -- 2023 clindamycin arrives as 0.264 where the old release said 26.4. A guard stops the script if such a value exceeds 1.5 so a future flip back fails loudly instead of inflating everything 100x, and the emm/syndrome topics stop() if they stop matching rather than writing an empty file. Two further fixes: - The 65+ age band arrives as a non-ASCII ">=65 years old". Under the C locale dcf_process runs in, that literal cannot be translated for comparison, so matching it directly silently dropped every 65+ row (28 rows). Labels are now stripped to ASCII before comparison, with a stop() if no 65+ rows survive. - Counts are written to their own data_counts.csv.gz. They exist only at age/sex/race = Overall, so folding them into data.csv.gz left a column ~93% NA -- sparse enough for vroom's type guessing to infer logical and blank out every real value downstream. Also picks up linezolid resistance and a survivals count, both new in 2026. measure_info.json is rewritten too: the version on the `gas` branch used generic keys (case_rate, syndrome, pct_resistant, emm_type) that matched none of the actual column names, leaving 36 columns undocumented. Now 64 entries, dcf_check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ABCs publishes one dataset per pathogen, and only Group A was being read in.
This adds Group B (CDC dataset 95m5-agj4), national, annual 1997-2024,
initialized with dcf::dcf_add_source("abcs_gbs").
Six standard files: case/death rates, estimated counts, clinical syndromes,
antibiotic resistance, capsular serotypes, and ALPH surface-gene types.
GBS is shaped differently from GAS, so the ingest is not a copy:
- Infant onset timing is central (GBS is the leading cause of neonatal sepsis),
so an `onset` column separates early-onset (0-6 days of life) from late-onset
(7-89 days) disease. Case rates split by onset and by onset x race.
- A `population_group` column replaces sex, since the syndrome / resistance /
serotype / ALPH topics are reported within Overall, infant early- and
late-onset, adults 18-64, and adults 65+. Each group has its own isolate
denominator, so percentages are not comparable across groups unweighted.
- Serotypes (Ia, Ib, II-V, VI-IX, non-typeable) and ALPH typing stand in for
Group A's emm types.
Same two traps handled as in abcs_gas: "Percent" values are proportions and are
scaled by 100 (guarded), and non-ASCII labels are stripped to ASCII before
comparison -- here the adult group appears as both "Adults, >=65 years old" and
"Adults, >= 65 years old", so internal whitespace is squeezed too.
Rates and counts go to separate files. Before that split, vroom's type guessing
inferred `logical` for the sparse count columns and silently turned 140 real
values into NA on read; every file now parses with zero problems.
Validated: serotype percents sum to ~100 per group-year (99.7-100.2), ALPH to
99.8-100.2, resistance within 0-100, no duplicate index rows. measure_info.json
has 33 entries, dcf_check clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Initialized with dcf::dcf_add_bundle("bundle_gas", source_files = ...), so
process.json carries type: "bundle" and scripts: build.R.
13 long-format parquets in dist/, one per contributing source standard file:
epic_gas, nnds_stss, five abcs_gas files, and six abcs_gbs files. Every parquet
shares the same shape -- geography (state name or "United States"),
geography_fips (the FIPS code, kept for joining), time (ISO period end),
measure, value -- plus whatever dimension columns the source carries and
`suppressed` for Epic.
Changes from the earlier version of this bundle:
- Epic GAS is read from epic_resp_infections/standard/quarterly_gas.csv.gz
rather than a separate epic_gas source.
- geography_fips is carried alongside the human-readable geography name, so
these files can still be joined to FIPS-keyed data. Verified 1:1 with the
name in every parquet, national always "00".
- Group B Streptococcus is included, which ABCs publishes separately and was
previously missing entirely.
NNDSS publishes a cumulative year-to-date count that resets each MMWR year
(national 2024 runs 5 -> 647 across weeks 1-52), so build.R de-accumulates it
into stss_cases_weekly and keeps the published cumulative series alongside; the
two are not additive. 27 weekly increments are negative, from NNDSS revising
earlier weeks downward -- left as reported rather than clamped, and logged.
Epic suppression is mapped per measure: the upstream numerator flag covers
n_strep_throat and pct_strep_throat (the percent derives from that same cell),
the denominator flag covers n_patients.
measure_info.json documents all 78 columns across the 13 parquets with 0
undocumented and 0 falling back to STANDARD_VARS defaults, and all 97 measure
levels resolve against the contributing sources. Includes a _bundle block
mapping each parquet to its source file.
Two shape questions are deliberately left open and noted in the README: whether
the Epic and NNDSS parquets should be this tall (cf.
bundle_childhood_immunizations/overall_rates_by_source.parquet and
bundle_youth_wellbeing/yrbss_state_age_demographics.parquet), and whether the
two NNDSS series belong as measure levels or as columns.
docs/ and resources/ regenerated with scripts/build_docs.R (14 bundles, 46
sources); every bundle_gas column renders with a name and a description or
value list.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
bundle_gas, plus the two ABCs Strep sources it needs. Supersedes #187.@DanWeinberger — two shape questions for you in Open questions below, plus a data-quality oddity. @anshap32 — the ABCs findings in What checking Group A turned up are the substantive part.
What's here
bundle_gas— 13 long-format parquets indist/, one per contributing source standard file. Every parquet shares the same shape:geography"United States"for the national totalgeography_fips"00"national), kept so these join to FIPS-keyed datatimeYYYY-mm-ddperiod endmeasurevalueplus each source's dimension columns (
age,sex,race_ethnicity,onset,population_group) andsuppressedfor Epic.epic_gas.parquetepic_resp_infectionsnnds_stss.parquetnndsabcs_gas{,_counts,_syndromes,_resistance,_emm}.parquetabcs_gasabcs_gbs{,_counts,_syndromes,_resistance,_serotypes,_alph}.parquetabcs_gbsabcs_gbs(new) — ABCs Group B Streptococcus, CDC dataset95m5-agj4, national, annual 1997–2024. ABCs publishes one dataset per pathogen and only Group A was being read in. GBS is organized differently, so the ingest isn't a copy: infant onset timing is central (early-onset 0–6 days vs late-onset 7–89 days), apopulation_groupcolumn replaces sex, and serotypes plus ALPH typing stand in for emm types.abcs_gas— brought onto main-line development from the stalegasbranch and substantially rewritten (below). Notedata/abcsis pneumococcus, a different pathogen despite the similar name.What checking Group A turned up
Re-running the existing
abcs_gasingest against current CDC data does not just add a year. Three real problems:CDC restructured the 2026 release, changing meaning.
emm typeswas one topic whoseviewbyheld the type; now each type is its own topic (emm 1,emm 12, …),viewbyholds that type's isolate count, andvalueis a proportion despite units reading "Percent". The old parsing keyed on the vanished topic and produced an empty emm file. Separately, syndromes went from percent-of-cases (1997+) to rate per 100,000 (2001+), so those columns are nowabcs_gas_rate_syndrome_*— carrying rates under apct_name would mislead silently.Verified against the old release: 1997
emm 1arrives asviewby=99,value=0.225; per-type counts sum to 440; 99/440 = 0.225 = the 22.5 percent previously reported on 440 isolates.A 100× scale error. Every "Percent" value is now a proportion — 2023 clindamycin arrives as
0.264where the old release said26.4. Caught by diffing against the old committed file; resistance and emm now both reproduce the old values exactly.The
≥65age band was being silently dropped. Under the C localedcf_processruns in, that non-ASCII literal can't be translated for comparison, so all 28 rows vanished. Labels are now stripped to ASCII before matching.Each of these now fails loudly: the script
stop()s if "Percent" values exceed 1.5, if the emm or syndrome topics stop matching, or if no 65+ rows survive — rather than writing an empty or 100×-wrong file.Also picked up: GAS 2024 data (the repo's raw file was from May 2025), linezolid resistance, and a survivals count.
Counts are split into their own
data_counts.csv.gzin both sources. Folded in, they left columns ~85–93% NA — sparse enough thatvroom's type guessing inferslogicaland blanks out real values. This was observed, not theoretical: 140 real GBS values silently became NA before the split.Open questions
1. Parquet shape — @DanWeinberger. These are fully long (one row per measure). Looking at
bundle_childhood_immunizations/overall_rates_by_source.parquetandbundle_youth_wellbeing/yrbss_state_age_demographics.parquet, those are noticeably less tall, so it's unclear whether the Epic and NNDSS outputs should be long, semi-wide, or just a parquet copy of the standard file. Happy to reshape once there's a house rule.2. NNDSS long vs wide, and negative values — @DanWeinberger. NNDSS publishes STSS as a cumulative year-to-date count that resets each MMWR year (national 2024 runs 5 → 647 across weeks 1–52), so
build.Rde-accumulates it intostss_cases_weeklyand keepsstss_cases_cumulativealongside. Two questions: should those be twomeasurelevels or two columns, and what should happen to the 27 negative weekly increments? They come from NNDSS revising earlier weeks downward. Currently left as reported rather than clamped to zero, withbuild.Rlogging the count — clamping is a one-line change.3. 13 parquets is a lot. One per source standard file, following the earlier "keep the upstream split" decision. Reasonable place to push back if consolidation is preferred.
Verification
dcf::dcf_processclean on all three projects; bundle rebuild is deterministic (re-running produces no diff).dcf_check: 0 undocumented columns onabcs_gas(was 36 — thegas-branch measure_info used generic keys matching no actual column name),abcs_gbs, andepic_resp_infections.measure_info.jsondocuments all 78 columns across 13 parquets — 0 undocumented, 0 falling back toSTANDARD_VARSdefaults — and all 97 measure levels resolve against the contributing sources. Includes a_bundleblock mapping each parquet to its source file.geography↔geography_fipsis 1:1 in every parquet, national always"00".docs/andresources/regenerated (14 bundles, 46 sources); everybundle_gascolumn renders with a name and a description or value list.Caveats worth carrying forward
valuemixes units within a parquet; always filter or facet bymeasurefirst.🤖 Generated with Claude Code