Shahd1sayed's picture
Deploy v2: multimodal ensemble router (Framingham tabular + ECG ResNet)
0f12291 verified
|
Raw
History Blame Contribute Delete
11.8 kB
metadata
title: Heart Attack Risk Predictor
emoji: πŸ«€
colorFrom: blue
colorTo: purple
sdk: docker
app_file: app.py
pinned: false

πŸ«€ Heart Attack Risk Predictor β€” Multimodal

An AI-powered clinical decision-support tool that estimates a patient's heart-attack risk from tabular patient data, an ECG image, or both β€” using an ensemble-router architecture with a tabular model, an ECG image model, and late-fusion of their scores.

πŸ”΄ TRY THE LIVE DEMO ON HUGGING FACE SPACES πŸ”΄

⚠️ Educational / portfolio demo β€” not a medical device. Do not use for real clinical decisions.


πŸ“‘ Table of Contents


πŸ†• What's New in v2

Version 1 was a single-model biomarker classifier (8 vitals β†’ Random Forest). A leakage test showed its ~98% accuracy was largely a biomarker-threshold rule (accuracy fell to ~62% without Troponin & CK-MB). Version 2 re-architects the project into a multimodal ensemble router:

  • Two models instead of one β€” a tabular model and an ECG image model.
  • A router that picks the model(s) based on what the user submits, and averages their scores when both are provided.
  • Honest evaluation β€” proper metrics (ROC-AUC for the imbalanced tabular task, per-class metrics for the ECG task) and a clear statement of limitations.

The original v1 biomarker research is preserved in research_and_experiments/.


✨ Key Features

  • Multimodal input β€” enter patient data, upload an ECG image, or do both.
  • Ensemble router β€” one POST /predict endpoint routes to the right model(s): tabular β†’ Model A, ECG β†’ Model B, both β†’ averaged score.
  • Missing-data friendly β€” blank tabular fields are filled automatically by K-Nearest-Neighbours imputation, so a partial form still works.
  • Server-side validation β€” out-of-range or non-numeric fields are rejected with a clear message (HTTP 422).
  • ECG confidence check β€” low-confidence ECG predictions are flagged ("may not be a clear 12-lead ECG").
  • Transparent results β€” the UI shows each model's score and, in "both" mode, the combined average, so nothing is a black box.
  • Modern UI β€” dark glassmorphism theme, drag-and-drop ECG upload, color-coded risk badges (πŸ”΄ High / 🟑 Moderate / 🟒 Low).

🧠 How It Works β€” Architecture

                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ POST /predict (multipart/form-data) ────────────────┐
   tabular only ──→ Model A (Framingham: KNN-impute β†’ scale β†’ RandomForest) β†’ p_a ─┐    β”‚
                 β”‚                                                                  β”œβ”€ both β†’ average β†’ p β†’ band (Low/Mod/High)
   ECG only ──────→ Model B (ResNet18 transfer learning on ECG images)      β†’ p_b β”€β”˜    β”‚
                 β”‚                                                                       β”‚
   neither ───────→ HTTP 400                                                             β”‚
                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Each model outputs a scalar risk probability p ∈ [0, 1]. That value is mapped to a band β€” Low (< 0.33), Moderate (0.33–0.66), High (> 0.66) β€” and, when both models run, the two scores are combined by an equal-weight average.


πŸ€– The Two Models

Model A β€” Tabular (Framingham 10-year CHD)

  • Data: Framingham Heart Study (4,240 patients, 15 clinical features).
  • Target: TenYearCHD β€” probability of coronary heart disease within 10 years.
  • Pipeline: StandardScaler β†’ KNNImputer(k=5) β†’ classifier, saved as one artifact.
  • Model selection: RandomForest vs XGBoost by 5-fold CV ROC-AUC β€” RandomForest won (0.69 vs 0.67).
  • Handles imbalance (~15% positive) with class weighting; the app uses the continuous probability, not a hard 0.5 cutoff.

Model B β€” ECG image (ResNet18)

  • Data: ECG Images Dataset of Cardiac Patients (928 images, 4 classes).
  • Method: transfer learning β€” a ResNet18 pretrained on ImageNet, with a new 4-class head; train the head, then fine-tune the last block.
  • Classes β†’ risk weight: Normal 0.0, abnormal heartbeat 0.5, post-MI history 0.8, myocardial infarction 1.0. The 4-class softmax is collapsed to one risk score via these weights.

πŸ› οΈ Tech Stack

Layer Technology Purpose
Backend FastAPI + Uvicorn Async web framework + ASGI server; multipart /predict router
Tabular ML scikit-learn (RandomForest, KNNImputer, StandardScaler), XGBoost Model A pipeline + model comparison
Image ML PyTorch + torchvision (ResNet18) Model B transfer learning
Images / Uploads Pillow, python-multipart ECG image decoding + file uploads
Data pandas, NumPy Data handling
Frontend HTML5, CSS3, Vanilla JS Single-page glassmorphism UI with ECG drag-and-drop
Deployment Docker β†’ Hugging Face Spaces Containerized serving on port 7860

πŸ“ Project Structure

heart-attack-risk-predictor/
β”‚
β”œβ”€β”€ app.py                       # FastAPI app + ensemble router (multipart /predict)
β”œβ”€β”€ requirements.txt             # Production dependencies
β”œβ”€β”€ Dockerfile                   # Container build (copies app, inference/, models/, static/)
β”‚
β”œβ”€β”€ inference/                   # Serving-time prediction package
β”‚   β”œβ”€β”€ fusion.py                # Risk bands + late-fusion (combine)
β”‚   β”œβ”€β”€ framingham.py            # Model A inference (with KNN imputation)
β”‚   β”œβ”€β”€ ecg.py                   # Model B inference (softmax β†’ risk scalar)
β”‚   └── validation.py            # Server-side field validation
β”‚
β”œβ”€β”€ models/                      # Trained artifacts (Git LFS)
β”‚   β”œβ”€β”€ framingham_pipeline.joblib   # Model A bundle
β”‚   β”œβ”€β”€ ecg_resnet.pt                # Model B weights
β”‚   └── ecg_classes.json             # ECG class list + risk weights + preprocessing
β”‚
β”œβ”€β”€ train_framingham.py          # Trains Model A
β”œβ”€β”€ train_ecg.py                 # Trains Model B
β”‚
β”œβ”€β”€ static/index.html            # Frontend (form + ECG upload + per-branch results)
β”‚
β”œβ”€β”€ DOCUMENTATION.md             # Full technical documentation
β”œβ”€β”€ DEFENSE_GUIDE.md             # Beginner-friendly project defense guide
β”‚
└── research_and_experiments/    # v1 biomarker research (notebook, dataset, old model)

πŸš€ Getting Started

Prerequisites

  • Python 3.11+ (3.12 recommended)

Install

git clone https://github.com/Shahd1Sayed/heart-attack-risk-predictor.git
cd heart-attack-risk-predictor
pip install -r requirements.txt

Data (only needed to (re)train β€” download from Kaggle)

data/framingham.csv                  # "Framingham Heart Study dataset"
data/ecg_data/<class>/*.jpg          # "ECG Images Dataset of Cardiac Patients"

Train (produces the files in models/)

python train_framingham.py
python train_ecg.py

Run

uvicorn app:app --host 127.0.0.1 --port 8000
# open http://127.0.0.1:8000

The app boots even before models are trained; a branch needing an untrained model returns HTTP 503 with a hint.


▢️ Usage & API

POST /predict accepts multipart/form-data with optional tabular fields and an optional ecg image file.

# tabular only
curl -F age=61 -F male=1 -F sysBP=150 -F totChol=240 http://127.0.0.1:8000/predict

# ECG only
curl -F ecg=@some_ecg.jpg http://127.0.0.1:8000/predict

# both (multimodal)
curl -F age=61 -F sysBP=150 -F ecg=@some_ecg.jpg http://127.0.0.1:8000/predict

Tabular fields: male, age, education, currentSmoker, cigsPerDay, BPMeds, prevalentStroke, prevalentHyp, diabetes, totChol, sysBP, diaBP, BMI, heartRate, glucose (all optional; blanks are KNN-imputed).

Example response (multimodal):

{
  "mode": "multimodal",
  "risk_level": "High",
  "p_risk": 0.7306,
  "branches": {
    "tabular": { "p_risk": 0.4825, "band": "Moderate",
                 "detail": {"CHD": 0.4825, "No CHD": 0.5175}, "imputed_fields": ["glucose"] },
    "ecg": { "p_risk": 0.9787, "band": "High", "ecg_class": "myocardial_infarction_ecg_images",
             "confidence": 0.9587, "low_confidence": false, "warning": null }
  }
}

Status codes: 200 success Β· 400 no input / unreadable image Β· 422 invalid tabular field Β· 503 model not trained yet.


πŸ“Š Model Performance

Model Metric Value
Model A β€” Framingham CV ROC-AUC 0.69
Hold-out ROC-AUC 0.64
Accuracy 0.85 (β‰ˆ all-negative baseline β€” see note)
Model B β€” ECG ResNet18 Test accuracy 0.90
Macro F1 0.90
MI precision 1.00

⚠️ Metric honesty: Framingham is imbalanced (~15% positive), so 85% accuracy is essentially the "always predict no-CHD" baseline β€” which is why we lead with ROC-AUC. Predicting a decade ahead from basic clinical features is genuinely hard, so ~0.64–0.69 AUC is expected. The ECG numbers are strong for a small dataset but optimistic vs. other acquisition setups (the images are photos of printed ECGs).


πŸ§ͺ Testing

The project ships with a test harness (router paths, response invariants, adversarial inputs, determinism, an ECG serving sweep, input validation, ECG confidence, and concurrency). Result: 62/62 checks pass. See DOCUMENTATION.md for details.


βš–οΈ Limitations & Honesty

  • The two models predict different things β€” Model A estimates 10-year prognosis; Model B classifies the current ECG. They are trained on different, unpaired populations, so the combined score is a transparent heuristic demonstrating the architecture, not a validated clinical measure.
  • The fusion weights are hand-set (equal average) because no paired dataset exists to learn/validate them. A learned fusion on paired data (e.g. PTB-XL) is future work.
  • ECG images are photos of printouts β€” a small, imbalanced dataset; the CNN may not generalize to other setups.
  • No out-of-distribution rejection β€” a non-ECG image is still classified (now flagged low-confidence, but not refused).
  • Not for clinical use.

πŸ‘₯ Team Members

Name Role GitHub
Shahd Sayed Machine Learning Engineer @Shahd1Sayed
Shahd Mohammed Full-Stack Developer