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, martTransformation dbt-postgres Industry-standard; models, tests, docs, lineage graph Orchestration Prefect on cyrionDocker 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-q36u5,426 dim_hospital + answer key HAI (Infections) 77hc-ibv8172,404 Safety Complications & Deaths ynj2-r87795,780 Mortality Readmissions 632h-zaca67,046 Readmission HCAHPS (Patient Survey) dgck-syfz~200K+ Patient Experience Timely & Effective Care yv7e-xc69~100K+ Timeliness HAC Reduction Program yq43-i98g3,055 Validation (safety z-scores) HVBP General ypbt-wvdk2,455 Validation (domain scores) HVBP Efficiency su9h-3pvj2,455 Validation HVBP Safety dgmq-aat32,455 Validation HVBP Clinical pudb-wetr2,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
Wesley Ray · blog ·
git ·
resume ·
linkedin
bytes in + out of the homelab since ·
ticking at the lifetime average
built 2026-09-13 14:35 ET · b6c6233
· 117 notes