IDKHowToCodeFr commited on
Commit
76eab21
·
verified ·
1 Parent(s): bb86403

Upload folder using huggingface_hub

Browse files
.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 directories for the backend to run
15
- COPY backend/ ./backend/
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> &nbsp; <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)