| --- |
| 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 | | |
|
|
| --- |
|
|