Approach

Before touching the data: literature scan. Then understanding, cleaning, and feature engineering for separate FY2023 inpatient and CY2023 outpatient tracks.

145,879 / 116,182
Rows · inpatient FY2023 / outpatient CY2023
45.33%
Outpatient allowed-amount suppression · 52,664 rows
2,833
Providers present in both datasets · full-frame join
Methodological transparency

I did not invent the analytical framework for this dataset. I used peer-reviewed and government work as methodological reference and adapted the relevant pieces. The point of this page is to lay bare what is borrowed versus what is mine.

Round 3 publication review: pass with documented conditions. Round 1's headline reframing and validation changes are implemented. Mapping families remain exploratory, geographic summaries exclude Maryland with a sensitivity artifact, and all proxy, overlap, and predictor results remain descriptive and non-causal.

1. Literature survey — methodology baseline

This 2013–present body of work provides methodological context for the descriptive analyses we adapt and extend:

SourceWhat they didWhat we adoptWhat we extend
Agrawal & Choudhary
KDD-DMH 2013
3-tier coefficient-of-variation framework: CV_b (billing), CV_p (payment), and CV_nb (normalized billing) per DRG, with state maps and a billing-payment association analysis. The historical reference metrics use a different service universe and aggregation than this project. Descriptive CV tables and state maps using the current FY2023/CY2023 fields; source attribution is retained. Apply the framework to the outpatient track and provide interactive per-service filtering. These are adaptations, not controlled historical reproductions or causal estimates.
Mulani
GitHub notebook 2017
State-by-state strip plots; CA-vs-MD charge-payment pairplots on most-common DRG. Noted Maryland's all-payer rate-setting system as structural exception. Visual comparison style; structural outlier callout Provider-level cross-dataset join on Rndrng_Prvdr_CCN
OIG / HHS Report
OEI-06-10-00520
Hospital-level outlier-payment percentages with a 75th-percentile-plus-1.5-IQR threshold, cohort comparisons including actual CCR, and payment-dollar concentration in 2009–'11. Those estimands differ from this project's charge-flag outputs. Conceptual screening reference only; the project uses a separately named high-charge cohort. Project-defined submitted-charge top quartile, IQR, and z-score flags; descriptive provider overlap on the CCN join. No CCR or payment-dollar conclusion is drawn.

Full research notes: notebooks/PRIOR_WORK_RESEARCH.md in the project repo.

2. Data understanding

Two publicly-available CMS datasets are aggregated at provider × service level, but their annual periods differ: inpatient = FY2023 and outpatient = CY2023. They should not be described as a same-period cohort for causal or longitudinal interpretation.

Inpatient

Medicare Inpatient Hospitals by Provider and Service (CMS data portal)

  • 145,879 rows (one provider × DRG pair each)
  • 2,906 providers, 540 DRGs, 51 states incl. DC
  • Service codes via MS-DRG
  • Observed measures: Avg_Submtd_Cvrd_Chrg (submitted charge), Avg_Tot_Pymt_Amt (average total payment), and Avg_Mdcr_Pymt_Amt (average Medicare payment)
  • No suppression — CMS rule excludes rows with ≤10 discharges entirely, so all remaining rows have n≥11

Outpatient

Medicare Outpatient Hospitals by Provider and Service

  • 116,182 rows in the full frame; 63,518 cost-observed rows
  • 3,126 providers, 72 APCs, 50 states (Maryland is absent from the CMS outpatient file)
  • Service codes via APC (different classification — not directly mergeable with DRG)
  • Observed measures: Avg_Tot_Sbmtd_Chrgs (submitted charge), Avg_Mdcr_Alowd_Amt (allowed amount, including beneficiary share)
  • 45.33% suppression — 52,664 of 116,182 full-frame rows have a blank allowed amount under the CMS ≤10-service rule. Cost analyses use the 63,518-row observed denominator; suppressed ≠ zero.
Cross-dataset join feasibility

The full-frame join contains 2,833 providers in both datasets. After restricting outpatient to rows with an observed allowed amount, the cost-observed join contains 2,812 providers (2,993 outpatient providers in that denominator). The proxy cohort uses 2,812 as its provider denominator. DRGs and APCs are different coding systems; we cannot merge at service level, only at provider level via Rndrng_Prvdr_CCN.

Maryland note: Maryland is present inpatient (44 providers; 3,531 rows) and absent from the CMS outpatient file. Maryland's all-payer rate-setting system is an exception to the standard Medicare outpatient payment context. Do not interpret the outpatient absence as zero or an imputed observation. Primary inpatient geographic maps and correlations exclude Maryland; data/maryland_sensitivity.json records the included-versus-excluded comparison.

Data-dictionary deep dive in src/config.py — the renamed schema is documented inline.

3. Cleaning choices

Cleaning happens in two stages: loading with explicit dtype handling, then explicit suppression flags:

Explicit string → numeric coercion

All columns read as strings first (dtype=str), then numerics coerced via pd.to_numeric(..., errors="coerce"). This ensures CMS suppression (blank cells when sample < threshold) becomes NaN rather than causing dtype inference to fail silently or rows to be dropped:

# From src/load.py
def load_inpatient() -> pd.DataFrame:
    df = pd.read_csv(DATA_RAW / "inpatient_2023.csv", dtype=str)
    df = df.rename(columns=COLUMN_RENAME)
    for col in INPATIENT_NUMERIC:
        if col in df.columns:
            df[col] = pd.to_numeric(df[col], errors="coerce")
    df = df.dropna(subset=["total_discharges", "avg_total_payment",
                           "avg_medicare_payment"])
    return df.reset_index(drop=True)

Suppression surfaced explicitly, never silently zeroed

Outpatient has 45.33% suppressed allowed-amount rows (52,664 of 116,182). Instead of imputing, we add explicit boolean flags so downstream analysis can choose deliberately:

# From src/clean.py
df["cost_suppressed"] = df["avg_allowed_amount"].isna()
df["outlier_suppressed"] = df["outlier_services"].isna()
if drop_suppressed:
    df = df[~df["cost_suppressed"]].copy()
return df.reset_index(drop=True)

Coverage counts (provider, APC availability) still use the full frame; only charge/payment-distribution analyses drop suppressed rows.

Column rename schema

The CMS column names are difficult to read at a glance — renamed for clarity and unified across both datasets:

CMS originalRenamed
Rndrng_Prvdr_CCNprovider_ccn (join key)
Rndrng_Prvdr_Stprovider_street (gotcha: St = street, not state)
Rndrng_Prvdr_State_Abrvtnstate
Avg_Tot_Pymt_Amtavg_total_payment (inpatient payment field)
Avg_Mdcr_Alowd_Amtavg_allowed_amount (outpatient allowed-amount field)
...see config.py for the full map

4. Feature engineering

Per-row additions in src/features.py:

Geographic & stratification features

Ratio-derived features

# From src/features.py — provider summaries joined
def provider_summary_inpatient(df):
    return df.groupby("provider_ccn", as_index=False).agg(
        inp_drgs_seen=("drg_code", "nunique"),
        inp_total_discharges=("total_discharges", "sum"),
        inp_avg_payment=("avg_total_payment", "mean"),
        # ... + median, charge ratio
    )

def build_provider_combined(inp_feat, out_feat, out_full_feat):
    inp_s = provider_summary_inpatient(inp_feat)
    out_s = provider_summary_outpatient(out_feat)
    # Full outpatient presence is joined separately from cost-observed metrics.
    return inp_s.merge(out_s, on="provider_ccn", how="outer")

DRG → MDC mapping (exploratory disclosure)

The mapping uses description keywords with a coarse numeric fallback because MS-DRG numbering is not strictly monotonic by clinical category. The generated validation artifact records coverage, known quarantined codes, and sanity examples. Known-invalid DRG families are excluded from predictor modeling; all remaining families are still exploratory text-derived features, not an official CMS DRG-to-MDC or APC crosswalk. Coverage is not mapping validity.

All cleaning & feature operations are reproducible via uv run python -m src.run_pipeline; cleaned parquet snapshots saved to data/processed/.

5. Reproducibility

The entire pipeline produces everything this site displays in one command:

uv sync
uv run python -m src.build_site

That command runs:

  1. src.run_pipeline — load → clean → features → parquet snapshots + meta_summary.json
  2. src.analysis_insights — Phase 3 → 5 JSONs in site/data/
  3. src.analysis_outliers — Phase 4 → 6 JSONs
  4. src.analysis_predictors — Phase 5 → predictors.json
  5. src.build_site — generates this static site, including the code browser you're inside right now

Outputs land in data/processed/ (intermediate) and site/data/ (consumed by you). The narrative HTML you're reading is hand-written; the code browser is generated. See /code/ for every Python source file with syntax highlighting.

Audit artifacts: site/data/outlier_flags.json is a preview of 2,000 rows per dataset from full counts of 36,101 inpatient and 15,899 outpatient flags; outlier_cohort_comparison.json previews 50 rows per dataset from 315 and 64 service summaries. Complete portable tables are bundled under site/data/audit/, including outlier audits and provider-disjoint predictor validation files with matching JSON metadata. site/data/audit_manifest.json records source, code, dependency, denominator, mapping, artifact, and contract-test provenance; focused summaries are also available in mapping_validation.json, denominator_audit.json, maryland_sensitivity.json, and model_validation_manifest.json.

← overview · next: Insights →