CMS Quality Scorecard - End-to-End Pipeline Project
Project Objective
Rebuild the CMS 5-Star Hospital Quality Rating from first principles using public data. Extract 11 datasets from the CMS Provider Data Catalog REST API, land raw JSON in a MinIO data lake, transform through a PostgreSQL warehouse with dbt, score with k-means clustering, and validate against CMS-published results. Doubles as interview-ready portfolio piece and blog post series.
Stack
| Layer | Tool | Why |
|---|
| Extraction | Python + httpx | Paginated REST API pulls from CMS Provider Data Catalog |
| Data Lake (Bronze) | MinIO on cyrion | S3-compatible object store; raw JSON lands here immutably |
| Warehouse | PostgreSQL on phlegethon (LXC 302) | cms_scorecard database, 3 schemas: staging, transform, mart |
| Transformation | dbt-postgres | Industry-standard; models, tests, docs, lineage graph |
| Orchestration | Prefect on cyrion | Docker container; persistent server + UI at prefect.c0smere.net |
| Visualization | Streamlit | Self-contained Python app; portable for demos |
| Scoring | Python (scikit-learn) | K-means clustering can’t be expressed in pure SQL |
CMS Datasets (11 total)
| Dataset | UUID | Est. Rows | Domain |
|---|
| Hospital General Info | xubh-q36u | 5,426 | dim_hospital + answer key |
| HAI (Infections) | 77hc-ibv8 | 172,404 | Safety |
| Complications & Deaths | ynj2-r877 | 95,780 | Mortality |
| Readmissions | 632h-zaca | 67,046 | Readmission |
| HCAHPS (Patient Survey) | dgck-syfz | ~200K+ | Patient Experience |
| Timely & Effective Care | yv7e-xc69 | ~100K+ | Timeliness |
| HAC Reduction Program | yq43-i98g | 3,055 | Validation (safety z-scores) |
| HVBP General | ypbt-wvdk | 2,455 | Validation (domain scores) |
| HVBP Efficiency | su9h-3pvj | 2,455 | Validation |
| HVBP Safety | dgmq-aat3 | 2,455 | Validation |
| HVBP Clinical | pudb-wetr | 2,455 | Validation |
API: https://data.cms.gov/data-api/v1/dataset/{uuid}/data?size=5000&offset=0 — no auth, all strings, paginate until empty array.
Project Structure
cms-scorecard/
.venv/ # Python 3.13 (gitignored)
.env # DB/MinIO creds (gitignored)
.env.example
pyproject.toml # Project metadata + dependencies
extract/
cms_api.py # Paginated API client (httpx)
datasets.py # Dataset registry (UUIDs, names)
extract_all.py # Prefect flow: all datasets -> MinIO bronze
load/
minio_client.py # MinIO read/write helpers
load_staging.py # Prefect flow: MinIO bronze -> PG staging
dbt_cms/ # dbt project
dbt_project.yml
profiles.yml
models/
staging/ # 1:1 views on raw staging tables
transform/ # Star schema (dim/fact)
mart/ # Scoring aggregations
tests/
macros/
score/
cluster.py # K-means (5 clusters on summary scores)
safety_cap.py # Bottom-quartile safety -> cap at 4 stars
assign_stars.py # Prefect flow: run scoring, write mart.star_rating
validate/
compare_stars.py # Our stars vs CMS published stars
compare_hac.py # Our safety z-scores vs HAC dataset
compare_hvbp.py # Our domain scores vs HVBP dataset
report.py # Generate validation markdown + plots
app/
streamlit_app.py # Interactive dashboard
pages/
national_overview.py
hospital_detail.py
validation.py
lib/
config.py # Env var loading
db.py # psycopg connection helper
notebooks/ # EDA at each pipeline stage
flows/
pipeline.py # Master Prefect flow chaining all stages
Domain Weights (CMS 2026 Methodology)
| Domain | Weight | Source Datasets |
|---|
| Mortality | 22% | Complications & Deaths |
| Safety of Care | 22% | HAI |
| Readmission | 22% | Readmissions |
| Patient Experience | 22% | HCAHPS |
| Timely & Effective Care | 12% | Timely & Effective Care |
Scoring Algorithm
- Z-score per measure:
z = (hospital_score - national_mean) / national_stddev — flip sign for “lower is better” measures
- Domain rollup: Average z-scores within each domain per hospital
- Weighted summary:
0.22*mort + 0.22*safety + 0.22*readm + 0.22*pt_exp + 0.12*te — redistribute weights if domain missing
- K-means clustering:
sklearn.cluster.KMeans(n_clusters=5) on summary scores, sort centroids to assign 1-5 stars
- Safety cap: Hospitals in bottom quartile for safety capped at 4 stars max
Dimensional Model
dim_hospital
facility_id (PK) — 6-character CMS facility identifier
facility_name, address, city, state, zip_code, county
hospital_type, hospital_ownership
emergency_services (boolean)
overall_rating — CMS-published star rating (answer key)
dim_measure
measure_id (PK) — e.g., HAI_1_SIR, MORT_30_AMI
measure_name
domain — Mortality, Safety, Readmission, Patient Experience, Timely & Effective Care
higher_is_better (boolean) — crucial for z-score sign inversion
fact_hospital_measure
facility_id (FK), measure_id (FK)
reporting_period_start, reporting_period_end
score (float), denominator (int)
- UNION ALL across HAI, mortality, readmissions, HCAHPS, T&E — normalized to common grain
Implementation Phases
Phase 0: Scaffolding & Infrastructure ✅
Phase 2: Load to Staging
Phase 4: Scoring
Phase 5: Validation & Analysis
Phase 6: Streamlit Dashboard
Phase 7: Prefect Pipeline Orchestration
Blog Post Map
- “Extracting CMS Hospital Data at Scale” — API pagination, MinIO bronze layer, ELT philosophy
- “Dimensional Modeling for Healthcare Quality Data” — Star schema, dbt models, staging→transform→mart
- “The Math Behind CMS 5-Star Ratings” — Z-scores, domain rollups, k-means, safety cap
- “Validating a Data Pipeline Against Ground Truth” — Confusion matrices, correlation, honest gap discussion
- “Orchestrating It All with Prefect” — Flow composition, retries, observability
Infrastructure
| Component | Host | RAM | Port(s) |
|---|
MinIO (cms-minio) | cyrion | ~150MB | 9754 (API), 9755 (console) |
PostgreSQL (cms_scorecard) | phlegethon | shared | 5432 |
| Prefect server | cyrion | ~400MB | 4200 |
| Streamlit | local/cyrion | ~200MB | 8501 |
- MinIO bucket:
cms-bronze
- Prefect UI:
https://prefect.c0smere.net
- Credentials in
Infra_RELOADED/CMS Scorecard — Credentials & Ops.md