Spaces:
Runtime error
Runtime error
File size: 7,040 Bytes
55f07bf 604d53b 55f07bf 604d53b 55f07bf 604d53b 8c204aa 604d53b 8c204aa 604d53b 55f07bf 604d53b 55f07bf 604d53b 55f07bf 604d53b 55f07bf 604d53b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | import os
import io
import threading
from dataclasses import dataclass
import pandas as pd
from typing import Optional
@dataclass
class RetrainResult:
success: bool
message: str
CANONICAL_COLUMNS = [
'Patient Number',
'Heart Rate (bpm)',
'SpO2 Level (%)',
'Systolic Blood Pressure (mmHg)',
'Diastolic Blood Pressure (mmHg)',
'Body Temperature (°C)',
'Fall Detection',
'Predicted Disease',
'Data Accuracy (%)',
'Heart Rate Alert',
'SpO2 Level Alert',
'Blood Pressure Alert',
'Temperature Alert'
]
# Case-insensitive alias map for telemetry and label columns
COLUMN_ALIASES = {
'heart rate (bpm)': 'Heart Rate (bpm)',
'heart rate': 'Heart Rate (bpm)',
'heart_rate': 'Heart Rate (bpm)',
'heartrate': 'Heart Rate (bpm)',
'hr': 'Heart Rate (bpm)',
'spo2 level (%)': 'SpO2 Level (%)',
'spo2 level': 'SpO2 Level (%)',
'spo2_level': 'SpO2 Level (%)',
'spo2': 'SpO2 Level (%)',
'oxygen': 'SpO2 Level (%)',
'o2_saturation': 'SpO2 Level (%)',
'systolic blood pressure (mmhg)': 'Systolic Blood Pressure (mmHg)',
'systolic blood pressure': 'Systolic Blood Pressure (mmHg)',
'systolic_bp': 'Systolic Blood Pressure (mmHg)',
'systolic bp': 'Systolic Blood Pressure (mmHg)',
'sys_bp': 'Systolic Blood Pressure (mmHg)',
'systolic': 'Systolic Blood Pressure (mmHg)',
'diastolic blood pressure (mmhg)': 'Diastolic Blood Pressure (mmHg)',
'diastolic blood pressure': 'Diastolic Blood Pressure (mmHg)',
'diastolic_bp': 'Diastolic Blood Pressure (mmHg)',
'diastolic bp': 'Diastolic Blood Pressure (mmHg)',
'dia_bp': 'Diastolic Blood Pressure (mmHg)',
'diastolic': 'Diastolic Blood Pressure (mmHg)',
'body temperature (°c)': 'Body Temperature (°C)',
'body temperature': 'Body Temperature (°C)',
'body_temp': 'Body Temperature (°C)',
'temperature': 'Body Temperature (°C)',
'temp': 'Body Temperature (°C)',
'predicted disease': 'Predicted Disease',
'predicted_disease': 'Predicted Disease',
'disease': 'Predicted Disease',
'condition': 'Predicted Disease',
'diagnosis': 'Predicted Disease',
'label': 'Predicted Disease',
}
class MLOpsEngine:
"""
Encapsulates the MLOps Lifecycle:
- Normalizes and sanitizes telemetry schemas with alias resolution.
- Handles data ingestion and schema validation.
- Orchestrates model retraining and hot-swapping.
"""
def __init__(self, dataset_path: str):
self.dataset_path = dataset_path
self.active_ensemble = None
self._lock = threading.Lock()
self._load_active_models()
def _load_active_models(self):
try:
from ensemble import EnsembleModel
self.active_ensemble = EnsembleModel()
except Exception as e:
print(f"MLOps: Model loading failed (likely untrained): {e}")
def get_ensemble(self):
return self.active_ensemble
def normalize_batch(self, df: pd.DataFrame) -> pd.DataFrame:
"""Standardizes column names, patches encoding issues, and fills missing non-essential metadata."""
# Clean column names
cleaned_cols = {}
for col in df.columns:
normalized_key = str(col).strip().lower().replace('\ufffd', '°')
matched_canonical = COLUMN_ALIASES.get(normalized_key, col)
cleaned_cols[col] = matched_canonical
df = df.rename(columns=cleaned_cols)
# Check required continuous physiological signals
required_signals = [
'Heart Rate (bpm)',
'SpO2 Level (%)',
'Systolic Blood Pressure (mmHg)',
'Diastolic Blood Pressure (mmHg)',
'Body Temperature (°C)'
]
missing_signals = [s for s in required_signals if s not in df.columns]
if missing_signals:
raise ValueError(f"Missing required physiological signals: {', '.join(missing_signals)}")
# Ensure target label exists; default to 'Healthy' if not specified
if 'Predicted Disease' not in df.columns:
df['Predicted Disease'] = 'Healthy'
# Auto-populate optional canonical fields with consistent values
if 'Patient Number' not in df.columns:
df['Patient Number'] = range(1, len(df) + 1)
if 'Data Accuracy (%)' not in df.columns:
df['Data Accuracy (%)'] = 100
if 'Fall Detection' not in df.columns:
df['Fall Detection'] = 'No'
if 'Heart Rate Alert' not in df.columns:
df['Heart Rate Alert'] = 'NORMAL'
if 'SpO2 Level Alert' not in df.columns:
df['SpO2 Level Alert'] = 'NORMAL'
if 'Blood Pressure Alert' not in df.columns:
df['Blood Pressure Alert'] = 'NORMAL'
if 'Temperature Alert' not in df.columns:
df['Temperature Alert'] = 'NORMAL'
# Ensure numeric types for continuous signals
for sig in required_signals:
df[sig] = pd.to_numeric(df[sig], errors='coerce')
# Drop rows where all signals are NaN
df = df.dropna(subset=required_signals, how='all')
# Align columns to canonical ordering
return df[[c for c in CANONICAL_COLUMNS if c in df.columns]]
def ingest_batch_sync(self, csv_bytes: bytes) -> RetrainResult:
with self._lock:
try:
raw_df = pd.read_csv(io.BytesIO(csv_bytes), encoding_errors='replace')
new_df = self.normalize_batch(raw_df)
if len(new_df) == 0:
return RetrainResult(False, "Uploaded CSV contains no valid telemetry rows.")
if os.path.exists(self.dataset_path):
existing_df = pd.read_csv(self.dataset_path, encoding='utf-8', encoding_errors='replace')
existing_df.columns = [c.replace('\ufffd', '°') for c in existing_df.columns]
# Adjust Patient Number to maintain continuous increment
if 'Patient Number' in existing_df.columns:
start_id = int(existing_df['Patient Number'].max() or 0) + 1
new_df['Patient Number'] = range(start_id, start_id + len(new_df))
combined_df = pd.concat([existing_df, new_df], join="inner", ignore_index=True)
else:
combined_df = new_df
os.makedirs(os.path.dirname(self.dataset_path), exist_ok=True)
combined_df.to_csv(self.dataset_path, index=False, encoding='utf-8')
# Re-train models synchronously
from models import train_models
train_models()
# Hot-swap the live models
self._load_active_models()
return RetrainResult(True, f"Dataset successfully updated ({len(combined_df)} total observations) and ensemble hot-swapped.")
except Exception as e:
return RetrainResult(False, f"Ingestion failed: {str(e)}")
|