techavenger123 commited on
Commit
f3608b3
·
1 Parent(s): 8acd13b

Dual Model Files

Browse files
Files changed (3) hide show
  1. README.md +10 -1
  2. app.py +271 -82
  3. requirements.txt +9 -0
README.md CHANGED
@@ -10,4 +10,13 @@ pinned: false
10
  ---
11
 
12
  # FaultSense - Industrial Equipment Fault Predictor
13
- Real-time binary fault detection using LightGBM and Flask.
 
 
 
 
 
 
 
 
 
 
10
  ---
11
 
12
  # FaultSense - Industrial Equipment Fault Predictor
13
+ Real-time binary fault detection using LightGBM and Flask.
14
+
15
+ ---
16
+ title: FaultSense
17
+ emoji: ⚡
18
+ colorFrom: green
19
+ colorTo: blue
20
+ sdk: docker # or "gradio" / "streamlit" if not using Docker
21
+ app_port: 7860
22
+ ---
app.py CHANGED
@@ -1,8 +1,6 @@
1
  """
2
- FaultSense — LightGBM Fault Prediction App
3
- Fixes applied:
4
- 1. Model loads at module level so gunicorn workers pick it up
5
- 2. Select dropdown works cross-platform
6
  """
7
 
8
  import os
@@ -20,6 +18,7 @@ from sklearn.metrics import (
20
  from sklearn.preprocessing import OneHotEncoder
21
  from sklearn.compose import ColumnTransformer
22
  from sklearn.pipeline import Pipeline
 
23
  import joblib
24
  from lightgbm import LGBMClassifier
25
  from flask import Flask, request, jsonify, render_template_string
@@ -28,9 +27,9 @@ from flask import Flask, request, jsonify, render_template_string
28
  # CONFIG
29
  # ─────────────────────────────────────────────
30
 
31
-
32
  DATA_PATH = "synthetic_nim_parallel_10000.csv"
33
- MODEL_PATH = "/tmp/faultsense_model.joblib"
 
34
 
35
  DROP_COLS = ["location"]
36
  TARGET = "faulty"
@@ -40,7 +39,7 @@ NUM_COLS = ["temperature", "pressure", "vibration", "humidity"]
40
  RANDOM_STATE = 42
41
  THRESHOLD = 0.5
42
 
43
- FIXED_PARAMS = dict(
44
  max_depth=8,
45
  num_leaves=50,
46
  min_child_samples=20,
@@ -49,14 +48,24 @@ FIXED_PARAMS = dict(
49
  class_weight="balanced",
50
  random_state=RANDOM_STATE,
51
  verbose=-1,
 
 
 
 
 
 
 
 
 
 
 
 
52
  )
53
 
54
  BEST_CONFIG = {
55
- "learning_rate": 0.05,
56
- "n_estimators": 10,
57
- "train_ratio": 0.50,
58
- "val_ratio": 0.20,
59
- "test_ratio": 0.30,
60
  }
61
 
62
  EQUIPMENT_OPTIONS = ["pump", "compressor", "motor", "valve", "sensor"]
@@ -71,13 +80,11 @@ def make_preprocessor():
71
  ("num", "passthrough", NUM_COLS),
72
  ])
73
 
74
- def train_model(cfg: dict):
75
- print(f"Training: lr={cfg['learning_rate']}, n_est={cfg['n_estimators']}")
76
  df_raw = pd.read_csv(DATA_PATH)
77
  df_raw = df_raw.drop(columns=DROP_COLS, errors="ignore")
78
  X = df_raw.drop(columns=[TARGET])
79
  y = df_raw[TARGET]
80
-
81
  train_r, val_r, test_r = cfg["train_ratio"], cfg["val_ratio"], cfg["test_ratio"]
82
  X_trainval, X_test, y_trainval, y_test = train_test_split(
83
  X, y, test_size=test_r, stratify=y, random_state=RANDOM_STATE
@@ -87,43 +94,70 @@ def train_model(cfg: dict):
87
  X_trainval, y_trainval, test_size=val_relative,
88
  stratify=y_trainval, random_state=RANDOM_STATE
89
  )
 
90
 
91
- pipeline = Pipeline([
92
- ("pre", make_preprocessor()),
93
- ("clf", LGBMClassifier(
94
- n_estimators=cfg["n_estimators"],
95
- learning_rate=cfg["learning_rate"],
96
- **FIXED_PARAMS
97
- ))
98
- ])
99
- pipeline.fit(X_train, y_train)
100
-
101
  y_prob = pipeline.predict_proba(X_test)[:, 1]
102
  y_pred = (y_prob >= THRESHOLD).astype(int)
103
-
104
- test_metrics = {
105
  "test_auc": round(roc_auc_score(y_test, y_prob), 4),
106
  "test_accuracy": round(accuracy_score(y_test, y_pred), 4),
107
  "test_precision": round(precision_score(y_test, y_pred, zero_division=0), 4),
108
  "test_recall": round(recall_score(y_test, y_pred, zero_division=0), 4),
109
  "test_f1": round(f1_score(y_test, y_pred, zero_division=0), 4),
110
  "test_logloss": round(log_loss(y_test, y_prob), 4),
111
- }
112
- cm = confusion_matrix(y_test, y_pred).tolist()
113
- artifact = {"pipeline": pipeline, "config": cfg, "test_metrics": test_metrics, "cm": cm}
114
- print(f"Model saved → {MODEL_PATH} AUC={test_metrics['test_auc']} F1={test_metrics['test_f1']}")
115
- return artifact
116
-
117
- def load_or_train():
118
- if os.path.exists(MODEL_PATH):
119
- print(f"Loading saved model from {MODEL_PATH}")
120
- return joblib.load(MODEL_PATH)
121
- return train_model(BEST_CONFIG)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
  # ─────────────────────────────────────────────
124
- # LOAD MODEL AT MODULE LEVEL (runs under gunicorn)
125
  # ─────────────────────────────────────────────
126
- ARTIFACT = load_or_train()
127
 
128
  # ─────────────────────────────────────────────
129
  # FLASK APP
@@ -143,7 +177,8 @@ HTML = r"""<!DOCTYPE html>
143
  :root {
144
  --bg: #0a0c10; --surface: #111318; --surface2: #181c24;
145
  --border: #232838; --accent: #00e5a0; --accent2: #ff4d6d;
146
- --accent3: #4d9fff; --text: #e8eaf0; --muted: #6b7280;
 
147
  --mono: 'Space Mono', monospace; --sans: 'DM Sans', sans-serif;
148
  }
149
  html { font-size: 16px; }
@@ -157,14 +192,30 @@ body::before {
157
  .blob-1 { background: var(--accent); top: -200px; left: -200px; }
158
  .blob-2 { background: var(--accent3); bottom: -200px; right: -100px; animation-delay: -6s; }
159
  @keyframes drift { from { transform: translate(0,0) scale(1); } to { transform: translate(40px,30px) scale(1.05); } }
160
- .wrapper { position: relative; z-index: 1; max-width: 1100px; margin: 0 auto; padding: 40px 24px 80px; }
161
- header { display: flex; align-items: center; gap: 16px; margin-bottom: 48px; border-bottom: 1px solid var(--border); padding-bottom: 24px; }
162
  .logo-mark { width: 44px; height: 44px; background: var(--accent); border-radius: 10px; display: grid; place-items: center; font-family: var(--mono); font-weight: 700; font-size: 18px; color: var(--bg); flex-shrink: 0; }
163
  header h1 { font-family: var(--mono); font-size: 1.5rem; letter-spacing: -.5px; }
164
  header p { font-size: .85rem; color: var(--muted); margin-top: 2px; }
165
  .badge { margin-left: auto; font-family: var(--mono); font-size: .7rem; background: rgba(0,229,160,.12); color: var(--accent); border: 1px solid rgba(0,229,160,.3); border-radius: 6px; padding: 4px 10px; white-space: nowrap; }
166
- .main-grid { display: grid; grid-template-columns: 1fr 380px; gap: 24px; align-items: start; }
167
- @media (max-width: 860px) { .main-grid { grid-template-columns: 1fr; } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  .card { background: var(--surface); border: 1px solid var(--border); border-radius: 16px; padding: 28px; }
169
  .card-title { font-family: var(--mono); font-size: .75rem; letter-spacing: 1.5px; text-transform: uppercase; color: var(--muted); margin-bottom: 20px; display: flex; align-items: center; gap: 8px; }
170
  .card-title::before { content: ''; display: inline-block; width: 6px; height: 6px; background: var(--accent); border-radius: 50%; }
@@ -206,16 +257,23 @@ input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 16px;
206
  input[type=range]::-webkit-slider-thumb:hover { transform: scale(1.3); }
207
  input[type=range]::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: var(--accent); border: none; }
208
  .slider-val { font-family: var(--mono); font-size: .85rem; color: var(--accent); min-width: 60px; text-align: right; }
 
209
  .btn-predict { margin-top: 24px; width: 100%; padding: 14px; background: var(--accent); color: var(--bg); border: none; border-radius: 12px; font-family: var(--mono); font-size: 1rem; font-weight: 700; letter-spacing: 1px; cursor: pointer; transition: transform .15s, box-shadow .2s; }
210
  .btn-predict:hover { transform: translateY(-2px); box-shadow: 0 0 32px rgba(0,229,160,.5); }
211
- .btn-predict:disabled { background: var(--muted); cursor: not-allowed; transform: none; }
 
 
 
212
  .result-card { border-radius: 16px; padding: 28px; border: 1px solid var(--border); background: var(--surface); transition: border-color .4s; }
213
  .result-card.faulty { border-color: var(--accent2); background: rgba(255,77,109,.06); }
214
  .result-card.healthy { border-color: var(--accent); background: rgba(0,229,160,.06); }
215
  .verdict { font-family: var(--mono); font-size: 2rem; font-weight: 700; letter-spacing: -1px; margin-bottom: 6px; }
216
  .verdict.faulty { color: var(--accent2); }
217
  .verdict.healthy { color: var(--accent); }
218
- .verdict-sub { font-size: .85rem; color: var(--muted); margin-bottom: 24px; }
 
 
 
219
  .prob-bar-wrap { margin-bottom: 24px; }
220
  .prob-label { font-family: var(--mono); font-size: .72rem; color: var(--muted); margin-bottom: 6px; display: flex; justify-content: space-between; }
221
  .prob-track { height: 10px; background: var(--border); border-radius: 10px; overflow: hidden; }
@@ -226,17 +284,39 @@ input[type=range]::-moz-range-thumb { width: 16px; height: 16px; border-radius:
226
  .mini-metric { background: var(--surface2); border-radius: 10px; padding: 12px; border: 1px solid var(--border); }
227
  .mini-metric .mm-val { font-family: var(--mono); font-size: 1.1rem; font-weight: 700; color: var(--accent3); }
228
  .mini-metric .mm-key { font-size: .7rem; color: var(--muted); margin-top: 2px; font-family: var(--mono); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  .info-row { display: flex; justify-content: space-between; align-items: center; padding: 9px 0; border-bottom: 1px solid var(--border); font-size: .82rem; }
230
  .info-row:last-child { border-bottom: none; }
231
  .info-key { color: var(--muted); font-family: var(--mono); font-size: .72rem; }
232
  .info-val { font-family: var(--mono); color: var(--text); font-weight: 700; }
233
  .info-val.green { color: var(--accent); }
 
 
234
  .history-list { max-height: 260px; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; }
235
  .hist-item { background: var(--surface2); border: 1px solid var(--border); border-radius: 10px; padding: 10px 14px; display: flex; justify-content: space-between; align-items: center; font-size: .78rem; }
236
  .hist-equip { color: var(--muted); font-family: var(--mono); font-size: .7rem; }
237
  .hist-badge { font-family: var(--mono); font-size: .68rem; padding: 3px 8px; border-radius: 6px; font-weight: 700; }
238
  .hist-badge.faulty { background: rgba(255,77,109,.2); color: var(--accent2); }
239
  .hist-badge.healthy { background: rgba(0,229,160,.2); color: var(--accent); }
 
 
 
 
240
  .spinner { display: none; width: 20px; height: 20px; border: 2px solid rgba(10,12,16,.3); border-top-color: var(--bg); border-radius: 50%; animation: spin .6s linear infinite; margin: 0 auto; }
241
  @keyframes spin { to { transform: rotate(360deg); } }
242
  .btn-predict.loading .btn-text { display: none; }
@@ -255,11 +335,25 @@ input[type=range]::-moz-range-thumb { width: 16px; height: 16px; border-radius:
255
  <div class="logo-mark">FS</div>
256
  <div>
257
  <h1>FaultSense</h1>
258
- <p>LightGBM Equipment Fault Predictor</p>
259
  </div>
260
- <div class="badge" id="model-badge">Model Loading…</div>
261
  </header>
262
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  <div class="main-grid">
264
  <div style="display:flex;flex-direction:column;gap:20px;">
265
  <div class="card">
@@ -321,12 +415,20 @@ input[type=range]::-moz-range-thumb { width: 16px; height: 16px; border-radius:
321
  </div>
322
 
323
  </div>
324
- <button class="btn-predict" id="predict-btn" onclick="runPredict()">
325
  <span class="btn-text">⚡ Run Prediction</span>
326
  <div class="spinner"></div>
327
  </button>
328
  </div>
329
 
 
 
 
 
 
 
 
 
330
  <div class="card">
331
  <div class="card-title">Prediction History</div>
332
  <div class="history-list" id="history-list">
@@ -339,11 +441,12 @@ input[type=range]::-moz-range-thumb { width: 16px; height: 16px; border-radius:
339
  <div class="result-card" id="result-card">
340
  <div class="idle-state" id="idle-state">
341
  <div class="idle-icon">🔬</div>
342
- <p>Enter sensor readings<br>and run a prediction<br>to see results here.</p>
343
  </div>
344
  <div id="result-content" style="display:none;">
345
  <div class="verdict" id="verdict-text"></div>
346
  <div class="verdict-sub" id="verdict-sub"></div>
 
347
  <div class="prob-bar-wrap">
348
  <div class="prob-label"><span>Fault Probability</span><span id="prob-pct"></span></div>
349
  <div class="prob-track"><div class="prob-fill" id="prob-fill" style="width:0%"></div></div>
@@ -353,7 +456,7 @@ input[type=range]::-moz-range-thumb { width: 16px; height: 16px; border-radius:
353
  </div>
354
 
355
  <div class="card">
356
- <div class="card-title">Model Configuration</div>
357
  <div id="model-info">
358
  <div style="color:var(--muted);font-size:.8rem;font-family:var(--mono);text-align:center;padding:12px 0;">Loading…</div>
359
  </div>
@@ -366,6 +469,19 @@ input[type=range]::-moz-range-thumb { width: 16px; height: 16px; border-radius:
366
 
367
  <script>
368
  let csOpen = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  function toggleDropdown() {
370
  csOpen = !csOpen;
371
  document.getElementById('cs-trigger').classList.toggle('open', csOpen);
@@ -387,37 +503,94 @@ document.addEventListener('click', function(e) {
387
  document.getElementById('cs-options').classList.remove('open');
388
  }
389
  });
 
390
  async function loadModelInfo() {
391
  try {
392
  const res = await fetch('/model_info');
393
- const data = await res.json();
394
- if (data.error) { showToast('Model not ready: ' + data.error); return; }
395
- document.getElementById('model-badge').textContent =
396
- 'LR=' + data.config.learning_rate + ' · N=' + data.config.n_estimators;
397
- const rows = [
398
- ['Learning Rate', data.config.learning_rate],
399
- ['N Estimators', data.config.n_estimators],
400
- ['Split', data.config.train_ratio + '/' + data.config.val_ratio + '/' + data.config.test_ratio],
401
- ['Test AUC', (data.test_metrics.test_auc * 100).toFixed(2) + '%'],
402
- ['Test F1', (data.test_metrics.test_f1 * 100).toFixed(2) + '%'],
403
- ['Test Accuracy', (data.test_metrics.test_accuracy * 100).toFixed(2) + '%'],
404
- ['Precision', (data.test_metrics.test_precision* 100).toFixed(2) + '%'],
405
- ['Recall', (data.test_metrics.test_recall * 100).toFixed(2) + '%'],
 
 
 
406
  ];
407
- document.getElementById('model-info').innerHTML = rows.map(([k,v],i) =>
408
- '<div class="info-row"><span class="info-key">' + k + '</span>' +
409
- '<span class="info-val' + (i >= 3 ? ' green' : '') + '">' + v + '</span></div>'
410
- ).join('');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  } catch(e) {
412
- document.getElementById('model-badge').textContent = 'Model Error';
413
  }
414
  }
415
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
  async function runPredict() {
417
  const btn = document.getElementById('predict-btn');
418
  btn.classList.add('loading');
419
  btn.disabled = true;
420
  const payload = {
 
421
  equipment: document.getElementById('equipment').value,
422
  temperature: parseFloat(document.getElementById('temperature').value),
423
  pressure: parseFloat(document.getElementById('pressure').value),
@@ -446,6 +619,7 @@ function showResult(data, payload) {
446
  const isFaulty = data.prediction === 1;
447
  const prob = (data.probability * 100).toFixed(1);
448
  const cls = isFaulty ? 'faulty' : 'healthy';
 
449
  document.getElementById('result-card').className = 'result-card ' + cls;
450
  document.getElementById('idle-state').style.display = 'none';
451
  document.getElementById('result-content').style.display = 'block';
@@ -455,6 +629,9 @@ function showResult(data, payload) {
455
  document.getElementById('verdict-sub').textContent = isFaulty
456
  ? 'High fault probability — immediate inspection recommended.'
457
  : 'Equipment readings within normal operating range.';
 
 
 
458
  document.getElementById('prob-pct').textContent = prob + '%';
459
  const fill = document.getElementById('prob-fill');
460
  fill.className = 'prob-fill ' + cls;
@@ -472,13 +649,17 @@ function showResult(data, payload) {
472
  function addHistory(data, payload) {
473
  const isFaulty = data.prediction === 1;
474
  const cls = isFaulty ? 'faulty' : 'healthy';
 
475
  const list = document.getElementById('history-list');
476
- if (list.children.length === 1 && list.firstElementChild.style.color) list.innerHTML = '';
 
 
477
  const item = document.createElement('div');
478
  item.className = 'hist-item';
479
  item.innerHTML =
480
  '<div><div style="font-family:var(--mono);font-size:.78rem;">' + payload.equipment + '</div>' +
481
- '<div class="hist-equip">T=' + payload.temperature + '° P=' + payload.pressure + 'bar V=' + payload.vibration + '</div></div>' +
 
482
  '<span class="hist-badge ' + cls + '">' + (isFaulty ? 'FAULT' : 'OK') + ' · ' + (data.probability*100).toFixed(1) + '%</span>';
483
  list.prepend(item);
484
  if (list.children.length > 20) list.removeChild(list.lastChild);
@@ -503,24 +684,30 @@ loadModelInfo();
503
 
504
  @app.route("/")
505
  def index():
506
- options = "\n".join(
507
- f'<option value="{e}">{e.capitalize()}</option>'
508
- for e in EQUIPMENT_OPTIONS
509
- )
510
- return render_template_string(HTML, equipment_options=options)
511
 
512
  @app.route("/model_info")
513
  def model_info():
514
- cfg = ARTIFACT["config"]
515
  return jsonify({
516
- "config": cfg,
517
- "test_metrics": ARTIFACT["test_metrics"],
518
- "cm": ARTIFACT["cm"],
 
 
 
 
 
 
 
519
  })
520
 
521
  @app.route("/predict", methods=["POST"])
522
  def predict():
523
  body = request.get_json(force=True)
 
 
 
 
524
  try:
525
  row = pd.DataFrame([{
526
  "equipment": body["equipment"],
@@ -532,11 +719,13 @@ def predict():
532
  except (KeyError, ValueError) as e:
533
  return jsonify({"error": f"Bad input: {e}"}), 400
534
 
535
- prob = float(ARTIFACT["pipeline"].predict_proba(row)[0, 1])
 
536
  pred = int(prob >= THRESHOLD)
537
  confidence = "HIGH" if prob > 0.85 or prob < 0.15 else "MEDIUM" if prob > 0.65 or prob < 0.35 else "LOW"
538
 
539
  return jsonify({
 
540
  "prediction": pred,
541
  "probability": round(prob, 4),
542
  "confidence": confidence,
 
1
  """
2
+ FaultSense — LightGBM + Random Forest Fault Prediction App
3
+ Both models trained at startup; UI lets user switch between them.
 
 
4
  """
5
 
6
  import os
 
18
  from sklearn.preprocessing import OneHotEncoder
19
  from sklearn.compose import ColumnTransformer
20
  from sklearn.pipeline import Pipeline
21
+ from sklearn.ensemble import RandomForestClassifier
22
  import joblib
23
  from lightgbm import LGBMClassifier
24
  from flask import Flask, request, jsonify, render_template_string
 
27
  # CONFIG
28
  # ─────────────────────────────────────────────
29
 
 
30
  DATA_PATH = "synthetic_nim_parallel_10000.csv"
31
+ LGBM_PATH = "/tmp/faultsense_lgbm.joblib"
32
+ RF_PATH = "/tmp/faultsense_rf.joblib"
33
 
34
  DROP_COLS = ["location"]
35
  TARGET = "faulty"
 
39
  RANDOM_STATE = 42
40
  THRESHOLD = 0.5
41
 
42
+ LGBM_PARAMS = dict(
43
  max_depth=8,
44
  num_leaves=50,
45
  min_child_samples=20,
 
48
  class_weight="balanced",
49
  random_state=RANDOM_STATE,
50
  verbose=-1,
51
+ learning_rate=0.05,
52
+ n_estimators=100,
53
+ )
54
+
55
+ RF_PARAMS = dict(
56
+ n_estimators=200,
57
+ max_depth=10,
58
+ min_samples_split=10,
59
+ min_samples_leaf=5,
60
+ class_weight="balanced",
61
+ random_state=RANDOM_STATE,
62
+ n_jobs=-1,
63
  )
64
 
65
  BEST_CONFIG = {
66
+ "train_ratio": 0.50,
67
+ "val_ratio": 0.20,
68
+ "test_ratio": 0.30,
 
 
69
  }
70
 
71
  EQUIPMENT_OPTIONS = ["pump", "compressor", "motor", "valve", "sensor"]
 
80
  ("num", "passthrough", NUM_COLS),
81
  ])
82
 
83
+ def load_data(cfg):
 
84
  df_raw = pd.read_csv(DATA_PATH)
85
  df_raw = df_raw.drop(columns=DROP_COLS, errors="ignore")
86
  X = df_raw.drop(columns=[TARGET])
87
  y = df_raw[TARGET]
 
88
  train_r, val_r, test_r = cfg["train_ratio"], cfg["val_ratio"], cfg["test_ratio"]
89
  X_trainval, X_test, y_trainval, y_test = train_test_split(
90
  X, y, test_size=test_r, stratify=y, random_state=RANDOM_STATE
 
94
  X_trainval, y_trainval, test_size=val_relative,
95
  stratify=y_trainval, random_state=RANDOM_STATE
96
  )
97
+ return X_train, X_val, X_test, y_train, y_val, y_test
98
 
99
+ def compute_metrics(pipeline, X_test, y_test):
 
 
 
 
 
 
 
 
 
100
  y_prob = pipeline.predict_proba(X_test)[:, 1]
101
  y_pred = (y_prob >= THRESHOLD).astype(int)
102
+ return {
 
103
  "test_auc": round(roc_auc_score(y_test, y_prob), 4),
104
  "test_accuracy": round(accuracy_score(y_test, y_pred), 4),
105
  "test_precision": round(precision_score(y_test, y_pred, zero_division=0), 4),
106
  "test_recall": round(recall_score(y_test, y_pred, zero_division=0), 4),
107
  "test_f1": round(f1_score(y_test, y_pred, zero_division=0), 4),
108
  "test_logloss": round(log_loss(y_test, y_prob), 4),
109
+ }, confusion_matrix(y_test, y_pred).tolist()
110
+
111
+ def train_lgbm(X_train, X_test, y_train, y_test):
112
+ print("Training LightGBM...")
113
+ pipeline = Pipeline([
114
+ ("pre", make_preprocessor()),
115
+ ("clf", LGBMClassifier(**LGBM_PARAMS))
116
+ ])
117
+ pipeline.fit(X_train, y_train)
118
+ metrics, cm = compute_metrics(pipeline, X_test, y_test)
119
+ print(f"LGBM AUC={metrics['test_auc']} F1={metrics['test_f1']}")
120
+ return {"pipeline": pipeline, "test_metrics": metrics, "cm": cm,
121
+ "config": {**BEST_CONFIG, "model": "LightGBM",
122
+ "learning_rate": LGBM_PARAMS["learning_rate"],
123
+ "n_estimators": LGBM_PARAMS["n_estimators"]}}
124
+
125
+ def train_rf(X_train, X_test, y_train, y_test):
126
+ print("Training Random Forest...")
127
+ pipeline = Pipeline([
128
+ ("pre", make_preprocessor()),
129
+ ("clf", RandomForestClassifier(**RF_PARAMS))
130
+ ])
131
+ pipeline.fit(X_train, y_train)
132
+ metrics, cm = compute_metrics(pipeline, X_test, y_test)
133
+ print(f"RF AUC={metrics['test_auc']} F1={metrics['test_f1']}")
134
+ return {"pipeline": pipeline, "test_metrics": metrics, "cm": cm,
135
+ "config": {**BEST_CONFIG, "model": "Random Forest",
136
+ "n_estimators": RF_PARAMS["n_estimators"],
137
+ "max_depth": RF_PARAMS["max_depth"]}}
138
+
139
+ def load_or_train_all():
140
+ X_train, X_val, X_test, y_train, y_val, y_test = load_data(BEST_CONFIG)
141
+ if os.path.exists(LGBM_PATH):
142
+ print(f"Loading LGBM from {LGBM_PATH}")
143
+ lgbm_artifact = joblib.load(LGBM_PATH)
144
+ else:
145
+ lgbm_artifact = train_lgbm(X_train, X_test, y_train, y_test)
146
+ joblib.dump(lgbm_artifact, LGBM_PATH)
147
+
148
+ if os.path.exists(RF_PATH):
149
+ print(f"Loading RF from {RF_PATH}")
150
+ rf_artifact = joblib.load(RF_PATH)
151
+ else:
152
+ rf_artifact = train_rf(X_train, X_test, y_train, y_test)
153
+ joblib.dump(rf_artifact, RF_PATH)
154
+
155
+ return {"lgbm": lgbm_artifact, "rf": rf_artifact}
156
 
157
  # ─────────────────────────────────────────────
158
+ # LOAD MODELS AT MODULE LEVEL
159
  # ─────────────────────────────────────────────
160
+ ARTIFACTS = load_or_train_all()
161
 
162
  # ─────────────────────────────────────────────
163
  # FLASK APP
 
177
  :root {
178
  --bg: #0a0c10; --surface: #111318; --surface2: #181c24;
179
  --border: #232838; --accent: #00e5a0; --accent2: #ff4d6d;
180
+ --accent3: #4d9fff; --accent4: #f59e0b;
181
+ --text: #e8eaf0; --muted: #6b7280;
182
  --mono: 'Space Mono', monospace; --sans: 'DM Sans', sans-serif;
183
  }
184
  html { font-size: 16px; }
 
192
  .blob-1 { background: var(--accent); top: -200px; left: -200px; }
193
  .blob-2 { background: var(--accent3); bottom: -200px; right: -100px; animation-delay: -6s; }
194
  @keyframes drift { from { transform: translate(0,0) scale(1); } to { transform: translate(40px,30px) scale(1.05); } }
195
+ .wrapper { position: relative; z-index: 1; max-width: 1200px; margin: 0 auto; padding: 40px 24px 80px; }
196
+ header { display: flex; align-items: center; gap: 16px; margin-bottom: 32px; border-bottom: 1px solid var(--border); padding-bottom: 24px; }
197
  .logo-mark { width: 44px; height: 44px; background: var(--accent); border-radius: 10px; display: grid; place-items: center; font-family: var(--mono); font-weight: 700; font-size: 18px; color: var(--bg); flex-shrink: 0; }
198
  header h1 { font-family: var(--mono); font-size: 1.5rem; letter-spacing: -.5px; }
199
  header p { font-size: .85rem; color: var(--muted); margin-top: 2px; }
200
  .badge { margin-left: auto; font-family: var(--mono); font-size: .7rem; background: rgba(0,229,160,.12); color: var(--accent); border: 1px solid rgba(0,229,160,.3); border-radius: 6px; padding: 4px 10px; white-space: nowrap; }
201
+
202
+ /* MODEL SELECTOR TABS */
203
+ .model-tabs { display: flex; gap: 10px; margin-bottom: 24px; }
204
+ .model-tab {
205
+ flex: 1; padding: 14px 20px; border-radius: 12px; border: 1px solid var(--border);
206
+ background: var(--surface); cursor: pointer; font-family: var(--mono);
207
+ font-size: .8rem; color: var(--muted); transition: all .2s; text-align: center;
208
+ display: flex; flex-direction: column; gap: 4px; align-items: center;
209
+ }
210
+ .model-tab:hover { border-color: var(--accent); color: var(--text); }
211
+ .model-tab.active.lgbm { border-color: var(--accent); background: rgba(0,229,160,.08); color: var(--accent); }
212
+ .model-tab.active.rf { border-color: var(--accent4); background: rgba(245,158,11,.08); color: var(--accent4); }
213
+ .model-tab .tab-name { font-size: .95rem; font-weight: 700; }
214
+ .model-tab .tab-desc { font-size: .65rem; color: inherit; opacity: .7; }
215
+ .tab-auc { font-size: .7rem; opacity: .85; margin-top: 2px; }
216
+
217
+ .main-grid { display: grid; grid-template-columns: 1fr 400px; gap: 24px; align-items: start; }
218
+ @media (max-width: 900px) { .main-grid { grid-template-columns: 1fr; } }
219
  .card { background: var(--surface); border: 1px solid var(--border); border-radius: 16px; padding: 28px; }
220
  .card-title { font-family: var(--mono); font-size: .75rem; letter-spacing: 1.5px; text-transform: uppercase; color: var(--muted); margin-bottom: 20px; display: flex; align-items: center; gap: 8px; }
221
  .card-title::before { content: ''; display: inline-block; width: 6px; height: 6px; background: var(--accent); border-radius: 50%; }
 
257
  input[type=range]::-webkit-slider-thumb:hover { transform: scale(1.3); }
258
  input[type=range]::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: var(--accent); border: none; }
259
  .slider-val { font-family: var(--mono); font-size: .85rem; color: var(--accent); min-width: 60px; text-align: right; }
260
+
261
  .btn-predict { margin-top: 24px; width: 100%; padding: 14px; background: var(--accent); color: var(--bg); border: none; border-radius: 12px; font-family: var(--mono); font-size: 1rem; font-weight: 700; letter-spacing: 1px; cursor: pointer; transition: transform .15s, box-shadow .2s; }
262
  .btn-predict:hover { transform: translateY(-2px); box-shadow: 0 0 32px rgba(0,229,160,.5); }
263
+ .btn-predict.rf-active { background: var(--accent4); }
264
+ .btn-predict.rf-active:hover { box-shadow: 0 0 32px rgba(245,158,11,.5); }
265
+ .btn-predict:disabled { background: var(--muted); cursor: not-allowed; transform: none; box-shadow: none; }
266
+
267
  .result-card { border-radius: 16px; padding: 28px; border: 1px solid var(--border); background: var(--surface); transition: border-color .4s; }
268
  .result-card.faulty { border-color: var(--accent2); background: rgba(255,77,109,.06); }
269
  .result-card.healthy { border-color: var(--accent); background: rgba(0,229,160,.06); }
270
  .verdict { font-family: var(--mono); font-size: 2rem; font-weight: 700; letter-spacing: -1px; margin-bottom: 6px; }
271
  .verdict.faulty { color: var(--accent2); }
272
  .verdict.healthy { color: var(--accent); }
273
+ .verdict-sub { font-size: .85rem; color: var(--muted); margin-bottom: 8px; }
274
+ .model-used-tag { display: inline-block; font-family: var(--mono); font-size: .65rem; padding: 3px 8px; border-radius: 6px; margin-bottom: 20px; }
275
+ .model-used-tag.lgbm { background: rgba(0,229,160,.12); color: var(--accent); border: 1px solid rgba(0,229,160,.3); }
276
+ .model-used-tag.rf { background: rgba(245,158,11,.12); color: var(--accent4); border: 1px solid rgba(245,158,11,.3); }
277
  .prob-bar-wrap { margin-bottom: 24px; }
278
  .prob-label { font-family: var(--mono); font-size: .72rem; color: var(--muted); margin-bottom: 6px; display: flex; justify-content: space-between; }
279
  .prob-track { height: 10px; background: var(--border); border-radius: 10px; overflow: hidden; }
 
284
  .mini-metric { background: var(--surface2); border-radius: 10px; padding: 12px; border: 1px solid var(--border); }
285
  .mini-metric .mm-val { font-family: var(--mono); font-size: 1.1rem; font-weight: 700; color: var(--accent3); }
286
  .mini-metric .mm-key { font-size: .7rem; color: var(--muted); margin-top: 2px; font-family: var(--mono); }
287
+
288
+ /* METRICS COMPARISON TABLE */
289
+ .compare-table { width: 100%; border-collapse: collapse; }
290
+ .compare-table th { font-family: var(--mono); font-size: .65rem; letter-spacing: 1px; text-transform: uppercase; color: var(--muted); padding: 8px 10px; text-align: left; border-bottom: 1px solid var(--border); }
291
+ .compare-table th.lgbm-col { color: var(--accent); }
292
+ .compare-table th.rf-col { color: var(--accent4); }
293
+ .compare-table td { font-family: var(--mono); font-size: .78rem; padding: 9px 10px; border-bottom: 1px solid var(--border); }
294
+ .compare-table tr:last-child td { border-bottom: none; }
295
+ .compare-table td.metric-name { color: var(--muted); font-size: .7rem; }
296
+ .compare-table td.win { font-weight: 700; }
297
+ .compare-table td.win.lgbm { color: var(--accent); }
298
+ .compare-table td.win.rf { color: var(--accent4); }
299
+ .win-tag { font-size: .55rem; padding: 1px 5px; border-radius: 4px; margin-left: 5px; vertical-align: middle; }
300
+ .win-tag.lgbm { background: rgba(0,229,160,.15); color: var(--accent); }
301
+ .win-tag.rf { background: rgba(245,158,11,.15); color: var(--accent4); }
302
+
303
  .info-row { display: flex; justify-content: space-between; align-items: center; padding: 9px 0; border-bottom: 1px solid var(--border); font-size: .82rem; }
304
  .info-row:last-child { border-bottom: none; }
305
  .info-key { color: var(--muted); font-family: var(--mono); font-size: .72rem; }
306
  .info-val { font-family: var(--mono); color: var(--text); font-weight: 700; }
307
  .info-val.green { color: var(--accent); }
308
+ .info-val.amber { color: var(--accent4); }
309
+
310
  .history-list { max-height: 260px; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; }
311
  .hist-item { background: var(--surface2); border: 1px solid var(--border); border-radius: 10px; padding: 10px 14px; display: flex; justify-content: space-between; align-items: center; font-size: .78rem; }
312
  .hist-equip { color: var(--muted); font-family: var(--mono); font-size: .7rem; }
313
  .hist-badge { font-family: var(--mono); font-size: .68rem; padding: 3px 8px; border-radius: 6px; font-weight: 700; }
314
  .hist-badge.faulty { background: rgba(255,77,109,.2); color: var(--accent2); }
315
  .hist-badge.healthy { background: rgba(0,229,160,.2); color: var(--accent); }
316
+ .hist-model-tag { font-family: var(--mono); font-size: .58rem; padding: 2px 6px; border-radius: 4px; margin-top: 3px; display: inline-block; }
317
+ .hist-model-tag.lgbm { background: rgba(0,229,160,.1); color: var(--accent); }
318
+ .hist-model-tag.rf { background: rgba(245,158,11,.1); color: var(--accent4); }
319
+
320
  .spinner { display: none; width: 20px; height: 20px; border: 2px solid rgba(10,12,16,.3); border-top-color: var(--bg); border-radius: 50%; animation: spin .6s linear infinite; margin: 0 auto; }
321
  @keyframes spin { to { transform: rotate(360deg); } }
322
  .btn-predict.loading .btn-text { display: none; }
 
335
  <div class="logo-mark">FS</div>
336
  <div>
337
  <h1>FaultSense</h1>
338
+ <p>Multi-Model Equipment Fault Predictor</p>
339
  </div>
340
+ <div class="badge" id="model-badge">Loading Models…</div>
341
  </header>
342
 
343
+ <!-- MODEL SELECTOR TABS -->
344
+ <div class="model-tabs" id="model-tabs">
345
+ <div class="model-tab active lgbm" id="tab-lgbm" onclick="selectModel('lgbm')">
346
+ <span class="tab-name">⚡ LightGBM</span>
347
+ <span class="tab-desc">Gradient Boosting</span>
348
+ <span class="tab-auc" id="lgbm-tab-auc">Loading…</span>
349
+ </div>
350
+ <div class="model-tab rf" id="tab-rf" onclick="selectModel('rf')">
351
+ <span class="tab-name">🌲 Random Forest</span>
352
+ <span class="tab-desc">Ensemble Trees</span>
353
+ <span class="tab-auc" id="rf-tab-auc">Loading…</span>
354
+ </div>
355
+ </div>
356
+
357
  <div class="main-grid">
358
  <div style="display:flex;flex-direction:column;gap:20px;">
359
  <div class="card">
 
415
  </div>
416
 
417
  </div>
418
+ <button class="btn-predict lgbm-active" id="predict-btn" onclick="runPredict()">
419
  <span class="btn-text">⚡ Run Prediction</span>
420
  <div class="spinner"></div>
421
  </button>
422
  </div>
423
 
424
+ <!-- METRICS COMPARISON -->
425
+ <div class="card">
426
+ <div class="card-title">Model Comparison</div>
427
+ <div id="compare-content">
428
+ <div style="color:var(--muted);font-size:.8rem;font-family:var(--mono);text-align:center;padding:12px 0;">Loading…</div>
429
+ </div>
430
+ </div>
431
+
432
  <div class="card">
433
  <div class="card-title">Prediction History</div>
434
  <div class="history-list" id="history-list">
 
441
  <div class="result-card" id="result-card">
442
  <div class="idle-state" id="idle-state">
443
  <div class="idle-icon">🔬</div>
444
+ <p>Select a model, enter sensor<br>readings, and run a prediction<br>to see results here.</p>
445
  </div>
446
  <div id="result-content" style="display:none;">
447
  <div class="verdict" id="verdict-text"></div>
448
  <div class="verdict-sub" id="verdict-sub"></div>
449
+ <div id="model-used-tag" class="model-used-tag lgbm"></div>
450
  <div class="prob-bar-wrap">
451
  <div class="prob-label"><span>Fault Probability</span><span id="prob-pct"></span></div>
452
  <div class="prob-track"><div class="prob-fill" id="prob-fill" style="width:0%"></div></div>
 
456
  </div>
457
 
458
  <div class="card">
459
+ <div class="card-title" id="model-config-title">Active Model Config</div>
460
  <div id="model-info">
461
  <div style="color:var(--muted);font-size:.8rem;font-family:var(--mono);text-align:center;padding:12px 0;">Loading…</div>
462
  </div>
 
469
 
470
  <script>
471
  let csOpen = false;
472
+ let activeModel = 'lgbm';
473
+ let modelData = {};
474
+
475
+ function selectModel(model) {
476
+ activeModel = model;
477
+ document.getElementById('tab-lgbm').className = 'model-tab' + (model === 'lgbm' ? ' active lgbm' : '');
478
+ document.getElementById('tab-rf').className = 'model-tab' + (model === 'rf' ? ' active rf' : '');
479
+ const btn = document.getElementById('predict-btn');
480
+ btn.className = model === 'lgbm' ? 'btn-predict lgbm-active' : 'btn-predict rf-active';
481
+ btn.querySelector('.btn-text').textContent = model === 'lgbm' ? '⚡ Run Prediction' : '🌲 Run Prediction';
482
+ updateModelInfo(model);
483
+ }
484
+
485
  function toggleDropdown() {
486
  csOpen = !csOpen;
487
  document.getElementById('cs-trigger').classList.toggle('open', csOpen);
 
503
  document.getElementById('cs-options').classList.remove('open');
504
  }
505
  });
506
+
507
  async function loadModelInfo() {
508
  try {
509
  const res = await fetch('/model_info');
510
+ modelData = await res.json();
511
+ if (modelData.error) { showToast('Model error: ' + modelData.error); return; }
512
+
513
+ const lg = modelData.lgbm, rf = modelData.rf;
514
+ document.getElementById('lgbm-tab-auc').textContent = 'AUC ' + (lg.test_metrics.test_auc * 100).toFixed(1) + '%';
515
+ document.getElementById('rf-tab-auc').textContent = 'AUC ' + (rf.test_metrics.test_auc * 100).toFixed(1) + '%';
516
+ document.getElementById('model-badge').textContent = '2 Models Ready';
517
+
518
+ // Build comparison table
519
+ const metrics = [
520
+ ['AUC', 'test_auc'],
521
+ ['Accuracy', 'test_accuracy'],
522
+ ['Precision', 'test_precision'],
523
+ ['Recall', 'test_recall'],
524
+ ['F1 Score', 'test_f1'],
525
+ ['Log Loss', 'test_logloss'],
526
  ];
527
+ const rows = metrics.map(([label, key]) => {
528
+ const lv = lg.test_metrics[key], rv = rf.test_metrics[key];
529
+ const higherBetter = key !== 'test_logloss';
530
+ const lgWins = higherBetter ? lv > rv : lv < rv;
531
+ const rfWins = higherBetter ? rv > lv : rv < lv;
532
+ const fmt = v => key === 'test_logloss' ? v.toFixed(4) : (v * 100).toFixed(2) + '%';
533
+ return `<tr>
534
+ <td class="metric-name">${label}</td>
535
+ <td class="${lgWins ? 'win lgbm' : ''}">${fmt(lv)}${lgWins ? '<span class="win-tag lgbm">▲</span>' : ''}</td>
536
+ <td class="${rfWins ? 'win rf' : ''}">${fmt(rv)}${rfWins ? '<span class="win-tag rf">▲</span>' : ''}</td>
537
+ </tr>`;
538
+ }).join('');
539
+ document.getElementById('compare-content').innerHTML = `
540
+ <table class="compare-table">
541
+ <thead><tr>
542
+ <th>Metric</th>
543
+ <th class="lgbm-col">⚡ LightGBM</th>
544
+ <th class="rf-col">🌲 Random Forest</th>
545
+ </tr></thead>
546
+ <tbody>${rows}</tbody>
547
+ </table>`;
548
+
549
+ updateModelInfo('lgbm');
550
  } catch(e) {
551
+ document.getElementById('model-badge').textContent = 'Load Error';
552
  }
553
  }
554
 
555
+ function updateModelInfo(model) {
556
+ if (!modelData[model]) return;
557
+ const d = modelData[model];
558
+ const isLgbm = model === 'lgbm';
559
+ const color = isLgbm ? 'green' : 'amber';
560
+
561
+ const rows = isLgbm ? [
562
+ ['Model', 'LightGBM'],
563
+ ['Learning Rate', d.config.learning_rate],
564
+ ['N Estimators', d.config.n_estimators],
565
+ ['Split', d.config.train_ratio + '/' + d.config.val_ratio + '/' + d.config.test_ratio],
566
+ ] : [
567
+ ['Model', 'Random Forest'],
568
+ ['N Estimators', d.config.n_estimators],
569
+ ['Max Depth', d.config.max_depth],
570
+ ['Split', d.config.train_ratio + '/' + d.config.val_ratio + '/' + d.config.test_ratio],
571
+ ];
572
+
573
+ const metricRows = [
574
+ ['Test AUC', (d.test_metrics.test_auc * 100).toFixed(2) + '%'],
575
+ ['Test F1', (d.test_metrics.test_f1 * 100).toFixed(2) + '%'],
576
+ ['Test Accuracy', (d.test_metrics.test_accuracy * 100).toFixed(2) + '%'],
577
+ ['Precision', (d.test_metrics.test_precision * 100).toFixed(2) + '%'],
578
+ ['Recall', (d.test_metrics.test_recall * 100).toFixed(2) + '%'],
579
+ ];
580
+
581
+ document.getElementById('model-info').innerHTML =
582
+ [...rows, ...metricRows].map(([k, v], i) =>
583
+ '<div class="info-row"><span class="info-key">' + k + '</span>' +
584
+ '<span class="info-val' + (i >= rows.length ? ' ' + color : '') + '">' + v + '</span></div>'
585
+ ).join('');
586
+ }
587
+
588
  async function runPredict() {
589
  const btn = document.getElementById('predict-btn');
590
  btn.classList.add('loading');
591
  btn.disabled = true;
592
  const payload = {
593
+ model: activeModel,
594
  equipment: document.getElementById('equipment').value,
595
  temperature: parseFloat(document.getElementById('temperature').value),
596
  pressure: parseFloat(document.getElementById('pressure').value),
 
619
  const isFaulty = data.prediction === 1;
620
  const prob = (data.probability * 100).toFixed(1);
621
  const cls = isFaulty ? 'faulty' : 'healthy';
622
+ const isLgbm = data.model === 'lgbm';
623
  document.getElementById('result-card').className = 'result-card ' + cls;
624
  document.getElementById('idle-state').style.display = 'none';
625
  document.getElementById('result-content').style.display = 'block';
 
629
  document.getElementById('verdict-sub').textContent = isFaulty
630
  ? 'High fault probability — immediate inspection recommended.'
631
  : 'Equipment readings within normal operating range.';
632
+ const tag = document.getElementById('model-used-tag');
633
+ tag.className = 'model-used-tag ' + (isLgbm ? 'lgbm' : 'rf');
634
+ tag.textContent = isLgbm ? '⚡ LightGBM' : '🌲 Random Forest';
635
  document.getElementById('prob-pct').textContent = prob + '%';
636
  const fill = document.getElementById('prob-fill');
637
  fill.className = 'prob-fill ' + cls;
 
649
  function addHistory(data, payload) {
650
  const isFaulty = data.prediction === 1;
651
  const cls = isFaulty ? 'faulty' : 'healthy';
652
+ const isLgbm = data.model === 'lgbm';
653
  const list = document.getElementById('history-list');
654
+ if (list.children.length === 1 && list.firstElementChild.style.color !== undefined
655
+ && list.firstElementChild.querySelector) list.innerHTML = '';
656
+ if (list.children.length === 1 && !list.firstElementChild.classList.contains('hist-item')) list.innerHTML = '';
657
  const item = document.createElement('div');
658
  item.className = 'hist-item';
659
  item.innerHTML =
660
  '<div><div style="font-family:var(--mono);font-size:.78rem;">' + payload.equipment + '</div>' +
661
+ '<div class="hist-equip">T=' + payload.temperature + '° P=' + payload.pressure + 'bar V=' + payload.vibration + '</div>' +
662
+ '<span class="hist-model-tag ' + (isLgbm ? 'lgbm' : 'rf') + '">' + (isLgbm ? '⚡ LGBM' : '🌲 RF') + '</span></div>' +
663
  '<span class="hist-badge ' + cls + '">' + (isFaulty ? 'FAULT' : 'OK') + ' · ' + (data.probability*100).toFixed(1) + '%</span>';
664
  list.prepend(item);
665
  if (list.children.length > 20) list.removeChild(list.lastChild);
 
684
 
685
  @app.route("/")
686
  def index():
687
+ return render_template_string(HTML)
 
 
 
 
688
 
689
  @app.route("/model_info")
690
  def model_info():
 
691
  return jsonify({
692
+ "lgbm": {
693
+ "config": ARTIFACTS["lgbm"]["config"],
694
+ "test_metrics": ARTIFACTS["lgbm"]["test_metrics"],
695
+ "cm": ARTIFACTS["lgbm"]["cm"],
696
+ },
697
+ "rf": {
698
+ "config": ARTIFACTS["rf"]["config"],
699
+ "test_metrics": ARTIFACTS["rf"]["test_metrics"],
700
+ "cm": ARTIFACTS["rf"]["cm"],
701
+ },
702
  })
703
 
704
  @app.route("/predict", methods=["POST"])
705
  def predict():
706
  body = request.get_json(force=True)
707
+ model_key = body.get("model", "lgbm")
708
+ if model_key not in ARTIFACTS:
709
+ return jsonify({"error": f"Unknown model '{model_key}'. Use 'lgbm' or 'rf'."}), 400
710
+
711
  try:
712
  row = pd.DataFrame([{
713
  "equipment": body["equipment"],
 
719
  except (KeyError, ValueError) as e:
720
  return jsonify({"error": f"Bad input: {e}"}), 400
721
 
722
+ artifact = ARTIFACTS[model_key]
723
+ prob = float(artifact["pipeline"].predict_proba(row)[0, 1])
724
  pred = int(prob >= THRESHOLD)
725
  confidence = "HIGH" if prob > 0.85 or prob < 0.15 else "MEDIUM" if prob > 0.65 or prob < 0.35 else "LOW"
726
 
727
  return jsonify({
728
+ "model": model_key,
729
  "prediction": pred,
730
  "probability": round(prob, 4),
731
  "confidence": confidence,
requirements.txt CHANGED
@@ -37,3 +37,12 @@ openpyxl>=3.1.0
37
 
38
 
39
  gunicorn>=21.2.0
 
 
 
 
 
 
 
 
 
 
37
 
38
 
39
  gunicorn>=21.2.0
40
+
41
+
42
+ flask
43
+ lightgbm
44
+ scikit-learn
45
+ pandas
46
+ numpy
47
+ joblib
48
+ gunicorn