Every figure on this site can be re-derived by hand. This page shows you how.
Project Crossfoot exists because the cost of checking a regulated industry's numbers has collapsed. The law already makes this data public; what has been missing is the connective work — finding the documents, reading them consistently, doing the arithmetic, and admitting what was missed. That work is now small enough to be open source, and openness is not a license formality here: it is the trust model. You should not believe a denial rate because this site printed it. You should believe it because the harvest method is written down, the equation is stated, the source document is linked, and the code that connects them runs on your machine.
This page is that method, in full: how the documents were harvested, every calculation as an equation over named columns, the data dictionary for every table, and a step-by-step guide to rebuilding the dataset from the public repository — no permission required, no account, no special hardware.
How the documents were found and read
Both analyses start from the same fact: the law requires these documents to exist, and requires nothing else. CMS-0057-F names no repository, no format and no arithmetic check for prior authorization filings; 45 CFR 180 requires each hospital to publish a machine-readable price file but lets it live anywhere on the hospital's site. So the harvest is the hard part, and it is where a dataset earns or loses its credibility — which is why every choice below is recorded in the data itself rather than in anyone's memory.
Analysis 01 — prior authorization filings
The unit of collection is a collector segment: one crawl session pointed at one universe of payers — the national carriers, the Blues, the state Medicaid agencies, the Marketplace issuers, a region's regional plans. Each segment writes one JSON file to data/ with two lists: filings, the plan-level rows it extracted, and failures, every document it found but could not read, with the reason in prose. Twenty-four segments were run across three waves, and their universes overlap on purpose — when two collectors cover the same payer, a document missed by one surfaces in the other, and a document read twice becomes a free consistency check, because two independent reads of the same table should produce the same numbers. When they did not, the disagreement was recorded in merge_conflicts.json and resolved against the source document.
For each filing a collector transcribes only what the payer printed: the request counts (standard and expedited: received, approved, denied), appeal counts and outcomes, turnaround times, the payer's own printed denial percentage where one appears, the plan identifiers, and the URL of the document itself. No rate is ever transcribed as the rate — the printed percentage is kept in its own column so it can be compared with the computed one. Where the payer printed nothing, the field stays empty. An empty cell means "the payer did not say", and the pipeline defends that meaning all the way to the API.
Documents that resist machines — scans, image-embedded tables, spreadsheets, pages that assemble in the browser — go to a harvest queue (tools/unread.txt), where tools/harvest.py turns each one into an artifact a reader can extract from. Documents that stayed unreadable are not dropped: they become rows in the coverage-gap table, classified by what stopped the read. Publishing the misses is not modesty — it is what makes the hits checkable.
Analysis 02 — healthcare prices
The pricing sources split into two kinds. The central files — Medicare's inpatient, outpatient and physician utilization files, County Health Rankings, the Urban Institute's Debt in America, the Census ZCTA-to-county relationship file — have stable published URLs, pinned in pipeline/prices/sources.py. The hospital price files do not: each is discovered the way the rule says it must be discoverable, by fetching cms-hpt.txt from the hospital's web root and following the machine-readable-file URL it declares. 3,109 hospitals across every state have been sampled this way, in waves — and every file that could not be found, fetched or parsed is recorded with a reason and classified exactly as the prior authorization gaps are. The sample describes the hospitals sampled, never the thousands that were not.
Every download, central or hospital, writes a manifest entry: the exact URL fetched, the fetch time, the byte count, and the SHA-256 of the bytes. The hash is the harvest's anchor. A year from now, anyone can fetch the same URL, hash what they receive, and know with certainty whether they are looking at the same document the pipeline read — no trust in this site required. The manifest is published as the price_source table and through costs_api.php?set=source.
Large files are streamed rather than loaded — a hospital's price file can run to gigabytes — and read for every CPT/HCPCS and MS-DRG string they carry. What counts as a code at all is decided downstream by the code dictionary, by set membership against the CMS lists, never by judgment. The crawler is standard-library Python: no headless browser, no API keys, nothing a reader could not run.
Every computed figure, as an equation
The arithmetic below is all of it — there is no model, adjustment or estimate hiding behind any number in analysis 01, and the pricing analysis keeps its one statistical layer clearly labelled. Terms in the equations are the exact column names from the data dictionary, so each one can be checked against any row of the CSV with a calculator. Division by nothing is never fudged: if a term is missing or a denominator is zero, the result is null — "not published", never zero.
Denial rates
Rounded to two decimal places. The same formula gives exp_denial_rate from the expedited counts. The rate the payer printed is stored separately as std_denial_rate_printed and is never used as the rate — except for the 125 filings that published percentages with no counts at all, where the printed figure is carried, flagged percentages_only, and marked unverifiable.
Appeal overturn rate
Of the denials that were appealed, the share the payer itself reversed. Where a payer published only its own overturn percentage, that figure is used and the counts stay null.
The crossfoot: do the counts reconcile?
The project's namesake rule, applied to standard and expedited counts alike. Up to 0.5% of the total may be unaccounted — pending and withdrawn requests legitimately fall outside approved and denied. Beyond that, the filing contradicts itself and an error-severity finding records the exact shortfall. One special case: if the unaccounted balance equals ext_review_approved within the same tolerance, the filing is reporting extended-review outcomes as a separate bucket — a warn, because its published percentages then understate the full population rather than contradict it.
Printed versus computed
The payer's own printed percentage against the one computed from the payer's own counts. A point of tolerance forgives rounding; past it, the payer's summary disagrees with the payer's table, and the finding quotes both figures.
Plausibility checks
Expedited means urgent: a decision clock that runs in hours, on a smaller population. An expedited mean slower than the standard mean, or expedited volume above standard volume, is nearly always a transposed column in the payer's document — flagged warn, with the ratio, and never silently corrected. Two band checks complete the set: a denial rate of exactly 0%, or above 40%, is flagged for a look before anyone quotes it — implausible is not the same as impossible, which is why these are warnings, not errors.
| Rule | Severity | Fires when |
|---|---|---|
| std_counts_reconcile · exp_counts_reconcile | error | Approved + denied fail to account for the stated total beyond max(1, 0.5%). |
| std_headline_excludes_extended | warn | The unaccounted balance matches the separately reported extended-review bucket. |
| printed_pct_mismatch | error | The printed denial rate differs from the computed one by more than 1.0 point. |
| appeals_exceed_total | error | Overturned appeals exceed appeals filed. |
| appeals_not_reported | info | No appeal outcome data published. |
| expedited_slower_than_standard | warn | Expedited mean turnaround exceeds the standard mean. |
| expedited_exceeds_standard_volume | warn | Expedited volume exceeds standard volume — columns likely transposed. |
| zero_denial_rate · high_denial_rate | warn | A 0% rate, or a rate above 40%, far outside the peer range. |
| percentages_only | info | Percentages published without counts; the arithmetic cannot be verified. |
Today these rules have produced 363 findings across 658 filings — every one is listed, with the numbers that tripped it. The rules run in validate.py; a finding is always a report, never a correction.
Analysis 02 — the pricing arithmetic
What a hospital bills against what it is actually paid, computed per row of the Medicare file — both figures come from the same row of the same publisher's file, never across files. The system-level ratio sums each member hospital's every DRG, weighted by discharges, so a big hospital counts for its size. State and national figures for the 48-service comparison basket are medians across the sampled price files' gross, cash and negotiated values — medians, because a single miscoded chargemaster row should not move a state.
| Price rule | Severity | Fires when |
|---|---|---|
| cash_above_gross | error | A hospital's cash price exceeds its own list price for the same code. |
| rate_outside_bounds | error | A negotiated rate falls outside the minimum–maximum the same file states. |
| pct_without_dollar | warn | A percentage-of-charges rate with no dollar estimate alongside. |
| stale_file | warn | The price file is more than a year old, against the rule's annual refresh. |
| list_vs_medicare | warn | The published list price sits far from the average charge the same hospital reported to Medicare for the same admission. |
The one statistical layer in the whole project is the county models behind the Analysis page: gradient-boosted trees and a ridge regression, cross-validated with a fixed seed, reported through permutation importance and partial dependence. They are descriptive — "counties that look like X also tend to have Y" — and every figure they produce carries that caveat in the data itself. Nothing on this site claims a causal estimate, because nothing in this data can support one.
Every column, defined once
The dictionary below is written against the schema the site actually runs, and it is the authoritative copy — every column of every table, defined here. One convention governs everything: null means "not published". No loader ever writes a zero it did not read, and the API keeps the distinction all the way to your terminal.
The spine of the prior authorization dataset: 658 rows, one per disclosure published by a payer or state agency under CMS-0057-F for CY2025. The CSV export carries these same columns.
| Column | Type | Meaning |
|---|---|---|
| filing_id | varchar(32) | Primary key. Usually fNNNN; filings that arrived with a stable publisher-side identifier keep it. |
| parent_org | varchar(160) | Parent organization, canonicalized (all Centene brands roll up to Centene, and so on — the alias table is in merge.py). |
| plan_name | varchar(220) | The plan as the document names it, verbatim. |
| contract_id | varchar(32) | CMS contract ID (H/R/S numbers) or state contract identifier, where printed. |
| coverage_type | varchar(48) | Canonicalized market: Medicare Advantage, Medicaid Managed Care, Medicaid FFS, Marketplace QHP, CHIP Managed Care, Medicare-Medicaid Plan. |
| state | char(2) | Filing state; null for national filings. |
| reporting_period | varchar(64) | The period as printed. Null where the payer stated none — a fact worth keeping. |
| std_total / std_approved / std_denied | int | Standard (non-urgent) prior authorization requests received, approved, denied. Null = not published. |
| std_denial_rate | decimal(6,2) | Computed: 100 × std_denied ÷ std_total. Falls back to the printed rate only when no counts exist (then flagged percentages_only). |
| std_denial_rate_printed | decimal(6,2) | The rate the payer's own document prints, kept for comparison — never used in place of the computed rate. |
| std_appeals_total / std_appeals_overturned | int | Appeals of standard denials, and how many the payer reversed. |
| std_appeal_overturn_rate | decimal(6,2) | Computed: 100 × overturned ÷ appeals; the payer's printed figure where counts are absent. |
| exp_total / exp_approved / exp_denied | int | Expedited (urgent) requests received, approved, denied. |
| exp_denial_rate | decimal(6,2) | Computed, same formula as the standard rate. |
| ext_review_approved | int | Approvals granted in an extended-review window, where reported as a separate bucket. |
| std_tat_mean_days / std_tat_median_days | decimal(7,2) | Turnaround for standard decisions, in days, as published. Null where the unit could not be established — never guessed. |
| exp_tat_mean_hours / exp_tat_median_hours | decimal(7,2) | Turnaround for expedited decisions, in hours. |
| reports_counts | tinyint | 1 if the filing published raw counts; 0 if percentages only, meaning its arithmetic cannot be verified. |
| quality_flags | varchar(255) | clean, or a semicolon-separated list of severity:rule for every rule the filing trips. |
| error_count / warn_count | tinyint | Denormalized finding counts, for filtering. |
| source_url | varchar(500) | The payer's or state agency's own document — where every number in the row came from. |
| source_segment | varchar(32) | Which collector segment captured the row. |
| extraction_note | text | Anything a reader needs to interpret the row — combined programs, unusual layouts, unit ambiguities. |
The audit trail: 363 rows today. Deleted and rebuilt with its filings; never edited by hand.
| Column | Type | Meaning |
|---|---|---|
| finding_id | int | Primary key. |
| filing_id | varchar(32) | The filing the rule fired on (foreign key, cascades on delete). |
| rule | varchar(64) | The rule name, exactly as in validate.py and the table above. |
| severity | enum | error — the filing contradicts itself · warn — implausible but not provably wrong · info — a disclosure gap worth recording. |
| detail | text | The numbers that tripped the rule, so the finding can be re-derived without the pipeline. |
The confession: 497 rows. Classification is done in code (gaps.py), so every consumer gets the same split.
| Column | Type | Meaning |
|---|---|---|
| gap_id | int | Primary key. |
| source_segment | varchar(32) | Which collector recorded the miss. |
| gap_kind | varchar(32) | What stopped the read: robots, http_403, bot_protection, image_only, binary, client_rendered, partial_read, http_404, not_located, tool_limited, and so on. |
| gap_group | enum | blocked — the document exists and could not be read (a finding about the payer) · missing — a search came up empty (a finding about the crawl) · not_a_gap — a duplicate, or no CY2025 filing exists to find. |
| url | varchar(500) | Where the document is, or where it was looked for. |
| reason | text | What happened, in prose, as the collector recorded it. |
Each table below is documented column-by-column in the schema file itself; this is what each one holds and how they connect. Every row that carries a dollar figure also carries, directly or through a join, the source file it came from.
| Table | Grain | What it holds |
|---|---|---|
| county_profile | county (+ state, national rows) | Medical debt in collections (share and median), uninsured rate, premature death, provider ratios, income, inequality, race, rurality, PLACES disease rates, ACS demographics — and the price ratios computed from the county's hospitals. |
| price_hospital | hospital (CCN) | Every hospital in the Medicare files: name, location, county FIPS via the Census ZCTA→county file, AHRQ system membership, beds, ownership, and its price-file status where sampled. |
| health_system | AHRQ system | Member counts and Medicare charges and payments summed across every member's every DRG, discharge-weighted — computed, never copied. |
| price_inpatient | hospital × MS-DRG | Medicare inpatient rows: discharges, average covered charge, average total payment, and the computed charge-to-payment ratio. |
| price_outpatient | hospital × APC | The same shape for outpatient services. |
| price_physician_geo | state × HCPCS | The physician fee file: services, average billed, average allowed, office and facility. |
| price_state_basket | state × basket item | The 48-service comparison basket: Medicare charge and payment beside the sampled files' gross, cash and negotiated medians. |
| price_code | code string | The dictionary: 85,000+ strings with a status decided by set membership (official · hospital_only · unverified) and a description copied from a named source or left null — never invented. |
| price_code_state | code × state | The catalog: every verified non-basket code, priced state by state. |
| price_mrf_file | sampled hospital file | The document record for each of the 3,109 sampled price files: URL, fetch time, bytes, SHA-256, and the outcome — parsed, or why not. |
| price_mrf_charge | file × code | Every coded charge a sampled file carries: gross, cash, negotiated min/median/max, aggregated across payers. |
| price_finding | finding | Every price-rule violation, with scope, severity and the figures that tripped it — the same design as analysis 01's findings. |
| price_source | source file | Provenance for every central file: exact URL, fetch time, bytes, SHA-256, license. |
| model_run / model_feature / model_pd | model · feature · grid point | The county models: fit quality per target, permutation importance per feature, partial dependence curves — with the descriptive-not-causal note stored in the data. |
| price_zip | ZCTA | ZIP centroids projected onto the price map's canvas. |
Rebuild every number yourself, step by step
This is the whole point of the project's openness: the walkthrough below takes you from a bare machine to the same CSVs, findings and database this site serves. You need Python 3.10+ and git; the only third-party requirements are openpyxl for the workbook step and the scientific stack for the optional models. Everything else is standard library, on purpose — every dependency a reproduction needs is a reader it loses. Total time on a laptop: about an hour, most of it downloads.
Get the code
Clone the pipeline repository. Nothing here needs credentials, an account, or configuration.
Rebuild the prior authorization dataset
The raw collector segments ship in data/; derived files are never committed, so what you build is what you can check. Merge dedupes the overlapping segments and prints per-segment counts as it goes; build runs normalization and all 10 validation rules, then writes the metrics CSV and the findings CSV to out/.
Expect: merged 658 unique filings, the duplicate count, the conflict count, and 363 findings — the same totals this site reports on its Validation page.
Check the arithmetic by hand
Pick any row and apply the equations. No tooling is required beyond division, but a one-liner does a whole-file sweep — this recomputes every standard denial rate from its own row's counts and prints any row where the published dataset disagrees with your arithmetic:
Then trace one row to its source: every row's source_url is the payer's own document. Open it and find the counts. That trip — from a figure on this site back to a payer's PDF — is the audit this entire project exists to make possible.
Rebuild the pricing dataset
The pricing pipeline starts with an offline self-test of every parser and rule — run it first, so you know the code is sound before spending bandwidth. The full build fetches the central files and every sampled hospital file, records the manifest, and writes its artifacts to out/prices/.
Every download lands in the manifest with its SHA-256. To confirm you read the same bytes this site did, compare hashes: curl -s 'https://ryangomez.nyc/crossfoot/costs_api.php?set=source&key=inpatient' | jq -r '.rows[0].sha256'
Optional: fit the county models
The one step outside the standard library. It reads only what the build already produced, runs cross-validated with a fixed seed, and writes models.json — so two machines produce the same model, importances and curves.
Or skip the rebuild: the data is already free
Every table on this site is available as it stands — JSON through the read-only API and CSV from the explorers, no key, no account. Pull the slice you need, check it against your rebuild, or skip the pipeline entirely and start from the published data.
Disagree with something? That is the system working
If your rebuild produces a different number, one of us has a bug — and either way the public record improves. Open an issue with the filing ID or code, your figure, and the source URL. The About page lists more ways to contribute, from recovering machine-resistant documents to proposing the next analysis.