Pipeline

A pipeline you can read is a dataset you can trust.

Every number on this site was produced by code that anyone can read, run and check. That is not a footnote to the project — it is the design. Healthcare data that is technically public arrives scattered, inconsistent and unchecked; the pipeline's job is to turn that published record into something a person can question, and to leave a trail at every step so the questioning never has to stop at "trust us". The tools to do this used to require an institution. They now fit in a public repository that runs on a laptop, and that shift — the collapsing cost of holding a system's numbers up to the light — is the bet the whole project is built on.

This page is the map: the architecture end to end, the data flow of each analysis stage by stage, and the structure of every table the site serves. The documentation is the manual — the equations, the column-level dictionaries, and the step-by-step guide to reproducing every figure from the open-source repository.

01 · The architecture

From the published record to a database anyone can question

The system is two analyses sharing one discipline. Each starts from documents the law already requires to exist, collects them with code that records exactly what it fetched, computes every rate from the publisher's own counts, checks the arithmetic, and publishes the results three ways at once — as files, as pages, and as an API. Nothing flows in the other direction: the site can only display what the pipeline produced, so a wrong number on a page is reproducible all the way back to the bytes a payer served.

The whole system, left to right. Data moves one way: from documents the law requires, through open-source code, into artifacts, into one database, onto this site. Navy boxes are code that runs; white boxes are files it writes; the orange-edged box is the database the site reads.

Why it is shaped this way

The design has one governing idea: at no point should a reader have to take a number on faith. So collection records provenance (the exact URL, and for the pricing sources the SHA-256 of the bytes), computation happens in short Python files a non-programmer can read in an afternoon, and the database is loaded from versioned dumps rather than edited by hand. Every stage writes files the next stage reads — there is no hidden state, which means any stage can be re-run alone and its output compared against what is published.

The stack is deliberately boring: standard-library Python, MySQL, PHP on shared hosting, no build step, no external requests. Boring is a feature. A pipeline that needs a cluster to run is a pipeline most people cannot check; this one asks for a laptop and about an hour.

The numbers it carries today

Analysis 01 holds 658 prior authorization filings from 124 organizations across 48 states, with 497 coverage gaps recorded and classified rather than dropped. Analysis 02 holds the three CMS Medicare utilization files, a sample of 3,109 hospitals' mandated price-transparency files, county-level medical debt and health outcomes, and a code dictionary of the tens of thousands of strings the price files claim are codes.

Both analyses publish their misses next to their findings. A dataset that hides what it missed is asking to be trusted rather than checked, and being checkable is the product.

02 · Data flow · Prior authorization

Analysis 01: from 658 scattered filings to one checked table

CMS-0057-F obliges payers to publish prior authorization metrics but sets no format, requires no counts and checks no arithmetic — so each filing is a small act of interpretation, and the pipeline's job is to make every interpretation visible and reversible. The flow below runs in five commands. Each stage reads files, writes files, and prints what it did.

The filings flow. Solid navy stages are the five Python files; every white box is a file on disk that can be inspected between stages. Note the two side-products of merging — conflicts and failures — which most pipelines discard and this one publishes.

STAGE 1merge.py

Twenty-four collector segments overlap on purpose: several crawlers were pointed at overlapping universes of payers, so a document missed by one pass surfaces in another rather than disappearing silently. The price of overlap is duplication, so merging dedupes in two passes — first on numeric identity (same source URL, same approved and denied counts: two collectors reading the same row produce the same numbers), then on name identity (same URL, contract ID and normalized plan name), which catches filings that published no counts at all. When two independent reads of the same document disagree, the disagreement is not resolved by keeping the prettier row — it is written to merge_conflicts.json and resolved against the source document, because a disagreement between readers is itself evidence about how hard the document is to read.

STAGE 2normalize.py

Every record lands in one flat schema, and this is where the project's central rule is enforced: rates are computed from counts, never transcribed. If a payer published 1,000 standard requests and 214 denials, its denial rate is 21.4% by division, whatever its PDF says. The percentage the payer printed is kept in its own column — std_denial_rate_printed — precisely so the two can disagree in public. A filing that published only percentages keeps them, flagged as unverifiable; its count columns stay null, because "the payer did not say" is a fact, and zero would be a lie.

STAGE 3validate.py

The part that exists nowhere else. CMS mandates publication but checks no arithmetic, so the 10 consistency rules do what an auditor would: approved plus denied must account for the stated total, the printed rate must match the computed one, overturned appeals cannot exceed appeals filed, expedited decisions should not take longer than standard ones, and rates at the extremes get flagged before anyone quotes them. A rule that fires produces a finding — the rule name, a severity, and the exact numbers that tripped it — never a correction. The pipeline does not fix payers' arithmetic; it reports it. The documentation states each rule as an equation with its thresholds.

STAGE 4build.py · gaps.py

Build stamps every filing with its quality flags and writes the two principal artifacts: the metrics CSV and the findings CSV. Gaps does the same honest bookkeeping for the misses: every document the collectors could not read is classified — in code, reproducibly — into blocked (the document exists and resisted machines: robots.txt, HTTP 403, bot walls, scans), missing (a search that came up empty, a finding about the crawl rather than the payer), or not a gap (duplicates, plans with no CY2025 filing to find). The distinction matters because only the first group supports the claim that a filing is being withheld — and conflating the three would have overstated that claim by a factor of five.

STAGE 5render.py · export_xlsx.py

The same rows go out in every form someone might actually use: a standalone HTML report that opens without a server, and a formatted two-sheet workbook — Metrics and Validation — for the spreadsheet-first reader. Then tools/ in the web repository turns the CSVs into the versioned SQL dumps this site imports. Nothing is retyped anywhere in that chain; the number in a payer's PDF and the number in this page's HTML are connected by code alone.

03 · Run it

Seven files, one command line

The whole filings pipeline is seven short Python files in the public repository, standard library except for the workbook step. Derived files are never committed — they rebuild from the segments in data/, which is the point: if you do not trust an output, delete it and watch it come back.

01
merge.py
Two-pass dedupe across the overlapping collector segments
02
normalize.py
One flat schema; every rate computed from counts
03
validate.py
The 10 consistency rules
04
build.py
Writes the CSV and the findings
05
render.py
Writes the standalone HTML page
06
gaps.py
Classifies the coverage gaps by why
07
export_xlsx.py
Writes the formatted workbook
$ git clone https://github.com/RyanGomez-NYC/project_crossfoot
$ cd project_crossfoot/filings
$ python3 merge.py && python3 build.py && python3 render.py && python3 export_xlsx.py

The full walkthrough — what each command prints, what to check, and how to load the results into your own database — is in the documentation.

04 · Data flow · Healthcare prices

Analysis 02: five sources, one manifest, every byte accounted for

The pricing analysis applies the same discipline to a harder collection problem. Medicare publishes clean central files; hospitals publish their mandated price files wherever and however they like, exactly as payers do with prior authorization filings. So the pipeline splits into fetchers that record provenance for every download, parsers per source family, a code dictionary that decides what is even a billing code, and the same validate-and-publish tail as analysis 01.

The prices flow. Everything downstream of fetch.py can be re-run offline against the recorded downloads, and every figure the site shows can be traced through manifest.json to a SHA-256 of the publisher's own bytes.

STAGE 1sources.py
fetch.py

Every source the analysis reads is declared in one file — the pinned URL for each Medicare file, the CHR and Urban Institute releases, the Census relationship file, and the discovery rule for hospital price files. Fetching writes a manifest entry per download: the exact URL, the fetch time, the byte count and the SHA-256. That last field is the quiet backbone of the whole analysis — anyone can download the same file tomorrow, hash it, and know with certainty whether they are looking at what the pipeline read.

STAGE 2medicare.py
mrf.py
counties.py

Three parser families, one per source shape. The Medicare files are large, clean CSVs — every hospital by every MS-DRG and APC, every state by every HCPCS code — where the only judgment is remembering that CMS suppresses rows under 11 patients, so a missing row means "not published", never "none performed". The hospital price files are the opposite: each is discovered through the cms-hpt.txt pointer the rule requires at the hospital's web root, streamed rather than loaded, and read for every code-like string it carries. Files that cannot be found or parsed are recorded with a reason and classified exactly as the prior authorization gaps are. The county parsers join health outcomes, medical debt and demographics onto one county spine via the Census ZCTA-to-county file.

STAGE 3codes.py
basket.py

Most strings a hospital's price file calls a code are not codes — chargemaster numbers and fragments that happen to pass a regex. The dictionary gives every string a status by set membership, never by judgment: official if it appears in a CMS list, hospitals only if well-formed and independently listed by two or more sampled hospitals, unverified otherwise — and unverified strings are priced nowhere on this site, because a price for a non-code is not a price. Descriptions are copied from a named source or left blank; the pipeline never writes one, and CPT text outside the Medicare file is AMA copyright and stays blank rather than guessed.

STAGE 4enrich.py
validate.py

Enrichment computes what the sources imply but do not say — charge-to-payment ratios per row, state medians for the comparison basket, system-level sums weighted by discharges — always from figures in the same row or the same declared join, never across an undeclared boundary. Validation then runs the price rules: a cash price above list, a negotiated rate outside the file's own stated bounds, a price file more than a year old, and a hospital whose published list price sits far from the average charge it reported to Medicare for the same admission. As in analysis 01, every miss is a finding with the numbers attached.

STAGE 5county_models.py

The one stage that is not standard library (numpy, pandas, scikit-learn) fits gradient-boosted trees and a ridge regression for three measured county outcomes — medical debt in collections, premature mortality, years of life lost — from prices, insurance, income, age, race, rurality, access and behaviour. Cross-validated, fixed seed, permutation importance, partial dependence. Nothing in it is causal and every figure it produces says so; the Analysis page renders the module's output and nothing else.

05 · The data structures

Twenty tables, and why each one exists

The database mirrors the pipeline's honesty rules in its shape. Count columns are nullable because "not published" must stay distinct from zero. Findings live in their own table rather than as flags, because they are the product. Every pricing table traces to a row in price_source carrying the URL and SHA-256 of the file it came from. The full column-by-column dictionary is in the documentation; this is the shape.

The shape of the database. Navy-headed tables are the spines — one row per filing, one per hospital; everything else hangs off them or stands alone as reference. "1 ─ n" reads "one row here owns many rows there".

Reading analysis 01's shape

One table per claim the dataset makes. filing is the record — one row per plan-level disclosure, its counts nullable because a payer that printed no counts gets nulls, not zeros. finding is the audit — one row per rule violation, cascading with its filing, holding the exact numbers that tripped the rule so the finding can be re-derived. coverage_gap is the confession — every document the collectors could not read, classified by why. Most datasets ship the first table; the honesty lives in the other two.

Reading analysis 02's shape

Two spines and a dictionary. Geography hangs on county_profile — debt, outcomes and demographics per county — and institutions hang on price_hospital, which joins each hospital to its county, its system, its Medicare rows and, where sampled, its own published price file with every coded charge. price_code arbitrates what counts as a billing code at all. The model_* tables carry the county models' output with their caveats stored alongside, so no page can quote a model without its warning label. Every column of every table is defined in the data dictionary.

06 · Method

What this is, and what it is not

The rule

CMS-0057-F requires Medicare Advantage organizations, Medicaid and CHIP fee-for-service programs and managed care plans, and federally-facilitated exchange QHP issuers to publish annual prior authorization metrics. The first deadline was 31 March 2026, covering calendar year 2025.

There is no central repository, no machine-readable requirement, and the CMS template is optional. Each payer posts a document wherever it likes, in whatever shape it likes. That is why this dataset has to exist.

Collection

658 filings from 124 organizations across 48 states, each traced to the payer's or state agency's own document. Collectors overlapped deliberately, so a document missed by one pass surfaced in another rather than disappearing. Deduplication runs on numeric identity first, then name identity; where two independent reads disagreed, the disagreement was itself recorded and resolved against the source.

Computed versus transcribed

Counts, turnaround values and plan identifiers are transcribed from the source. Every rate is computed from those counts — no rate is estimated or carried over from a payer's own summary. Where a payer printed its own percentage, that figure is stored separately so the two can be compared rather than conflated.

A blank is a blank. 125 filings published percentages with no counts; their count columns are null, not zero. "This payer denied nothing" and "this payer published nothing" are different claims, and only one of them is interesting.

Known limits

Coverage is broad, not complete. 497 gaps are logged, and classified in the pipeline into three groups: 126 documents that exist and could not be read, 304 searches that came up empty, and 67 that are not gaps at all. Only the first shows a filing being withheld from machines. Humana publishes no readable filing; Cigna publishes exactly one. Where a turnaround unit could not be established, the value is null rather than guessed.

Every gap is listed individually →

07 · Outputs

Take the data

08 · Analysis 02 · Prices

Where each price came from

The pricing analysis uses the same pipeline discipline with different sources. Each is fetched by pipeline/prices/ in the open-source repository — standard library Python — and every downloaded byte is recorded with its URL, fetch time and SHA-256.

CMS
Medicare utilization & payment files, 2024
Inpatient Hospitals by Provider and Service (every hospital × MS-DRG: discharges, average charge, average payment); Outpatient Hospitals by Provider and Service (× APC); Physician & Other Practitioners by Geography and Service (state × HCPCS: billed vs. allowed). Rows under 11 patients are suppressed by CMS before publication, so a missing row is "not published", never "none performed". Charge-to-payment ratios are computed per row.
45 CFR 180
Hospital price-transparency files, sampled
3,109 hospitals across every state, sampled in waves. Each is discovered through the cms-hpt.txt the rule requires at the hospital's web root, streamed, and read for every CPT/HCPCS and MS-DRG string it carries. Files that cannot be found, read or parsed are recorded with a reason and classified — blocked, missing, tool-limited — exactly as the prior authorization gaps are. The sample describes the hospitals sampled, not the 6,000 that were not.
Codes
The code dictionary
Most strings a price file calls a code are not codes — chargemaster numbers and fragments that pass a regex. pipeline/prices/codes.py gives each string a status by set membership, never judgment: official if it is in the Medicare physician or inpatient file, the HCPCS Level II release or the MS-DRG table; hospitals only if well-formed and listed by two or more sampled hospitals; unverified otherwise, and then it is priced nowhere. Descriptions are copied from a named source in that order, then from the hospitals' own wording, tagged as theirs; CPT text outside the Medicare file is AMA copyright and is left blank. The 48-item basket the comparisons run on is curated in basket.py; the dictionary serves every other code.
Urban
Debt in America 2025
Share of adults with medical debt in collections and the median amount, by county, from a 4% panel of de-identified credit records (August 2025), split by whether a ZIP code is majority people of color. Suppressed under 50 records. This is debt in collections, not bankruptcy: no public dataset records why a bankruptcy was filed, and this site does not claim one.
CHR · Census
County Health Rankings 2025; ZCTA→county
Premature death, self-reported health, uninsured rate, primary-care and mental-health provider ratios, preventable hospital stays, income, inequality, race and rurality per county. The Census 2020 ZCTA-to-county relationship file places each hospital in the county holding most of its ZIP code's land area, so prices can sit beside debt and outcomes.

Every file the pricing analysis reads

the files read, as fetched — not our copy of them

As fetched by the pipeline: the exact URL, when, the size and the SHA-256 of the bytes. The 3,109 sampled hospital price files are listed on each hospital's page with the same fields.

  1. MUP_INP_RY26_P03_V10_DY24_PrvSvc.CSV · 38.0M bytes · fetched 2026-08-21 · sha256 2ab6da15be4c… · publisher's page · public domain (US federal)
  2. MUP_OUT_RY26_P04_V10_DY24_Prov_Svc.csv · 28.1M bytes · fetched 2026-08-21 · sha256 f293918edbf6… · publisher's page · public domain (US federal)
  3. MUP_PHY_R26_P05_V10_D24_Geo.csv · 42.1M bytes · fetched 2026-08-21 · sha256 c26956788333… · publisher's page · public domain (US federal)
  4. july-2025-alpha-numeric-hcpcs-file.zip · 2.4M bytes · fetched 2026-08-22 · sha256 91a9e95e0681… · publisher's page · public domain (US federal); HCPCS Level II is CMS-maintained
  5. fy-2025-ipps-final-rule-table-5.zip · 146K bytes · fetched 2026-08-22 · sha256 d5e0a0bd2314… · publisher's page · public domain (US federal)
  6. County Health Rankings 2025 — national analytic data · University of Wisconsin Population Health Institute · 2025 release (measures 2019–2023)
    analytic_data2025_v3.csv · 13.1M bytes · fetched 2026-08-21 · sha256 5a129b856489… · publisher's page · free for non-commercial use with attribution; see countyhealthrankings.org
  7. Debt in America 2025 — county-level medical debt · Urban Institute · August 2025 credit-bureau panel
    Debt%20in%20America%20County-Level%20Medical%20Debt.xlsx · 396K bytes · fetched 2026-08-21 · sha256 4b2ec3982817… · publisher's page · ODC-BY
  8. Debt in America 2025 — state-level medical debt · Urban Institute · August 2025 credit-bureau panel
    Debt%20in%20America%20State-Level%20Medical%20Debt.xlsx · 10K bytes · fetched 2026-08-21 · sha256 343a63cba9ba… · publisher's page · ODC-BY
  9. Debt in America 2025 — national medical debt · Urban Institute · August 2025 credit-bureau panel
    Debt%20in%20America%20National-Level%20Medical%20Debt.xlsx · 3K bytes · fetched 2026-08-21 · sha256 c4e5f02de5a9… · publisher's page · ODC-BY
  10. CDC PLACES — county health measures, 2025 release · CDC · 2025 release (BRFSS 2022–2023 model-based estimates)
    rows.csv · 4.8M bytes · fetched 2026-08-21 · sha256 a47cad3a852c… · publisher's page · public domain (US federal)
  11. ACS 5-year 2023 — county age and poverty · US Census Bureau · 2019–2023
    acs5 · publisher's page · public domain (US federal) · not fetched in the last build
  12. chsp-hospital-linkage-2023.csv · 1.5M bytes · fetched 2026-08-21 · sha256 a86146f10c8d… · publisher's page · public; cite AHRQ Compendium of U.S. Health Systems
  13. 2020 ZCTA to County relationship file · US Census Bureau · 2020
    tab20_zcta520_county20_natl.txt · 6.8M bytes · fetched 2026-08-21 · sha256 3ed41278d637… · publisher's page · public domain (US federal)

Every figure on this page is computed from these files by pipeline/prices/; the same rows are in the API.

Consistency rules for prices live in pipeline/prices/validate.py: a cash price above the list price, a negotiated rate outside the file's own stated minimum and maximum, a percentage rate with no dollar estimate, a file more than a year old, and — across two of the hospital's own publications — a list price far from the average charge it submitted to Medicare for the same admission. The Costs page and the CSV read the same findings table.