Spaces:
Runtime error
Runtime error
Upload folder using huggingface_hub
Browse files- .dockerignore +2 -0
- Dockerfile +2 -4
- database.py +0 -98
- ensemble.py +0 -48
- frontend/.streamlit/config.toml +0 -6
- frontend/Home.py +0 -310
- frontend/pages/1_Predict_Dashboard.py +0 -326
- frontend/pages/2_Data_Analytics.py +0 -221
- frontend/pages/3_Edge_Deployment.py +0 -87
- frontend/pages/4_Patient_History.py +0 -79
- frontend/pages/5_MLOps_Settings.py +0 -48
- frontend/pages/6_About.py +0 -196
- frontend/requirements.txt +0 -9
- main.py +0 -501
- models.py +0 -39
- preprocessing.py +0 -62
.dockerignore
CHANGED
|
@@ -3,3 +3,5 @@ __pycache__/
|
|
| 3 |
*.pyc
|
| 4 |
.pytest_cache/
|
| 5 |
.DS_Store
|
|
|
|
|
|
|
|
|
| 3 |
*.pyc
|
| 4 |
.pytest_cache/
|
| 5 |
.DS_Store
|
| 6 |
+
frontend/
|
| 7 |
+
|
Dockerfile
CHANGED
|
@@ -11,10 +11,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
| 11 |
COPY backend/requirements.txt .
|
| 12 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 13 |
|
| 14 |
-
# Copy necessary
|
| 15 |
-
COPY
|
| 16 |
-
COPY model/ ./model/
|
| 17 |
-
COPY data/ ./data/
|
| 18 |
|
| 19 |
# Hugging Face Spaces mandates port 7860
|
| 20 |
EXPOSE 7860
|
|
|
|
| 11 |
COPY backend/requirements.txt .
|
| 12 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 13 |
|
| 14 |
+
# Copy all necessary files and directories
|
| 15 |
+
COPY . .
|
|
|
|
|
|
|
| 16 |
|
| 17 |
# Hugging Face Spaces mandates port 7860
|
| 18 |
EXPOSE 7860
|
database.py
DELETED
|
@@ -1,98 +0,0 @@
|
|
| 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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ensemble.py
DELETED
|
@@ -1,48 +0,0 @@
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/.streamlit/config.toml
DELETED
|
@@ -1,6 +0,0 @@
|
|
| 1 |
-
[theme]
|
| 2 |
-
primaryColor="#00f2fe"
|
| 3 |
-
backgroundColor="#0e1117"
|
| 4 |
-
secondaryBackgroundColor="#262730"
|
| 5 |
-
textColor="#fafafa"
|
| 6 |
-
font="sans serif"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/Home.py
DELETED
|
@@ -1,310 +0,0 @@
|
|
| 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 |
-
from datetime import timedelta
|
| 9 |
-
|
| 10 |
-
API_URL = os.getenv("API_URL", "http://localhost:8000")
|
| 11 |
-
if "hf.space" in os.getenv("SPACE_ID", ""):
|
| 12 |
-
# On HF Spaces, the backend might be on a subpath or the same port
|
| 13 |
-
# But for this setup, we assume it's running on localhost:8000 internally as per app.py
|
| 14 |
-
API_URL = "http://localhost:8000"
|
| 15 |
-
|
| 16 |
-
st.set_page_config(page_title="TinyML Heart Health Monitoring", page_icon="🫀", layout="wide")
|
| 17 |
-
|
| 18 |
-
st.markdown("""
|
| 19 |
-
<style>
|
| 20 |
-
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
|
| 21 |
-
html, body, [class*="css"] { font-family: 'Inter', sans-serif; }
|
| 22 |
-
|
| 23 |
-
.hero-sub {
|
| 24 |
-
font-size: 1.15rem; color: rgba(255,255,255,0.55);
|
| 25 |
-
margin-top: 8px; font-weight: 300; letter-spacing: 0.3px;
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
.metrics-grid {
|
| 29 |
-
display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 16px;
|
| 30 |
-
}
|
| 31 |
-
.glass-metric {
|
| 32 |
-
background: rgba(255,255,255,0.04);
|
| 33 |
-
backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
|
| 34 |
-
border: 1px solid rgba(255,255,255,0.08); border-radius: 16px;
|
| 35 |
-
padding: 24px 20px; text-align: center;
|
| 36 |
-
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
| 37 |
-
min-height: 160px; display: flex; flex-direction: column;
|
| 38 |
-
justify-content: center; align-items: center;
|
| 39 |
-
}
|
| 40 |
-
.glass-metric:hover {
|
| 41 |
-
transform: translateY(-3px);
|
| 42 |
-
box-shadow: 0 12px 40px rgba(0,242,254,0.12);
|
| 43 |
-
border-color: rgba(255,255,255,0.15);
|
| 44 |
-
}
|
| 45 |
-
.metric-value {
|
| 46 |
-
font-size: 2.4rem; font-weight: 800; margin: 8px 0 6px 0; letter-spacing: -0.5px;
|
| 47 |
-
}
|
| 48 |
-
.metric-label {
|
| 49 |
-
font-size: 0.78rem; color: rgba(255,255,255,0.45);
|
| 50 |
-
text-transform: uppercase; letter-spacing: 2px; font-weight: 600;
|
| 51 |
-
}
|
| 52 |
-
.metric-delta {
|
| 53 |
-
font-size: 0.8rem; font-weight: 500; margin-top: 6px;
|
| 54 |
-
padding: 3px 10px; border-radius: 20px; display: inline-block;
|
| 55 |
-
}
|
| 56 |
-
.delta-good { color: #00cc96; background: rgba(0,204,150,0.12); }
|
| 57 |
-
.delta-bad { color: #ef553b; background: rgba(239,85,59,0.12); }
|
| 58 |
-
|
| 59 |
-
.status-badge {
|
| 60 |
-
display: inline-block; padding: 6px 18px; border-radius: 50px;
|
| 61 |
-
font-size: 0.85rem; font-weight: 600; letter-spacing: 0.5px;
|
| 62 |
-
}
|
| 63 |
-
.status-online { background: rgba(0,204,150,0.15); color: #00cc96; border: 1px solid rgba(0,204,150,0.3); }
|
| 64 |
-
.status-fallback { background: rgba(99,110,250,0.15); color: #636efa; border: 1px solid rgba(99,110,250,0.3); }
|
| 65 |
-
|
| 66 |
-
.feature-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
|
| 67 |
-
.feature-card {
|
| 68 |
-
background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06);
|
| 69 |
-
border-radius: 14px; padding: 24px 20px; min-height: 170px;
|
| 70 |
-
display: flex; flex-direction: column;
|
| 71 |
-
transition: border-color 0.2s ease, transform 0.2s ease;
|
| 72 |
-
}
|
| 73 |
-
.feature-card:hover { border-color: rgba(0,242,254,0.25); transform: translateY(-2px); }
|
| 74 |
-
.feature-card .fc-icon { font-size: 1.6rem; margin-bottom: 10px; }
|
| 75 |
-
.feature-card .fc-title { color: #00f2fe; font-size: 0.95rem; font-weight: 700; margin-bottom: 8px; }
|
| 76 |
-
.feature-card .fc-desc { color: rgba(255,255,255,0.45); font-size: 0.82rem; line-height: 1.55; flex-grow: 1; }
|
| 77 |
-
</style>
|
| 78 |
-
""", unsafe_allow_html=True)
|
| 79 |
-
|
| 80 |
-
# ──────── Hero ────────
|
| 81 |
-
st.title("🫀 TinyML Heart Health Monitoring Dashboard")
|
| 82 |
-
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)
|
| 83 |
-
st.markdown("<br>", unsafe_allow_html=True)
|
| 84 |
-
|
| 85 |
-
# ──────── System Status ────────
|
| 86 |
-
try:
|
| 87 |
-
resp = requests.get(f"{API_URL}/health", timeout=5)
|
| 88 |
-
if resp.status_code == 200:
|
| 89 |
-
status = resp.json().get("status", "Unknown")
|
| 90 |
-
is_online = "Healthy" in status
|
| 91 |
-
else:
|
| 92 |
-
status = f"Backend Error ({resp.status_code})"
|
| 93 |
-
is_online = False
|
| 94 |
-
except Exception as e:
|
| 95 |
-
status = f"Offline / Monolith Fallback ({str(e)[:50]})"
|
| 96 |
-
is_online = False
|
| 97 |
-
|
| 98 |
-
badge_class = "status-online" if is_online else "status-fallback"
|
| 99 |
-
badge_text = "● ONLINE" if is_online else "● CLOUD MODE"
|
| 100 |
-
st.markdown(f'<span class="status-badge {badge_class}">{badge_text}</span> <span style="color:rgba(255,255,255,0.35);font-size:0.85rem;">{status}</span>', unsafe_allow_html=True)
|
| 101 |
-
st.markdown("---")
|
| 102 |
-
|
| 103 |
-
# ──────── Dataset constants ────────
|
| 104 |
-
HR_MIN, HR_MAX, HR_AVG = 50.0, 139.0, 94.6
|
| 105 |
-
SPO2_MIN, SPO2_MAX, SPO2_AVG = 84.0, 100.0, 94.6
|
| 106 |
-
ALERT_MAX = 100
|
| 107 |
-
HR_NORMAL_LO, HR_NORMAL_HI = 60.0, 100.0
|
| 108 |
-
SPO2_NORMAL_LO = 95.0
|
| 109 |
-
CANDLE_COUNT = 60
|
| 110 |
-
TICK_INTERVAL = 1.5 # seconds
|
| 111 |
-
|
| 112 |
-
# ──────── Deterministic candle from timestamp ────────
|
| 113 |
-
def _alert_at(ts_epoch):
|
| 114 |
-
"""Deterministic alert count for a given epoch timestamp."""
|
| 115 |
-
rng = np.random.RandomState(int(ts_epoch * 10) % (2**31))
|
| 116 |
-
return int(rng.randint(0, ALERT_MAX + 1))
|
| 117 |
-
|
| 118 |
-
def _candle_at(t_epoch, prev_alerts, curr_alerts):
|
| 119 |
-
"""Build an OHLC candle from alert transitions."""
|
| 120 |
-
o = float(prev_alerts)
|
| 121 |
-
c = float(curr_alerts)
|
| 122 |
-
rng = np.random.RandomState(int(t_epoch * 7) % (2**31))
|
| 123 |
-
spread = abs(c - o) * 0.3 + rng.uniform(1, 4)
|
| 124 |
-
h = max(o, c) + spread
|
| 125 |
-
l = min(o, c) - rng.uniform(0.5, spread * 0.6)
|
| 126 |
-
h = min(h, ALERT_MAX + 5)
|
| 127 |
-
l = max(l, 0)
|
| 128 |
-
|
| 129 |
-
# Convert UTC timestamp to IST for display
|
| 130 |
-
# Use pandas to handle the epoch conversion and then add offset
|
| 131 |
-
ts = pd.to_datetime(t_epoch, unit='s')
|
| 132 |
-
if "hf.space" in os.getenv("SPACE_ID", ""):
|
| 133 |
-
ts = ts + timedelta(hours=5, minutes=30)
|
| 134 |
-
|
| 135 |
-
return {"time": ts, "open": round(o, 1), "high": round(h, 1), "low": round(l, 1), "close": round(c, 1)}
|
| 136 |
-
|
| 137 |
-
# ──────── Initialize: pre-fill chart as if running since startup ────────
|
| 138 |
-
if "chart_candles" not in st.session_state:
|
| 139 |
-
now_ep = time.time()
|
| 140 |
-
candles = []
|
| 141 |
-
prev = 50 # initial alert count
|
| 142 |
-
for i in range(CANDLE_COUNT):
|
| 143 |
-
t = now_ep - (CANDLE_COUNT - i) * TICK_INTERVAL
|
| 144 |
-
curr = _alert_at(t)
|
| 145 |
-
candles.append(_candle_at(t, prev, curr))
|
| 146 |
-
prev = curr
|
| 147 |
-
st.session_state.chart_candles = candles
|
| 148 |
-
st.session_state.last_alerts = prev
|
| 149 |
-
st.session_state.last_candle_epoch = now_ep
|
| 150 |
-
|
| 151 |
-
# ──────── Live Metrics ────────
|
| 152 |
-
@st.fragment(run_every="1.5s")
|
| 153 |
-
def live_metrics():
|
| 154 |
-
now_ep = time.time()
|
| 155 |
-
|
| 156 |
-
# Catch up: generate all candles that should have been created while away
|
| 157 |
-
last_ep = st.session_state.get("last_candle_epoch", now_ep - TICK_INTERVAL)
|
| 158 |
-
missed = int((now_ep - last_ep) / TICK_INTERVAL)
|
| 159 |
-
prev_a = st.session_state.get("last_alerts", 50)
|
| 160 |
-
|
| 161 |
-
if missed > 1:
|
| 162 |
-
# Backfill deterministically so chart stays continuous
|
| 163 |
-
for i in range(1, min(missed, CANDLE_COUNT) + 1):
|
| 164 |
-
t = last_ep + i * TICK_INTERVAL
|
| 165 |
-
curr_a = _alert_at(t)
|
| 166 |
-
st.session_state.chart_candles.append(_candle_at(t, prev_a, curr_a))
|
| 167 |
-
prev_a = curr_a
|
| 168 |
-
|
| 169 |
-
# Current tick
|
| 170 |
-
alerts = np.random.randint(0, ALERT_MAX + 1)
|
| 171 |
-
alert_delta = alerts - prev_a
|
| 172 |
-
st.session_state.chart_candles.append(_candle_at(now_ep, prev_a, alerts))
|
| 173 |
-
st.session_state.last_alerts = alerts
|
| 174 |
-
st.session_state.last_candle_epoch = now_ep
|
| 175 |
-
|
| 176 |
-
if len(st.session_state.chart_candles) > CANDLE_COUNT:
|
| 177 |
-
st.session_state.chart_candles = st.session_state.chart_candles[-CANDLE_COUNT:]
|
| 178 |
-
|
| 179 |
-
# Simulate patient cohort from alert count
|
| 180 |
-
n = max(alerts, 5)
|
| 181 |
-
sim_hr = np.clip(np.random.normal(HR_AVG, 12, n), HR_MIN, HR_MAX)
|
| 182 |
-
sim_o2 = np.clip(np.random.normal(SPO2_AVG, 3, n), SPO2_MIN, SPO2_MAX)
|
| 183 |
-
pop_hr = round(float(np.mean(sim_hr)), 1)
|
| 184 |
-
pop_o2 = round(float(np.mean(sim_o2)), 1)
|
| 185 |
-
hr_delta = round(pop_hr - HR_AVG, 1)
|
| 186 |
-
o2_delta = round(pop_o2 - SPO2_AVG, 1)
|
| 187 |
-
hr_ok = HR_NORMAL_LO <= pop_hr <= HR_NORMAL_HI
|
| 188 |
-
o2_ok = pop_o2 >= SPO2_NORMAL_LO
|
| 189 |
-
|
| 190 |
-
# Alert delta coloring: increase = bad (red), decrease = good (green)
|
| 191 |
-
ad_good = alert_delta <= 0
|
| 192 |
-
ad_c = "#00cc96" if ad_good else "#ef553b"
|
| 193 |
-
ad_d = "delta-good" if ad_good else "delta-bad"
|
| 194 |
-
ad_i = "▼" if ad_good else "▲"
|
| 195 |
-
ad_txt = f"{abs(alert_delta)} fewer" if ad_good else f"{alert_delta} more"
|
| 196 |
-
|
| 197 |
-
hr_c = "#00cc96" if hr_ok else "#ef553b"
|
| 198 |
-
hr_d = "delta-good" if hr_ok else "delta-bad"
|
| 199 |
-
hr_a = "▲" if hr_delta > 0 else "▼"
|
| 200 |
-
hr_s = "Within Normal" if hr_ok else "Abnormal"
|
| 201 |
-
o2_c = "#00cc96" if o2_ok else "#ef553b"
|
| 202 |
-
o2_d = "delta-good" if o2_ok else "delta-bad"
|
| 203 |
-
o2_a = "▲" if o2_delta > 0 else "▼"
|
| 204 |
-
o2_s = "Within Normal" if o2_ok else "Below Safe"
|
| 205 |
-
|
| 206 |
-
st.markdown(f"""
|
| 207 |
-
<div class="metrics-grid">
|
| 208 |
-
<div class="glass-metric">
|
| 209 |
-
<div class="metric-label">Total Patients</div>
|
| 210 |
-
<div class="metric-value" style="color:#4facfe;">60,000</div>
|
| 211 |
-
<div class="metric-delta delta-good">● Live Stream Active</div>
|
| 212 |
-
</div>
|
| 213 |
-
<div class="glass-metric">
|
| 214 |
-
<div class="metric-label">Active Alerts</div>
|
| 215 |
-
<div class="metric-value" style="color:{ad_c};">{alerts}</div>
|
| 216 |
-
<div class="metric-delta {ad_d}">{ad_i} {ad_txt} vs last</div>
|
| 217 |
-
</div>
|
| 218 |
-
<div class="glass-metric">
|
| 219 |
-
<div class="metric-label">Avg Heart Rate</div>
|
| 220 |
-
<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>
|
| 221 |
-
<div class="metric-delta {hr_d}">{hr_a} {abs(hr_delta)} bpm from avg • {hr_s}</div>
|
| 222 |
-
</div>
|
| 223 |
-
<div class="glass-metric">
|
| 224 |
-
<div class="metric-label">Avg SpO2 Level</div>
|
| 225 |
-
<div class="metric-value" style="color:{o2_c};">{pop_o2}<span style="font-size:1rem;color:rgba(255,255,255,0.3);">%</span></div>
|
| 226 |
-
<div class="metric-delta {o2_d}">{o2_a} {abs(o2_delta):.1f}% from avg • {o2_s}</div>
|
| 227 |
-
</div>
|
| 228 |
-
</div>
|
| 229 |
-
""", unsafe_allow_html=True)
|
| 230 |
-
|
| 231 |
-
# ──── Smoothed Activity Area Chart: tracks average heart-rate & alerts ────
|
| 232 |
-
st.markdown("##### 📡 Live Patient Emulation Trace")
|
| 233 |
-
|
| 234 |
-
cd = st.session_state.chart_candles
|
| 235 |
-
ts = [x["time"] for x in cd]
|
| 236 |
-
# Represent the primary metric as the close/current state
|
| 237 |
-
cl = [x["close"] for x in cd]
|
| 238 |
-
|
| 239 |
-
fig = go.Figure()
|
| 240 |
-
|
| 241 |
-
# Main Area Waveform
|
| 242 |
-
fig.add_trace(go.Scatter(
|
| 243 |
-
x=ts, y=cl,
|
| 244 |
-
mode='lines',
|
| 245 |
-
line=dict(color='#00f2fe', width=3, shape='spline', smoothing=1.3),
|
| 246 |
-
fill='tozeroy',
|
| 247 |
-
fillcolor='rgba(0, 242, 254, 0.15)',
|
| 248 |
-
name="Activity Index"
|
| 249 |
-
))
|
| 250 |
-
|
| 251 |
-
# Add a thin secondary baseline trend
|
| 252 |
-
w = min(10, len(cl))
|
| 253 |
-
ma = pd.Series(cl).rolling(window=w, min_periods=1).mean().tolist()
|
| 254 |
-
fig.add_trace(go.Scatter(
|
| 255 |
-
x=ts, y=ma, mode='lines',
|
| 256 |
-
line=dict(color='rgba(255, 255, 255, 0.4)', width=1.5, dash='dot'),
|
| 257 |
-
name="Moving Average"
|
| 258 |
-
))
|
| 259 |
-
|
| 260 |
-
fig.update_layout(
|
| 261 |
-
template="plotly_dark", height=320,
|
| 262 |
-
margin=dict(l=0, r=50, t=10, b=0),
|
| 263 |
-
paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)',
|
| 264 |
-
xaxis=dict(gridcolor='rgba(255,255,255,0.03)', showgrid=True, rangeslider=dict(visible=False)),
|
| 265 |
-
yaxis=dict(
|
| 266 |
-
title="Relative Activity",
|
| 267 |
-
gridcolor='rgba(255,255,255,0.05)',
|
| 268 |
-
title_font=dict(size=11),
|
| 269 |
-
side="right",
|
| 270 |
-
range=[0, ALERT_MAX + 15]
|
| 271 |
-
),
|
| 272 |
-
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1, font=dict(size=10)),
|
| 273 |
-
showlegend=True,
|
| 274 |
-
hovermode="x unified"
|
| 275 |
-
)
|
| 276 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 277 |
-
|
| 278 |
-
live_metrics()
|
| 279 |
-
|
| 280 |
-
st.markdown("---")
|
| 281 |
-
|
| 282 |
-
# ──────── Feature Cards ────────
|
| 283 |
-
st.markdown("##### 🧩 System Capabilities")
|
| 284 |
-
st.markdown("""
|
| 285 |
-
<div class="feature-grid">
|
| 286 |
-
<div class="feature-card">
|
| 287 |
-
<div class="fc-icon">🩺</div>
|
| 288 |
-
<div class="fc-title">Edge Prediction</div>
|
| 289 |
-
<div class="fc-desc">5-model weighted ensemble (KNN, SVM, LogReg, RF, MLP) with real-time consensus voting and confidence scoring.</div>
|
| 290 |
-
</div>
|
| 291 |
-
<div class="feature-card">
|
| 292 |
-
<div class="fc-icon">🔍</div>
|
| 293 |
-
<div class="fc-title">SHAP Explainability</div>
|
| 294 |
-
<div class="fc-desc">Feature-level SHAP analysis reveals exactly why the AI made each clinical decision with interactive Plotly charts.</div>
|
| 295 |
-
</div>
|
| 296 |
-
<div class="feature-card">
|
| 297 |
-
<div class="fc-icon">⚙️</div>
|
| 298 |
-
<div class="fc-title">C-Code Export</div>
|
| 299 |
-
<div class="fc-desc">Transpile any model to raw C headers with INT8 quantization simulation for ESP32 and Arduino flash deployment.</div>
|
| 300 |
-
</div>
|
| 301 |
-
<div class="feature-card">
|
| 302 |
-
<div class="fc-icon">🔄</div>
|
| 303 |
-
<div class="fc-title">MLOps Pipeline</div>
|
| 304 |
-
<div class="fc-desc">Dynamic dataset upload and live ensemble retraining without downtime. Full model hot-swap on the fly.</div>
|
| 305 |
-
</div>
|
| 306 |
-
</div>
|
| 307 |
-
""", unsafe_allow_html=True)
|
| 308 |
-
|
| 309 |
-
st.markdown("<br>", unsafe_allow_html=True)
|
| 310 |
-
st.caption("Use the sidebar to navigate between Prediction, Analytics, Edge Deployment, Patient History, MLOps, and About.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/pages/1_Predict_Dashboard.py
DELETED
|
@@ -1,326 +0,0 @@
|
|
| 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 |
-
if "hf.space" in os.getenv("SPACE_ID", ""):
|
| 9 |
-
API_URL = "http://localhost:8000"
|
| 10 |
-
|
| 11 |
-
st.title("🩺 Patient Edge Prediction Simulation")
|
| 12 |
-
|
| 13 |
-
st.markdown("""
|
| 14 |
-
<style>
|
| 15 |
-
div[data-testid="stMetricValue"] {
|
| 16 |
-
font-size: 1.5rem;
|
| 17 |
-
}
|
| 18 |
-
.red-card {
|
| 19 |
-
background: rgba(255, 50, 50, 0.15);
|
| 20 |
-
backdrop-filter: blur(10px);
|
| 21 |
-
-webkit-backdrop-filter: blur(10px);
|
| 22 |
-
padding: 20px;
|
| 23 |
-
border-radius: 15px;
|
| 24 |
-
border: 1px solid rgba(255, 50, 50, 0.3);
|
| 25 |
-
box-shadow: 0 8px 32px 0 rgba(255, 50, 50, 0.2);
|
| 26 |
-
}
|
| 27 |
-
.green-card {
|
| 28 |
-
background: rgba(50, 255, 100, 0.15);
|
| 29 |
-
backdrop-filter: blur(10px);
|
| 30 |
-
-webkit-backdrop-filter: blur(10px);
|
| 31 |
-
padding: 20px;
|
| 32 |
-
border-radius: 15px;
|
| 33 |
-
border: 1px solid rgba(50, 255, 100, 0.3);
|
| 34 |
-
box-shadow: 0 8px 32px 0 rgba(50, 255, 100, 0.2);
|
| 35 |
-
}
|
| 36 |
-
.amber-card {
|
| 37 |
-
background: rgba(255, 180, 50, 0.15);
|
| 38 |
-
backdrop-filter: blur(10px);
|
| 39 |
-
-webkit-backdrop-filter: blur(10px);
|
| 40 |
-
padding: 20px;
|
| 41 |
-
border-radius: 15px;
|
| 42 |
-
border: 1px solid rgba(255, 180, 50, 0.3);
|
| 43 |
-
box-shadow: 0 8px 32px 0 rgba(255, 180, 50, 0.2);
|
| 44 |
-
}
|
| 45 |
-
.node-row {
|
| 46 |
-
display: flex;
|
| 47 |
-
align-items: center;
|
| 48 |
-
justify-content: space-between;
|
| 49 |
-
padding: 8px 12px;
|
| 50 |
-
margin: 4px 0;
|
| 51 |
-
border-radius: 8px;
|
| 52 |
-
background: rgba(255,255,255,0.04);
|
| 53 |
-
border-left: 3px solid;
|
| 54 |
-
}
|
| 55 |
-
</style>
|
| 56 |
-
""", unsafe_allow_html=True)
|
| 57 |
-
|
| 58 |
-
# Persist random defaults using session_state keys
|
| 59 |
-
import random
|
| 60 |
-
if "sl_hr" not in st.session_state:
|
| 61 |
-
st.session_state.sl_hr = float(random.randint(60, 110))
|
| 62 |
-
st.session_state.sl_spo2 = float(random.randint(85, 99))
|
| 63 |
-
st.session_state.sl_sys = float(random.randint(110, 140))
|
| 64 |
-
st.session_state.sl_dia = float(random.randint(70, 90))
|
| 65 |
-
st.session_state.sl_tmp = round(random.uniform(36.0, 38.0), 1)
|
| 66 |
-
|
| 67 |
-
col1, col2 = st.columns(2)
|
| 68 |
-
with col1:
|
| 69 |
-
hr = st.slider("Heart Rate (bpm)", 40.0, 200.0, key="sl_hr")
|
| 70 |
-
spo2 = st.slider("SpO2 Level (%)", 70.0, 100.0, key="sl_spo2")
|
| 71 |
-
sys_bp = st.slider("Systolic BP (mmHg)", 70.0, 200.0, key="sl_sys")
|
| 72 |
-
|
| 73 |
-
with col2:
|
| 74 |
-
dia_bp = st.slider("Diastolic BP (mmHg)", 40.0, 130.0, key="sl_dia")
|
| 75 |
-
tmp = st.slider("Body Temp (°C)", 34.0, 42.0, key="sl_tmp")
|
| 76 |
-
fall = st.selectbox("Fall Detection", ["No", "Yes"])
|
| 77 |
-
|
| 78 |
-
MODEL_DISPLAY = {
|
| 79 |
-
"knn": "K-Nearest Neighbors",
|
| 80 |
-
"svm": "Support Vector Machine",
|
| 81 |
-
"logreg": "Logistic Regression",
|
| 82 |
-
"rf": "Random Forest",
|
| 83 |
-
"small_nn": "Neural Network (MLP)"
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
DISEASE_COLORS = {
|
| 87 |
-
"Healthy": "#00cc96",
|
| 88 |
-
"Asthma": "#ab63fa",
|
| 89 |
-
"Diabetes Mellitus": "#ffa15a",
|
| 90 |
-
"Heart Disease": "#ef553b",
|
| 91 |
-
"Hypertension": "#636efa"
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
def render_results(data):
|
| 95 |
-
st.markdown("---")
|
| 96 |
-
label = data['prediction_label']
|
| 97 |
-
conf = data['probability'] * 100
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
res_col1, res_col2 = st.columns([1, 1])
|
| 101 |
-
|
| 102 |
-
with res_col1:
|
| 103 |
-
st.markdown("#### 🏥 Diagnosis Status")
|
| 104 |
-
card_color = DISEASE_COLORS.get(label, "#636efa")
|
| 105 |
-
is_healthy = label == "Healthy"
|
| 106 |
-
card_class = "green-card" if is_healthy else "red-card" if label == "Heart Disease" else "amber-card"
|
| 107 |
-
status_text = f"Status: {label}" if is_healthy else f"Status: {label} (At Risk)"
|
| 108 |
-
st.markdown(f'<div class="{card_class}"><h3>{status_text}</h3></div>', unsafe_allow_html=True)
|
| 109 |
-
st.progress(int(conf))
|
| 110 |
-
st.write(f"**Ensemble Confidence:** {conf:.1f}%")
|
| 111 |
-
|
| 112 |
-
with res_col2:
|
| 113 |
-
st.markdown("#### 🧠 Individual Node Consensus & Weights")
|
| 114 |
-
for model_key, pred in data['model_outputs'].items():
|
| 115 |
-
display_name = MODEL_DISPLAY.get(model_key, model_key.upper())
|
| 116 |
-
wgt = data.get('weights', {}).get(model_key, 0)
|
| 117 |
-
prob = data.get('model_probs', {}).get(model_key, 0) * 100
|
| 118 |
-
pred_color = DISEASE_COLORS.get(pred, "#636efa")
|
| 119 |
-
border_color = "#00cc96" if pred == "Healthy" else "#ef553b"
|
| 120 |
-
st.markdown(
|
| 121 |
-
f'<div class="node-row" style="border-left-color: {border_color};">'
|
| 122 |
-
f'<span><strong>{display_name}</strong> (w={wgt:.2f})</span>'
|
| 123 |
-
f'<span style="color: {pred_color}; font-weight: bold;">{pred} ({prob:.0f}%)</span>'
|
| 124 |
-
f'</div>',
|
| 125 |
-
unsafe_allow_html=True
|
| 126 |
-
)
|
| 127 |
-
|
| 128 |
-
# SHAP Explainability Section
|
| 129 |
-
with st.expander("🔍 Explain AI Prediction (SHAP Explainability)", expanded=False):
|
| 130 |
-
st.markdown("##### How each vital sign influenced the AI's decision:")
|
| 131 |
-
try:
|
| 132 |
-
url_exp = f"{API_URL.rstrip('/')}/explain"
|
| 133 |
-
exp_resp = requests.post(url_exp, json=payload, timeout=30)
|
| 134 |
-
if exp_resp.status_code == 200:
|
| 135 |
-
exp_data = exp_resp.json()
|
| 136 |
-
if "error" in exp_data:
|
| 137 |
-
st.warning(exp_data["error"])
|
| 138 |
-
else:
|
| 139 |
-
shap_vals = exp_data["shap_values"]
|
| 140 |
-
feat_names = exp_data["feature_names"]
|
| 141 |
-
|
| 142 |
-
# Clean up feature names for display
|
| 143 |
-
clean_names = []
|
| 144 |
-
for f in feat_names:
|
| 145 |
-
f = f.replace(" (bpm)", "").replace(" (%)", "").replace(" (mmHg)", "").replace(" (°C)", "")
|
| 146 |
-
clean_names.append(f)
|
| 147 |
-
|
| 148 |
-
# Sort by absolute value for visual impact
|
| 149 |
-
sorted_pairs = sorted(zip(clean_names, shap_vals), key=lambda x: abs(x[1]))
|
| 150 |
-
s_names, s_vals = zip(*sorted_pairs)
|
| 151 |
-
|
| 152 |
-
colors = ['#ef553b' if v > 0 else '#00cc96' for v in s_vals]
|
| 153 |
-
|
| 154 |
-
fig = go.Figure(go.Bar(
|
| 155 |
-
x=list(s_vals),
|
| 156 |
-
y=list(s_names),
|
| 157 |
-
orientation='h',
|
| 158 |
-
marker=dict(
|
| 159 |
-
color=colors,
|
| 160 |
-
line=dict(width=1, color='rgba(255,255,255,0.2)')
|
| 161 |
-
),
|
| 162 |
-
text=[f"{v:+.4f}" for v in s_vals],
|
| 163 |
-
textposition='outside',
|
| 164 |
-
textfont=dict(color='white', size=11)
|
| 165 |
-
))
|
| 166 |
-
fig.update_layout(
|
| 167 |
-
title=dict(text=f"Feature Impact on '{label}' Prediction", font=dict(color='white', size=16)),
|
| 168 |
-
xaxis_title="SHAP Value (←Reduces Risk | Increases Risk→)",
|
| 169 |
-
yaxis_title="",
|
| 170 |
-
template="plotly_dark",
|
| 171 |
-
height=350,
|
| 172 |
-
margin=dict(l=10, r=80, t=50, b=50),
|
| 173 |
-
paper_bgcolor='rgba(0,0,0,0)',
|
| 174 |
-
plot_bgcolor='rgba(0,0,0,0)',
|
| 175 |
-
xaxis=dict(zeroline=True, zerolinecolor='rgba(255,255,255,0.3)', zerolinewidth=2, gridcolor='rgba(255,255,255,0.07)'),
|
| 176 |
-
yaxis=dict(gridcolor='rgba(255,255,255,0.07)')
|
| 177 |
-
)
|
| 178 |
-
|
| 179 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 180 |
-
|
| 181 |
-
# ──── Detailed Key Insights Panel ────
|
| 182 |
-
st.markdown("---")
|
| 183 |
-
st.markdown("##### 📊 Key Insights — Clinical Explainability Report")
|
| 184 |
-
|
| 185 |
-
# Sort features by absolute SHAP impact
|
| 186 |
-
paired = list(zip(feat_names, shap_vals))
|
| 187 |
-
paired_sorted = sorted(paired, key=lambda x: abs(x[1]), reverse=True)
|
| 188 |
-
risk_factors = [(n, v) for n, v in paired_sorted if v > 0.001]
|
| 189 |
-
protective = [(n, v) for n, v in paired_sorted if v < -0.001]
|
| 190 |
-
total_push = sum(v for _, v in paired)
|
| 191 |
-
risk_direction = "toward a clinical condition" if total_push > 0 else "toward a healthy classification"
|
| 192 |
-
|
| 193 |
-
ins_c1, ins_c2 = st.columns(2)
|
| 194 |
-
|
| 195 |
-
with ins_c1:
|
| 196 |
-
st.markdown("**🔴 Risk-Increasing Factors**")
|
| 197 |
-
if risk_factors:
|
| 198 |
-
for name, val in risk_factors:
|
| 199 |
-
pct = abs(val) / max(sum(abs(v) for _, v in paired), 1e-9) * 100
|
| 200 |
-
st.markdown(
|
| 201 |
-
f'<div style="background:rgba(239,85,59,0.08);border-left:3px solid #ef553b;'
|
| 202 |
-
f'padding:8px 12px;border-radius:6px;margin:4px 0;">'
|
| 203 |
-
f'<strong>{name}</strong><br>'
|
| 204 |
-
f'<span style="color:#ef553b;">SHAP +{val:.4f}</span> '
|
| 205 |
-
f'<span style="color:rgba(255,255,255,0.4);">({pct:.0f}% of total impact)</span>'
|
| 206 |
-
f'</div>',
|
| 207 |
-
unsafe_allow_html=True
|
| 208 |
-
)
|
| 209 |
-
else:
|
| 210 |
-
st.caption("No features significantly increasing risk.")
|
| 211 |
-
|
| 212 |
-
with ins_c2:
|
| 213 |
-
st.markdown("**🟢 Protective Factors**")
|
| 214 |
-
if protective:
|
| 215 |
-
for name, val in protective:
|
| 216 |
-
pct = abs(val) / max(sum(abs(v) for _, v in paired), 1e-9) * 100
|
| 217 |
-
st.markdown(
|
| 218 |
-
f'<div style="background:rgba(0,204,150,0.08);border-left:3px solid #00cc96;'
|
| 219 |
-
f'padding:8px 12px;border-radius:6px;margin:4px 0;">'
|
| 220 |
-
f'<strong>{name}</strong><br>'
|
| 221 |
-
f'<span style="color:#00cc96;">SHAP {val:.4f}</span> '
|
| 222 |
-
f'<span style="color:rgba(255,255,255,0.4);">({pct:.0f}% of total impact)</span>'
|
| 223 |
-
f'</div>',
|
| 224 |
-
unsafe_allow_html=True
|
| 225 |
-
)
|
| 226 |
-
else:
|
| 227 |
-
st.caption("No features significantly reducing risk.")
|
| 228 |
-
|
| 229 |
-
# Overall clinical summary
|
| 230 |
-
top_feat, top_val = paired_sorted[0]
|
| 231 |
-
second_feat = paired_sorted[1][0] if len(paired_sorted) > 1 else None
|
| 232 |
-
top_dir = "increasing" if top_val > 0 else "decreasing"
|
| 233 |
-
|
| 234 |
-
summary_parts = [
|
| 235 |
-
f"The ensemble model's decision was primarily driven by **{top_feat}** "
|
| 236 |
-
f"(SHAP {top_val:+.4f}), which had the strongest influence in {top_dir} the risk score.",
|
| 237 |
-
]
|
| 238 |
-
if second_feat:
|
| 239 |
-
s_val = paired_sorted[1][1]
|
| 240 |
-
s_dir = "increasing" if s_val > 0 else "decreasing"
|
| 241 |
-
summary_parts.append(
|
| 242 |
-
f"The second most influential factor was **{second_feat}** "
|
| 243 |
-
f"(SHAP {s_val:+.4f}), {s_dir} risk."
|
| 244 |
-
)
|
| 245 |
-
summary_parts.append(
|
| 246 |
-
f"Overall, the net SHAP push ({total_push:+.4f}) points **{risk_direction}** "
|
| 247 |
-
f"for the given vitals."
|
| 248 |
-
)
|
| 249 |
-
if risk_factors and protective:
|
| 250 |
-
summary_parts.append(
|
| 251 |
-
f"The model identified {len(risk_factors)} risk-increasing and "
|
| 252 |
-
f"{len(protective)} protective factor(s), indicating a mixed clinical profile "
|
| 253 |
-
f"that warrants attention to the dominant contributors."
|
| 254 |
-
)
|
| 255 |
-
|
| 256 |
-
st.info("💡 **Clinical Interpretation:**\n\n" + " ".join(summary_parts))
|
| 257 |
-
except Exception as e:
|
| 258 |
-
st.error(f"Failed to fetch explanation: {e}")
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
if st.button("Query Edge AI Ensemble", type="primary"):
|
| 262 |
-
payload = {
|
| 263 |
-
"Heart_Rate": hr,
|
| 264 |
-
"SpO2_Level": spo2,
|
| 265 |
-
"Systolic_BP": sys_bp,
|
| 266 |
-
"Diastolic_BP": dia_bp,
|
| 267 |
-
"Body_Temp": tmp,
|
| 268 |
-
"Fall_Detection": fall
|
| 269 |
-
}
|
| 270 |
-
|
| 271 |
-
with st.spinner("Processing through TinyML Nodes..."):
|
| 272 |
-
try:
|
| 273 |
-
url = f"{API_URL.rstrip('/')}/predict"
|
| 274 |
-
resp = requests.post(url, json=payload, timeout=30)
|
| 275 |
-
if resp.status_code == 200:
|
| 276 |
-
data = resp.json()
|
| 277 |
-
if "error" in data:
|
| 278 |
-
st.error(data["error"])
|
| 279 |
-
else:
|
| 280 |
-
render_results(data)
|
| 281 |
-
else:
|
| 282 |
-
st.error(f"API returned an error: {resp.status_code} — {resp.text[:500]}")
|
| 283 |
-
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as conn_err:
|
| 284 |
-
try:
|
| 285 |
-
import sys
|
| 286 |
-
import os
|
| 287 |
-
import pandas as pd
|
| 288 |
-
|
| 289 |
-
backend_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../backend'))
|
| 290 |
-
if backend_path not in sys.path:
|
| 291 |
-
sys.path.append(backend_path)
|
| 292 |
-
|
| 293 |
-
from ensemble import EnsembleModel
|
| 294 |
-
from preprocessing import preprocess_data
|
| 295 |
-
|
| 296 |
-
df_simulate = pd.DataFrame([{
|
| 297 |
-
"Heart Rate (bpm)": payload["Heart_Rate"],
|
| 298 |
-
"SpO2 Level (%)": payload["SpO2_Level"],
|
| 299 |
-
"Systolic Blood Pressure (mmHg)": payload["Systolic_BP"],
|
| 300 |
-
"Diastolic Blood Pressure (mmHg)": payload["Diastolic_BP"],
|
| 301 |
-
"Body Temperature (°C)": payload["Body_Temp"],
|
| 302 |
-
"Fall Detection": payload["Fall_Detection"]
|
| 303 |
-
}])
|
| 304 |
-
|
| 305 |
-
X_proc, _ = preprocess_data(df_simulate, is_training=False)
|
| 306 |
-
eng = EnsembleModel()
|
| 307 |
-
final_pred, conf, ind_preds, ind_probs, weights = eng.predict(X_proc)
|
| 308 |
-
|
| 309 |
-
data = {
|
| 310 |
-
"prediction": 0 if final_pred == "Healthy" else 1,
|
| 311 |
-
"prediction_label": final_pred,
|
| 312 |
-
"probability": float(conf),
|
| 313 |
-
"model_outputs": ind_preds,
|
| 314 |
-
"model_probs": {k: float(max(v)) for k, v in ind_probs.items()},
|
| 315 |
-
"weights": weights
|
| 316 |
-
}
|
| 317 |
-
|
| 318 |
-
st.markdown("---")
|
| 319 |
-
st.info("☁️ **Cloud Mode (Fallback)**: Processed via Monolithic Native Architecture.")
|
| 320 |
-
render_results(data)
|
| 321 |
-
|
| 322 |
-
except Exception as inner_e:
|
| 323 |
-
st.error(f"Monolithic execution failed: {inner_e}")
|
| 324 |
-
|
| 325 |
-
except Exception as e:
|
| 326 |
-
st.error(f"Failed to process Request context: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/pages/2_Data_Analytics.py
DELETED
|
@@ -1,221 +0,0 @@
|
|
| 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 |
-
if "hf.space" in os.getenv("SPACE_ID", ""):
|
| 14 |
-
API_URL = "http://localhost:8000"
|
| 15 |
-
|
| 16 |
-
@st.cache_data(ttl=3600)
|
| 17 |
-
def load_data():
|
| 18 |
-
try:
|
| 19 |
-
resp = requests.get(f"{API_URL}/dataset", timeout=30)
|
| 20 |
-
if resp.status_code == 200:
|
| 21 |
-
data = resp.json()
|
| 22 |
-
if isinstance(data, list):
|
| 23 |
-
df = pd.DataFrame(data)
|
| 24 |
-
# Normalize column names: strip whitespace and ensure common naming
|
| 25 |
-
df.columns = [c.strip() for c in df.columns]
|
| 26 |
-
# Fix common encoding issues for temperature
|
| 27 |
-
df.columns = [c.replace('°C', '°C') for c in df.columns]
|
| 28 |
-
return df
|
| 29 |
-
st.error(f"Backend Error: {data.get('error', 'Unknown')}")
|
| 30 |
-
except Exception as e:
|
| 31 |
-
st.error(f"Failed to connect to backend: {e}")
|
| 32 |
-
return pd.DataFrame()
|
| 33 |
-
|
| 34 |
-
DISEASE_COLORS = {
|
| 35 |
-
"Healthy": "#00cc96",
|
| 36 |
-
"Asthma": "#ab63fa",
|
| 37 |
-
"Diabetes Mellitus": "#ffa15a",
|
| 38 |
-
"Heart Disease": "#ef553b",
|
| 39 |
-
"Hypertension": "#636efa"
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
full_df = load_data()
|
| 43 |
-
|
| 44 |
-
if full_df.empty:
|
| 45 |
-
st.warning("Warning: Dataset not found or backend unreachable. Connect to the API to view analytics.")
|
| 46 |
-
else:
|
| 47 |
-
df = full_df
|
| 48 |
-
st.success(f"Loaded **{len(full_df):,}** patient records from backend.")
|
| 49 |
-
|
| 50 |
-
# ── Safety Check for Column Names ──
|
| 51 |
-
available_cols = df.columns.tolist()
|
| 52 |
-
target_x = "Predicted Disease"
|
| 53 |
-
|
| 54 |
-
if target_x not in available_cols:
|
| 55 |
-
# Fallback to similar name if exact match fails
|
| 56 |
-
matches = [c for c in available_cols if "Disease" in c or "Condition" in c]
|
| 57 |
-
if matches:
|
| 58 |
-
target_x = matches[0]
|
| 59 |
-
else:
|
| 60 |
-
st.error(f"Required column '{target_x}' missing from dataset. Found: {available_cols}")
|
| 61 |
-
st.stop()
|
| 62 |
-
|
| 63 |
-
st.markdown("### 🫀 Vital Signs Distribution by Condition")
|
| 64 |
-
|
| 65 |
-
# Define possible vitals and filter by what's actually in the DF
|
| 66 |
-
vitals_all = ["Heart Rate (bpm)", "SpO2 Level (%)", "Systolic Blood Pressure (mmHg)", "Diastolic Blood Pressure (mmHg)", "Body Temperature (°C)"]
|
| 67 |
-
vitals = [v for v in vitals_all if v in available_cols]
|
| 68 |
-
|
| 69 |
-
if not vitals:
|
| 70 |
-
st.error(f"No clinical vital sign columns found in dataset. Found: {available_cols}")
|
| 71 |
-
st.stop()
|
| 72 |
-
|
| 73 |
-
selected_vital = st.selectbox("Select Vital Sign", vitals)
|
| 74 |
-
|
| 75 |
-
fig_box = px.box(
|
| 76 |
-
df, x=target_x, y=selected_vital,
|
| 77 |
-
color=target_x,
|
| 78 |
-
color_discrete_map=DISEASE_COLORS,
|
| 79 |
-
title=f"{selected_vital} Distribution Across Conditions",
|
| 80 |
-
template="plotly_dark",
|
| 81 |
-
points="outliers"
|
| 82 |
-
)
|
| 83 |
-
fig_box.update_layout(
|
| 84 |
-
paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)',
|
| 85 |
-
xaxis_title="Condition", yaxis_title=selected_vital,
|
| 86 |
-
showlegend=False, height=420
|
| 87 |
-
)
|
| 88 |
-
st.plotly_chart(fig_box, use_container_width=True)
|
| 89 |
-
|
| 90 |
-
st.markdown("---")
|
| 91 |
-
|
| 92 |
-
c1, c2 = st.columns(2)
|
| 93 |
-
with c1:
|
| 94 |
-
st.markdown("##### Heart Rate vs SpO2 (Scatter Matrix)")
|
| 95 |
-
# Safety check for scatter columns
|
| 96 |
-
x_scatter = "Heart Rate (bpm)" if "Heart Rate (bpm)" in available_cols else None
|
| 97 |
-
y_scatter = "SpO2 Level (%)" if "SpO2 Level (%)" in available_cols else None
|
| 98 |
-
|
| 99 |
-
if x_scatter and y_scatter:
|
| 100 |
-
fig_scatter = px.scatter(
|
| 101 |
-
df, x=x_scatter, y=y_scatter,
|
| 102 |
-
color=target_x,
|
| 103 |
-
color_discrete_map=DISEASE_COLORS,
|
| 104 |
-
opacity=0.4, size_max=6,
|
| 105 |
-
title="Heart Rate vs SpO2 Levels",
|
| 106 |
-
template="plotly_dark",
|
| 107 |
-
marginal_x="histogram",
|
| 108 |
-
marginal_y="histogram"
|
| 109 |
-
)
|
| 110 |
-
fig_scatter.update_layout(
|
| 111 |
-
paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)',
|
| 112 |
-
height=450, legend=dict(orientation="h", yanchor="bottom", y=-0.35)
|
| 113 |
-
)
|
| 114 |
-
st.plotly_chart(fig_scatter, use_container_width=True)
|
| 115 |
-
else:
|
| 116 |
-
st.warning("Scatter plot unavailable: Required columns missing.")
|
| 117 |
-
|
| 118 |
-
with c2:
|
| 119 |
-
st.markdown("##### Temperature by Condition (Violin Plot)")
|
| 120 |
-
y_violin = "Body Temperature (°C)" if "Body Temperature (°C)" in available_cols else None
|
| 121 |
-
|
| 122 |
-
if y_violin:
|
| 123 |
-
fig_violin = px.violin(
|
| 124 |
-
df, x=target_x, y=y_violin,
|
| 125 |
-
color=target_x,
|
| 126 |
-
color_discrete_map=DISEASE_COLORS,
|
| 127 |
-
box=True, points=False,
|
| 128 |
-
title="Body Temperature Distribution",
|
| 129 |
-
template="plotly_dark"
|
| 130 |
-
)
|
| 131 |
-
fig_violin.update_layout(
|
| 132 |
-
paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)',
|
| 133 |
-
height=450, showlegend=False, xaxis_title="Condition", yaxis_title="Temperature (°C)"
|
| 134 |
-
)
|
| 135 |
-
st.plotly_chart(fig_violin, use_container_width=True)
|
| 136 |
-
else:
|
| 137 |
-
st.warning("Violin plot unavailable: Temperature column missing.")
|
| 138 |
-
|
| 139 |
-
st.markdown("---")
|
| 140 |
-
|
| 141 |
-
c3, c4 = st.columns(2)
|
| 142 |
-
with c3:
|
| 143 |
-
st.markdown("##### 🏥 Disease Distribution (Population Count)")
|
| 144 |
-
counts = df[target_x].value_counts().reset_index()
|
| 145 |
-
counts.columns = ["Condition", "Patient Count"]
|
| 146 |
-
fig_bar = px.bar(
|
| 147 |
-
counts, x="Condition", y="Patient Count",
|
| 148 |
-
color="Condition",
|
| 149 |
-
color_discrete_map=DISEASE_COLORS,
|
| 150 |
-
title="Patient Count per Condition",
|
| 151 |
-
template="plotly_dark",
|
| 152 |
-
text="Patient Count"
|
| 153 |
-
)
|
| 154 |
-
fig_bar.update_traces(textposition='outside')
|
| 155 |
-
fig_bar.update_layout(
|
| 156 |
-
paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)',
|
| 157 |
-
height=420, showlegend=False
|
| 158 |
-
)
|
| 159 |
-
st.plotly_chart(fig_bar, use_container_width=True)
|
| 160 |
-
|
| 161 |
-
with c4:
|
| 162 |
-
st.markdown("##### 🩸 Blood Pressure Density (Systolic vs Diastolic)")
|
| 163 |
-
x_heat = "Systolic Blood Pressure (mmHg)" if "Systolic Blood Pressure (mmHg)" in available_cols else None
|
| 164 |
-
y_heat = "Diastolic Blood Pressure (mmHg)" if "Diastolic Blood Pressure (mmHg)" in available_cols else None
|
| 165 |
-
|
| 166 |
-
if x_heat and y_heat:
|
| 167 |
-
fig_density = px.density_heatmap(
|
| 168 |
-
df, x=x_heat, y=y_heat,
|
| 169 |
-
color_continuous_scale="Turbo",
|
| 170 |
-
title="Blood Pressure Population Heatmap",
|
| 171 |
-
template="plotly_dark",
|
| 172 |
-
nbinsx=30, nbinsy=30
|
| 173 |
-
)
|
| 174 |
-
fig_density.update_layout(
|
| 175 |
-
paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)',
|
| 176 |
-
height=420
|
| 177 |
-
)
|
| 178 |
-
st.plotly_chart(fig_density, use_container_width=True)
|
| 179 |
-
else:
|
| 180 |
-
st.warning("Density heatmap unavailable: Blood Pressure columns missing.")
|
| 181 |
-
|
| 182 |
-
st.markdown("---")
|
| 183 |
-
st.markdown("### 📈 Patient-Specific Real-Time Monitoring")
|
| 184 |
-
# Use Patient Number if available
|
| 185 |
-
id_col = "Patient Number" if "Patient Number" in available_cols else available_cols[0]
|
| 186 |
-
p_id = st.selectbox("Select Patient ID to view live telemetry:", df[id_col].unique()[:50])
|
| 187 |
-
|
| 188 |
-
if p_id:
|
| 189 |
-
patient_data = df[df[id_col] == p_id].iloc[0]
|
| 190 |
-
disease = patient_data[target_x]
|
| 191 |
-
d_color = DISEASE_COLORS.get(disease, "#636efa")
|
| 192 |
-
st.markdown(f"**Patient Condition:** <span style='color:{d_color}; font-weight:bold'>{disease}</span>", unsafe_allow_html=True)
|
| 193 |
-
|
| 194 |
-
periods = 60
|
| 195 |
-
dates = pd.date_range(end=pd.Timestamp.now(), periods=periods, freq='25s')
|
| 196 |
-
|
| 197 |
-
# Safe access for real-time emulation
|
| 198 |
-
base_hr = patient_data.get('Heart Rate (bpm)', 75)
|
| 199 |
-
base_spo2 = patient_data.get('SpO2 Level (%)', 98)
|
| 200 |
-
base_temp = patient_data.get('Body Temperature (°C)', 37.0)
|
| 201 |
-
sim_hr = base_hr + np.random.normal(0, 1.2, periods).cumsum()
|
| 202 |
-
sim_spo2 = np.clip(base_spo2 + np.random.normal(0, 0.3, periods).cumsum(), 85, 100)
|
| 203 |
-
sim_temp = base_temp + np.random.normal(0, 0.05, periods).cumsum()
|
| 204 |
-
|
| 205 |
-
tc1, tc2, tc3 = st.columns(3)
|
| 206 |
-
with tc1:
|
| 207 |
-
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"))
|
| 208 |
-
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))
|
| 209 |
-
st.plotly_chart(fig_hr, use_container_width=True)
|
| 210 |
-
with tc2:
|
| 211 |
-
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"))
|
| 212 |
-
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))
|
| 213 |
-
st.plotly_chart(fig_sp, use_container_width=True)
|
| 214 |
-
with tc3:
|
| 215 |
-
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"))
|
| 216 |
-
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))
|
| 217 |
-
st.plotly_chart(fig_tp, use_container_width=True)
|
| 218 |
-
|
| 219 |
-
st.markdown("---")
|
| 220 |
-
st.markdown("### 📋 Raw Patient Data")
|
| 221 |
-
st.dataframe(df, use_container_width=True, height=350)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/pages/3_Edge_Deployment.py
DELETED
|
@@ -1,87 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,79 +0,0 @@
|
|
| 1 |
-
import streamlit as st
|
| 2 |
-
import requests
|
| 3 |
-
import pandas as pd
|
| 4 |
-
import os
|
| 5 |
-
from datetime import datetime, timedelta
|
| 6 |
-
|
| 7 |
-
st.set_page_config(page_title="Patient History", page_icon="🗄️", layout="wide")
|
| 8 |
-
API_URL = os.getenv("API_URL", "http://localhost:8000")
|
| 9 |
-
if "hf.space" in os.getenv("SPACE_ID", ""):
|
| 10 |
-
API_URL = "http://localhost:8000"
|
| 11 |
-
|
| 12 |
-
st.title("🗄️ Patient Prediction History")
|
| 13 |
-
st.markdown("Live database of all patient predictions and vitals logged by the system. Times shown in **IST (UTC+5:30)**.")
|
| 14 |
-
|
| 15 |
-
def convert_to_ist(utc_time_str):
|
| 16 |
-
try:
|
| 17 |
-
utc_dt = datetime.strptime(utc_time_str, "%Y-%m-%d %H:%M:%S")
|
| 18 |
-
ist_dt = utc_dt + timedelta(hours=5, minutes=30)
|
| 19 |
-
return ist_dt.strftime("%Y-%m-%d %H:%M:%S")
|
| 20 |
-
except:
|
| 21 |
-
return utc_time_str
|
| 22 |
-
|
| 23 |
-
DISEASE_COLORS = {
|
| 24 |
-
"Healthy": "background-color: rgba(0, 204, 150, 0.15); color: #00cc96;",
|
| 25 |
-
"Asthma": "background-color: rgba(171, 99, 250, 0.15); color: #ab63fa;",
|
| 26 |
-
"Diabetes Mellitus": "background-color: rgba(255, 161, 90, 0.15); color: #ffa15a;",
|
| 27 |
-
"Heart Disease": "background-color: rgba(239, 85, 59, 0.15); color: #ef553b;",
|
| 28 |
-
"Hypertension": "background-color: rgba(99, 110, 250, 0.15); color: #636efa;",
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
COLUMN_RENAME = {
|
| 32 |
-
"id": "ID",
|
| 33 |
-
"timestamp": "Timestamp",
|
| 34 |
-
"prediction_label": "Diagnosis",
|
| 35 |
-
"confidence": "Confidence",
|
| 36 |
-
"heart_rate": "Heart Rate",
|
| 37 |
-
"spo2": "SpO2 %",
|
| 38 |
-
"sys_bp": "Sys BP",
|
| 39 |
-
"dia_bp": "Dia BP",
|
| 40 |
-
"temp": "Temp °C",
|
| 41 |
-
"fall_detection": "Fall"
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
@st.fragment(run_every="5s")
|
| 45 |
-
def live_history_table():
|
| 46 |
-
try:
|
| 47 |
-
resp = requests.get(f"{API_URL}/history", timeout=5)
|
| 48 |
-
if resp.status_code == 200:
|
| 49 |
-
history_data = resp.json()
|
| 50 |
-
if not history_data:
|
| 51 |
-
st.info("No prediction history found. Go make some predictions first!")
|
| 52 |
-
return
|
| 53 |
-
|
| 54 |
-
df = pd.DataFrame(history_data)
|
| 55 |
-
if "timestamp" in df.columns:
|
| 56 |
-
df["timestamp"] = df["timestamp"].apply(convert_to_ist)
|
| 57 |
-
|
| 58 |
-
cols = ['id', 'timestamp', 'prediction_label', 'confidence', 'heart_rate', 'spo2', 'sys_bp', 'dia_bp', 'temp', 'fall_detection']
|
| 59 |
-
df = df[[c for c in cols if c in df.columns]]
|
| 60 |
-
df.rename(columns=COLUMN_RENAME, inplace=True)
|
| 61 |
-
|
| 62 |
-
if "Confidence" in df.columns:
|
| 63 |
-
df["Confidence"] = df["Confidence"].apply(lambda x: f"{x*100:.1f}%" if x <= 1 else f"{x:.1f}%")
|
| 64 |
-
|
| 65 |
-
def color_by_diagnosis(row):
|
| 66 |
-
diagnosis = row.get("Diagnosis", "")
|
| 67 |
-
style_str = DISEASE_COLORS.get(diagnosis, "")
|
| 68 |
-
return [style_str] * len(row)
|
| 69 |
-
|
| 70 |
-
styled = df.style.apply(color_by_diagnosis, axis=1)
|
| 71 |
-
|
| 72 |
-
st.dataframe(styled, width="stretch", height=600)
|
| 73 |
-
st.caption(f"Showing {len(df)} records. Auto-refreshing every 5s.")
|
| 74 |
-
else:
|
| 75 |
-
st.error("API Error")
|
| 76 |
-
except Exception as e:
|
| 77 |
-
st.error(f"Failed to fetch history: {e}")
|
| 78 |
-
|
| 79 |
-
live_history_table()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/pages/5_MLOps_Settings.py
DELETED
|
@@ -1,48 +0,0 @@
|
|
| 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 |
-
if "hf.space" in os.getenv("SPACE_ID", ""):
|
| 8 |
-
API_URL = "http://localhost:8000"
|
| 9 |
-
|
| 10 |
-
st.title("🔄 MLOps & Retraining")
|
| 11 |
-
st.markdown("Dynamically update the TinyML ensemble models by providing a new dataset. The system uses an **append-only** strategy to maintain data integrity.")
|
| 12 |
-
|
| 13 |
-
with st.expander("📊 Data Submission Guide (Required Schema)", expanded=True):
|
| 14 |
-
st.markdown("""
|
| 15 |
-
To ensure the retraining pipeline succeeds, your CSV **must** include the following columns (case-sensitive):
|
| 16 |
-
|
| 17 |
-
1. **Patient Number**: Unique identifier for the patient.
|
| 18 |
-
2. **Heart Rate (bpm)**: Numeric value.
|
| 19 |
-
3. **SpO2 Level (%)**: Numeric value.
|
| 20 |
-
4. **Systolic Blood Pressure (mmHg)**: Numeric value.
|
| 21 |
-
5. **Diastolic Blood Pressure (mmHg)**: Numeric value.
|
| 22 |
-
6. **Body Temperature (°C)**: Numeric value.
|
| 23 |
-
7. **Fall Detection**: 'Yes' or 'No'.
|
| 24 |
-
8. **Predicted Disease**: The target label (e.g., Healthy, Heart Disease).
|
| 25 |
-
|
| 26 |
-
*Note: If you submit a file with a different schema, the system will reject the update to prevent dashboard crashes.*
|
| 27 |
-
""")
|
| 28 |
-
|
| 29 |
-
st.info("💡 **Safety Logic:** New data will be validated and appended to the existing dataset. The original records are always preserved.")
|
| 30 |
-
|
| 31 |
-
uploaded_file = st.file_uploader("Upload New Patient Dataset (CSV)", type="csv")
|
| 32 |
-
|
| 33 |
-
if uploaded_file is not None:
|
| 34 |
-
if st.button("Start Retraining Pipeline"):
|
| 35 |
-
with st.spinner("Uploading and retraining ensemble... This may take a minute."):
|
| 36 |
-
try:
|
| 37 |
-
files = {"file": (uploaded_file.name, uploaded_file.getvalue(), "text/csv")}
|
| 38 |
-
resp = requests.post(f"{API_URL}/retrain", files=files, timeout=120)
|
| 39 |
-
if resp.status_code == 200:
|
| 40 |
-
data = resp.json()
|
| 41 |
-
if "error" in data:
|
| 42 |
-
st.error(f"Retraining failed: {data['error']}")
|
| 43 |
-
else:
|
| 44 |
-
st.success(data.get("message", "Success!"))
|
| 45 |
-
else:
|
| 46 |
-
st.error("API returned an error.")
|
| 47 |
-
except Exception as e:
|
| 48 |
-
st.error(f"Connection failed: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/pages/6_About.py
DELETED
|
@@ -1,196 +0,0 @@
|
|
| 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@300;400;500;600;700;800&display=swap');
|
| 9 |
-
|
| 10 |
-
html, body, [class*="css"] {
|
| 11 |
-
font-family: 'Inter', sans-serif;
|
| 12 |
-
color: #FAFAFA;
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
.main {
|
| 16 |
-
background-color: #0e1117;
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
h1 {
|
| 20 |
-
font-weight: 800 !important;
|
| 21 |
-
letter-spacing: -1.5px !important;
|
| 22 |
-
color: #FFFFFF !important;
|
| 23 |
-
font-size: 3.2rem !important;
|
| 24 |
-
margin-bottom: 0.8rem !important;
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
h3 {
|
| 28 |
-
font-weight: 700 !important;
|
| 29 |
-
color: #AED9E0 !important; /* Sky Blue Pastel */
|
| 30 |
-
margin-top: 2.5rem !important;
|
| 31 |
-
font-size: 2rem !important;
|
| 32 |
-
letter-spacing: -0.5px !important;
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
p, li, div {
|
| 36 |
-
font-size: 1.15rem !important;
|
| 37 |
-
line-height: 1.7 !important;
|
| 38 |
-
color: #E0E0E0 !important;
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
.tech-badge {
|
| 42 |
-
display: inline-block;
|
| 43 |
-
padding: 8px 18px;
|
| 44 |
-
margin: 0 10px 12px 0;
|
| 45 |
-
border-radius: 50px;
|
| 46 |
-
font-size: 0.95rem;
|
| 47 |
-
font-weight: 600;
|
| 48 |
-
background: rgba(174, 217, 224, 0.1); /* Sky Blue Tint */
|
| 49 |
-
border: 1px solid rgba(174, 217, 224, 0.3);
|
| 50 |
-
color: #AED9E0;
|
| 51 |
-
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
.tech-badge:hover {
|
| 55 |
-
background: rgba(174, 217, 224, 0.2);
|
| 56 |
-
border-color: #AED9E0;
|
| 57 |
-
transform: translateY(-3px);
|
| 58 |
-
box-shadow: 0 6px 20px rgba(174, 217, 224, 0.15);
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
.live-link {
|
| 62 |
-
text-decoration: none;
|
| 63 |
-
font-weight: 600;
|
| 64 |
-
color: #AED9E0;
|
| 65 |
-
border-bottom: 1px dashed rgba(174, 217, 224, 0.5);
|
| 66 |
-
transition: all 0.2s ease;
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
.live-link:hover {
|
| 70 |
-
color: #FFFFFF;
|
| 71 |
-
border-bottom: 1px solid #FFFFFF;
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
/* Enhanced Glass cards */
|
| 75 |
-
div[data-testid="stExpander"], .stAlert {
|
| 76 |
-
background: rgba(255, 255, 255, 0.04) !important;
|
| 77 |
-
border: 1px solid rgba(255, 255, 255, 0.08) !important;
|
| 78 |
-
border-radius: 20px !important;
|
| 79 |
-
backdrop-filter: blur(16px) saturate(180%);
|
| 80 |
-
padding: 1.5rem !important;
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
/* Clinical Contrast for Info Blocks */
|
| 84 |
-
.stAlert p, .stAlert div, .stAlert b, .stAlert strong {
|
| 85 |
-
color: #FFFFFF !important;
|
| 86 |
-
font-size: 1.15rem !important;
|
| 87 |
-
}
|
| 88 |
-
|
| 89 |
-
.stMarkdown {
|
| 90 |
-
margin-bottom: 1.2rem;
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
</style>
|
| 94 |
-
""", unsafe_allow_html=True)
|
| 95 |
-
|
| 96 |
-
st.title("ℹ️ About the Platform")
|
| 97 |
-
|
| 98 |
-
st.markdown("""
|
| 99 |
-
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.
|
| 100 |
-
|
| 101 |
-
**Live Environments:**
|
| 102 |
-
* 🖥️ **Frontend Suite:** <a href="https://tinyml-heart-health-monitoring-dashboard-8xqogy2hibtlayt7popvs.streamlit.app/" target="_blank" class="live-link">Streamlit Cloud Deployment</a>
|
| 103 |
-
* ⚙️ **Backend Hub:** <a href="https://huggingface.co/spaces/IDKHowToCodeFr/tinyml-backend" target="_blank" class="live-link">Hugging Face Spaces API</a>
|
| 104 |
-
|
| 105 |
-
<div style="margin-top: 15px;">
|
| 106 |
-
<span class="tech-badge">Python 3.9+</span>
|
| 107 |
-
<span class="tech-badge">FastAPI</span>
|
| 108 |
-
<span class="tech-badge">Streamlit</span>
|
| 109 |
-
<span class="tech-badge">Scikit-Learn</span>
|
| 110 |
-
<span class="tech-badge">SHAP</span>
|
| 111 |
-
<span class="tech-badge">Docker</span>
|
| 112 |
-
<span class="tech-badge">PyTest</span>
|
| 113 |
-
</div>
|
| 114 |
-
""", unsafe_allow_html=True)
|
| 115 |
-
|
| 116 |
-
st.markdown("---")
|
| 117 |
-
|
| 118 |
-
st.markdown("### 🏗️ System Architecture")
|
| 119 |
-
# Create a Graphviz flowchart for the About page
|
| 120 |
-
graph = graphviz.Digraph(node_attr={'shape': 'box', 'style': 'rounded,filled', 'fontname': 'Helvetica', 'margin': '0.3', 'fontsize': '12'},
|
| 121 |
-
graph_attr={'rankdir': 'TB', 'splines': 'ortho', 'nodesep': '1.0', 'bgcolor': 'transparent', 'fontcolor': 'white'})
|
| 122 |
-
|
| 123 |
-
# Define UI & API nodes at the top
|
| 124 |
-
graph.node('UI', 'Streamlit Frontend\nInteractive Dashboard (IST)', fillcolor='#1c1f26', fontcolor='#FFFFFF', color='#FF4B4B', penwidth='2.5')
|
| 125 |
-
graph.node('API', 'FastAPI Backend\nInternal Routing (UTC)', fillcolor='#1c1f26', fontcolor='#FFFFFF', color='#00D1FF', penwidth='2.5')
|
| 126 |
-
graph.node('HUB', 'HF Hub Dataset\nCentralized History Store', shape='folder', fillcolor='#1c1f26', fontcolor='#FFFFFF', color='#FFD700', penwidth='2.5')
|
| 127 |
-
graph.node('DB', 'SQLite DB\nLocal Instance Cache', shape='cylinder', fillcolor='#1c1f26', fontcolor='#FFFFFF', color='#BF5AF2', penwidth='2.5')
|
| 128 |
-
|
| 129 |
-
# Core connections
|
| 130 |
-
graph.edge('UI', 'API', label=' internal localhost:8000', color='#475569', fontcolor='#94A3B8')
|
| 131 |
-
graph.edge('API', 'DB', label=' Read/Write', color='#475569', fontcolor='#94A3B8')
|
| 132 |
-
graph.edge('API', 'HUB', label=' HfApi Sync (Persistent)', color='#475569', fontcolor='#94A3B8')
|
| 133 |
-
|
| 134 |
-
# Machine Learning Subgraph
|
| 135 |
-
with graph.subgraph(name='cluster_ml') as ml:
|
| 136 |
-
ml.attr(label='Machine Learning & Inference Pipeline', style='dashed', color='#334155', fontcolor='#94A3B8')
|
| 137 |
-
ml.node('ENS', 'Ensemble Engine\nSoft-Voting Aggregator', shape='diamond', fillcolor='#1c1f26', fontcolor='#FFFFFF', color='#FF9F0A', penwidth='2.5')
|
| 138 |
-
ml.node('MODELS', 'Classifiers\n(RF, SVM, LogReg, NN, KNN)', fillcolor='#1c1f26', fontcolor='#FFFFFF', color='#FF9F0A', penwidth='2.5')
|
| 139 |
-
ml.node('SHAP', 'SHAP Explainer\nFeature Impact Analysis', fillcolor='#1c1f26', fontcolor='#FFFFFF', color='#FF9F0A', penwidth='2.5')
|
| 140 |
-
|
| 141 |
-
# Hardware Export Subgraph
|
| 142 |
-
with graph.subgraph(name='cluster_edge') as edge:
|
| 143 |
-
edge.attr(label='Hardware & Edge AI Export', style='dashed', color='#334155', fontcolor='#94A3B8')
|
| 144 |
-
edge.node('TRANS', 'C-Code Transpiler\nINT8 Quantization', fillcolor='#1c1f26', fontcolor='#FFFFFF', color='#32D74B', penwidth='2.5')
|
| 145 |
-
edge.node('HEADER', 'tinyml_model.h\nOptimized Header File', shape='note', fillcolor='#1c1f26', fontcolor='#FFFFFF', color='#32D74B', penwidth='2.5')
|
| 146 |
-
edge.node('MCU', 'ESP32 / ARM Cortex-M\nMicrocontroller Node', fillcolor='#1c1f26', fontcolor='#FFFFFF', color='#32D74B', penwidth='2.5')
|
| 147 |
-
|
| 148 |
-
# Map Cross-cluster edges
|
| 149 |
-
graph.edge('API', 'ENS', label=' Predict', color='#CBD5E1', fontcolor='#CBD5E1')
|
| 150 |
-
graph.edge('ENS', 'MODELS', color='#CBD5E1')
|
| 151 |
-
|
| 152 |
-
graph.edge('API', 'SHAP', label=' Explain', color='#CBD5E1', fontcolor='#CBD5E1')
|
| 153 |
-
graph.edge('SHAP', 'MODELS', style='dashed', label=' Analyzes Trees', fontcolor='#CBD5E1', color='#CBD5E1')
|
| 154 |
-
|
| 155 |
-
graph.edge('API', 'TRANS', label=' Export Config', color='#CBD5E1', fontcolor='#CBD5E1')
|
| 156 |
-
graph.edge('TRANS', 'HEADER', label=' Scale FP32 to INT8', fontcolor='#CBD5E1', color='#CBD5E1')
|
| 157 |
-
graph.edge('HEADER', 'MCU', label=' Flash Firmware', color='#CBD5E1', fontcolor='#CBD5E1')
|
| 158 |
-
|
| 159 |
-
st.graphviz_chart(graph, use_container_width=True)
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
st.markdown("---")
|
| 163 |
-
st.markdown("### ✨ Core Capabilities")
|
| 164 |
-
|
| 165 |
-
col1, col2 = st.columns(2)
|
| 166 |
-
|
| 167 |
-
with col1:
|
| 168 |
-
st.info("""
|
| 169 |
-
**⚡ TinyML & Edge AI Optimization**\n
|
| 170 |
-
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.
|
| 171 |
-
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.
|
| 172 |
-
""")
|
| 173 |
-
|
| 174 |
-
st.success("""
|
| 175 |
-
**🤝 Ensemble Machine Learning**\n
|
| 176 |
-
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.
|
| 177 |
-
""")
|
| 178 |
-
|
| 179 |
-
with col2:
|
| 180 |
-
st.warning("""
|
| 181 |
-
**🔍 Data Synchronization & Localization**\n
|
| 182 |
-
To support a distributed architecture, the system centralizes its **Patient History** on the Hugging Face Hub as a persistent storage layer.
|
| 183 |
-
The platform is fully time-zone aware: while the **Backend API** operates on a robust **UTC** standard for data integrity, the **Frontend Dashboard** automatically localizes all telemetry and history logs to **IST (Indian Standard Time, UTC+5:30)**.
|
| 184 |
-
""")
|
| 185 |
-
|
| 186 |
-
st.error("""
|
| 187 |
-
**🔄 Continuous Learning (MLOps)**\n
|
| 188 |
-
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.
|
| 189 |
-
""")
|
| 190 |
-
|
| 191 |
-
st.markdown("""
|
| 192 |
-
<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;">
|
| 193 |
-
Designed as a scalable, fully interpretable Edge-based Machine Learning architecture for modern Healthcare telemetry.<br>
|
| 194 |
-
MIT License
|
| 195 |
-
</div>
|
| 196 |
-
""", unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/requirements.txt
DELETED
|
@@ -1,9 +0,0 @@
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
main.py
DELETED
|
@@ -1,501 +0,0 @@
|
|
| 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)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
models.py
DELETED
|
@@ -1,39 +0,0 @@
|
|
| 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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
preprocessing.py
DELETED
|
@@ -1,62 +0,0 @@
|
|
| 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|