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
- Key Features
- How It Works β Architecture
- The Two Models
- Tech Stack
- Project Structure
- Getting Started
- Usage & API
- Model Performance
- Testing
- Limitations & Honesty
- Team Members
π 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 /predictendpoint 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 heartbeat0.5, post-MI history0.8, myocardial infarction1.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 |