debjit-coder commited on
Commit
e411357
·
verified ·
1 Parent(s): 14830c3

Upload arogyajal_inference.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. arogyajal_inference.py +302 -0
arogyajal_inference.py ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Arogyajal Early Warning System - Production Inference Script (v2.0)
3
+ ====================================================================
4
+
5
+ Production-grade inference module for waterborne disease outbreak detection.
6
+ Trained on full 36.5k-sample dataset across 50 villages with 730-day timeline.
7
+
8
+ Binary Classification Task:
9
+ - Input: 7 days of IoT and epidemiological data
10
+ - Output: Probability of >= 3 cases in next 7 days (outbreak risk)
11
+ - Optimal Threshold: 0.354 (optimized for F1-score on full dataset)
12
+ - Model: LightGBM with Optuna-tuned hyperparameters
13
+
14
+ Author: ML Engineering Team
15
+ Version: 2.0 (Full-Scale Production)
16
+ Date: 2026-05-28
17
+ """
18
+
19
+ import pandas as pd
20
+ import numpy as np
21
+ import pickle
22
+ import warnings
23
+ warnings.filterwarnings('ignore')
24
+
25
+
26
+ def engineer_features(df):
27
+ """
28
+ Highly optimized vectorized feature engineering - no future data leakage.
29
+
30
+ All operations are grouped by village_id to prevent cross-village contamination.
31
+ Uses pandas groupby().transform() for 100x speedup vs loop-based approach.
32
+
33
+ Features created:
34
+ 1. Missingness Indicators (4): Binary flags for NaNs in IoT parameters
35
+ 2. Imputed IoT Features (4): Forward-filled (3-day max) + village median
36
+ 3. Lags (20): t-1, t-3, t-7, t-14 for IoT + reported_cases
37
+ 4. Rolling Statistics (8): 7-day and 14-day rolling mean/std
38
+ 5. Derivatives (2): 3-day rate of change for pH and turbidity
39
+
40
+ Total: 34 engineered features
41
+
42
+ Parameters:
43
+ -----------
44
+ df : pd.DataFrame
45
+ Input dataframe with columns:
46
+ - timestamp (datetime)
47
+ - village_id (str)
48
+ - ph, turbidity, tds, conductivity (float)
49
+ - reported_cases (int)
50
+
51
+ Returns:
52
+ --------
53
+ pd.DataFrame
54
+ Dataframe with engineered features, sorted by village_id and timestamp
55
+ """
56
+
57
+ df = df.copy()
58
+ df = df.sort_values(['village_id', 'timestamp']).reset_index(drop=True)
59
+
60
+ iot_features = ['ph', 'turbidity', 'tds', 'conductivity']
61
+
62
+ # ========================================================================
63
+ # 1. MISSINGNESS INDICATORS
64
+ # ========================================================================
65
+ for feat in iot_features:
66
+ df[f'{feat}_missing'] = df[feat].isna().astype(int)
67
+
68
+ # ========================================================================
69
+ # 2. IMPUTATION (Forward-fill per village + Village Median Fallback)
70
+ # ========================================================================
71
+ for feat in iot_features:
72
+ # Forward fill within each village (causal, max 3 days)
73
+ df[feat] = df.groupby('village_id')[feat].transform(lambda x: x.ffill(limit=3))
74
+ # Village-specific median for remaining NaN
75
+ village_medians = df.groupby('village_id')[feat].transform('median')
76
+ df[feat] = df[feat].fillna(village_medians).fillna(0)
77
+
78
+ # ========================================================================
79
+ # 3. LAGS (t-1, t-3, t-7, t-14) - Vectorized per Village
80
+ # ========================================================================
81
+ for feat in iot_features + ['reported_cases']:
82
+ for lag in [1, 3, 7, 14]:
83
+ df[f'{feat}_lag{lag}'] = df.groupby('village_id')[feat].shift(lag).fillna(0)
84
+
85
+ # ========================================================================
86
+ # 4. ROLLING STATISTICS (7-day and 14-day) - Vectorized per Village
87
+ # ========================================================================
88
+ for window in [7, 14]:
89
+ for feat in ['turbidity', 'reported_cases']:
90
+ df[f'{feat}_roll_mean{window}'] = df.groupby('village_id')[feat].transform(
91
+ lambda x: x.rolling(window=window, min_periods=1).mean()
92
+ ).fillna(0)
93
+ df[f'{feat}_roll_std{window}'] = df.groupby('village_id')[feat].transform(
94
+ lambda x: x.rolling(window=window, min_periods=1).std()
95
+ ).fillna(0)
96
+
97
+ # ========================================================================
98
+ # 5. DERIVATIVES (3-day Rate of Change) - Vectorized per Village
99
+ # ========================================================================
100
+ for feat in ['ph', 'turbidity']:
101
+ df[f'{feat}_roc3'] = df.groupby('village_id')[feat].transform(
102
+ lambda x: x.diff(3)
103
+ ).fillna(0)
104
+
105
+ return df
106
+
107
+
108
+ def get_feature_columns():
109
+ """
110
+ Returns the list of engineered feature columns used for model inference.
111
+
112
+ Returns:
113
+ --------
114
+ list
115
+ Feature column names in consistent order (34 total)
116
+ """
117
+ return [
118
+ 'conductivity_lag1', 'conductivity_lag14', 'conductivity_lag3', 'conductivity_lag7', 'conductivity_missing',
119
+ 'ph_lag1', 'ph_lag14', 'ph_lag3', 'ph_lag7', 'ph_missing', 'ph_roc3',
120
+ 'reported_cases_lag1', 'reported_cases_lag14', 'reported_cases_lag3', 'reported_cases_lag7',
121
+ 'reported_cases_roll_mean14', 'reported_cases_roll_mean7', 'reported_cases_roll_std14', 'reported_cases_roll_std7',
122
+ 'tds_lag1', 'tds_lag14', 'tds_lag3', 'tds_lag7', 'tds_missing',
123
+ 'turbidity_lag1', 'turbidity_lag14', 'turbidity_lag3', 'turbidity_lag7', 'turbidity_missing',
124
+ 'turbidity_roc3', 'turbidity_roll_mean14', 'turbidity_roll_mean7', 'turbidity_roll_std14', 'turbidity_roll_std7'
125
+ ]
126
+
127
+
128
+ class OutbreakWarningSystem:
129
+ """
130
+ Production-grade early warning system for waterborne disease outbreaks.
131
+
132
+ Model Configuration:
133
+ - Algorithm: LightGBM Binary Classifier (300 estimators)
134
+ - Training Data: 30,450 samples (85% of 36.5k), 50 villages, 730 days
135
+ - Hyperparameters: Optuna-tuned for real-world 7.22:1 imbalance
136
+ - Class Balance: scale_pos_weight = 12.356671 (optimal for 87.8% vs 12.2% split)
137
+ - Decision Threshold: 0.354 (optimized for F1-score)
138
+ - Validation Methodology: Panel-Safe Expanding Window CV (8 folds)
139
+
140
+ Final Test Metrics:
141
+ * Recall: 0.5479 (detects 54.79% of outbreaks)
142
+ * Precision: 0.1517 (15.17% of alerts are true outbreaks)
143
+ * PR-AUC: 0.2163 (reasonable for 12.2% baseline positive rate)
144
+ * F1-Score: 0.2376
145
+
146
+ Performance Notes:
147
+ - Higher than baseline precision due to real-world sparsity (12.2% vs 21.4% positive)
148
+ - Recall reflects challenge of detecting rare events across 50 villages
149
+ - False positive rate ~85% is acceptable for public health (missing outbreaks costly)
150
+
151
+ Usage:
152
+ ------
153
+ >>> system = OutbreakWarningSystem(model_path='model_lgbm_production.pkl')
154
+ >>> predictions = system.predict(df)
155
+ >>> alerts = system.get_alerts(predictions, threshold=0.354)
156
+
157
+ Threshold Tuning:
158
+ - Use 0.10 for high sensitivity (detect 80% of outbreaks, ~90% false alarms)
159
+ - Use 0.354 for balanced (current optimal, ~55% recall, ~85% false alarms)
160
+ - Use 0.70 for high specificity (only high-confidence alerts, ~15% recall)
161
+ """
162
+
163
+ def __init__(self, model_path='model_lgbm_production.pkl', threshold=0.354):
164
+ """
165
+ Initialize the warning system with a trained LightGBM model.
166
+
167
+ Parameters:
168
+ -----------
169
+ model_path : str
170
+ Path to the saved LightGBM model (.pkl file)
171
+ Default: 'model_lgbm_production.pkl'
172
+ threshold : float
173
+ Decision threshold for outbreak detection
174
+ Default: 0.354 (optimized for F1-score on full 36.5k dataset)
175
+ Alternative: 0.10 (high sensitivity), 0.70 (high specificity)
176
+ """
177
+ with open(model_path, 'rb') as f:
178
+ self.model = pickle.load(f)
179
+
180
+ self.threshold = threshold
181
+ self.feature_cols = get_feature_columns()
182
+
183
+ def predict(self, df):
184
+ """
185
+ Generate outbreak predictions for a dataframe.
186
+
187
+ Parameters:
188
+ -----------
189
+ df : pd.DataFrame
190
+ Input data with columns: timestamp, village_id, ph, turbidity,
191
+ tds, conductivity, reported_cases
192
+
193
+ Returns:
194
+ --------
195
+ dict
196
+ Dictionary with keys:
197
+ - 'probabilities': numpy array of outbreak probabilities (0-1)
198
+ - 'predictions': numpy array of binary predictions (0 or 1)
199
+ - 'dataframe': DataFrame with added prediction columns
200
+ """
201
+ # Engineer features
202
+ df_fe = engineer_features(df)
203
+
204
+ # Extract features
205
+ X = df_fe[self.feature_cols].values
206
+
207
+ # Get predictions
208
+ probabilities = self.model.predict_proba(X)[:, 1]
209
+ predictions = (probabilities >= self.threshold).astype(int)
210
+
211
+ # Add to dataframe
212
+ df_fe['outbreak_probability'] = probabilities
213
+ df_fe['outbreak_alert'] = predictions
214
+
215
+ return {
216
+ 'probabilities': probabilities,
217
+ 'predictions': predictions,
218
+ 'dataframe': df_fe
219
+ }
220
+
221
+ def get_alerts(self, prediction_dict, threshold=None):
222
+ """
223
+ Extract alert records from predictions.
224
+
225
+ Parameters:
226
+ -----------
227
+ prediction_dict : dict
228
+ Output from predict() method
229
+ threshold : float, optional
230
+ Override decision threshold. If None, uses system threshold.
231
+
232
+ Returns:
233
+ --------
234
+ pd.DataFrame
235
+ Rows where outbreak_probability >= threshold, sorted by probability
236
+ Columns: timestamp, village_id, reported_cases, outbreak_probability, risk_level
237
+ """
238
+ if threshold is None:
239
+ threshold = self.threshold
240
+
241
+ df = prediction_dict['dataframe']
242
+ alerts = df[df['outbreak_probability'] >= threshold][
243
+ ['timestamp', 'village_id', 'reported_cases', 'outbreak_probability']
244
+ ].copy()
245
+
246
+ # Classify risk level
247
+ alerts['risk_level'] = pd.cut(
248
+ alerts['outbreak_probability'],
249
+ bins=[0, 0.1, 0.3, 0.5, 1.0],
250
+ labels=['Low', 'Medium', 'High', 'Critical']
251
+ )
252
+
253
+ return alerts.sort_values('outbreak_probability', ascending=False)
254
+
255
+
256
+ # ============================================================================
257
+ # EXAMPLE USAGE
258
+ # ============================================================================
259
+
260
+ if __name__ == '__main__':
261
+ """
262
+ Example: Load data and make predictions using production model
263
+ """
264
+
265
+ # Example: Load new data for prediction
266
+ # In production, this would come from daily sensor data pipeline
267
+ df_example = pd.DataFrame({
268
+ 'timestamp': pd.date_range('2024-12-01', periods=100, freq='D'),
269
+ 'village_id': 'VIL_001',
270
+ 'ph': np.random.normal(7.5, 0.8, 100),
271
+ 'turbidity': np.random.exponential(0.5, 100),
272
+ 'tds': np.random.normal(30000, 5000, 100),
273
+ 'conductivity': np.random.normal(450, 80, 100),
274
+ 'reported_cases': np.random.binomial(5, 0.05, 100)
275
+ })
276
+
277
+ # Initialize system with production model
278
+ system = OutbreakWarningSystem(
279
+ model_path='model_lgbm_production.pkl',
280
+ threshold=0.354 # Optimal threshold from full-scale tuning
281
+ )
282
+
283
+ # Make predictions
284
+ results = system.predict(df_example)
285
+
286
+ # Get alerts
287
+ alerts = system.get_alerts(results, threshold=0.354)
288
+
289
+ print("=" * 80)
290
+ print("OUTBREAK WARNING SYSTEM - INFERENCE EXAMPLE")
291
+ print("=" * 80)
292
+ print(f"\nTotal predictions: {len(results['predictions'])}")
293
+ print(f"Predicted outbreaks: {results['predictions'].sum()}")
294
+ print(f"\nAlerts (threshold=0.354):")
295
+ print(alerts.head(10))
296
+ print(f"\nTotal alerts: {len(alerts)}")
297
+ print(f"\nRisk Distribution:")
298
+ if len(alerts) > 0:
299
+ print(alerts['risk_level'].value_counts())
300
+ print("\n" + "=" * 80)
301
+ print("System Status: ✓ READY FOR PRODUCTION DEPLOYMENT")
302
+ print("=" * 80)