IDKHowToCodeFr commited on
Commit
aad3f84
·
verified ·
1 Parent(s): 8f83768

Upload folder using huggingface_hub

Browse files
backend/ensemble.py CHANGED
@@ -9,20 +9,44 @@ if backend_dir not in sys.path:
9
 
10
  from preprocessing import resolve_model_dir
11
 
 
 
12
  class EnsembleModel:
13
  def __init__(self):
14
  self.model_dir = resolve_model_dir()
15
  self.model_names = ['knn', 'svm', 'logreg', 'rf', 'small_nn']
16
  self.models = {}
17
  self.load_models()
18
- self.label_encoder = joblib.load(f'{self.model_dir}/label_encoder.pkl')
 
 
 
 
 
 
19
  self.weights = {'knn': 0.15, 'svm': 0.15, 'logreg': 0.20, 'rf': 0.25, 'small_nn': 0.25}
20
 
21
  def load_models(self):
22
  for name in self.model_names:
23
- path = f'{self.model_dir}/{name}.pkl'
24
  if os.path.exists(path):
25
  self.models[name] = joblib.load(path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  def predict(self, X):
28
  individual_preds = {}
@@ -31,8 +55,8 @@ class EnsembleModel:
31
  for name, model in self.models.items():
32
  probs = model.predict_proba(X)
33
  individual_probs[name] = probs[0]
34
- pred_indices = np.argmax(probs, axis=1)
35
- individual_preds[name] = str(self.label_encoder.inverse_transform(pred_indices)[0])
36
 
37
  num_classes = list(individual_probs.values())[0].shape[0]
38
  weighted_probs = np.zeros(num_classes)
@@ -44,12 +68,12 @@ class EnsembleModel:
44
  total_weight += w
45
 
46
  weighted_probs /= total_weight
47
- final_pred_idx = np.argmax(weighted_probs)
48
- final_pred = str(self.label_encoder.inverse_transform([final_pred_idx])[0])
49
  confidence = float(np.max(weighted_probs))
50
 
51
  class_probs = {
52
- str(self.label_encoder.inverse_transform([i])[0]): float(prob)
53
  for i, prob in enumerate(weighted_probs)
54
  }
55
 
 
9
 
10
  from preprocessing import resolve_model_dir
11
 
12
+ CANONICAL_CLASSES = ['Asthma', 'Diabetes Mellitus', 'Healthy', 'Heart Disease', 'Hypertension']
13
+
14
  class EnsembleModel:
15
  def __init__(self):
16
  self.model_dir = resolve_model_dir()
17
  self.model_names = ['knn', 'svm', 'logreg', 'rf', 'small_nn']
18
  self.models = {}
19
  self.load_models()
20
+ self.label_encoder = None
21
+ le_path = os.path.join(self.model_dir, 'label_encoder.pkl')
22
+ if os.path.exists(le_path):
23
+ try:
24
+ self.label_encoder = joblib.load(le_path)
25
+ except Exception as e:
26
+ print(f"Warning: Failed to load label encoder: {e}")
27
  self.weights = {'knn': 0.15, 'svm': 0.15, 'logreg': 0.20, 'rf': 0.25, 'small_nn': 0.25}
28
 
29
  def load_models(self):
30
  for name in self.model_names:
31
+ path = os.path.join(self.model_dir, f'{name}.pkl')
32
  if os.path.exists(path):
33
  self.models[name] = joblib.load(path)
34
+
35
+ def decode_label(self, idx: int) -> str:
36
+ """Safely maps class index to clinical label without throwing on unseen labels."""
37
+ if self.label_encoder is not None:
38
+ if hasattr(self.label_encoder, 'classes_'):
39
+ classes = self.label_encoder.classes_
40
+ if 0 <= idx < len(classes):
41
+ return str(classes[idx])
42
+ if hasattr(self.label_encoder, 'inverse_transform'):
43
+ try:
44
+ return str(self.label_encoder.inverse_transform([idx])[0])
45
+ except Exception:
46
+ pass
47
+ if 0 <= idx < len(CANONICAL_CLASSES):
48
+ return CANONICAL_CLASSES[idx]
49
+ return f"Condition_{idx}"
50
 
51
  def predict(self, X):
52
  individual_preds = {}
 
55
  for name, model in self.models.items():
56
  probs = model.predict_proba(X)
57
  individual_probs[name] = probs[0]
58
+ pred_indices = int(np.argmax(probs, axis=1)[0])
59
+ individual_preds[name] = self.decode_label(pred_indices)
60
 
61
  num_classes = list(individual_probs.values())[0].shape[0]
62
  weighted_probs = np.zeros(num_classes)
 
68
  total_weight += w
69
 
70
  weighted_probs /= total_weight
71
+ final_pred_idx = int(np.argmax(weighted_probs))
72
+ final_pred = self.decode_label(final_pred_idx)
73
  confidence = float(np.max(weighted_probs))
74
 
75
  class_probs = {
76
+ self.decode_label(i): float(prob)
77
  for i, prob in enumerate(weighted_probs)
78
  }
79
 
backend/main.py CHANGED
@@ -187,6 +187,13 @@ async def explain(data: PatientData):
187
  return {"error": "RF Model unavailable for explanation."}
188
 
189
  df = pd.DataFrame([data.model_dump()])
 
 
 
 
 
 
 
190
  X_proc, _ = await asyncio.to_thread(preprocess_data, df, False)
191
 
192
  def compute_shap():
 
187
  return {"error": "RF Model unavailable for explanation."}
188
 
189
  df = pd.DataFrame([data.model_dump()])
190
+ df = df.rename(columns={
191
+ "Heart_Rate": "Heart Rate (bpm)",
192
+ "SpO2_Level": "SpO2 Level (%)",
193
+ "Systolic_BP": "Systolic Blood Pressure (mmHg)",
194
+ "Diastolic_BP": "Diastolic Blood Pressure (mmHg)",
195
+ "Body_Temp": "Body Temperature (°C)",
196
+ })
197
  X_proc, _ = await asyncio.to_thread(preprocess_data, df, False)
198
 
199
  def compute_shap():
backend/preprocessing.py CHANGED
@@ -31,15 +31,69 @@ def resolve_data_path():
31
  return os.path.abspath(p)
32
  return os.path.abspath(candidates[0])
33
 
34
- def preprocess_data(df, is_training=True):
35
- model_dir = resolve_model_dir()
36
- scaler_path = f'{model_dir}/scaler.pkl'
37
- label_encoder_path = f'{model_dir}/label_encoder.pkl'
38
- imputer_path = f'{model_dir}/imputer.pkl'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  columns_to_drop = ['Patient Number', 'Data Accuracy (%)', 'Heart Rate Alert', 'SpO2 Level Alert', 'Blood Pressure Alert', 'Temperature Alert', 'Fall Detection']
41
  cols_drop = [c for c in columns_to_drop if c in df.columns]
42
- X_raw = df.drop(columns=cols_drop)
43
 
44
  y = None
45
  if 'Predicted Disease' in X_raw.columns:
@@ -49,23 +103,31 @@ def preprocess_data(df, is_training=True):
49
  if is_training:
50
  label_encoder = LabelEncoder()
51
  y = label_encoder.fit_transform(y_raw)
52
- os.makedirs(os.path.dirname(label_encoder_path), exist_ok=True)
53
  joblib.dump(label_encoder, label_encoder_path)
54
- joblib.dump(label_encoder.classes_, f'{model_dir}/classes.pkl')
55
  else:
56
  if os.path.exists(label_encoder_path):
57
- label_encoder = joblib.load(label_encoder_path)
58
  try:
 
59
  y = label_encoder.transform(y_raw)
60
  except Exception:
61
  y = None
62
  else:
63
  y = y_raw
64
 
65
- # Fix broken encoding column names safely
66
- X_raw.rename(columns=lambda x: x.replace('\ufffd', '°') if isinstance(x, str) else x, inplace=True)
67
-
68
- # Engineered Feature
 
 
 
 
 
 
 
 
 
69
  X_raw['Risk_Severity'] = (X_raw['Heart Rate (bpm)'] > 105).astype(int) + (X_raw['SpO2 Level (%)'] < 94).astype(int)
70
 
71
  continuous_features = ['Heart Rate (bpm)', 'SpO2 Level (%)', 'Systolic Blood Pressure (mmHg)', 'Diastolic Blood Pressure (mmHg)', 'Body Temperature (°C)', 'Risk_Severity']
 
31
  return os.path.abspath(p)
32
  return os.path.abspath(candidates[0])
33
 
34
+ # Canonical Column Alias Mapping
35
+ COLUMN_ALIASES = {
36
+ 'heart rate (bpm)': 'Heart Rate (bpm)',
37
+ 'heart rate': 'Heart Rate (bpm)',
38
+ 'heart_rate': 'Heart Rate (bpm)',
39
+ 'heartrate': 'Heart Rate (bpm)',
40
+ 'hr': 'Heart Rate (bpm)',
41
+
42
+ 'spo2 level (%)': 'SpO2 Level (%)',
43
+ 'spo2 level': 'SpO2 Level (%)',
44
+ 'spo2_level': 'SpO2 Level (%)',
45
+ 'spo2': 'SpO2 Level (%)',
46
+ 'oxygen': 'SpO2 Level (%)',
47
+
48
+ 'systolic blood pressure (mmhg)': 'Systolic Blood Pressure (mmHg)',
49
+ 'systolic blood pressure': 'Systolic Blood Pressure (mmHg)',
50
+ 'systolic_bp': 'Systolic Blood Pressure (mmHg)',
51
+ 'sys_bp': 'Systolic Blood Pressure (mmHg)',
52
+ 'systolic': 'Systolic Blood Pressure (mmHg)',
53
+
54
+ 'diastolic blood pressure (mmhg)': 'Diastolic Blood Pressure (mmHg)',
55
+ 'diastolic blood pressure': 'Diastolic Blood Pressure (mmHg)',
56
+ 'diastolic_bp': 'Diastolic Blood Pressure (mmHg)',
57
+ 'dia_bp': 'Diastolic Blood Pressure (mmHg)',
58
+ 'diastolic': 'Diastolic Blood Pressure (mmHg)',
59
+
60
+ 'body temperature (°c)': 'Body Temperature (°C)',
61
+ 'body temperature': 'Body Temperature (°C)',
62
+ 'body_temp': 'Body Temperature (°C)',
63
+ 'temperature': 'Body Temperature (°C)',
64
+ 'temp': 'Body Temperature (°C)',
65
+
66
+ 'predicted disease': 'Predicted Disease',
67
+ 'predicted_disease': 'Predicted Disease',
68
+ 'disease': 'Predicted Disease',
69
+ 'condition': 'Predicted Disease',
70
+ 'diagnosis': 'Predicted Disease',
71
+ 'label': 'Predicted Disease',
72
+ }
73
+
74
+ def preprocess_data(df, is_training=False, model_dir=None):
75
+ if model_dir is None:
76
+ model_dir = resolve_model_dir()
77
+ os.makedirs(model_dir, exist_ok=True)
78
+
79
+ scaler_path = os.path.join(model_dir, 'scaler.pkl')
80
+ label_encoder_path = os.path.join(model_dir, 'label_encoder.pkl')
81
+ imputer_path = os.path.join(model_dir, 'imputer.pkl')
82
+
83
+ # 1. Normalize column names to canonical forms
84
+ col_map = {}
85
+ for col in df.columns:
86
+ norm_key = str(col).strip().lower().replace('\ufffd', '°')
87
+ if norm_key in COLUMN_ALIASES:
88
+ col_map[col] = COLUMN_ALIASES[norm_key]
89
+ elif isinstance(col, str) and '\ufffd' in col:
90
+ col_map[col] = col.replace('\ufffd', '°')
91
+ if col_map:
92
+ df = df.rename(columns=col_map)
93
 
94
  columns_to_drop = ['Patient Number', 'Data Accuracy (%)', 'Heart Rate Alert', 'SpO2 Level Alert', 'Blood Pressure Alert', 'Temperature Alert', 'Fall Detection']
95
  cols_drop = [c for c in columns_to_drop if c in df.columns]
96
+ X_raw = df.drop(columns=cols_drop).copy()
97
 
98
  y = None
99
  if 'Predicted Disease' in X_raw.columns:
 
103
  if is_training:
104
  label_encoder = LabelEncoder()
105
  y = label_encoder.fit_transform(y_raw)
 
106
  joblib.dump(label_encoder, label_encoder_path)
107
+ joblib.dump(label_encoder.classes_, os.path.join(model_dir, 'classes.pkl'))
108
  else:
109
  if os.path.exists(label_encoder_path):
 
110
  try:
111
+ label_encoder = joblib.load(label_encoder_path)
112
  y = label_encoder.transform(y_raw)
113
  except Exception:
114
  y = None
115
  else:
116
  y = y_raw
117
 
118
+ # Ensure all canonical continuous features exist (defaults for missing inputs)
119
+ defaults = {
120
+ 'Heart Rate (bpm)': 75.0,
121
+ 'SpO2 Level (%)': 98.0,
122
+ 'Systolic Blood Pressure (mmHg)': 120.0,
123
+ 'Diastolic Blood Pressure (mmHg)': 80.0,
124
+ 'Body Temperature (°C)': 37.0,
125
+ }
126
+ for col, default_val in defaults.items():
127
+ if col not in X_raw.columns:
128
+ X_raw[col] = default_val
129
+
130
+ # Engineered Feature: Risk_Severity
131
  X_raw['Risk_Severity'] = (X_raw['Heart Rate (bpm)'] > 105).astype(int) + (X_raw['SpO2 Level (%)'] < 94).astype(int)
132
 
133
  continuous_features = ['Heart Rate (bpm)', 'SpO2 Level (%)', 'Systolic Blood Pressure (mmHg)', 'Diastolic Blood Pressure (mmHg)', 'Body Temperature (°C)', 'Risk_Severity']
model/classes.pkl CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:1a279aa014fedd87496788d97cf12c3552ab7720aeb83866ab31155ded322811
3
- size 388
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:43241923778ab2ba4ee472ae79697c0730f7f325bf9d6607aea5d988fea6d7ab
3
+ size 440
model/imputer.pkl CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:6f5fa9da126a70dd33010185218f39105c643266a9ba82a6d2c34f058bff01a6
3
  size 935
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4b9ed7851cb3f2ef2d1db05bdcbd535259b5182bcf5f15ca24a174c86af19840
3
  size 935
model/label_encoder.pkl CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:383ae3e069a9f5aa7acd3948e54c95219b3ed3d5235c236620a17e8b8575b3f8
3
- size 491
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9cb3790496bceb78e1a77f585dd34d1fad77a64a5f9d40a410b4aa0dad119f92
3
+ size 543
model/scaler.pkl CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:46c920570f77720384e8fc6dfe04a43c58e78285232ad3c5f5376b0630c0a079
3
  size 1127
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b202b117d5d42740a293bd1c1e017f100ebdc4fdeffeaf7e84d0485d9a88636a
3
  size 1127
tests/test_preprocessing.py CHANGED
@@ -11,7 +11,7 @@ def test_resolve_model_dir():
11
  md = resolve_model_dir()
12
  assert md.endswith("model")
13
 
14
- def test_preprocess_data_training():
15
  df = pd.DataFrame({
16
  'Heart Rate (bpm)': [80, 90, np.nan],
17
  'SpO2 Level (%)': [98, 97, 95],
@@ -21,13 +21,26 @@ def test_preprocess_data_training():
21
  'Predicted Disease': ['Normal', 'Asthma', 'Normal']
22
  })
23
 
24
- X, y = preprocess_data(df, is_training=True)
 
25
 
26
  assert not X.isnull().values.any()
27
  assert len(y) == 3
28
  assert 'Risk_Severity' in X.columns
 
29
 
30
- def test_preprocess_data_inference():
 
 
 
 
 
 
 
 
 
 
 
31
  df = pd.DataFrame({
32
  'Heart Rate (bpm)': [100],
33
  'SpO2 Level (%)': [99],
@@ -36,7 +49,21 @@ def test_preprocess_data_inference():
36
  'Body Temperature (°C)': [37.0],
37
  })
38
 
39
- X, y = preprocess_data(df, is_training=False)
40
 
41
  assert y is None
42
  assert 'Risk_Severity' in X.columns
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  md = resolve_model_dir()
12
  assert md.endswith("model")
13
 
14
+ def test_preprocess_data_training(tmp_path):
15
  df = pd.DataFrame({
16
  'Heart Rate (bpm)': [80, 90, np.nan],
17
  'SpO2 Level (%)': [98, 97, 95],
 
21
  'Predicted Disease': ['Normal', 'Asthma', 'Normal']
22
  })
23
 
24
+ # Use tmp_path to isolate test artifacts from production models
25
+ X, y = preprocess_data(df, is_training=True, model_dir=str(tmp_path))
26
 
27
  assert not X.isnull().values.any()
28
  assert len(y) == 3
29
  assert 'Risk_Severity' in X.columns
30
+ assert os.path.exists(os.path.join(str(tmp_path), 'label_encoder.pkl'))
31
 
32
+ def test_preprocess_data_inference(tmp_path):
33
+ # First fit on dummy data inside tmp_path
34
+ train_df = pd.DataFrame({
35
+ 'Heart Rate (bpm)': [80, 90],
36
+ 'SpO2 Level (%)': [98, 97],
37
+ 'Systolic Blood Pressure (mmHg)': [120, 125],
38
+ 'Diastolic Blood Pressure (mmHg)': [80, 85],
39
+ 'Body Temperature (°C)': [37.0, 37.2],
40
+ 'Predicted Disease': ['Normal', 'Asthma']
41
+ })
42
+ preprocess_data(train_df, is_training=True, model_dir=str(tmp_path))
43
+
44
  df = pd.DataFrame({
45
  'Heart Rate (bpm)': [100],
46
  'SpO2 Level (%)': [99],
 
49
  'Body Temperature (°C)': [37.0],
50
  })
51
 
52
+ X, y = preprocess_data(df, is_training=False, model_dir=str(tmp_path))
53
 
54
  assert y is None
55
  assert 'Risk_Severity' in X.columns
56
+
57
+ def test_preprocess_data_alias_normalization(tmp_path):
58
+ """Test that schema field names (e.g. Heart_Rate) are automatically normalized without error."""
59
+ df = pd.DataFrame({
60
+ 'Heart_Rate': [115],
61
+ 'SpO2_Level': [92],
62
+ 'Systolic_BP': [145],
63
+ 'Diastolic_BP': [92],
64
+ 'Body_Temp': [37.4],
65
+ })
66
+ X, y = preprocess_data(df, is_training=False)
67
+ assert 'Risk_Severity' in X.columns
68
+ assert 'Heart Rate (bpm)' in X.columns
69
+ assert not X.isnull().values.any()