CardioAI is an end-to-end web application that estimates a person's heart disease risk from 13 clinical measurements. It combines a trained Random Forest model, SHAP explanations for every prediction, a Gemini-powered health assistant that knows the user's result, and downloadable PDF reports.
β οΈ Disclaimer: CardioAI is an educational and research project. It is not a medical device and must not be used to diagnose, treat or make decisions about any health condition. Always consult a qualified healthcare professional.
- Features
- How It Works (Pipeline)
- Project Structure
- Quick Start
- Configuration
- Input Features
- API Routes
- Database Schema
- Machine Learning Pipeline
- Docker and Deployment
- Security and Privacy Notes
- Troubleshooting
- Author
- License
| Feature | Description |
|---|---|
| π― Risk prediction | Random Forest classifier returns a probability and a Low / Moderate / High risk level |
| π§ SHAP explainability | Per-prediction chart showing which features pushed the risk up or down |
| π¬ Gemini AI chat | Assistant that receives the user's risk level, probability and key factors as context |
| π PDF reports | Downloadable report with inputs, risk factors, SHAP chart and recommendations |
| ποΈ Persistence | SQLite via SQLAlchemy stores predictions, chat messages and generated reports |
| π Rate limiting | Flask-Limiter protects every route |
| β Input validation | Server-side range and type checks on all 13 fields (rejects missing, non-numeric, NaN and out-of-range values) |
| π History dashboard | Browse past assessments |
| π Graceful fallback | If Gemini is unavailable, the chat falls back to built-in educational answers |
flowchart LR
A[Dataset CSV] --> B[01 EDA notebook]
B --> C[02 Preprocessing notebook]
C --> D[03 Model Training notebook]
D --> E[(models/*.pkl)]
E --> F[Flask app.py]
F --> G[Web UI + PDF + Chat]
Offline, the notebooks turn the dataset into trained artifacts. Online, the Flask app loads those artifacts once at startup and serves predictions.
flowchart TD
A[Form values as JSON] --> B[validate_input<br/>type, range, NaN checks]
B -->|invalid| X[422 with error list]
B --> C[build_feature_vector<br/>one-hot cp, restecg, slope, thal]
C --> D[StandardScaler.transform]
D --> E[Random Forest<br/>predict_proba + predict]
E --> F[Risk level<br/>below 30% Low, below 60% Moderate, else High]
D --> G[SHAP TreeExplainer<br/>class 1 values]
G --> H[Top-10 SHAP bar chart<br/>base64 PNG]
E --> I[Key factor rules<br/>age, chol, BP, HR, vessels, angina]
F --> J[(Save to SQLite)]
H --> J
I --> J
J --> K[JSON response to the browser]
flowchart TD
A[User message + last 6 messages] --> B{Prediction id given?}
B -->|yes| C[Build context: risk level, probability, age, sex, key factors]
B -->|no| D[General cardiovascular assistant context]
C --> E[Gemini generate_content<br/>context sent as system instruction]
D --> E
E -->|success| F[Reply]
E -->|error or empty| G[Built-in fallback answer]
G --> F
F --> H[(Save messages to SQLite)]
The saved prediction, its input values and the stored SHAP values are loaded from the database, rendered into a PDF with ReportLab (summary table, clinical inputs, key factors, SHAP chart, risk-based recommendations, disclaimer) and streamed to the browser. Each download is logged in the report table.
CardioAI/
βββ app.py # Flask app: routes, validation, prediction, SHAP, Gemini chat, PDF
βββ requirements.txt # Pinned Python dependencies
βββ Dockerfile # Container image
βββ render.yaml # Render.com deployment blueprint
βββ .env.example # Environment variable template (copy to .env)
βββ .gitignore / .dockerignore
βββ LICENSE
β
βββ .github/workflows/
β βββ ci.yml # Continuous integration
β
βββ templates/ # Jinja2 pages
β βββ index.html # Landing page + model metrics
β βββ predict.html # Assessment form and results
β βββ history.html # Past assessments
β βββ chat.html # AI assistant
β βββ 404.html # Not-found page
β
βββ models/ # Trained artifacts loaded at startup
β βββ cardioai_model.pkl # Trained Random Forest
β βββ scaler.pkl # Fitted StandardScaler
β βββ feature_columns.pkl # Exact feature order the model expects
β βββ model_metadata.pkl # Test metrics and parameters
β βββ shap_explainer.pkl # SHAP TreeExplainer
β
βββ data/
β βββ CardioAI_Merged_Heart_Dataset.csv # Source dataset
β βββ processed/ # X_train, X_test, y_train, y_test
β
βββ notebooks/
β βββ 01_EDA.ipynb # Exploratory data analysis
β βββ 02_Preprocessing.ipynb # Encoding, scaling, train/test split
β βββ 03_Model_Training.ipynb # Model comparison, final model, SHAP
β
βββ images/ # Plots produced by the notebooks
A SQLite database file (cardioai.db) is created automatically on first run. It is git-ignored because it contains user-submitted health data.
Requirements: Python 3.11 (recommended) and Git.
# 1. Clone
git clone https://github.com/Astreonix/CardioAI.git
cd CardioAI
# 2. Create and activate a virtual environment
python -m venv venv
venv\Scripts\activate # Windows (PowerShell)
# source venv/bin/activate # macOS / Linux
# 3. Install dependencies
pip install -r requirements.txt
# 4. Configure environment variables
copy .env.example .env # Windows
# cp .env.example .env # macOS / Linux
# then open .env and fill in your values (see Configuration)
# 5. Run
python app.pyOpen http://127.0.0.1:5000 in your browser.
The app works without a Gemini key: predictions, SHAP charts, history and PDF reports run normally, and the chat uses its built-in fallback answers.
Set these in a .env file (never commit it) or in your hosting provider's environment settings.
| Variable | Required | Default | Description |
|---|---|---|---|
GEMINI_API_KEY |
For AI chat | β | Free key from Google AI Studio |
GEMINI_MODEL |
No | gemini-2.5-flash |
Gemini model name. Google retires models often, so set this to one your key can use (see Troubleshooting). A lite model such as gemini-flash-lite-latest gives faster replies |
FLASK_SECRET_KEY |
Recommended | dev placeholder | Generate with python -c "import secrets; print(secrets.token_hex(32))" |
FLASK_ENV |
No | development |
development enables debug mode; set production when deployed |
HOST |
No | 127.0.0.1 |
Bind address. Use 0.0.0.0 only when the app must be reachable from other machines (e.g. inside a container) |
PORT |
No | 5000 |
Port to listen on |
DB_PATH |
No | ./cardioai.db |
Location of the SQLite file (point to a persistent disk when deploying) |
The model uses 13 clinical measurements. The server validates every field.
| Field | Meaning | Allowed range |
|---|---|---|
age |
Age in years | 1 β 120 |
sex |
0 = female, 1 = male | 0 β 1 |
cp |
Chest pain type: 1 typical angina, 2 atypical angina, 3 non-anginal pain, 4 asymptomatic | 1 β 4 |
trestbps |
Resting blood pressure (mmHg) | 50 β 300 |
chol |
Serum cholesterol (mg/dl) | 50 β 700 |
fbs |
Fasting blood sugar above 120 mg/dl (1 = yes) | 0 β 1 |
restecg |
Resting ECG: 0 normal, 1 ST-T abnormality, 2 left ventricular hypertrophy | 0 β 2 |
thalach |
Maximum heart rate achieved (bpm) | 40 β 250 |
exang |
Exercise-induced angina (1 = yes) | 0 β 1 |
oldpeak |
ST depression induced by exercise | 0 β 10 |
slope |
Slope of the peak exercise ST segment: 1 up, 2 flat, 3 down | 1 β 3 |
ca |
Number of major vessels with blockage (fluoroscopy) | 0 β 3 |
thal |
Thalassemia: 3 normal, 6 fixed defect, 7 reversible defect | 0 β 7 |
Risk levels are derived from the predicted probability: Low below 30%, Moderate from 30% to below 60%, High at 60% and above.
| Route | Method | Rate limit | Description |
|---|---|---|---|
/ |
GET | default | Landing page with model metrics and usage counts |
/predict |
GET | 30/min | Assessment form |
/predict |
POST | 30/min | Run a prediction (JSON in, JSON out with probability, risk level, key factors, SHAP chart) |
/history |
GET | default | History page |
/history/data |
GET | 60/min | JSON list of the 100 most recent assessments |
/chat |
GET | default | Chat page (optionally ?pred_id=<id> to attach an assessment) |
/chat/message |
POST | 20/min | Send a message; returns the assistant reply |
/report/<id> |
GET | 10/min | Download the PDF report for an assessment |
Default limits for every route: 200 requests per day and 50 per hour per IP. Exceeding a limit returns HTTP 429 with a JSON error.
Example request
curl -X POST http://127.0.0.1:5000/predict \
-H "Content-Type: application/json" \
-d '{"age":58,"sex":1,"cp":4,"trestbps":150,"chol":270,"fbs":0,
"restecg":1,"thalach":110,"exang":1,"oldpeak":2.5,"slope":2,"ca":2,"thal":7}'SQLite, created automatically on startup.
| Table | Key columns |
|---|---|
prediction |
id, timestamp, age, sex, probability, risk_level, prediction, input_data (JSON), key_factors (JSON), shap_values (JSON) |
chat_message |
id, timestamp, prediction_id β prediction.id, role (user / ai), content |
report |
id, prediction_id β prediction.id, generated_at, filename |
The notebooks in notebooks/ run in order and produce everything the app needs.
| Step | Notebook | What it does | Outputs |
|---|---|---|---|
| 1 | 01_EDA.ipynb |
Explores the dataset: class balance, feature distributions, correlations, disease rate per category | images/target_distribution.png, numeric_distributions.png, numeric_by_target.png, categorical_disease_rates.png, correlation_heatmap.png, target_correlation_ranked.png |
| 2 | 02_Preprocessing.ipynb |
Encodes categorical features (cp, restecg, slope, thal become one-hot columns), scales features with StandardScaler, splits into train and test sets |
data/processed/X_train.csv, X_test.csv, y_train.csv, y_test.csv, models/scaler.pkl, models/feature_columns.pkl |
| 3 | 03_Model_Training.ipynb |
Compares candidate models, selects and evaluates the final Random Forest, builds the SHAP explainer | images/model_comparison.png, final_model_evaluation.png, shap_summary.png, models/cardioai_model.pkl, model_metadata.pkl, shap_explainer.pkl |
| Metric | Value |
|---|---|
| ROC-AUC | β 0.86 |
The full set of test metrics (accuracy, precision, recall, F1) is stored in models/model_metadata.pkl and shown on the landing page. See 03_Model_Training.ipynb for the exact figures.
- Edit or replace the dataset in
data/. - Re-run the three notebooks in order.
- Commit the regenerated files in
models/. The app reads the metrics, feature order and scaler from them automatically, so no code changes are needed.
The
.pklfiles must be produced with the samescikit-learnandnumpyversions pinned inrequirements.txt, otherwise loading may fail or give warnings.
docker build -t cardioai .
docker run -p 5000:5000 \
-e GEMINI_API_KEY=your_key \
-e GEMINI_MODEL=gemini-flash-lite-latest \
-e FLASK_SECRET_KEY=change_me \
cardioaiOpen http://localhost:5000. If the container starts the app with python app.py rather than gunicorn, also pass -e HOST=0.0.0.0 so the port is reachable from outside the container.
- Push the repository to GitHub.
- On render.com: New β Web Service β connect
Astreonix/CardioAI. - Render reads
render.yamland theDockerfile. - In the Render dashboard β Environment, add
GEMINI_API_KEY,GEMINI_MODELandFLASK_SECRET_KEY. - For history to survive restarts, attach a persistent disk and set
DB_PATHto a path on it. - Deploy.
Free-tier instances sleep when idle, and without a persistent disk the SQLite data is lost on redeploy.
- Never commit
.env. It holds your API key. It is already in.gitignore. If a key is ever pushed by mistake, delete it in Google AI Studio and create a new one. - Never commit
cardioai.db. It stores the health values users submit. - Debug mode exposes an interactive debugger, so the app binds to
127.0.0.1by default. Only useHOST=0.0.0.0behind a production server and withFLASK_ENV=production. - Error details are logged on the server and are not returned to the browser.
- Rate limiting uses in-memory storage, so limits reset on restart and are per process.
- Set a strong
FLASK_SECRET_KEYin any deployment.
| Problem | Fix |
|---|---|
requirements.txt not found |
Run commands from the project folder that contains app.py |
venv\Scripts\activate not recognised |
Activate from the folder that contains venv, then cd into the project |
PowerShell blocks activate |
Run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned once |
| Chat shows generic answers | The server log shows the real cause. Look for Gemini call failed in the terminal |
Gemini 404 NOT_FOUND / model "no longer available" |
Google retired the model. List usable ones with the command below and set GEMINI_MODEL in .env, then restart |
| Chat replies are slow | Use a lite model, e.g. GEMINI_MODEL=gemini-flash-lite-latest |
Changes to .env ignored |
Stop the server with Ctrl+C and start it again. The reloader does not watch .env |
| scikit-learn warning about feature names | Harmless; predictions are unaffected |
| Missing model file error on start | Make sure models/ contains the five .pkl files, or re-run the notebooks |
List the Gemini models your key can use:
python -c "from dotenv import load_dotenv; import os; load_dotenv(); from google import genai; c = genai.Client(api_key=os.getenv('GEMINI_API_KEY')); [print(m.name) for m in c.models.list()]"Use a name containing flash, without the models/ prefix.
| Layer | Tools |
|---|---|
| Backend | Flask, Flask-SQLAlchemy, Flask-Limiter, Gunicorn |
| ML | scikit-learn (Random Forest, StandardScaler), XGBoost (model comparison), pandas, NumPy |
| Explainability | SHAP, Matplotlib |
| AI chat | Google Gemini via google-genai |
| Reports | ReportLab |
| Frontend | Jinja2 templates, HTML, CSS, JavaScript |
| DevOps | Docker, GitHub Actions, Render |
Mehr Ali β BS Computer Science, FAST NUCES Peshawar
Released under the MIT License. See LICENSE for details.



