IDKHowToCodeFr commited on
Commit
0b2f8bd
·
verified ·
1 Parent(s): 4dccba4

Upload folder using huggingface_hub

Browse files
.dockerignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .DS_Store
.gitignore ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # Environment
30
+ .env
31
+ .venv
32
+ env/
33
+ venv/
34
+ ENV/
35
+ env.bak/
36
+ venv.bak/
37
+
38
+ # Streamlit
39
+ .streamlit/credentials.toml
40
+
41
+ # IDEs
42
+ .vscode/
43
+ .idea/
44
+
45
+ # macOS
46
+ .DS_Store
README.md CHANGED
@@ -1,10 +1,148 @@
1
- ---
2
- title: Tinyml Backend
3
- emoji: 🔥
4
- colorFrom: yellow
5
- colorTo: blue
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TinyML Heart Health Monitoring Dashboard
2
+
3
+ A production-ready application for real-time heart health analytics, ensemble prediction modeling, and automated C-code transpilation for resource-constrained embedded systems.
4
+
5
+ **Live Environments:**
6
+ - 🖥️ **Frontend UI:** [Streamlit Cloud](https://tinyml-heart-health-monitoring-dashboard-8xqogy2hibtlayt7popvs.streamlit.app/)
7
+ - ⚙️ **Backend API:** [Hugging Face Spaces](https://huggingface.co/spaces/IDKHowToCodeFr/tinyml-backend)
8
+
9
+ ## Overview
10
+
11
+ This application bridges the gap between high-level cloud machine learning and low-level embedded hardware. It evaluates patient vitals using an ensemble of classifiers, provides strict clinical explainability, and compresses these Python-trained models into raw C-code ready to be flashed onto IoT devices.
12
+
13
+ ### Key Engineering Features
14
+
15
+ - **Edge Computing & TinyML**: Transpiles complex Scikit-Learn models into highly optimized, dependency-free **C-code headers**. This allows predictive intelligence to be deployed directly to the edge, enabling offline, ultra-low latency, and privacy-preserving inference.
16
+ - **INT8 Quantization Engine**: Mathematically scales 64-bit floating-point weights down to 8-bit integers, effectively shrinking the flash memory payload by **~75%** for resource-constrained microcontrollers.
17
+ - **Dynamic Hardware Profiling**: The deployment suite actively simulates target hardware (e.g., *ESP32, Arduino Nano 33 BLE, Raspberry Pi Pico*) to calculate expected inference latency metrics (in µs) and final byte-size footprints prior to flashing.
18
+ - **Interpretable AI (SHAP)**: Solves the medical "black box" concern by generating real-time SHAP feature impact analyses. This visualization proves exactly *why* the AI assigned a specific risk score based on individual patient variables.
19
+ - **Soft-Voting Ensemble**: Rather than relying on a single algorithm, the backend aggregates outputs across five independent models (Random Forest, SVM, KNN, Logistic Regression, Neural Network) to output the highest-confidence predictions.
20
+
21
+ ## Architecture
22
+
23
+ The project follows a decoupled, modular design pattern separating the client interface from the ML processing pipeline.
24
+
25
+ ```mermaid
26
+ flowchart TD
27
+ %% Define styles
28
+ classDef frontend fill:#ff4b4b,stroke:#a30000,stroke-width:2px,color:#fff;
29
+ classDef backend fill:#009688,stroke:#004d40,stroke-width:2px,color:#fff;
30
+ classDef ml_node fill:#f2c94c,stroke:#b28900,stroke-width:2px,color:#333;
31
+ classDef edge_tech fill:#2d9cdb,stroke:#106093,stroke-width:2px,color:#fff;
32
+ classDef db fill:#9b59b6,stroke:#4a235a,stroke-width:2px,color:#fff;
33
+
34
+ %% Core Components
35
+ UI[🖥️ Streamlit Frontend<br/>Interactive Dashboard]:::frontend
36
+ API[⚙️ FastAPI Backend<br/>Routing & API Logic]:::backend
37
+ DB[(🗄️ SQLite DB<br/>Patient History)]:::db
38
+
39
+ UI <-->|REST API JSON| API
40
+ API <-->|Read / Write| DB
41
+
42
+ %% ML Subgraph
43
+ subgraph ML [🧠 Machine Learning & Inference Pipeline]
44
+ ENS{Ensemble Engine<br/>Soft-Voting}:::ml_node
45
+ Models[Classifiers:<br/>RF, SVM, LogReg, NN, KNN]:::ml_node
46
+ SHAP[🔍 SHAP Explainer]:::ml_node
47
+ end
48
+
49
+ %% Edge Quantization Subgraph
50
+ subgraph Quantization [⚡ Hardware Export]
51
+ TRANS[C-Code Transpiler<br/>INT8 Quantization]:::edge_tech
52
+ HEADER((tinyml_model.h)):::edge_tech
53
+ MCU>ESP32 / Cortex-M Node]:::edge_tech
54
+ end
55
+
56
+ API -->|Predict Request| ENS
57
+ ENS --> Models
58
+ API -->|Explain Request| SHAP
59
+ SHAP -.-> Models
60
+ API -->|Export Request| TRANS
61
+ TRANS --> HEADER
62
+ HEADER -->|Flash Firmware| MCU
63
+ ```
64
+
65
+ ### Modular Repository Structure
66
+
67
+ ```text
68
+ TinyML-Dashboard/
69
+ ├── backend/ # FastAPI server, ML pipelines, SQLite API, C-code logic
70
+ ├── frontend/ # Streamlit UI mapping, Plotly charts, web routing
71
+ ├── model/ # Local cache of serialized SciKit-Learn .pkl weights
72
+ ├── data/ # Patient cohort CSVs for MLOps retraining pipelines
73
+ ├── tests/ # Full PyTest suite for backend continuous integration
74
+ ├── .github/workflows/ # CI/CD actions for auto-syncing to Hugging Face
75
+ └── docker-compose.yml # Orchestration spec for local containerized deployment
76
+ ```
77
+
78
+ ## Setup & Installation
79
+
80
+ ### Requirements
81
+ - Python 3.9 or higher
82
+ - Git
83
+
84
+ ### Installation Steps
85
+
86
+ 1. **Clone the repository:**
87
+ ```bash
88
+ git clone https://github.com/IDKHowToCodeFR/TinyML-Heart-Health-Monitoring-Dashboard.git
89
+ cd TinyML-Heart-Health-Monitoring-Dashboard
90
+ ```
91
+
92
+ 2. **Install global requirements:**
93
+ ```bash
94
+ pip install -r requirements.txt
95
+ pip install -r backend/requirements.txt
96
+ pip install -r frontend/requirements.txt
97
+ ```
98
+
99
+ 3. **Train the baseline local `.pkl` models (Required on first run):**
100
+ ```bash
101
+ cd backend
102
+ python models.py
103
+ cd ..
104
+ ```
105
+
106
+ ## Usage
107
+
108
+ ### 🚀 Quick Start (Windows)
109
+ We've bundled an automated startup script that boots both environments natively:
110
+ ```bash
111
+ run.bat
112
+ ```
113
+
114
+ ### 🐳 Docker Deployment
115
+ To avoid local python pathing issues, you can spin up the application via Docker Compose:
116
+ ```bash
117
+ docker-compose up --build
118
+ ```
119
+ *The Streamlit interface will bind automatically to `http://localhost:8501`, connecting directly to the internal API.*
120
+
121
+ ### 🛠️ Manual Startup
122
+ If you prefer running the stack manually in separate terminal instances:
123
+
124
+ **Start the Backend:**
125
+ ```bash
126
+ cd backend
127
+ uvicorn main:app --host 0.0.0.0 --port 8000
128
+ ```
129
+ **Start the Frontend:**
130
+ ```bash
131
+ cd frontend
132
+ streamlit run Home.py
133
+ ```
134
+
135
+ ## Development & Automation
136
+
137
+ ### API Documentation
138
+ FastAPI serves auto-generated interactive documentation. While the backend is running, you can hit the endpoints directly at:
139
+ - **Swagger UI:** `http://localhost:8000/docs`
140
+ - **ReDoc:** `http://localhost:8000/redoc`
141
+
142
+ ### CI/CD Pipelines
143
+ The project utilizes GitHub actions natively located in `.github/workflows/`:
144
+ 1. **PyTest Validation**: Verifies endpoint data sanitization and schema parameters immediately on Push/Pull Requests.
145
+ 2. **Hugging Face Sync**: Auto-deploys `main` branch updates precisely to the Hugging Face Space Docker container ensuring CI/CD alignment.
146
+
147
+ ## Support & License
148
+ This project is licensed under the MIT License. Designed as a scalable, educational, and fully interpretable Edge-based Machine Learning architecture.
backend/Dockerfile ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+ WORKDIR /app
3
+ COPY backend/requirements.txt .
4
+ RUN pip install --no-cache-dir -r requirements.txt
5
+ COPY . .
6
+ WORKDIR /app/backend
7
+ EXPOSE 8000
8
+ CMD sh -c "uvicorn main:app --host 0.0.0.0 --port ${PORT:-8000}"
backend/alerts.log ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ ALERT: Patient at risk! HR: 171.28, SpO2: 98.0, Confidence: 0.82
2
+ ALERT: Patient at risk! HR: 68.0, SpO2: 99.0, Confidence: 0.95
3
+ ALERT: Patient at risk! HR: 97.0, SpO2: 88.0, Confidence: 0.87
4
+ ALERT: Patient at risk! HR: 93.0, SpO2: 88.0, Confidence: 0.99
5
+ ALERT: Patient at risk! HR: 93.0, SpO2: 88.0, Confidence: 0.99
6
+ ALERT: Patient at risk! HR: 93.0, SpO2: 85.0, Confidence: 0.98
7
+ ALERT: Patient at risk! HR: 65.0, SpO2: 93.0, Confidence: 0.92
8
+ ALERT: Patient at risk! HR: 81.0, SpO2: 96.0, Confidence: 0.97
9
+ ALERT: Patient at risk! HR: 91.0, SpO2: 87.0, Confidence: 0.92
backend/database.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ import os
3
+ from datetime import datetime
4
+ from huggingface_hub import HfApi, hf_hub_download
5
+
6
+ REPO_ID = "IDKHowToCodeFr/tinyml-logs"
7
+ DB_NAME = 'patient_history.db'
8
+ DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), DB_NAME)
9
+ HF_TOKEN = os.getenv("HF_TOKEN")
10
+
11
+ api = HfApi()
12
+
13
+ def sync_from_hub():
14
+ if not HF_TOKEN:
15
+ print("No HF_TOKEN found. Skipping sync.")
16
+ return
17
+ try:
18
+ print(f"Downloading {DB_NAME} from Hub...")
19
+ path = hf_hub_download(repo_id=REPO_ID, filename=DB_NAME, repo_type="dataset", token=HF_TOKEN)
20
+ import shutil
21
+ shutil.copy(path, DB_PATH)
22
+ print("Sync from Hub complete.")
23
+ except Exception as e:
24
+ print(f"Sync from Hub failed (maybe first run?): {e}")
25
+
26
+ def sync_to_hub():
27
+ if not HF_TOKEN:
28
+ return
29
+ try:
30
+ api.upload_file(
31
+ path_or_fileobj=DB_PATH,
32
+ path_in_repo=DB_NAME,
33
+ repo_id=REPO_ID,
34
+ repo_type="dataset",
35
+ token=HF_TOKEN
36
+ )
37
+ except Exception as e:
38
+ print(f"Sync to Hub failed: {e}")
39
+
40
+ def init_db():
41
+ sync_from_hub()
42
+ conn = sqlite3.connect(DB_PATH)
43
+ cursor = conn.cursor()
44
+ cursor.execute('''
45
+ CREATE TABLE IF NOT EXISTS predictions (
46
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
47
+ timestamp DATETIME,
48
+ heart_rate REAL,
49
+ spo2 REAL,
50
+ sys_bp REAL,
51
+ dia_bp REAL,
52
+ temp REAL,
53
+ fall_detection TEXT,
54
+ prediction_label TEXT,
55
+ confidence REAL
56
+ )
57
+ ''')
58
+ conn.commit()
59
+ conn.close()
60
+
61
+ def log_prediction(data, prediction_label, confidence):
62
+ conn = sqlite3.connect(DB_PATH)
63
+ cursor = conn.cursor()
64
+ cursor.execute('''
65
+ INSERT INTO predictions (timestamp, heart_rate, spo2, sys_bp, dia_bp, temp, fall_detection, prediction_label, confidence)
66
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
67
+ ''', (
68
+ datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
69
+ data.Heart_Rate,
70
+ data.SpO2_Level,
71
+ data.Systolic_BP,
72
+ data.Diastolic_BP,
73
+ data.Body_Temp,
74
+ data.Fall_Detection,
75
+ prediction_label,
76
+ confidence
77
+ ))
78
+ conn.commit()
79
+ conn.close()
80
+ sync_to_hub()
81
+
82
+ def get_history():
83
+ if not os.path.exists(DB_PATH):
84
+ return []
85
+ conn = sqlite3.connect(DB_PATH)
86
+ cursor = conn.cursor()
87
+ cursor.execute('SELECT * FROM predictions ORDER BY timestamp DESC LIMIT 100')
88
+ rows = cursor.fetchall()
89
+ col_names = [description[0] for description in cursor.description]
90
+ conn.close()
91
+
92
+ history = []
93
+ for row in rows:
94
+ history.append(dict(zip(col_names, row)))
95
+ return history
96
+
97
+ # Initialize db when this module is loaded
98
+ init_db()
backend/ensemble.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import joblib
2
+ import numpy as np
3
+ import os
4
+ import sys
5
+
6
+ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
7
+ from preprocessing import resolve_model_dir
8
+
9
+ class EnsembleModel:
10
+ def __init__(self):
11
+ self.model_dir = resolve_model_dir()
12
+ self.model_names = ['knn', 'svm', 'logreg', 'rf', 'small_nn']
13
+ self.models = {}
14
+ self.load_models()
15
+ self.label_encoder = joblib.load(f'{self.model_dir}/label_encoder.pkl')
16
+ self.weights = {'knn': 0.15, 'svm': 0.15, 'logreg': 0.20, 'rf': 0.25, 'small_nn': 0.25}
17
+
18
+ def load_models(self):
19
+ for name in self.model_names:
20
+ path = f'{self.model_dir}/{name}.pkl'
21
+ if os.path.exists(path):
22
+ self.models[name] = joblib.load(path)
23
+
24
+ def predict(self, X):
25
+ individual_preds = {}
26
+ individual_probs = {}
27
+
28
+ for name, model in self.models.items():
29
+ probs = model.predict_proba(X)
30
+ individual_probs[name] = probs[0]
31
+ pred_indices = np.argmax(probs, axis=1)
32
+ individual_preds[name] = str(self.label_encoder.inverse_transform(pred_indices)[0])
33
+
34
+ num_classes = list(individual_probs.values())[0].shape[0]
35
+ weighted_probs = np.zeros(num_classes)
36
+
37
+ total_weight = 0
38
+ for name in self.models.keys():
39
+ w = self.weights.get(name, 1.0)
40
+ weighted_probs += individual_probs[name] * w
41
+ total_weight += w
42
+
43
+ weighted_probs /= total_weight
44
+ final_pred_idx = np.argmax(weighted_probs)
45
+ final_pred = str(self.label_encoder.inverse_transform([final_pred_idx])[0])
46
+ confidence = float(np.max(weighted_probs))
47
+
48
+ return final_pred, confidence, individual_preds, individual_probs, self.weights
backend/main.py ADDED
@@ -0,0 +1,501 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from pydantic import BaseModel
4
+ import pandas as pd
5
+ import asyncio
6
+ import sys
7
+ import os
8
+ import m2cgen as m2c
9
+ import shap
10
+ from database import log_prediction, get_history
11
+ import numpy as np
12
+
13
+ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
14
+ from preprocessing import preprocess_data
15
+
16
+ app = FastAPI(title="TinyML Healthcare API")
17
+
18
+ app.add_middleware(
19
+ CORSMiddleware,
20
+ allow_origins=["*"],
21
+ allow_credentials=True,
22
+ allow_methods=["*"],
23
+ allow_headers=["*"],
24
+ )
25
+
26
+ ensemble_system = None
27
+ def get_ensemble():
28
+ global ensemble_system
29
+ if ensemble_system is None:
30
+ try:
31
+ from ensemble import EnsembleModel
32
+ ensemble_system = EnsembleModel()
33
+ except Exception:
34
+ return None
35
+ return ensemble_system
36
+
37
+ class PatientData(BaseModel):
38
+ Heart_Rate: float
39
+ SpO2_Level: float
40
+ Systolic_BP: float
41
+ Diastolic_BP: float
42
+ Body_Temp: float
43
+ Fall_Detection: str
44
+
45
+ @app.get("/health")
46
+ def health_check():
47
+ return {"status": "Healthy" if get_ensemble() else "Warning - Models Offline"}
48
+
49
+ @app.post("/predict")
50
+ async def predict(data: PatientData):
51
+ try:
52
+ eng = get_ensemble()
53
+ if not eng:
54
+ return {"error": "Models untrained. Ensure python backend/models.py executes."}
55
+
56
+ df = pd.DataFrame([{
57
+ 'Heart Rate (bpm)': data.Heart_Rate,
58
+ 'SpO2 Level (%)': data.SpO2_Level,
59
+ 'Systolic Blood Pressure (mmHg)': data.Systolic_BP,
60
+ 'Diastolic Blood Pressure (mmHg)': data.Diastolic_BP,
61
+ 'Body Temperature (°C)': data.Body_Temp,
62
+ 'Fall Detection': data.Fall_Detection
63
+ }])
64
+
65
+ X_proc, _ = await asyncio.to_thread(preprocess_data, df, False)
66
+ final_pred, conf, ind_preds, ind_probs, weights = await asyncio.to_thread(eng.predict, X_proc)
67
+
68
+ is_at_risk = 0 if final_pred == "Healthy" else 1
69
+
70
+ # Critical Alert System — fault-tolerant
71
+ if is_at_risk == 1 and float(conf) > 0.80:
72
+ try:
73
+ with open("alerts.log", "a") as f:
74
+ f.write(f"ALERT: Patient at risk! HR: {data.Heart_Rate}, SpO2: {data.SpO2_Level}, Confidence: {conf:.2f}\n")
75
+ except Exception:
76
+ pass
77
+
78
+ # Log to SQLite History — fault-tolerant
79
+ try:
80
+ await asyncio.to_thread(log_prediction, data, final_pred, float(conf))
81
+ except Exception as db_e:
82
+ print(f"DB logging skipped: {db_e}")
83
+
84
+ return {
85
+ "prediction": is_at_risk,
86
+ "prediction_label": final_pred,
87
+ "probability": float(conf),
88
+ "ensemble_prediction": is_at_risk,
89
+ "model_outputs": ind_preds,
90
+ "model_probs": {k: float(np.max(v)) for k, v in ind_probs.items()},
91
+ "weights": weights
92
+ }
93
+ except Exception as e:
94
+ import traceback
95
+ from fastapi import HTTPException
96
+ raise HTTPException(status_code=500, detail=f"Backend Error: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
97
+
98
+ @app.get("/history")
99
+ def history():
100
+ return get_history()
101
+
102
+ @app.get("/dataset")
103
+ async def get_dataset():
104
+ data_path = '/app/data/patient_dataset.csv' if os.path.exists('/app/data') else '../data/patient_dataset.csv' if os.path.exists('../data') else 'data/patient_dataset.csv'
105
+ if os.path.exists(data_path):
106
+ df = pd.read_csv(data_path)
107
+ # Return summary or partial data to avoid huge payloads
108
+ return df.to_dict(orient="records")
109
+ return {"error": "Dataset not found"}
110
+
111
+ @app.get("/sync")
112
+ def force_sync():
113
+ from database import sync_from_hub, sync_to_hub
114
+ sync_from_hub()
115
+ return {"status": "Sync attempted"}
116
+
117
+ @app.post("/explain")
118
+ async def explain(data: PatientData):
119
+ eng = get_ensemble()
120
+ if not eng or 'rf' not in eng.models:
121
+ return {"error": "RF Model unavailable for explanation."}
122
+
123
+ df = pd.DataFrame([{
124
+ 'Heart Rate (bpm)': data.Heart_Rate,
125
+ 'SpO2 Level (%)': data.SpO2_Level,
126
+ 'Systolic Blood Pressure (mmHg)': data.Systolic_BP,
127
+ 'Diastolic Blood Pressure (mmHg)': data.Diastolic_BP,
128
+ 'Body Temperature (°C)': data.Body_Temp,
129
+ 'Fall Detection': data.Fall_Detection
130
+ }])
131
+
132
+ X_proc, _ = await asyncio.to_thread(preprocess_data, df, False)
133
+
134
+ def compute_shap():
135
+ import numpy as np
136
+ rf_model = eng.models['rf']
137
+ explainer = shap.TreeExplainer(rf_model)
138
+ shap_values = explainer.shap_values(X_proc)
139
+ pred_idx = int(rf_model.predict(X_proc)[0])
140
+ if isinstance(shap_values, list):
141
+ vals = shap_values[pred_idx][0]
142
+ elif isinstance(shap_values, np.ndarray) and len(shap_values.shape) == 3:
143
+ vals = shap_values[0, :, pred_idx]
144
+ else:
145
+ vals = shap_values[0]
146
+ return vals.tolist(), X_proc.columns.tolist()
147
+
148
+ try:
149
+ shap_vals, features = await asyncio.to_thread(compute_shap)
150
+ return {"shap_values": shap_vals, "feature_names": features}
151
+ except Exception as e:
152
+ return {"error": str(e)}
153
+
154
+ @app.get("/export_tinyml")
155
+ def export_tinyml(model_name: str = "rf", quantize: bool = False):
156
+ import numpy as np
157
+ eng = get_ensemble()
158
+ if not eng or model_name not in eng.models:
159
+ return {"error": f"Model {model_name} not found"}
160
+
161
+ model = eng.models[model_name]
162
+
163
+ # m2cgen handles Random Forest beautifully, but outputs FP32/double rules
164
+ if model_name == "rf":
165
+ try:
166
+ code = m2c.export_to_c(model)
167
+ if quantize:
168
+ code = "/* WARNING: M2CGen generated FP32 output. INT8 Quantization is not supported directly for Random Forest trees. */\n" + code
169
+ return {"code": code}
170
+ except Exception:
171
+ pass
172
+
173
+ # For LogReg, use m2cgen if FP32, otherwise manual generation for INT8
174
+ if not quantize and model_name == "logreg":
175
+ try:
176
+ code = m2c.export_to_c(model)
177
+ return {"code": code}
178
+ except Exception:
179
+ pass
180
+
181
+ # Manual C-code generation for all model types
182
+ try:
183
+ L = []
184
+ L.append("/* ====================================================== */")
185
+ L.append(f"/* TinyML C Export: {model_name} */")
186
+ q_text = "INT8 Quantized" if quantize else "FP32 Double"
187
+ L.append(f"/* Auto-generated for ARM Cortex-M / ESP32 ({q_text}) */")
188
+ L.append("/* ====================================================== */")
189
+ L.append("")
190
+ L.append("#include <math.h>")
191
+ L.append("#include <stdint.h>")
192
+ L.append("#include <string.h>")
193
+ L.append("")
194
+
195
+ if model_name == "svm" and hasattr(model, 'coef_'):
196
+ coefs = model.coef_
197
+ intercepts = model.intercept_
198
+ n_classes = len(model.classes_)
199
+ n_features = coefs.shape[1]
200
+ L.append(f"/* Linear SVM with {n_classes} classes, {n_features} features */")
201
+ L.append(f"#define N_FEATURES {n_features}")
202
+ L.append(f"#define N_CLASSES {n_classes}")
203
+ L.append(f"#define N_HYPERPLANES {coefs.shape[0]}")
204
+ L.append("")
205
+
206
+ if quantize:
207
+ scale_factor = 127.0 / max(np.max(np.abs(coefs)), np.max(np.abs(intercepts)), 1e-6)
208
+ L.append(f"/* Quantization Scale: {scale_factor:.4f} */")
209
+ L.append("static const int8_t SVM_COEF[N_HYPERPLANES][N_FEATURES] = {")
210
+ for row in coefs:
211
+ vals = ", ".join([str(int(round(v * scale_factor))) for v in row])
212
+ L.append(f" {{{vals}}},")
213
+ L.append("};")
214
+ L.append("")
215
+ vals = ", ".join([str(int(round(v * scale_factor))) for v in intercepts])
216
+ L.append(f"static const int8_t SVM_INTERCEPT[N_HYPERPLANES] = {{{vals}}};")
217
+ L.append("")
218
+ L.append("int predict(int8_t *features) {")
219
+ L.append(" int32_t scores[N_CLASSES] = {0};")
220
+ L.append(" int h = 0;")
221
+ L.append(" for (int i = 0; i < N_CLASSES; i++) {")
222
+ L.append(" for (int j = i + 1; j < N_CLASSES; j++) {")
223
+ L.append(" int32_t decision = SVM_INTERCEPT[h];")
224
+ L.append(" for (int f = 0; f < N_FEATURES; f++) {")
225
+ L.append(" decision += (int32_t)SVM_COEF[h][f] * features[f];")
226
+ L.append(" }")
227
+ L.append(" if (decision > 0) scores[i] += 1;")
228
+ L.append(" else scores[j] += 1;")
229
+ L.append(" h++;")
230
+ L.append(" }")
231
+ L.append(" }")
232
+ L.append(" int best = 0;")
233
+ L.append(" for (int c = 1; c < N_CLASSES; c++) {")
234
+ L.append(" if (scores[c] > scores[best]) best = c;")
235
+ L.append(" }")
236
+ L.append(" return best;")
237
+ L.append("}")
238
+ else:
239
+ L.append("static const double SVM_COEF[N_HYPERPLANES][N_FEATURES] = {")
240
+ for row in coefs:
241
+ vals = ", ".join([f"{v:.6f}" for v in row])
242
+ L.append(f" {{{vals}}},")
243
+ L.append("};")
244
+ L.append("")
245
+ vals = ", ".join([f"{v:.6f}" for v in intercepts])
246
+ L.append(f"static const double SVM_INTERCEPT[N_HYPERPLANES] = {{{vals}}};")
247
+ L.append("")
248
+ L.append("int predict(double *features) {")
249
+ L.append(" double scores[N_CLASSES] = {0};")
250
+ L.append(" int h = 0;")
251
+ L.append(" for (int i = 0; i < N_CLASSES; i++) {")
252
+ L.append(" for (int j = i + 1; j < N_CLASSES; j++) {")
253
+ L.append(" double decision = SVM_INTERCEPT[h];")
254
+ L.append(" for (int f = 0; f < N_FEATURES; f++) {")
255
+ L.append(" decision += SVM_COEF[h][f] * features[f];")
256
+ L.append(" }")
257
+ L.append(" if (decision > 0) scores[i] += 1.0;")
258
+ L.append(" else scores[j] += 1.0;")
259
+ L.append(" h++;")
260
+ L.append(" }")
261
+ L.append(" }")
262
+ L.append(" int best = 0;")
263
+ L.append(" for (int c = 1; c < N_CLASSES; c++) {")
264
+ L.append(" if (scores[c] > scores[best]) best = c;")
265
+ L.append(" }")
266
+ L.append(" return best;")
267
+ L.append("}")
268
+
269
+ elif model_name == "logreg" and hasattr(model, 'coef_'):
270
+ coefs = model.coef_
271
+ intercepts = model.intercept_
272
+ n_classes = coefs.shape[0] if len(model.classes_) > 2 else 2
273
+ n_features = coefs.shape[1]
274
+ L.append(f"/* Logistic Regression with {n_classes} classes, {n_features} features */")
275
+ L.append(f"#define N_FEATURES {n_features}")
276
+ L.append(f"#define N_CLASSES {coefs.shape[0]}")
277
+ L.append("")
278
+
279
+ if quantize:
280
+ scale_factor = 127.0 / max(np.max(np.abs(coefs)), np.max(np.abs(intercepts)), 1e-6)
281
+ L.append(f"/* Quantization Scale: {scale_factor:.4f} */")
282
+ L.append("static const int8_t LOGREG_COEF[N_CLASSES][N_FEATURES] = {")
283
+ for row in coefs:
284
+ vals = ", ".join([str(int(round(v * scale_factor))) for v in row])
285
+ L.append(f" {{{vals}}},")
286
+ L.append("};")
287
+ L.append("")
288
+ vals = ", ".join([str(int(round(v * scale_factor))) for v in intercepts])
289
+ L.append(f"static const int8_t LOGREG_INTERCEPT[N_CLASSES] = {{{vals}}};")
290
+ L.append("")
291
+ L.append("int predict(int8_t *features) {")
292
+ L.append(" int32_t scores[N_CLASSES];")
293
+ L.append(" for (int c = 0; c < N_CLASSES; c++) {")
294
+ L.append(f" scores[c] = LOGREG_INTERCEPT[c] * {int(scale_factor)};")
295
+ L.append(" for (int f = 0; f < N_FEATURES; f++) {")
296
+ L.append(" scores[c] += (int32_t)LOGREG_COEF[c][f] * features[f];")
297
+ L.append(" }")
298
+ L.append(" }")
299
+ L.append(" int best = 0;")
300
+ L.append(" for (int c = 1; c < N_CLASSES; c++) {")
301
+ L.append(" if (scores[c] > scores[best]) best = c;")
302
+ L.append(" }")
303
+ L.append(" return best;")
304
+ L.append("}")
305
+ else:
306
+ L.append("static const double LOGREG_COEF[N_CLASSES][N_FEATURES] = {")
307
+ for row in coefs:
308
+ vals = ", ".join([f"{v:.6f}" for v in row])
309
+ L.append(f" {{{vals}}},")
310
+ L.append("};")
311
+ L.append("")
312
+ vals = ", ".join([f"{v:.6f}" for v in intercepts])
313
+ L.append(f"static const double LOGREG_INTERCEPT[N_CLASSES] = {{{vals}}};")
314
+ L.append("")
315
+ L.append("int predict(double *features) {")
316
+ L.append(" double scores[N_CLASSES];")
317
+ L.append(" for (int c = 0; c < N_CLASSES; c++) {")
318
+ L.append(" scores[c] = LOGREG_INTERCEPT[c];")
319
+ L.append(" for (int f = 0; f < N_FEATURES; f++) {")
320
+ L.append(" scores[c] += LOGREG_COEF[c][f] * features[f];")
321
+ L.append(" }")
322
+ L.append(" }")
323
+ L.append(" int best = 0;")
324
+ L.append(" for (int c = 1; c < N_CLASSES; c++) {")
325
+ L.append(" if (scores[c] > scores[best]) best = c;")
326
+ L.append(" }")
327
+ L.append(" return best;")
328
+ L.append("}")
329
+
330
+ elif model_name == "small_nn" and hasattr(model, 'coefs_'):
331
+ layers = model.coefs_
332
+ biases = model.intercepts_
333
+ arch = " -> ".join([str(l.shape[0]) for l in layers] + [str(layers[-1].shape[1])])
334
+ L.append(f"/* MLP Neural Network: {len(layers)} layers */")
335
+ L.append(f"/* Architecture: {arch} */")
336
+ L.append("")
337
+
338
+ for idx, (W, b) in enumerate(zip(layers, biases)):
339
+ n_in, n_out = W.shape
340
+ L.append(f"#define L{idx}_IN {n_in}")
341
+ L.append(f"#define L{idx}_OUT {n_out}")
342
+ L.append(f"static const double W{idx}[{n_in}][{n_out}] = {{")
343
+ for row in W:
344
+ vals = ", ".join([f"{v:.6f}" for v in row])
345
+ L.append(f" {{{vals}}},")
346
+ L.append("};")
347
+ bvals = ", ".join([f"{v:.6f}" for v in b])
348
+ L.append(f"static const double B{idx}[{n_out}] = {{{bvals}}};")
349
+ L.append("")
350
+
351
+ L.append("static inline double relu(double x) { return x > 0 ? x : 0; }")
352
+ L.append("")
353
+
354
+ if quantize:
355
+ # Calculate global max for int8 scaling
356
+ max_val = max([np.max(np.abs(w)) for w in layers] + [np.max(np.abs(b)) for b in biases] + [1e-6])
357
+ scale_factor = 127.0 / max_val
358
+ L.append(f"/* INT8 Quantization Scale Factor: {scale_factor:.4f} */")
359
+ for idx, (W, b) in enumerate(zip(layers, biases)):
360
+ n_in, n_out = W.shape
361
+ L.append(f"static const int8_t W{idx}[{n_in}][{n_out}] = {{")
362
+ for row in W:
363
+ vals = ", ".join([str(int(round(v * scale_factor))) for v in row])
364
+ L.append(f" {{{vals}}},")
365
+ L.append("};")
366
+ bvals = ", ".join([str(int(round(v * scale_factor))) for v in b])
367
+ L.append(f"static const int8_t B{idx}[{n_out}] = {{{bvals}}};")
368
+ L.append("")
369
+ L.append("static inline int32_t relu_int(int32_t x) { return x > 0 ? x : 0; }")
370
+ L.append("")
371
+ L.append("int predict(int8_t *input) {")
372
+ for idx in range(len(layers)):
373
+ n_in = layers[idx].shape[0]
374
+ n_out = layers[idx].shape[1]
375
+ is_last = (idx == len(layers) - 1)
376
+ src = "input" if idx == 0 else f"a{idx-1}"
377
+ L.append(f" int32_t a{idx}[{n_out}];")
378
+ L.append(f" for (int j = 0; j < {n_out}; j++) {{")
379
+ L.append(f" a{idx}[j] = B{idx}[j] * {int(scale_factor)}; /* scale bias */")
380
+ L.append(f" for (int i = 0; i < {n_in}; i++) {{")
381
+ L.append(f" a{idx}[j] += (int32_t){src}[i] * W{idx}[i][j];")
382
+ L.append(f" }}")
383
+ if not is_last:
384
+ L.append(f" a{idx}[j] = relu_int(a{idx}[j]) / {int(scale_factor)}; /* rescale */")
385
+ L.append(f" }}")
386
+ last_idx = len(layers) - 1
387
+ last_out = layers[-1].shape[1]
388
+ L.append(f" int best = 0;")
389
+ L.append(f" for (int c = 1; c < {last_out}; c++) {{")
390
+ L.append(f" if (a{last_idx}[c] > a{last_idx}[best]) best = c;")
391
+ L.append(f" }}")
392
+ L.append(f" return best;")
393
+ L.append("}")
394
+ else:
395
+ for idx, (W, b) in enumerate(zip(layers, biases)):
396
+ n_in, n_out = W.shape
397
+ L.append(f"#define L{idx}_IN {n_in}")
398
+ L.append(f"#define L{idx}_OUT {n_out}")
399
+ L.append(f"static const double W{idx}[{n_in}][{n_out}] = {{")
400
+ for row in W:
401
+ vals = ", ".join([f"{v:.6f}" for v in row])
402
+ L.append(f" {{{vals}}},")
403
+ L.append("};")
404
+ bvals = ", ".join([f"{v:.6f}" for v in b])
405
+ L.append(f"static const double B{idx}[{n_out}] = {{{bvals}}};")
406
+ L.append("")
407
+
408
+ L.append("static inline double relu(double x) { return x > 0 ? x : 0; }")
409
+ L.append("")
410
+ L.append("int predict(double *input) {")
411
+ for idx in range(len(layers)):
412
+ n_in = layers[idx].shape[0]
413
+ n_out = layers[idx].shape[1]
414
+ is_last = (idx == len(layers) - 1)
415
+ src = "input" if idx == 0 else f"a{idx-1}"
416
+ L.append(f" double a{idx}[{n_out}];")
417
+ L.append(f" for (int j = 0; j < {n_out}; j++) {{")
418
+ L.append(f" a{idx}[j] = B{idx}[j];")
419
+ L.append(f" for (int i = 0; i < {n_in}; i++) {{")
420
+ L.append(f" a{idx}[j] += {src}[i] * W{idx}[i][j];")
421
+ L.append(f" }}")
422
+ if not is_last:
423
+ L.append(f" a{idx}[j] = relu(a{idx}[j]);")
424
+ L.append(f" }}")
425
+
426
+ last_idx = len(layers) - 1
427
+ last_out = layers[-1].shape[1]
428
+ L.append(f" int best = 0;")
429
+ L.append(f" for (int c = 1; c < {last_out}; c++) {{")
430
+ L.append(f" if (a{last_idx}[c] > a{last_idx}[best]) best = c;")
431
+ L.append(f" }}")
432
+ L.append(f" return best;")
433
+ L.append("}")
434
+
435
+ elif model_name == "knn" and hasattr(model, '_fit_X'):
436
+ n_samples = min(model._fit_X.shape[0], 100)
437
+ n_feats = model._fit_X.shape[1]
438
+ L.append(f"/* KNN Lookup Table: {n_samples} reference samples */")
439
+ L.append(f"#define N_NEIGHBORS {model.n_neighbors}")
440
+ L.append(f"#define N_SAMPLES {n_samples}")
441
+ L.append(f"#define N_FEATURES {n_feats}")
442
+ L.append("")
443
+ L.append("static const double REF[N_SAMPLES][N_FEATURES] = {")
444
+ for row in model._fit_X[:n_samples]:
445
+ vals = ", ".join([f"{v:.4f}" for v in row])
446
+ L.append(f" {{{vals}}},")
447
+ L.append("};")
448
+ L.append("")
449
+ labels_str = ", ".join([str(int(l)) for l in model._y[:n_samples]])
450
+ L.append(f"static const int LABELS[N_SAMPLES] = {{{labels_str}}};")
451
+ L.append("")
452
+ L.append("int predict(double *features) {")
453
+ L.append(" double dists[N_SAMPLES];")
454
+ L.append(" for (int i = 0; i < N_SAMPLES; i++) {")
455
+ L.append(" dists[i] = 0.0;")
456
+ L.append(" for (int f = 0; f < N_FEATURES; f++) {")
457
+ L.append(" double d = features[f] - REF[i][f];")
458
+ L.append(" dists[i] += d * d;")
459
+ L.append(" }")
460
+ L.append(" }")
461
+ L.append(" int votes[10] = {0};")
462
+ L.append(" for (int k = 0; k < N_NEIGHBORS; k++) {")
463
+ L.append(" int mi = 0;")
464
+ L.append(" for (int i = 1; i < N_SAMPLES; i++) {")
465
+ L.append(" if (dists[i] < dists[mi]) mi = i;")
466
+ L.append(" }")
467
+ L.append(" votes[LABELS[mi]]++;")
468
+ L.append(" dists[mi] = 1e18;")
469
+ L.append(" }")
470
+ L.append(" int best = 0;")
471
+ L.append(" for (int i = 1; i < 10; i++) {")
472
+ L.append(" if (votes[i] > votes[best]) best = i;")
473
+ L.append(" }")
474
+ L.append(" return best;")
475
+ L.append("}")
476
+ else:
477
+ return {"error": f"Model {model_name} cannot be exported to C."}
478
+
479
+ return {"code": "\n".join(L)}
480
+ except Exception as e:
481
+ return {"error": f"Export failed: {str(e)}"}
482
+
483
+ @app.post("/retrain")
484
+ async def retrain(file: UploadFile = File(...)):
485
+ data_path = '/app/data/patient_dataset.csv' if os.path.exists('/app/data') else '../data/patient_dataset.csv' if os.path.exists('../data') else 'data/patient_dataset.csv'
486
+ os.makedirs(os.path.dirname(data_path), exist_ok=True)
487
+
488
+ content = await file.read()
489
+ with open(data_path, "wb") as f:
490
+ f.write(content)
491
+
492
+ try:
493
+ from models import train_models
494
+ await asyncio.to_thread(train_models)
495
+
496
+ global ensemble_system
497
+ ensemble_system = None
498
+
499
+ return {"status": "success", "message": "Ensemble retrained successfully!"}
500
+ except Exception as e:
501
+ return {"error": str(e)}
backend/models.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import joblib
3
+ from sklearn.neighbors import KNeighborsClassifier
4
+ from sklearn.svm import SVC
5
+ from sklearn.linear_model import LogisticRegression
6
+ from sklearn.ensemble import RandomForestClassifier
7
+ from sklearn.neural_network import MLPClassifier
8
+ import pandas as pd
9
+ import sys
10
+
11
+ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
12
+ from preprocessing import get_train_test_split, resolve_model_dir
13
+
14
+ def train_models():
15
+ data_path = '/app/data/patient_dataset.csv' if os.path.exists('/app/data') else '../data/patient_dataset.csv' if os.path.exists('../data') else 'data/patient_dataset.csv'
16
+ model_dir = resolve_model_dir()
17
+ os.makedirs(model_dir, exist_ok=True)
18
+
19
+ print("Loading data...")
20
+ df = pd.read_csv(data_path)
21
+ X_train, X_test, y_train, y_test = get_train_test_split(df)
22
+
23
+ models = {
24
+ 'knn': KNeighborsClassifier(n_neighbors=5),
25
+ 'svm': SVC(kernel='linear', probability=True, max_iter=2000),
26
+ 'logreg': LogisticRegression(max_iter=1000),
27
+ 'rf': RandomForestClassifier(n_estimators=30, max_depth=5, random_state=42),
28
+ 'small_nn': MLPClassifier(hidden_layer_sizes=(16, 8), max_iter=500, random_state=42)
29
+ }
30
+
31
+ for name, model in models.items():
32
+ print(f"Training {name}...")
33
+ model.fit(X_train, y_train)
34
+ joblib.dump(model, f'{model_dir}/{name}.pkl')
35
+
36
+ print("Training Complete.")
37
+
38
+ if __name__ == '__main__':
39
+ train_models()
backend/patient_history.db ADDED
Binary file (12.3 kB). View file
 
backend/preprocessing.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ import os
4
+ import joblib
5
+ from sklearn.model_selection import train_test_split
6
+ from sklearn.preprocessing import StandardScaler, LabelEncoder
7
+
8
+ def resolve_model_dir():
9
+ # Resolve relative paths universally whether Docker or local execution
10
+ return '/app/model' if os.path.exists('/app/model') else '../model' if os.path.exists('../model') else 'model'
11
+
12
+ def preprocess_data(df, is_training=True):
13
+ model_dir = resolve_model_dir()
14
+ scaler_path = f'{model_dir}/scaler.pkl'
15
+ label_encoder_path = f'{model_dir}/label_encoder.pkl'
16
+
17
+ columns_to_drop = ['Patient Number', 'Data Accuracy (%)', 'Heart Rate Alert', 'SpO2 Level Alert', 'Blood Pressure Alert', 'Temperature Alert']
18
+ cols_drop = [c for c in columns_to_drop if c in df.columns]
19
+ X_raw = df.drop(columns=cols_drop)
20
+
21
+ y = None
22
+ if 'Predicted Disease' in X_raw.columns:
23
+ y_raw = X_raw['Predicted Disease']
24
+ X_raw = X_raw.drop(columns=['Predicted Disease'])
25
+
26
+ if is_training:
27
+ label_encoder = LabelEncoder()
28
+ y = label_encoder.fit_transform(y_raw)
29
+ os.makedirs(os.path.dirname(label_encoder_path), exist_ok=True)
30
+ joblib.dump(label_encoder, label_encoder_path)
31
+ joblib.dump(label_encoder.classes_, f'{model_dir}/classes.pkl')
32
+ else:
33
+ if os.path.exists(label_encoder_path):
34
+ label_encoder = joblib.load(label_encoder_path)
35
+ y = label_encoder.transform(y_raw)
36
+ else:
37
+ y = y_raw
38
+
39
+ if 'Fall Detection' in X_raw.columns:
40
+ X_raw['Fall Detection'] = X_raw['Fall Detection'].map({'Yes': 1, 'No': 0}).fillna(0)
41
+
42
+ X_raw.fillna(X_raw.mean(), inplace=True)
43
+
44
+ # Engineered Feature
45
+ X_raw['Risk_Severity'] = (X_raw['Heart Rate (bpm)'] > 105).astype(int) + (X_raw['SpO2 Level (%)'] < 94).astype(int)
46
+
47
+ continuous_features = ['Heart Rate (bpm)', 'SpO2 Level (%)', 'Systolic Blood Pressure (mmHg)', 'Diastolic Blood Pressure (mmHg)', 'Body Temperature (°C)', 'Risk_Severity']
48
+
49
+ if is_training:
50
+ scaler = StandardScaler()
51
+ X_raw[continuous_features] = scaler.fit_transform(X_raw[continuous_features])
52
+ joblib.dump(scaler, scaler_path)
53
+ else:
54
+ if os.path.exists(scaler_path):
55
+ scaler = joblib.load(scaler_path)
56
+ X_raw[continuous_features] = scaler.transform(X_raw[continuous_features])
57
+
58
+ return X_raw, y
59
+
60
+ def get_train_test_split(df):
61
+ X, y = preprocess_data(df, is_training=True)
62
+ return train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
backend/requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ scikit-learn==1.8.0
4
+ pandas
5
+ numpy
6
+ joblib
7
+ shap
8
+ m2cgen
9
+ matplotlib
10
+ python-multipart
11
+ huggingface_hub
data/patient_dataset.csv CHANGED
The diff for this file is too large to render. See raw diff
 
docker-compose.yml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ backend:
5
+ build:
6
+ context: .
7
+ dockerfile: backend/Dockerfile
8
+ ports:
9
+ - "8000:8000"
10
+ volumes:
11
+ - ./model:/app/model
12
+ - ./data:/app/data
13
+
14
+ frontend:
15
+ build:
16
+ context: .
17
+ dockerfile: frontend/Dockerfile
18
+ ports:
19
+ - "8501:8501"
20
+ environment:
21
+ - API_URL=http://backend:8000
22
+ depends_on:
23
+ - backend
frontend/.streamlit/config.toml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ [theme]
2
+ primaryColor="#00f2fe"
3
+ backgroundColor="#0e1117"
4
+ secondaryBackgroundColor="#262730"
5
+ textColor="#fafafa"
6
+ font="sans serif"
frontend/Dockerfile ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+ WORKDIR /app
3
+
4
+ RUN apt-get update && apt-get install -y graphviz && rm -rf /var/lib/apt/lists/*
5
+
6
+ COPY frontend/requirements.txt .
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+ COPY . .
9
+ WORKDIR /app/frontend
10
+ EXPOSE 8501
11
+ CMD sh -c "streamlit run Home.py --server.port=${PORT:-8501} --server.address=0.0.0.0"
frontend/Home.py ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import requests
3
+ import os
4
+ import time
5
+ import pandas as pd
6
+ import plotly.graph_objects as go
7
+ import numpy as np
8
+
9
+ API_URL = os.getenv("API_URL", "http://localhost:8000")
10
+
11
+ st.set_page_config(page_title="TinyML Heart Health Monitoring", page_icon="🫀", layout="wide")
12
+
13
+ st.markdown("""
14
+ <style>
15
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
16
+ html, body, [class*="css"] { font-family: 'Inter', sans-serif; }
17
+
18
+ .hero-sub {
19
+ font-size: 1.15rem; color: rgba(255,255,255,0.55);
20
+ margin-top: 8px; font-weight: 300; letter-spacing: 0.3px;
21
+ }
22
+
23
+ .metrics-grid {
24
+ display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 16px;
25
+ }
26
+ .glass-metric {
27
+ background: rgba(255,255,255,0.04);
28
+ backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
29
+ border: 1px solid rgba(255,255,255,0.08); border-radius: 16px;
30
+ padding: 24px 20px; text-align: center;
31
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
32
+ min-height: 160px; display: flex; flex-direction: column;
33
+ justify-content: center; align-items: center;
34
+ }
35
+ .glass-metric:hover {
36
+ transform: translateY(-3px);
37
+ box-shadow: 0 12px 40px rgba(0,242,254,0.12);
38
+ border-color: rgba(255,255,255,0.15);
39
+ }
40
+ .metric-value {
41
+ font-size: 2.4rem; font-weight: 800; margin: 8px 0 6px 0; letter-spacing: -0.5px;
42
+ }
43
+ .metric-label {
44
+ font-size: 0.78rem; color: rgba(255,255,255,0.45);
45
+ text-transform: uppercase; letter-spacing: 2px; font-weight: 600;
46
+ }
47
+ .metric-delta {
48
+ font-size: 0.8rem; font-weight: 500; margin-top: 6px;
49
+ padding: 3px 10px; border-radius: 20px; display: inline-block;
50
+ }
51
+ .delta-good { color: #00cc96; background: rgba(0,204,150,0.12); }
52
+ .delta-bad { color: #ef553b; background: rgba(239,85,59,0.12); }
53
+
54
+ .status-badge {
55
+ display: inline-block; padding: 6px 18px; border-radius: 50px;
56
+ font-size: 0.85rem; font-weight: 600; letter-spacing: 0.5px;
57
+ }
58
+ .status-online { background: rgba(0,204,150,0.15); color: #00cc96; border: 1px solid rgba(0,204,150,0.3); }
59
+ .status-fallback { background: rgba(99,110,250,0.15); color: #636efa; border: 1px solid rgba(99,110,250,0.3); }
60
+
61
+ .feature-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
62
+ .feature-card {
63
+ background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06);
64
+ border-radius: 14px; padding: 24px 20px; min-height: 170px;
65
+ display: flex; flex-direction: column;
66
+ transition: border-color 0.2s ease, transform 0.2s ease;
67
+ }
68
+ .feature-card:hover { border-color: rgba(0,242,254,0.25); transform: translateY(-2px); }
69
+ .feature-card .fc-icon { font-size: 1.6rem; margin-bottom: 10px; }
70
+ .feature-card .fc-title { color: #00f2fe; font-size: 0.95rem; font-weight: 700; margin-bottom: 8px; }
71
+ .feature-card .fc-desc { color: rgba(255,255,255,0.45); font-size: 0.82rem; line-height: 1.55; flex-grow: 1; }
72
+ </style>
73
+ """, unsafe_allow_html=True)
74
+
75
+ # ──────── Hero ────────
76
+ st.title("🫀 TinyML Heart Health Monitoring Dashboard")
77
+ st.markdown('<p class="hero-sub">Edge Intelligence for Real-Time Patient Monitoring • 5-Model Ensemble • SHAP Explainability • IoT Edge Deployment</p>', unsafe_allow_html=True)
78
+ st.markdown("<br>", unsafe_allow_html=True)
79
+
80
+ # ──────── System Status ────────
81
+ try:
82
+ resp = requests.get(f"{API_URL}/health", timeout=5)
83
+ if resp.status_code == 200:
84
+ status = resp.json().get("status", "Unknown")
85
+ is_online = "Healthy" in status
86
+ else:
87
+ status = f"Backend Error ({resp.status_code})"
88
+ is_online = False
89
+ except Exception as e:
90
+ status = f"Offline / Monolith Fallback ({str(e)[:50]})"
91
+ is_online = False
92
+
93
+ badge_class = "status-online" if is_online else "status-fallback"
94
+ badge_text = "● ONLINE" if is_online else "● CLOUD MODE"
95
+ st.markdown(f'<span class="status-badge {badge_class}">{badge_text}</span> &nbsp; <span style="color:rgba(255,255,255,0.35);font-size:0.85rem;">{status}</span>', unsafe_allow_html=True)
96
+ st.markdown("---")
97
+
98
+ # ──────── Dataset constants ────────
99
+ HR_MIN, HR_MAX, HR_AVG = 50.0, 139.0, 94.6
100
+ SPO2_MIN, SPO2_MAX, SPO2_AVG = 84.0, 100.0, 94.6
101
+ ALERT_MAX = 100
102
+ HR_NORMAL_LO, HR_NORMAL_HI = 60.0, 100.0
103
+ SPO2_NORMAL_LO = 95.0
104
+ CANDLE_COUNT = 60
105
+ TICK_INTERVAL = 1.5 # seconds
106
+
107
+ # ──────── Deterministic candle from timestamp ────────
108
+ def _alert_at(ts_epoch):
109
+ """Deterministic alert count for a given epoch timestamp."""
110
+ rng = np.random.RandomState(int(ts_epoch * 10) % (2**31))
111
+ return int(rng.randint(0, ALERT_MAX + 1))
112
+
113
+ def _candle_at(t_epoch, prev_alerts, curr_alerts):
114
+ """Build an OHLC candle from alert transitions."""
115
+ o = float(prev_alerts)
116
+ c = float(curr_alerts)
117
+ rng = np.random.RandomState(int(t_epoch * 7) % (2**31))
118
+ spread = abs(c - o) * 0.3 + rng.uniform(1, 4)
119
+ h = max(o, c) + spread
120
+ l = min(o, c) - rng.uniform(0.5, spread * 0.6)
121
+ h = min(h, ALERT_MAX + 5)
122
+ l = max(l, 0)
123
+ return {"time": pd.Timestamp.fromtimestamp(t_epoch), "open": round(o, 1), "high": round(h, 1), "low": round(l, 1), "close": round(c, 1)}
124
+
125
+ # ──────── Initialize: pre-fill chart as if running since startup ────────
126
+ if "chart_candles" not in st.session_state:
127
+ now_ep = time.time()
128
+ candles = []
129
+ prev = 50 # initial alert count
130
+ for i in range(CANDLE_COUNT):
131
+ t = now_ep - (CANDLE_COUNT - i) * TICK_INTERVAL
132
+ curr = _alert_at(t)
133
+ candles.append(_candle_at(t, prev, curr))
134
+ prev = curr
135
+ st.session_state.chart_candles = candles
136
+ st.session_state.last_alerts = prev
137
+ st.session_state.last_candle_epoch = now_ep
138
+
139
+ # ──────── Live Metrics ────────
140
+ @st.fragment(run_every="1.5s")
141
+ def live_metrics():
142
+ now_ep = time.time()
143
+
144
+ # Catch up: generate all candles that should have been created while away
145
+ last_ep = st.session_state.get("last_candle_epoch", now_ep - TICK_INTERVAL)
146
+ missed = int((now_ep - last_ep) / TICK_INTERVAL)
147
+ prev_a = st.session_state.get("last_alerts", 50)
148
+
149
+ if missed > 1:
150
+ # Backfill deterministically so chart stays continuous
151
+ for i in range(1, min(missed, CANDLE_COUNT) + 1):
152
+ t = last_ep + i * TICK_INTERVAL
153
+ curr_a = _alert_at(t)
154
+ st.session_state.chart_candles.append(_candle_at(t, prev_a, curr_a))
155
+ prev_a = curr_a
156
+
157
+ # Current tick
158
+ alerts = np.random.randint(0, ALERT_MAX + 1)
159
+ alert_delta = alerts - prev_a
160
+ st.session_state.chart_candles.append(_candle_at(now_ep, prev_a, alerts))
161
+ st.session_state.last_alerts = alerts
162
+ st.session_state.last_candle_epoch = now_ep
163
+
164
+ if len(st.session_state.chart_candles) > CANDLE_COUNT:
165
+ st.session_state.chart_candles = st.session_state.chart_candles[-CANDLE_COUNT:]
166
+
167
+ # Simulate patient cohort from alert count
168
+ n = max(alerts, 5)
169
+ sim_hr = np.clip(np.random.normal(HR_AVG, 12, n), HR_MIN, HR_MAX)
170
+ sim_o2 = np.clip(np.random.normal(SPO2_AVG, 3, n), SPO2_MIN, SPO2_MAX)
171
+ pop_hr = round(float(np.mean(sim_hr)), 1)
172
+ pop_o2 = round(float(np.mean(sim_o2)), 1)
173
+ hr_delta = round(pop_hr - HR_AVG, 1)
174
+ o2_delta = round(pop_o2 - SPO2_AVG, 1)
175
+ hr_ok = HR_NORMAL_LO <= pop_hr <= HR_NORMAL_HI
176
+ o2_ok = pop_o2 >= SPO2_NORMAL_LO
177
+
178
+ # Alert delta coloring: increase = bad (red), decrease = good (green)
179
+ ad_good = alert_delta <= 0
180
+ ad_c = "#00cc96" if ad_good else "#ef553b"
181
+ ad_d = "delta-good" if ad_good else "delta-bad"
182
+ ad_i = "▼" if ad_good else "▲"
183
+ ad_txt = f"{abs(alert_delta)} fewer" if ad_good else f"{alert_delta} more"
184
+
185
+ hr_c = "#00cc96" if hr_ok else "#ef553b"
186
+ hr_d = "delta-good" if hr_ok else "delta-bad"
187
+ hr_a = "▲" if hr_delta > 0 else "▼"
188
+ hr_s = "Within Normal" if hr_ok else "Abnormal"
189
+ o2_c = "#00cc96" if o2_ok else "#ef553b"
190
+ o2_d = "delta-good" if o2_ok else "delta-bad"
191
+ o2_a = "▲" if o2_delta > 0 else "▼"
192
+ o2_s = "Within Normal" if o2_ok else "Below Safe"
193
+
194
+ st.markdown(f"""
195
+ <div class="metrics-grid">
196
+ <div class="glass-metric">
197
+ <div class="metric-label">Total Patients</div>
198
+ <div class="metric-value" style="color:#4facfe;">60,000</div>
199
+ <div class="metric-delta delta-good">● Live Stream Active</div>
200
+ </div>
201
+ <div class="glass-metric">
202
+ <div class="metric-label">Active Alerts</div>
203
+ <div class="metric-value" style="color:{ad_c};">{alerts}</div>
204
+ <div class="metric-delta {ad_d}">{ad_i} {ad_txt} vs last</div>
205
+ </div>
206
+ <div class="glass-metric">
207
+ <div class="metric-label">Avg Heart Rate</div>
208
+ <div class="metric-value" style="color:{hr_c};">{pop_hr} <span style="font-size:1rem;color:rgba(255,255,255,0.3);">bpm</span></div>
209
+ <div class="metric-delta {hr_d}">{hr_a} {abs(hr_delta)} bpm from avg • {hr_s}</div>
210
+ </div>
211
+ <div class="glass-metric">
212
+ <div class="metric-label">Avg SpO2 Level</div>
213
+ <div class="metric-value" style="color:{o2_c};">{pop_o2}<span style="font-size:1rem;color:rgba(255,255,255,0.3);">%</span></div>
214
+ <div class="metric-delta {o2_d}">{o2_a} {abs(o2_delta):.1f}% from avg • {o2_s}</div>
215
+ </div>
216
+ </div>
217
+ """, unsafe_allow_html=True)
218
+
219
+ # ──── Smoothed Activity Area Chart: tracks average heart-rate & alerts ────
220
+ st.markdown("##### 📡 Live Patient Emulation Trace")
221
+
222
+ cd = st.session_state.chart_candles
223
+ ts = [x["time"] for x in cd]
224
+ # Represent the primary metric as the close/current state
225
+ cl = [x["close"] for x in cd]
226
+
227
+ fig = go.Figure()
228
+
229
+ # Main Area Waveform
230
+ fig.add_trace(go.Scatter(
231
+ x=ts, y=cl,
232
+ mode='lines',
233
+ line=dict(color='#00f2fe', width=3, shape='spline', smoothing=1.3),
234
+ fill='tozeroy',
235
+ fillcolor='rgba(0, 242, 254, 0.15)',
236
+ name="Activity Index"
237
+ ))
238
+
239
+ # Add a thin secondary baseline trend
240
+ w = min(10, len(cl))
241
+ ma = pd.Series(cl).rolling(window=w, min_periods=1).mean().tolist()
242
+ fig.add_trace(go.Scatter(
243
+ x=ts, y=ma, mode='lines',
244
+ line=dict(color='rgba(255, 255, 255, 0.4)', width=1.5, dash='dot'),
245
+ name="Moving Average"
246
+ ))
247
+
248
+ fig.update_layout(
249
+ template="plotly_dark", height=320,
250
+ margin=dict(l=0, r=50, t=10, b=0),
251
+ paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)',
252
+ xaxis=dict(gridcolor='rgba(255,255,255,0.03)', showgrid=True, rangeslider=dict(visible=False)),
253
+ yaxis=dict(
254
+ title="Relative Activity",
255
+ gridcolor='rgba(255,255,255,0.05)',
256
+ title_font=dict(size=11),
257
+ side="right",
258
+ range=[0, ALERT_MAX + 15]
259
+ ),
260
+ legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1, font=dict(size=10)),
261
+ showlegend=True,
262
+ hovermode="x unified"
263
+ )
264
+ st.plotly_chart(fig, use_container_width=True)
265
+
266
+ live_metrics()
267
+
268
+ st.markdown("---")
269
+
270
+ # ──────── Feature Cards ────────
271
+ st.markdown("##### 🧩 System Capabilities")
272
+ st.markdown("""
273
+ <div class="feature-grid">
274
+ <div class="feature-card">
275
+ <div class="fc-icon">🩺</div>
276
+ <div class="fc-title">Edge Prediction</div>
277
+ <div class="fc-desc">5-model weighted ensemble (KNN, SVM, LogReg, RF, MLP) with real-time consensus voting and confidence scoring.</div>
278
+ </div>
279
+ <div class="feature-card">
280
+ <div class="fc-icon">🔍</div>
281
+ <div class="fc-title">SHAP Explainability</div>
282
+ <div class="fc-desc">Feature-level SHAP analysis reveals exactly why the AI made each clinical decision with interactive Plotly charts.</div>
283
+ </div>
284
+ <div class="feature-card">
285
+ <div class="fc-icon">⚙️</div>
286
+ <div class="fc-title">C-Code Export</div>
287
+ <div class="fc-desc">Transpile any model to raw C headers with INT8 quantization simulation for ESP32 and Arduino flash deployment.</div>
288
+ </div>
289
+ <div class="feature-card">
290
+ <div class="fc-icon">🔄</div>
291
+ <div class="fc-title">MLOps Pipeline</div>
292
+ <div class="fc-desc">Dynamic dataset upload and live ensemble retraining without downtime. Full model hot-swap on the fly.</div>
293
+ </div>
294
+ </div>
295
+ """, unsafe_allow_html=True)
296
+
297
+ st.markdown("<br>", unsafe_allow_html=True)
298
+ st.caption("Use the sidebar to navigate between Prediction, Analytics, Edge Deployment, Patient History, MLOps, and About.")
frontend/pages/1_Predict_Dashboard.py ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import requests
3
+ import os
4
+ import plotly.graph_objects as go
5
+
6
+ st.set_page_config(page_title="Predict Patient Health", page_icon="🩺", layout="wide")
7
+ API_URL = os.getenv("API_URL", "http://localhost:8000")
8
+
9
+ st.title("🩺 Patient Edge Prediction Simulation")
10
+
11
+ st.markdown("""
12
+ <style>
13
+ div[data-testid="stMetricValue"] {
14
+ font-size: 1.5rem;
15
+ }
16
+ .red-card {
17
+ background: rgba(255, 50, 50, 0.15);
18
+ backdrop-filter: blur(10px);
19
+ -webkit-backdrop-filter: blur(10px);
20
+ padding: 20px;
21
+ border-radius: 15px;
22
+ border: 1px solid rgba(255, 50, 50, 0.3);
23
+ box-shadow: 0 8px 32px 0 rgba(255, 50, 50, 0.2);
24
+ }
25
+ .green-card {
26
+ background: rgba(50, 255, 100, 0.15);
27
+ backdrop-filter: blur(10px);
28
+ -webkit-backdrop-filter: blur(10px);
29
+ padding: 20px;
30
+ border-radius: 15px;
31
+ border: 1px solid rgba(50, 255, 100, 0.3);
32
+ box-shadow: 0 8px 32px 0 rgba(50, 255, 100, 0.2);
33
+ }
34
+ .amber-card {
35
+ background: rgba(255, 180, 50, 0.15);
36
+ backdrop-filter: blur(10px);
37
+ -webkit-backdrop-filter: blur(10px);
38
+ padding: 20px;
39
+ border-radius: 15px;
40
+ border: 1px solid rgba(255, 180, 50, 0.3);
41
+ box-shadow: 0 8px 32px 0 rgba(255, 180, 50, 0.2);
42
+ }
43
+ .node-row {
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: space-between;
47
+ padding: 8px 12px;
48
+ margin: 4px 0;
49
+ border-radius: 8px;
50
+ background: rgba(255,255,255,0.04);
51
+ border-left: 3px solid;
52
+ }
53
+ </style>
54
+ """, unsafe_allow_html=True)
55
+
56
+ # Persist random defaults using session_state keys
57
+ import random
58
+ if "sl_hr" not in st.session_state:
59
+ st.session_state.sl_hr = float(random.randint(60, 110))
60
+ st.session_state.sl_spo2 = float(random.randint(85, 99))
61
+ st.session_state.sl_sys = float(random.randint(110, 140))
62
+ st.session_state.sl_dia = float(random.randint(70, 90))
63
+ st.session_state.sl_tmp = round(random.uniform(36.0, 38.0), 1)
64
+
65
+ col1, col2 = st.columns(2)
66
+ with col1:
67
+ hr = st.slider("Heart Rate (bpm)", 40.0, 200.0, key="sl_hr")
68
+ spo2 = st.slider("SpO2 Level (%)", 70.0, 100.0, key="sl_spo2")
69
+ sys_bp = st.slider("Systolic BP (mmHg)", 70.0, 200.0, key="sl_sys")
70
+
71
+ with col2:
72
+ dia_bp = st.slider("Diastolic BP (mmHg)", 40.0, 130.0, key="sl_dia")
73
+ tmp = st.slider("Body Temp (°C)", 34.0, 42.0, key="sl_tmp")
74
+ fall = st.selectbox("Fall Detection", ["No", "Yes"])
75
+
76
+ MODEL_DISPLAY = {
77
+ "knn": "K-Nearest Neighbors",
78
+ "svm": "Support Vector Machine",
79
+ "logreg": "Logistic Regression",
80
+ "rf": "Random Forest",
81
+ "small_nn": "Neural Network (MLP)"
82
+ }
83
+
84
+ DISEASE_COLORS = {
85
+ "Healthy": "#00cc96",
86
+ "Asthma": "#ab63fa",
87
+ "Diabetes Mellitus": "#ffa15a",
88
+ "Heart Disease": "#ef553b",
89
+ "Hypertension": "#636efa"
90
+ }
91
+
92
+ def render_results(data):
93
+ st.markdown("---")
94
+ label = data['prediction_label']
95
+ conf = data['probability'] * 100
96
+
97
+
98
+ res_col1, res_col2 = st.columns([1, 1])
99
+
100
+ with res_col1:
101
+ st.markdown("#### 🏥 Diagnosis Status")
102
+ card_color = DISEASE_COLORS.get(label, "#636efa")
103
+ is_healthy = label == "Healthy"
104
+ card_class = "green-card" if is_healthy else "red-card" if label == "Heart Disease" else "amber-card"
105
+ status_text = f"Status: {label}" if is_healthy else f"Status: {label} (At Risk)"
106
+ st.markdown(f'<div class="{card_class}"><h3>{status_text}</h3></div>', unsafe_allow_html=True)
107
+ st.progress(int(conf))
108
+ st.write(f"**Ensemble Confidence:** {conf:.1f}%")
109
+
110
+ with res_col2:
111
+ st.markdown("#### 🧠 Individual Node Consensus & Weights")
112
+ for model_key, pred in data['model_outputs'].items():
113
+ display_name = MODEL_DISPLAY.get(model_key, model_key.upper())
114
+ wgt = data.get('weights', {}).get(model_key, 0)
115
+ prob = data.get('model_probs', {}).get(model_key, 0) * 100
116
+ pred_color = DISEASE_COLORS.get(pred, "#636efa")
117
+ border_color = "#00cc96" if pred == "Healthy" else "#ef553b"
118
+ st.markdown(
119
+ f'<div class="node-row" style="border-left-color: {border_color};">'
120
+ f'<span><strong>{display_name}</strong> (w={wgt:.2f})</span>'
121
+ f'<span style="color: {pred_color}; font-weight: bold;">{pred} ({prob:.0f}%)</span>'
122
+ f'</div>',
123
+ unsafe_allow_html=True
124
+ )
125
+
126
+ # SHAP Explainability Section
127
+ with st.expander("🔍 Explain AI Prediction (SHAP Explainability)", expanded=False):
128
+ st.markdown("##### How each vital sign influenced the AI's decision:")
129
+ try:
130
+ url_exp = f"{API_URL.rstrip('/')}/explain"
131
+ exp_resp = requests.post(url_exp, json=payload, timeout=30)
132
+ if exp_resp.status_code == 200:
133
+ exp_data = exp_resp.json()
134
+ if "error" in exp_data:
135
+ st.warning(exp_data["error"])
136
+ else:
137
+ shap_vals = exp_data["shap_values"]
138
+ feat_names = exp_data["feature_names"]
139
+
140
+ # Clean up feature names for display
141
+ clean_names = []
142
+ for f in feat_names:
143
+ f = f.replace(" (bpm)", "").replace(" (%)", "").replace(" (mmHg)", "").replace(" (°C)", "")
144
+ clean_names.append(f)
145
+
146
+ # Sort by absolute value for visual impact
147
+ sorted_pairs = sorted(zip(clean_names, shap_vals), key=lambda x: abs(x[1]))
148
+ s_names, s_vals = zip(*sorted_pairs)
149
+
150
+ colors = ['#ef553b' if v > 0 else '#00cc96' for v in s_vals]
151
+
152
+ fig = go.Figure(go.Bar(
153
+ x=list(s_vals),
154
+ y=list(s_names),
155
+ orientation='h',
156
+ marker=dict(
157
+ color=colors,
158
+ line=dict(width=1, color='rgba(255,255,255,0.2)')
159
+ ),
160
+ text=[f"{v:+.4f}" for v in s_vals],
161
+ textposition='outside',
162
+ textfont=dict(color='white', size=11)
163
+ ))
164
+ fig.update_layout(
165
+ title=dict(text=f"Feature Impact on '{label}' Prediction", font=dict(color='white', size=16)),
166
+ xaxis_title="SHAP Value (←Reduces Risk | Increases Risk→)",
167
+ yaxis_title="",
168
+ template="plotly_dark",
169
+ height=350,
170
+ margin=dict(l=10, r=80, t=50, b=50),
171
+ paper_bgcolor='rgba(0,0,0,0)',
172
+ plot_bgcolor='rgba(0,0,0,0)',
173
+ xaxis=dict(zeroline=True, zerolinecolor='rgba(255,255,255,0.3)', zerolinewidth=2, gridcolor='rgba(255,255,255,0.07)'),
174
+ yaxis=dict(gridcolor='rgba(255,255,255,0.07)')
175
+ )
176
+
177
+ st.plotly_chart(fig, use_container_width=True)
178
+
179
+ # ──── Detailed Key Insights Panel ────
180
+ st.markdown("---")
181
+ st.markdown("##### 📊 Key Insights — Clinical Explainability Report")
182
+
183
+ # Sort features by absolute SHAP impact
184
+ paired = list(zip(feat_names, shap_vals))
185
+ paired_sorted = sorted(paired, key=lambda x: abs(x[1]), reverse=True)
186
+ risk_factors = [(n, v) for n, v in paired_sorted if v > 0.001]
187
+ protective = [(n, v) for n, v in paired_sorted if v < -0.001]
188
+ total_push = sum(v for _, v in paired)
189
+ risk_direction = "toward a clinical condition" if total_push > 0 else "toward a healthy classification"
190
+
191
+ ins_c1, ins_c2 = st.columns(2)
192
+
193
+ with ins_c1:
194
+ st.markdown("**🔴 Risk-Increasing Factors**")
195
+ if risk_factors:
196
+ for name, val in risk_factors:
197
+ pct = abs(val) / max(sum(abs(v) for _, v in paired), 1e-9) * 100
198
+ st.markdown(
199
+ f'<div style="background:rgba(239,85,59,0.08);border-left:3px solid #ef553b;'
200
+ f'padding:8px 12px;border-radius:6px;margin:4px 0;">'
201
+ f'<strong>{name}</strong><br>'
202
+ f'<span style="color:#ef553b;">SHAP +{val:.4f}</span> '
203
+ f'<span style="color:rgba(255,255,255,0.4);">({pct:.0f}% of total impact)</span>'
204
+ f'</div>',
205
+ unsafe_allow_html=True
206
+ )
207
+ else:
208
+ st.caption("No features significantly increasing risk.")
209
+
210
+ with ins_c2:
211
+ st.markdown("**🟢 Protective Factors**")
212
+ if protective:
213
+ for name, val in protective:
214
+ pct = abs(val) / max(sum(abs(v) for _, v in paired), 1e-9) * 100
215
+ st.markdown(
216
+ f'<div style="background:rgba(0,204,150,0.08);border-left:3px solid #00cc96;'
217
+ f'padding:8px 12px;border-radius:6px;margin:4px 0;">'
218
+ f'<strong>{name}</strong><br>'
219
+ f'<span style="color:#00cc96;">SHAP {val:.4f}</span> '
220
+ f'<span style="color:rgba(255,255,255,0.4);">({pct:.0f}% of total impact)</span>'
221
+ f'</div>',
222
+ unsafe_allow_html=True
223
+ )
224
+ else:
225
+ st.caption("No features significantly reducing risk.")
226
+
227
+ # Overall clinical summary
228
+ top_feat, top_val = paired_sorted[0]
229
+ second_feat = paired_sorted[1][0] if len(paired_sorted) > 1 else None
230
+ top_dir = "increasing" if top_val > 0 else "decreasing"
231
+
232
+ summary_parts = [
233
+ f"The ensemble model's decision was primarily driven by **{top_feat}** "
234
+ f"(SHAP {top_val:+.4f}), which had the strongest influence in {top_dir} the risk score.",
235
+ ]
236
+ if second_feat:
237
+ s_val = paired_sorted[1][1]
238
+ s_dir = "increasing" if s_val > 0 else "decreasing"
239
+ summary_parts.append(
240
+ f"The second most influential factor was **{second_feat}** "
241
+ f"(SHAP {s_val:+.4f}), {s_dir} risk."
242
+ )
243
+ summary_parts.append(
244
+ f"Overall, the net SHAP push ({total_push:+.4f}) points **{risk_direction}** "
245
+ f"for the given vitals."
246
+ )
247
+ if risk_factors and protective:
248
+ summary_parts.append(
249
+ f"The model identified {len(risk_factors)} risk-increasing and "
250
+ f"{len(protective)} protective factor(s), indicating a mixed clinical profile "
251
+ f"that warrants attention to the dominant contributors."
252
+ )
253
+
254
+ st.info("💡 **Clinical Interpretation:**\n\n" + " ".join(summary_parts))
255
+ except Exception as e:
256
+ st.error(f"Failed to fetch explanation: {e}")
257
+
258
+
259
+ if st.button("Query Edge AI Ensemble", type="primary"):
260
+ payload = {
261
+ "Heart_Rate": hr,
262
+ "SpO2_Level": spo2,
263
+ "Systolic_BP": sys_bp,
264
+ "Diastolic_BP": dia_bp,
265
+ "Body_Temp": tmp,
266
+ "Fall_Detection": fall
267
+ }
268
+
269
+ with st.spinner("Processing through TinyML Nodes..."):
270
+ try:
271
+ url = f"{API_URL.rstrip('/')}/predict"
272
+ resp = requests.post(url, json=payload, timeout=30)
273
+ if resp.status_code == 200:
274
+ data = resp.json()
275
+ if "error" in data:
276
+ st.error(data["error"])
277
+ else:
278
+ render_results(data)
279
+ else:
280
+ st.error(f"API returned an error: {resp.status_code} — {resp.text[:500]}")
281
+ except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as conn_err:
282
+ try:
283
+ import sys
284
+ import os
285
+ import pandas as pd
286
+
287
+ backend_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../backend'))
288
+ if backend_path not in sys.path:
289
+ sys.path.append(backend_path)
290
+
291
+ from ensemble import EnsembleModel
292
+ from preprocessing import preprocess_data
293
+
294
+ df_simulate = pd.DataFrame([{
295
+ "Heart Rate (bpm)": payload["Heart_Rate"],
296
+ "SpO2 Level (%)": payload["SpO2_Level"],
297
+ "Systolic Blood Pressure (mmHg)": payload["Systolic_BP"],
298
+ "Diastolic Blood Pressure (mmHg)": payload["Diastolic_BP"],
299
+ "Body Temperature (°C)": payload["Body_Temp"],
300
+ "Fall Detection": payload["Fall_Detection"]
301
+ }])
302
+
303
+ X_proc, _ = preprocess_data(df_simulate, is_training=False)
304
+ eng = EnsembleModel()
305
+ final_pred, conf, ind_preds, ind_probs, weights = eng.predict(X_proc)
306
+
307
+ data = {
308
+ "prediction": 0 if final_pred == "Healthy" else 1,
309
+ "prediction_label": final_pred,
310
+ "probability": float(conf),
311
+ "model_outputs": ind_preds,
312
+ "model_probs": {k: float(max(v)) for k, v in ind_probs.items()},
313
+ "weights": weights
314
+ }
315
+
316
+ st.markdown("---")
317
+ st.info("☁️ **Cloud Mode (Fallback)**: Processed via Monolithic Native Architecture.")
318
+ render_results(data)
319
+
320
+ except Exception as inner_e:
321
+ st.error(f"Monolithic execution failed: {inner_e}")
322
+
323
+ except Exception as e:
324
+ st.error(f"Failed to process Request context: {e}")
frontend/pages/2_Data_Analytics.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import plotly.express as px
4
+ import plotly.graph_objects as go
5
+ import numpy as np
6
+ import os
7
+ import requests
8
+
9
+ st.set_page_config(page_title="Advanced Analytics", page_icon="📊", layout="wide")
10
+ st.title("📊 Population Analytics Deep Dive")
11
+
12
+ API_URL = os.getenv("API_URL", "http://localhost:8000")
13
+
14
+ @st.cache_data(ttl=3600)
15
+ def load_data():
16
+ try:
17
+ resp = requests.get(f"{API_URL}/dataset", timeout=30)
18
+ if resp.status_code == 200:
19
+ data = resp.json()
20
+ if isinstance(data, list):
21
+ return pd.DataFrame(data)
22
+ st.error(f"Backend Error: {data.get('error', 'Unknown')}")
23
+ except Exception as e:
24
+ st.error(f"Failed to connect to backend: {e}")
25
+ return pd.DataFrame()
26
+
27
+ DISEASE_COLORS = {
28
+ "Healthy": "#00cc96",
29
+ "Asthma": "#ab63fa",
30
+ "Diabetes Mellitus": "#ffa15a",
31
+ "Heart Disease": "#ef553b",
32
+ "Hypertension": "#636efa"
33
+ }
34
+
35
+ full_df = load_data()
36
+
37
+ if full_df.empty:
38
+ st.warning("Warning: Dataset not found or backend unreachable. Connect to the API to view analytics.")
39
+ else:
40
+ df = full_df
41
+ st.success(f"Loaded **{len(full_df):,}** patient records from backend.")
42
+
43
+ # ... rest of the file stays same ...
44
+ st.markdown("### 🫀 Vital Signs Distribution by Condition")
45
+ vitals = ["Heart Rate (bpm)", "SpO2 Level (%)", "Systolic Blood Pressure (mmHg)", "Diastolic Blood Pressure (mmHg)", "Body Temperature (°C)"]
46
+ selected_vital = st.selectbox("Select Vital Sign", vitals)
47
+
48
+ fig_box = px.box(
49
+ df, x="Predicted Disease", y=selected_vital,
50
+ color="Predicted Disease",
51
+ color_discrete_map=DISEASE_COLORS,
52
+ title=f"{selected_vital} Distribution Across Conditions",
53
+ template="plotly_dark",
54
+ points="outliers"
55
+ )
56
+ fig_box.update_layout(
57
+ paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)',
58
+ xaxis_title="Condition", yaxis_title=selected_vital,
59
+ showlegend=False, height=420
60
+ )
61
+ st.plotly_chart(fig_box, use_container_width=True)
62
+
63
+ st.markdown("---")
64
+
65
+ c1, c2 = st.columns(2)
66
+ with c1:
67
+ st.markdown("##### Heart Rate vs SpO2 (Scatter Matrix)")
68
+ fig_scatter = px.scatter(
69
+ df, x="Heart Rate (bpm)", y="SpO2 Level (%)",
70
+ color="Predicted Disease",
71
+ color_discrete_map=DISEASE_COLORS,
72
+ opacity=0.4, size_max=6,
73
+ title="Heart Rate vs SpO2 Levels",
74
+ template="plotly_dark",
75
+ marginal_x="histogram",
76
+ marginal_y="histogram"
77
+ )
78
+ fig_scatter.update_layout(
79
+ paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)',
80
+ height=450, legend=dict(orientation="h", yanchor="bottom", y=-0.35)
81
+ )
82
+ st.plotly_chart(fig_scatter, use_container_width=True)
83
+
84
+ with c2:
85
+ st.markdown("##### Temperature by Condition (Violin Plot)")
86
+ fig_violin = px.violin(
87
+ df, x="Predicted Disease", y="Body Temperature (°C)",
88
+ color="Predicted Disease",
89
+ color_discrete_map=DISEASE_COLORS,
90
+ box=True, points=False,
91
+ title="Body Temperature Distribution",
92
+ template="plotly_dark"
93
+ )
94
+ fig_violin.update_layout(
95
+ paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)',
96
+ height=450, showlegend=False, xaxis_title="Condition", yaxis_title="Temperature (°C)"
97
+ )
98
+ st.plotly_chart(fig_violin, use_container_width=True)
99
+
100
+ st.markdown("---")
101
+
102
+ c3, c4 = st.columns(2)
103
+ with c3:
104
+ st.markdown("##### 🏥 Disease Distribution (Population Count)")
105
+ counts = df["Predicted Disease"].value_counts().reset_index()
106
+ counts.columns = ["Condition", "Patient Count"]
107
+ fig_bar = px.bar(
108
+ counts, x="Condition", y="Patient Count",
109
+ color="Condition",
110
+ color_discrete_map=DISEASE_COLORS,
111
+ title="Patient Count per Condition",
112
+ template="plotly_dark",
113
+ text="Patient Count"
114
+ )
115
+ fig_bar.update_traces(textposition='outside')
116
+ fig_bar.update_layout(
117
+ paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)',
118
+ height=420, showlegend=False
119
+ )
120
+ st.plotly_chart(fig_bar, use_container_width=True)
121
+
122
+ with c4:
123
+ st.markdown("##### 🩸 Blood Pressure Density (Systolic vs Diastolic)")
124
+ fig_density = px.density_heatmap(
125
+ df, x="Systolic Blood Pressure (mmHg)", y="Diastolic Blood Pressure (mmHg)",
126
+ color_continuous_scale="Turbo",
127
+ title="Blood Pressure Population Heatmap",
128
+ template="plotly_dark",
129
+ nbinsx=30, nbinsy=30
130
+ )
131
+ fig_density.update_layout(
132
+ paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)',
133
+ height=420
134
+ )
135
+ st.plotly_chart(fig_density, use_container_width=True)
136
+
137
+ st.markdown("---")
138
+ st.markdown("### 📈 Patient-Specific Real-Time Monitoring")
139
+ p_id = st.selectbox("Select Patient ID to view live telemetry:", df['Patient Number'].unique()[:50])
140
+ if p_id:
141
+ patient_data = df[df['Patient Number'] == p_id].iloc[0]
142
+ disease = patient_data['Predicted Disease']
143
+ d_color = DISEASE_COLORS.get(disease, "#636efa")
144
+ st.markdown(f"**Patient Condition:** <span style='color:{d_color}; font-weight:bold'>{disease}</span>", unsafe_allow_html=True)
145
+
146
+ periods = 60
147
+ dates = pd.date_range(end=pd.Timestamp.now(), periods=periods, freq='25s')
148
+ base_hr = patient_data['Heart Rate (bpm)']
149
+ base_spo2 = patient_data['SpO2 Level (%)']
150
+ base_temp = patient_data['Body Temperature (°C)']
151
+ sim_hr = base_hr + np.random.normal(0, 1.2, periods).cumsum()
152
+ sim_spo2 = np.clip(base_spo2 + np.random.normal(0, 0.3, periods).cumsum(), 85, 100)
153
+ sim_temp = base_temp + np.random.normal(0, 0.05, periods).cumsum()
154
+
155
+ tc1, tc2, tc3 = st.columns(3)
156
+ with tc1:
157
+ fig_hr = go.Figure(go.Scatter(x=dates, y=sim_hr, mode='lines+markers', line=dict(color='#00f2fe', width=2), marker=dict(size=3), name="Heart Rate"))
158
+ fig_hr.update_layout(title="Heart Rate (bpm)", template="plotly_dark", paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)', height=280, margin=dict(l=10, r=10, t=40, b=40))
159
+ st.plotly_chart(fig_hr, use_container_width=True)
160
+ with tc2:
161
+ fig_sp = go.Figure(go.Scatter(x=dates, y=sim_spo2, mode='lines+markers', line=dict(color='#00cc96', width=2), marker=dict(size=3), name="SpO2"))
162
+ fig_sp.update_layout(title="SpO2 Level (%)", template="plotly_dark", paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)', height=280, margin=dict(l=10, r=10, t=40, b=40))
163
+ st.plotly_chart(fig_sp, use_container_width=True)
164
+ with tc3:
165
+ fig_tp = go.Figure(go.Scatter(x=dates, y=sim_temp, mode='lines+markers', line=dict(color='#ffa15a', width=2), marker=dict(size=3), name="Temperature"))
166
+ fig_tp.update_layout(title="Body Temperature (°C)", template="plotly_dark", paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)', height=280, margin=dict(l=10, r=10, t=40, b=40))
167
+ st.plotly_chart(fig_tp, use_container_width=True)
168
+
169
+ st.markdown("---")
170
+ st.markdown("### 📋 Raw Patient Data")
171
+ st.dataframe(df, use_container_width=True, height=350)
frontend/pages/3_Edge_Deployment.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import requests
3
+ import os
4
+
5
+ st.set_page_config(page_title="TinyML Edge Deploy", page_icon="⚙️", layout="wide")
6
+ API_URL = os.getenv("API_URL", "http://localhost:8000")
7
+
8
+ st.title("⚙️ TinyML C-Code Exporter")
9
+ st.markdown("Export trained models directly to raw C code for compilation on embedded devices (Arduino, ESP32).")
10
+
11
+ model_mapping = {
12
+ "Random Forest": "rf",
13
+ "Logistic Regression": "logreg",
14
+ "K-Nearest Neighbors": "knn",
15
+ "Support Vector Machine": "svm",
16
+ "Neural Network (MLP)": "small_nn"
17
+ }
18
+
19
+ hw_mapping = {
20
+ "ESP32 (240 MHz, 520KB SRAM)": {"clock": 240, "ram": 520, "arch": "Xtensa LX6"},
21
+ "Arduino Nano 33 BLE Sense (64 MHz, 256KB SRAM)": {"clock": 64, "ram": 256, "arch": "Cortex-M4F"},
22
+ "STM32 Nucleo (84 MHz, 96KB SRAM)": {"clock": 84, "ram": 96, "arch": "Cortex-M4"},
23
+ "Raspberry Pi Pico (133 MHz, 264KB SRAM)": {"clock": 133, "ram": 264, "arch": "Cortex-M0+"}
24
+ }
25
+
26
+ col_m, col_h = st.columns(2)
27
+ with col_m:
28
+ model_display_name = st.selectbox("Select Model to Export", list(model_mapping.keys()))
29
+ model_choice = model_mapping[model_display_name]
30
+ with col_h:
31
+ hw_display_name = st.selectbox("Select Target Hardware Profile", list(hw_mapping.keys()))
32
+ hw_choice = hw_mapping[hw_display_name]
33
+
34
+ st.markdown("---")
35
+ st.markdown("##### ⚡ TinyML Optimization Settings")
36
+ opt_c1, opt_c2 = st.columns(2)
37
+ with opt_c1:
38
+ apply_quantization = st.checkbox("Apply INT8 Weight Quantization", value=True)
39
+ with opt_c2:
40
+ strip_comments = st.checkbox("Strip Comments for Minimal Flash Size", value=False)
41
+
42
+ if st.button("Generate C Code", type="primary"):
43
+ with st.spinner("Transpiling model to C..."):
44
+ try:
45
+ resp = requests.get(f"{API_URL}/export_tinyml", params={"model_name": model_choice, "quantize": apply_quantization}, timeout=15)
46
+ if resp.status_code == 200:
47
+ data = resp.json()
48
+ if "error" in data:
49
+ st.error(data["error"])
50
+ else:
51
+ c_code = data["code"]
52
+
53
+ if strip_comments:
54
+ import re
55
+ c_code = re.sub(r'/\*.*?\*/', '', c_code, flags=re.DOTALL)
56
+ c_code = re.sub(r'//.*', '', c_code)
57
+ c_code = re.sub(r'\n\s*\n', '\n', c_code)
58
+
59
+ st.success(f"✅ Transpilation successful for **{model_display_name}**!")
60
+
61
+ # Show stats
62
+ s1, s2, s3, s4 = st.columns(4)
63
+ code_len = len(c_code)
64
+ est_flash = code_len // (4 if apply_quantization else 1)
65
+
66
+ # Simple heuristic for estimated inference time
67
+ base_ops = {"rf": 500, "logreg": 20, "knn": 2000, "svm": 100, "small_nn": 800}[model_choice]
68
+ ops_multiplier = 0.5 if apply_quantization else 1.0
69
+ est_latency_us = int((base_ops * ops_multiplier) / (hw_choice["clock"] / 10.0))
70
+
71
+ s1.metric("Target Architecture", hw_choice["arch"])
72
+ s2.metric("Est. Code Flash", f"{est_flash:,} Bytes")
73
+ s3.metric("Precision", "INT8 (8-bit)" if apply_quantization else "FP32 (64-bit)")
74
+ s4.metric("Est. Inference Latency", f"~{max(1, est_latency_us)} µs")
75
+
76
+ st.download_button(
77
+ label="⬇️ Download tinyml_model.h",
78
+ data=c_code,
79
+ file_name=f"tinyml_{model_choice}.h",
80
+ mime="text/plain"
81
+ )
82
+ with st.expander("Preview C Code", expanded=True):
83
+ st.code(c_code, language="c")
84
+ else:
85
+ st.error("API returned an error code.")
86
+ except Exception as e:
87
+ st.error(f"Connection failed: {e}")
frontend/pages/4_Patient_History.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import requests
3
+ import pandas as pd
4
+ import os
5
+
6
+ st.set_page_config(page_title="Patient History", page_icon="🗄️", layout="wide")
7
+ API_URL = os.getenv("API_URL", "http://localhost:8000")
8
+
9
+ st.title("🗄️ Patient Prediction History")
10
+ st.markdown("Live database of all patient predictions and vitals logged by the system. Auto-refreshes every 5 seconds.")
11
+
12
+ DISEASE_COLORS = {
13
+ "Healthy": "background-color: rgba(0, 204, 150, 0.15); color: #00cc96;",
14
+ "Asthma": "background-color: rgba(171, 99, 250, 0.15); color: #ab63fa;",
15
+ "Diabetes Mellitus": "background-color: rgba(255, 161, 90, 0.15); color: #ffa15a;",
16
+ "Heart Disease": "background-color: rgba(239, 85, 59, 0.15); color: #ef553b;",
17
+ "Hypertension": "background-color: rgba(99, 110, 250, 0.15); color: #636efa;",
18
+ }
19
+
20
+ COLUMN_RENAME = {
21
+ "id": "ID",
22
+ "timestamp": "Timestamp",
23
+ "prediction_label": "Diagnosis",
24
+ "confidence": "Confidence",
25
+ "heart_rate": "Heart Rate",
26
+ "spo2": "SpO2 %",
27
+ "sys_bp": "Sys BP",
28
+ "dia_bp": "Dia BP",
29
+ "temp": "Temp °C",
30
+ "fall_detection": "Fall"
31
+ }
32
+
33
+ @st.fragment(run_every="5s")
34
+ def live_history_table():
35
+ try:
36
+ resp = requests.get(f"{API_URL}/history", timeout=5)
37
+ if resp.status_code == 200:
38
+ history_data = resp.json()
39
+ if not history_data:
40
+ st.info("No prediction history found. Go make some predictions first!")
41
+ return
42
+
43
+ df = pd.DataFrame(history_data)
44
+ cols = ['id', 'timestamp', 'prediction_label', 'confidence', 'heart_rate', 'spo2', 'sys_bp', 'dia_bp', 'temp', 'fall_detection']
45
+ df = df[[c for c in cols if c in df.columns]]
46
+ df.rename(columns=COLUMN_RENAME, inplace=True)
47
+
48
+ if "Confidence" in df.columns:
49
+ df["Confidence"] = df["Confidence"].apply(lambda x: f"{x*100:.1f}%" if x <= 1 else f"{x:.1f}%")
50
+
51
+ def color_by_diagnosis(row):
52
+ diagnosis = row.get("Diagnosis", "")
53
+ style_str = DISEASE_COLORS.get(diagnosis, "")
54
+ return [style_str] * len(row)
55
+
56
+ styled = df.style.apply(color_by_diagnosis, axis=1)
57
+
58
+ st.dataframe(styled, width="stretch", height=600)
59
+ st.caption(f"Showing {len(df)} records. Auto-refreshing every 5s.")
60
+ else:
61
+ st.error("API Error")
62
+ except Exception as e:
63
+ st.error(f"Failed to fetch history: {e}")
64
+
65
+ live_history_table()
frontend/pages/5_MLOps_Settings.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import requests
3
+ import os
4
+
5
+ st.set_page_config(page_title="MLOps Settings", page_icon="🔄", layout="wide")
6
+ API_URL = os.getenv("API_URL", "http://localhost:8000")
7
+
8
+ st.title("🔄 MLOps & Retraining")
9
+ st.markdown("Dynamically update the TinyML ensemble models by providing a new dataset.")
10
+
11
+ st.info("Upload a CSV file with the standard clinical schema to retrain all models on the fly.")
12
+
13
+ uploaded_file = st.file_uploader("Upload New Patient Dataset (CSV)", type="csv")
14
+
15
+ if uploaded_file is not None:
16
+ if st.button("Start Retraining Pipeline"):
17
+ with st.spinner("Uploading and retraining ensemble... This may take a minute."):
18
+ try:
19
+ files = {"file": (uploaded_file.name, uploaded_file.getvalue(), "text/csv")}
20
+ resp = requests.post(f"{API_URL}/retrain", files=files, timeout=120)
21
+ if resp.status_code == 200:
22
+ data = resp.json()
23
+ if "error" in data:
24
+ st.error(f"Retraining failed: {data['error']}")
25
+ else:
26
+ st.success(data.get("message", "Success!"))
27
+ else:
28
+ st.error("API returned an error.")
29
+ except Exception as e:
30
+ st.error(f"Connection failed: {e}")
frontend/pages/6_About.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import graphviz
3
+
4
+ st.set_page_config(page_title="About Project", page_icon="ℹ️", layout="wide")
5
+
6
+ st.markdown("""
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
9
+ html, body, [class*="css"] { font-family: 'Inter', sans-serif; }
10
+
11
+ .tech-badge {
12
+ display: inline-block; padding: 4px 12px; margin: 0 6px 8px 0;
13
+ border-radius: 20px; font-size: 0.82rem; font-weight: 600;
14
+ background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.15);
15
+ color: #e0e0e0; transition: all 0.2s ease;
16
+ }
17
+ .tech-badge:hover {
18
+ background: rgba(0, 242, 254, 0.1);
19
+ border-color: #00f2fe; color: #00f2fe;
20
+ transform: translateY(-1px);
21
+ }
22
+ .live-link {
23
+ text-decoration: none; font-weight: 500; color: #00f2fe;
24
+ }
25
+ .live-link:hover { text-decoration: underline; }
26
+ </style>
27
+ """, unsafe_allow_html=True)
28
+
29
+ st.title("ℹ️ About the Platform")
30
+
31
+ st.markdown("""
32
+ This platform is a prototype of an end-to-end Machine Learning pipeline specifically designed to operate natively on resource-constrained embedded edge hardware (**TinyML**), targeting microcontrollers such as the **ESP32** or **ARM Cortex-M** architectures.
33
+
34
+ **Live Environments:**
35
+ * 🖥️ **Frontend Suite:** <a href="https://tinyml-heart-health-monitoring-dashboard-8xqogy2hibtlayt7popvs.streamlit.app/" target="_blank" class="live-link">Streamlit Cloud Deployment</a>
36
+ * ⚙️ **Backend Hub:** <a href="https://huggingface.co/spaces/IDKHowToCodeFr/tinyml-backend" target="_blank" class="live-link">Hugging Face Spaces API</a>
37
+
38
+ <div style="margin-top: 15px;">
39
+ <span class="tech-badge">Python 3.9+</span>
40
+ <span class="tech-badge">FastAPI</span>
41
+ <span class="tech-badge">Streamlit</span>
42
+ <span class="tech-badge">Scikit-Learn</span>
43
+ <span class="tech-badge">SHAP</span>
44
+ <span class="tech-badge">Docker</span>
45
+ <span class="tech-badge">PyTest</span>
46
+ </div>
47
+ """, unsafe_allow_html=True)
48
+
49
+ st.markdown("---")
50
+
51
+ st.markdown("### 🏗️ System Architecture")
52
+ # Create a Graphviz flowchart for the About page
53
+ graph = graphviz.Digraph(node_attr={'shape': 'box', 'style': 'rounded,filled', 'fontname': 'Helvetica', 'margin': '0.2'}, graph_attr={'rankdir': 'TB', 'splines': 'ortho', 'nodesep': '0.8'})
54
+
55
+ # Define UI & API nodes at the top
56
+ graph.node('UI', '🖥️ Streamlit Frontend\nInteractive Dashboard', fillcolor='#ff4b4b', fontcolor='white', color='#a30000', penwidth='2')
57
+ graph.node('API', '⚙️ FastAPI Backend\nRouting & API Logic', fillcolor='#009688', fontcolor='white', color='#004d40', penwidth='2')
58
+ graph.node('DB', '🗄️ SQLite DB\nPatient History', shape='cylinder', fillcolor='#9b59b6', fontcolor='white', color='#4a235a', penwidth='2')
59
+
60
+ # Core connections
61
+ graph.edge('UI', 'API', label=' REST JSON', color='#666666', fontcolor='#666666')
62
+ graph.edge('API', 'DB', label=' Read/Write', color='#666666', fontcolor='#666666')
63
+
64
+ # Machine Learning Subgraph
65
+ with graph.subgraph(name='cluster_ml') as ml:
66
+ ml.attr(label='🧠 Machine Learning & Inference Pipeline', style='dashed', color='#aaaaaa', fontcolor='#888888')
67
+ ml.node('ENS', 'Ensemble Engine\nSoft-Voting Aggregator', shape='diamond', fillcolor='#f2c94c', fontcolor='black', color='#b28900', penwidth='2')
68
+ ml.node('MODELS', 'Classifiers\n(RF, SVM, LogReg, NN, KNN)', fillcolor='#f2c94c', fontcolor='black', color='#b28900', penwidth='2')
69
+ ml.node('SHAP', '🔍 SHAP Explainer\nFeature Impact Analysis', fillcolor='#f2c94c', fontcolor='black', color='#b28900', penwidth='2')
70
+
71
+ # Hardware Export Subgraph
72
+ with graph.subgraph(name='cluster_edge') as edge:
73
+ edge.attr(label='⚡ Hardware & Edge AI Export', style='dashed', color='#aaaaaa', fontcolor='#888888')
74
+ edge.node('TRANS', 'C-Code Transpiler\nINT8 Quantization', fillcolor='#2d9cdb', fontcolor='white', color='#106093', penwidth='2')
75
+ edge.node('HEADER', 'tinyml_model.h\nOptimized Header File', shape='note', fillcolor='#2d9cdb', fontcolor='white', color='#106093', penwidth='2')
76
+ edge.node('MCU', '📟 ESP32 / ARM Cortex-M\nMicrocontroller Node', fillcolor='#2d9cdb', fontcolor='white', color='#106093', penwidth='2')
77
+
78
+ # Map Cross-cluster edges
79
+ graph.edge('API', 'ENS', label=' Predict', color='#333333')
80
+ graph.edge('ENS', 'MODELS', color='#333333')
81
+
82
+ graph.edge('API', 'SHAP', label=' Explain', color='#333333')
83
+ graph.edge('SHAP', 'MODELS', style='dashed', label=' Analyzes Trees', fontcolor='#555555', color='#555555')
84
+
85
+ graph.edge('API', 'TRANS', label=' Export Config', color='#333333')
86
+ graph.edge('TRANS', 'HEADER', label=' Scale FP32 to INT8', fontcolor='#555555', color='#555555')
87
+ graph.edge('HEADER', 'MCU', label=' Flash Firmware', color='#333333')
88
+
89
+ st.graphviz_chart(graph, use_container_width=True)
90
+
91
+ st.markdown("---")
92
+ st.markdown("### ✨ Core Capabilities")
93
+
94
+ col1, col2 = st.columns(2)
95
+
96
+ with col1:
97
+ st.info("""
98
+ **⚡ TinyML & Edge AI Optimization**\n
99
+ Traditional machine learning models process inference entirely in the cloud, generating heavy dependency on network transmission. This project supports **Edge AI**, converting Python-trained traditional classifiers into compiled C-code arrays for offline deployment on limited hardware.
100
+ We implement **INT8 Quantization**, scaling 64-bit Floating Point (`double`) coefficients down into 8-bit integers (`int8_t`). This scaling effectively shrinks memory payload by nearly 75% on microcontrollers.
101
+ """)
102
+
103
+ st.success("""
104
+ **🤝 Ensemble Machine Learning**\n
105
+ The predictive core relies on an Ensemble Engine. Different constituent models (KNN, Random Forest, SVM, Small NN, Logistic Regression) independently evaluate patient input variables and compute probabilties. A Soft-Voting technique averages their confidence scores to produce an overall healthy/at-risk prediction.
106
+ """)
107
+
108
+ with col2:
109
+ st.warning("""
110
+ **🔍 SHAP Clinical Explainability**\n
111
+ In the medical space, a black-box model is often untrusted. The project leverages SHapley Additive exPlanations (**SHAP**) natively connected to the Random Forest model layer. SHAP assigns absolute baseline impact scores to every incoming feature, interpreting *why* the AI predicted a specific condition and quantifying mathematically whether individual factors act as risk vectors or protective measures.
112
+ """)
113
+
114
+ st.error("""
115
+ **🔄 Continuous Learning (MLOps)**\n
116
+ System robustness requires adaptability. The **MLOps settings** permit uploading newly captured CSV datasets spanning emerging patient patterns. Submitting retraining logic prompts the backend pipeline to independently spin up `Scikit-Learn` algorithms to overwrite `.pkl` binary files, seamlessly refreshing the intelligence available on the dashboard.
117
+ """)
118
+
119
+ st.markdown("""
120
+ <div style="text-align: center; margin-top: 40px; padding-top: 20px; border-top: 1px solid rgba(255,255,255,0.1); color: rgba(255,255,255,0.5); font-size: 0.85rem;">
121
+ Designed as a scalable, fully interpretable Edge-based Machine Learning architecture for modern Healthcare telemetry.<br>
122
+ MIT License
123
+ </div>
124
+ """, unsafe_allow_html=True)
frontend/requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ streamlit>=1.36.0
2
+ pandas>=2.2.2
3
+ numpy>=1.26.4
4
+ plotly>=5.22.0
5
+ requests>=2.32.3
6
+ scikit-learn>=1.5.1
7
+ joblib>=1.4.2
8
+ matplotlib
9
+ graphviz>=0.20.1
requirements.txt CHANGED
@@ -1,11 +1,11 @@
1
- fastapi
2
- uvicorn
3
- scikit-learn==1.8.0
4
- pandas
5
- numpy
6
- joblib
7
- shap
8
- m2cgen
9
- matplotlib
10
- python-multipart
11
- huggingface_hub
 
1
+ # --- STREAMLIT CLOUD DEPLOYMENT REQUIREMENTS ---
2
+ # (Merged backend + frontend libs to satisfy Streamlit Commmunity Cloud deployment)
3
+ fastapi>=0.111.0
4
+ uvicorn>=0.30.1
5
+ scikit-learn>=1.5.1
6
+ pandas>=2.2.2
7
+ numpy>=1.26.4
8
+ joblib>=1.4.2
9
+ streamlit>=1.36.0
10
+ plotly>=5.22.0
11
+ requests>=2.32.3
run.bat ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ echo ==============================================
3
+ echo TinyML Dashboard Launch Sequence
4
+ echo ==============================================
5
+
6
+ echo [1/3] Installing/Verifying Dependencies...
7
+ python -m pip install -r backend\requirements.txt
8
+ python -m pip install -r frontend\requirements.txt
9
+
10
+ echo [2/3] Spinning up FastAPI Backend...
11
+ echo Clearing previous instances on port 8000 (if any)...
12
+ for /f "tokens=5" %%a in ('netstat -aon ^| find ":8000" ^| find "LISTENING"') do taskkill /f /pid %%a >nul 2>&1
13
+ start /B "TinyML Backend" cmd /c "cd backend && python -m uvicorn main:app --host 0.0.0.0 --port 8000"
14
+
15
+ echo [3/3] Launching Streamlit Frontend...
16
+ cd frontend
17
+ python -m streamlit run Home.py
18
+
tests/test_backend.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from fastapi.testclient import TestClient
3
+ import sys
4
+ import os
5
+
6
+ # Ensure backend is in path
7
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../backend')))
8
+ from main import app
9
+
10
+ client = TestClient(app)
11
+
12
+ def test_health_check():
13
+ """Test that the backend API is up and running."""
14
+ # Assuming there varies a root or health endpoint. If not, this is a placeholder.
15
+ response = client.get("/")
16
+ # Even if it's 404 because no root exists, the server responds.
17
+ assert response.status_code in [200, 404]
18
+
19
+ def test_predict_endpoint_validation():
20
+ """Test that the predict endpoint rejects invalid data."""
21
+ payload = {
22
+ "Heart_Rate": "invalid_string",
23
+ "SpO2_Level": 98
24
+ }
25
+ response = client.post("/predict", json=payload)
26
+ assert response.status_code == 422 # Unprocessable Entity (Pydantic validation)
27
+