Spaces:
Runtime error
Runtime error
Upload folder using huggingface_hub
Browse files- backend/ensemble.py +31 -7
- backend/main.py +7 -0
- backend/preprocessing.py +75 -13
- model/classes.pkl +2 -2
- model/imputer.pkl +1 -1
- model/label_encoder.pkl +2 -2
- model/scaler.pkl +1 -1
- tests/test_preprocessing.py +31 -4
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 =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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] =
|
| 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 =
|
| 49 |
confidence = float(np.max(weighted_probs))
|
| 50 |
|
| 51 |
class_probs = {
|
| 52 |
-
|
| 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 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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_,
|
| 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 |
-
#
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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:
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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:
|
| 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 |
-
|
|
|
|
| 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()
|