Shahd1sayed's picture
Deploy v2: multimodal ensemble router (Framingham tabular + ECG ResNet)
0f12291 verified
|
Raw
History Blame Contribute Delete
11.8 kB
---
title: Heart Attack Risk Predictor
emoji: πŸ«€
colorFrom: blue
colorTo: purple
sdk: docker
app_file: app.py
pinned: false
---
<h1 align="center">πŸ«€ Heart Attack Risk Predictor β€” Multimodal</h1>
<p align="center">
<strong>An AI-powered clinical decision-support tool that estimates a patient's heart-attack risk from <em>tabular patient data</em>, an <em>ECG image</em>, or <em>both</em> β€” using an ensemble-router architecture with a tabular model, an ECG image model, and late-fusion of their scores.</strong>
</p>
<p align="center">
<a href="https://huggingface.co/spaces/Shahd1sayed/heart-attack-risk-predictor"><strong>πŸ”΄ TRY THE LIVE DEMO ON HUGGING FACE SPACES πŸ”΄</strong></a>
</p>
<p align="center">
<em>⚠️ Educational / portfolio demo β€” <strong>not a medical device</strong>. Do not use for real clinical decisions.</em>
</p>
---
## πŸ“‘ Table of Contents
- [What's New in v2](#-whats-new-in-v2)
- [Key Features](#-key-features)
- [How It Works β€” Architecture](#-how-it-works--architecture)
- [The Two Models](#-the-two-models)
- [Tech Stack](#-tech-stack)
- [Project Structure](#-project-structure)
- [Getting Started](#-getting-started)
- [Usage & API](#-usage--api)
- [Model Performance](#-model-performance)
- [Testing](#-testing)
- [Limitations & Honesty](#-limitations--honesty)
- [Team Members](#-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 /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](https://fastapi.tiangolo.com/) + [Uvicorn](https://www.uvicorn.org/) | Async web framework + ASGI server; multipart `/predict` router |
| **Tabular ML** | [scikit-learn](https://scikit-learn.org/) (`RandomForest`, `KNNImputer`, `StandardScaler`), [XGBoost](https://xgboost.readthedocs.io/) | Model A pipeline + model comparison |
| **Image ML** | [PyTorch](https://pytorch.org/) + [torchvision](https://pytorch.org/vision/) (ResNet18) | Model B transfer learning |
| **Images / Uploads** | [Pillow](https://python-pillow.org/), `python-multipart` | ECG image decoding + file uploads |
| **Data** | [pandas](https://pandas.pydata.org/), [NumPy](https://numpy.org/) | 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
```bash
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/)
```bash
python train_framingham.py
python train_ecg.py
```
### Run
```bash
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.
```bash
# 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):**
```json
{
"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](https://github.com/Shahd1Sayed) |
| **Shahd Mohammed** | Full-Stack Developer | |
---