Skip to content

Repository files navigation

🩺 Predictive Pulse

Predictive Pulse is a full-stack Machine Learning web application that classifies a patient's hypertension stage — Normal, Stage 1, Stage 2, or Hypertensive Crisis — from 13 clinical and lifestyle inputs, and returns stage-specific risk factors, urgency level, and medical recommendations in real time.

Built with Flask, scikit-learn, and a complete Docker + GitHub Actions CI/CD pipeline, deployed live on Render.

🔗 Live Demo: predictive-pulse-l0tw.onrender.com

📦 Container Image: ghcr.io/sreesyam064/predictive-pulse


🚀 Key Features

  • 4-Stage Hypertension Classification — Normal / Stage 1 / Stage 2 / Hypertensive Crisis, based on 13 patient features (demographics, symptoms, BP ranges, medical history).
  • Recall-Prioritized Model Selection — Since missing a hypertensive patient (a false negative) is clinically costlier than a false alarm, model comparison goes beyond accuracy to include precision, recall, and F1 (macro-averaged, so the rare Hypertensive Crisis class isn't overshadowed by the larger classes), plus 5-fold cross-validated accuracy and recall for stability. 7 candidate models were trained and compared; Decision Tree, Random Forest, and SVM scored a suspicious 100% across every metric and were rejected as overfitted. Logistic Regression was selected — not for the single highest recall (KNN edges it out marginally), but for the best combination of generalization (smallest train→test gap), CV-stable recall (98.0% ± 0.42), calibrated probabilities, and interpretable coefficients.
  • Class Probability Breakdown — Returns prediction confidence across all four stages, not just the top class.
  • Automated Risk Factor Detection — Flags contributing factors (age, family history, symptoms, medication adherence, diet) with High/Moderate/Low impact labels.
  • Stage-Specific Clinical Guidance — Each prediction returns urgency level, medical recommendations, and (for Hypertensive Crisis) emergency-response instructions.
  • Model Transparency Endpoint/model-info exposes full model comparison results (accuracy, precision, recall, F1 — macro and weighted — plus cv scores), feature importances, and the overfitting rejection rationale.
  • Production-Ready DevOps — Multi-stage Docker builds (prod/dev), Docker Compose profiles, health checks, and a 3-job GitHub Actions pipeline (test → build & push → deploy).
  • 32-Test Automated Suite — Covers routes, model selection logic, prediction correctness, dataset integrity, and model artifact validation.

🛠️ Tech Stack

Layer Technology
Backend Flask 3.0, Gunicorn
ML / Data scikit-learn 1.4, NumPy, Pandas, joblib
Frontend HTML5, CSS3, Vanilla JavaScript
Visualization (training) Matplotlib
Testing Pytest
Linting Flake8
Containerization Docker, Docker Compose
CI/CD GitHub Actions
Container Registry GitHub Container Registry (GHCR)
Deployment Render (Docker environment)
Config python-dotenv

🏗️ Architecture

                         ┌────────────────────────┐
                         │   train_models.py      │
                         │  (offline / CI step)   │
                         │                        │
                         │  Load patient_data.csv │
                         │  → Clean & encode      │
                         │  → 80/20 split         │
                         │  → Train 7 models      │
                         │  → Overfitting check   │
                         │  → Select best model   │
                         └──────────┬─────────────┘
                                    │ saves
                                    ▼
                 models/best_model.pkl, scaler.pkl, metadata.json
                                    │ loads at startup
                                    ▼
┌────────────┐    HTTP   ┌────────────────────────┐
│  Browser   │◄─────────►│      Flask App         │
│ (index.html│           │       (app.py)         │
│ + app.js)  │  /predict │                        │
└────────────┘   POST    │  encode_input()        │
                         │  scaler.transform()    │
                         │  best_model.predict()  │
                         │  build stage response  │
                         └──────────┬─────────────┘
                                    │
                         ┌──────────▼──────────────┐
                         │   STAGE_INFO lookup     │
                         │  (recommendations,      │
                         │   urgency, color, icon) │
                         └─────────────────────────┘

Request flow: the browser form (index.html / app.js) collects patient inputs → sends JSON to POST /predict → Flask encodes the inputs to match training-time encoding → scales features with the saved StandardScaler → runs inference with the saved best model → looks up stage metadata → returns a JSON response rendered back into the UI.


📊 Model Evaluation

All 7 candidate models are trained and scored on accuracy, precision, recall, and F1 (macro-averaged, so the rare Hypertensive Crisis class carries equal weight to the more common stages rather than being statistically overshadowed), plus 5-fold cross-validated accuracy and recall.

Model Accuracy Precision (macro) Recall (macro) F1 (macro) Status
Decision Tree 100.0% 100.0% 100.0% 100.0% ❌ Rejected — overfit
Random Forest 100.0% 100.0% 100.0% 100.0% ❌ Rejected — overfit
SVM 100.0% 100.0% 100.0% 100.0% ❌ Rejected — overfit
KNN 99.2% 98.8% 99.4% 99.1% ⚠️ Considered
Logistic Regression 97.3% 95.7% 97.9% 96.6% Selected
Ridge Classifier 95.3% 93.6% 94.3% 93.9% ⚠️ Considered
Gaussian Naive Bayes 88.5% 88.3% 91.2% 87.1% ⚠️ Considered

5-fold cross-validated recall (macro) for the selected model: 98.0% (± 0.42).

Why Logistic Regression, and not the model with the single highest recall:

  • Decision Tree, Random Forest, and SVM are overfit. A perfect score across every metric on tabular clinical data signals memorization, not real separability — there's no train/test gap to inspect because both scores are 100%, which is itself the warning sign.
  • KNN edges out Logistic Regression on raw recall (99.4% vs 97.9%), but is structurally riskier for deployment. It has no learned decision boundary — its predictions depend on which training rows happen to be nearest a new patient — it produces no interpretable output, and it must store the entire training set at inference time. All of this makes it fragile once real-world patient data drifts from the training distribution.
  • Logistic Regression generalizes cleanly (97.4% train → 97.3% test, the smallest gap of any strong performer) and its recall is the most stable across cross-validation folds.
  • Its coefficients are directly interpretable per feature — important for clinical trust and auditability — and it produces calibrated class probabilities, which the app uses to explain each prediction (probabilities in the /predict response), unlike Ridge Classifier (no native probability estimates) or KNN (poorly calibrated neighbor-vote ratios).

📈 Visualizations

The table above is also available as charts — generated by generate_visualizations.py directly from models/metadata.json and the saved model artifacts, so they always reflect the current training run rather than hand-typed numbers.

Metric comparison across all 7 models:

Accuracy, Precision, Recall, and F1 grouped bar chart for all 7 models

Recall isolated (the clinically prioritized metric — false negatives are the costliest error here):

Recall-only horizontal bar chart for all 7 models

Metric trend across models, useful for seeing at a glance how tightly accuracy/precision/recall/F1 track each other per model:

Line chart of all 4 metrics across the 7 models

Confusion matrix — Logistic Regression (the deployed model), on the 365-patient test set:

Confusion matrix heatmap for Logistic Regression

Its only errors are 10 STAGE-2 patients predicted as CRISIS — it never under-predicts severity (no CRISIS or STAGE-2 patient is ever misclassified as NORMAL or STAGE-1), which is the safer failure direction for a clinical screening tool.

Confusion matrices — all 7 models side by side, for comparing exactly where each model's errors fall:

Confusion matrices for all 7 models in a grid

Regenerate all of these at any time with:

python train_models.py            # regenerates models/metadata.json and model artifacts
python generate_visualizations.py # writes PNGs into plots/

📁 Project Structure

predictive_pulse/
├── .github/
│   └── workflows/
│       └── main.yaml            # CI/CD pipeline (test → docker → deploy)
├── data/
│   ├── patient_data.csv         # Raw dataset (1,825 records)
│   ├── hypertension_train.csv   # 80% train split (1,460 rows)
│   └── hypertension_test.csv    # 20% test split (365 rows)
├── models/
│   ├── best_model.pkl           # Selected production model (Logistic Regression)
│   ├── scaler.pkl               # Fitted StandardScaler
│   ├── metadata.json            # Model comparison, feature importance, dataset info
│   ├── decision-tree.pkl
│   ├── random-forest.pkl
│   ├── svm.pkl
│   ├── knn.pkl
│   ├── ridge-classifier.pkl
│   └── gaussian-naive-bayes.pkl
├── static/
│   ├── css/style.css
│   └── js/app.js                # Form handling, API calls, result rendering
├── templates/
│   └── index.html                # Single-page UI
├── plots/                        # Evaluation charts
│   ├── model_comparison_bar.png
│   ├── recall_focus_bar.png
│   ├── metric_trend_line.png
│   ├── confusion_matrix_lr.png
│   └── confusion_matrix_all_models.png
├── app.py                        # Flask application & API routes
├── train_models.py               # Data cleaning, training, model comparison, export
├── generate_visualizations.py    # Generates all plots in plots/ from metadata + saved models
├── test_api.py                   # Pytest suite (32 tests)
├── requirements.txt
├── Dockerfile                    # Production image (trains model at build time)
├── Dockerfile.dev                # Development image
├── docker-compose.yml            # prod + dev service profiles
├── render.yaml                   # Render deployment config
├── .env.example
├── .dockerignore
├── .gitignore
└── LICENSE

🔌 API Endpoints

Method Endpoint Description
GET / Renders the main UI (index.html) with app metadata.
POST /predict Accepts patient data as JSON, returns predicted hypertension stage, probabilities, risk factors, and recommendations.
GET /model-info Returns full model comparison results, feature importances, dataset info, and overfitting analysis.
GET /health Health check endpoint used by Docker/Render ({"status": "ok", "model": "..."}).

POST /predict — Request Body

{
  "gender": "male",
  "age": "2",
  "history": "1",
  "patient": "1",
  "when_diagnosed": "1",
  "severity": "1",
  "breath_shortness": "1",
  "visual_changes": "0",
  "nose_bleeding": "0",
  "systolic": "2",
  "diastolic": "2",
  "take_medication": "1",
  "controlled_diet": "0"
}

POST /predict — Response

{
  "success": true,
  "prediction": 2,
  "stage_map": "Hypertension Stage 2",
  "risk_level": "High ",
  "description": "...",
  "urgency": "Urgent: See physician within 1 week",
  "recommendations": ["..."],
  "probabilities": { "Normal": 2.1, "Hypertension Stage 1": 5.4, "...": "..." },
  "risk_factors": [{ "factor": "Family History of HTN", "impact": "High" }],
  "bp_range": { "systolic": "≥ 140", "diastolic": "≥ 90" },
  "model_used": "Logistic Regression",
  "model_accuracy": 97.3,
  "model_recall": 97.9,
  "model_precision": 95.7,
  "model_f1": 96.6
}

Field encodings (age brackets, systolic/diastolic bands, severity levels, etc.) are defined in models/metadata.json under encoders.


⚙️ Installation & Setup

Prerequisites

  • Python 3.12+
  • Docker & Docker Compose (optional, for containerized setup)
  • Git

1. Clone the repository

git clone https://github.com/sreesyam064/predictive_pulse.git
cd predictive_pulse

2. Local setup (without Docker)

python -m venv venv
source venv/bin/activate      # Windows: venv\Scripts\activate

pip install -r requirements.txt

cp .env.example .env          # then set SECRET_KEY in .env

python train_models.py        # trains all 7 models, saves best_model.pkl, scaler.pkl, metadata.json

python app.py                 # runs at http://127.0.0.1:5000

3. Docker setup

# Production image (trains model during build)
docker compose up app-prod --build
# → available at http://localhost:5000

# Development image (hot-reload, mounted volume)
docker compose --profile dev up app-dev --build
# → available at http://localhost:5001

Set SECRET_KEY in a .env file in the project root before running Compose — it's passed through to both services.


📖 Usage Guide

  1. Open the app in your browser (http://localhost:5000 or deployed URL).
  2. Fill out the patient form: demographics, blood pressure ranges, symptoms, medical history, medication and diet status.
  3. Submit the form — it sends a POST request to /predict.
  4. View the result: predicted hypertension stage, color-coded risk level, class probabilities, contributing risk factors, and stage-specific medical recommendations.
  5. Visit /model-info to inspect how the production model was selected over the other six candidates, including accuracy, cross-validation scores, and rejection reasons for overfitted models.
  6. Use /health to verify the app and model are loaded correctly (used by Docker health checks and Render).

🧪 Testing

The project ships with a 32-test Pytest suite (test_api.py) covering:

  • Route availability — homepage, health check, model-info, 404/405 handling
  • Overfitting analysis correctness — verifies Logistic Regression is the sole selected model, and Decision Tree/Random Forest/SVM are correctly flagged as overfitted
  • Prediction correctness — valid stage output, complete response schema, probabilities summing to ~100%, correct crisis-stage classification
  • Dataset integrity — row counts (1,825 total / 1,460 train / 365 test), no nulls, correct columns, valid stage labels
  • Model artifact integrity — presence and correctness of best_model.pkl, scaler.pkl, metadata.json

Run the suite:

python train_models.py        # required first — generates model artifacts used by the tests
pytest test_api.py -v --tb=short

Lint check (matches CI):

flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=venv,__pycache__,.github,models,data

🔄 CI/CD & Deployment

GitHub Actions workflow (.github/workflows/main.yaml) runs on every push/PR to main/master, in three sequential jobs:

1. Test

  • Installs dependencies, lints with Flake8
  • Runs train_models.py to regenerate models
  • Runs the full Pytest suite
  • Verifies model/dataset artifacts and asserts the expected model-selection outcome (Logistic Regression selected, ≥95% accuracy, 1,825 total samples, 13 features)
  • Uploads trained model and dataset split artifacts (30-day retention)

2. Docker Build & Push (push to main only, after tests pass)

  • Builds the production image (trains the model inside the image)
  • Pushes to GitHub Container Registry (ghcr.io/sreesyam064/predictive-pulse) tagged as latest, branch name, and short commit SHA
  • Uses GitHub Actions layer caching to cut rebuild time from ~5 min to ~1 min

3. Deploy to Render (after test + docker jobs succeed)

  • Triggers a deploy via Render's deploy-hook webhook (RENDER_DEPLOY_HOOK_URL secret)
  • Prints a deployment summary (branch, commit, model, dataset)

Required GitHub Secrets:

Secret Purpose
SECRET_KEY Flask app secret key (used during test job)
RENDER_DEPLOY_HOOK_URL Render deploy hook to trigger auto-deploy

Render is configured via render.yaml as a Docker-environment web service with healthCheckPath: /health and autoDeploy: false (deploys are triggered explicitly by the CI pipeline).


🙏 Acknowledgements


📄 License

This project is licensed under the Apache License 2.0 — see the LICENSE file for details.


👤 Author

Pathakota Megha Sri Syam

⚠️ Disclaimer: Predictive Pulse is a portfolio/educational ML project. It is not a certified medical device and should not be used as a substitute for professional medical diagnosis or advice.

Releases

Packages

Contributors

Languages