jaimalklyde commited on
Commit
d1b98f9
·
verified ·
1 Parent(s): d21b03b

Update src/module/Forecasting.py

Browse files
Files changed (1) hide show
  1. src/module/Forecasting.py +205 -768
src/module/Forecasting.py CHANGED
@@ -1,839 +1,276 @@
1
  """
2
- MODULE: FORECASTING & MODEL COMPARISON
3
- ======================================
4
- Responsabilité : Entraînement, évaluation et comparaison de modèles ML
5
- pour la prédiction des besoins hospitaliers.
6
- Style : Gotham UI
7
-
8
- Modèles Entraînés :
9
- - XGBoost : Champion des compétitions, excellent sur données tabulaires
10
- - LightGBM : Rapide, performant, moins de surapprentissage
11
- - Random Forest : Baseline robuste et interprétable
12
-
13
- Métriques de Performance :
14
- - RMSE : Pénalise fortement les grandes erreurs (critique en santé)
15
- - MAE : Robuste aux outliers
16
- - MAPE : Erreur en pourcentage (compréhensible business)
17
- - R² : Variance expliquée
18
- - SHAP : Interprétabilité des prédictions
19
  """
20
 
21
  import pandas as pd
22
  import numpy as np
23
  import streamlit as st
24
- import plotly.express as px
25
  import plotly.graph_objects as go
26
- from plotly.subplots import make_subplots
27
- from datetime import datetime
28
  import warnings
29
- warnings.filterwarnings('ignore')
30
 
31
  # ML Models
32
  from sklearn.ensemble import RandomForestRegressor
33
  from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
34
- from sklearn.preprocessing import LabelEncoder
35
  import xgboost as xgb
36
  import lightgbm as lgb
37
 
38
- # SHAP pour l'interprétabilité
39
  try:
40
  import shap
 
41
  SHAP_AVAILABLE = True
42
  except ImportError:
43
  SHAP_AVAILABLE = False
44
 
 
45
 
46
  class HospitalForecaster:
47
- """Moteur de prédiction hospitalière avec comparaison de modèles"""
48
-
49
  def __init__(self, ml_dataset):
50
  self.df = ml_dataset.copy()
 
51
  self.models = {}
52
- self.predictions = {}
53
  self.metrics = {}
 
54
  self.best_model_name = None
 
55
 
56
- # Encodeurs pour variables catégorielles
57
- self.label_encoders = {}
58
-
59
- def prepare_data(self, target_col='nb_admissions', test_size=0.15, val_size=0.15):
60
- """
61
- Préparation des données avec split temporel (CRITIQUE pour séries temporelles).
62
-
63
- Args:
64
- target_col: Variable cible à prédire
65
- test_size: Proportion pour test set
66
- val_size: Proportion pour validation set
67
-
68
- Returns:
69
- Tuple (X_train, X_val, X_test, y_train, y_val, y_test, feature_names)
70
- """
71
- df = self.df.copy()
72
-
73
- # Tri chronologique (essentiel pour séries temporelles)
74
- df = df.sort_values('date').reset_index(drop=True)
75
-
76
- # Colonnes à exclure
77
- exclude_cols = [
78
- target_col, 'date', 'stay_id', 'patient_id',
79
- 'scenario', 'service_nom', 'periode_conges_label',
80
- 'dq_is_anomaly', 'dq_anomaly_type', 'dq_anomaly_detail',
81
- 'jour_semaine_nom', 'mois_nom', 'saison',
82
- 'pathologie_nom', 'service_code', 'materiel_code'
83
- ]
84
 
85
- # Sélection des features
86
- feature_cols = [col for col in df.columns if col not in exclude_cols]
87
 
88
- # Gestion des variables catégorielles
89
- categorical_cols = df[feature_cols].select_dtypes(include=['object', 'category']).columns.tolist()
90
 
91
- for col in categorical_cols:
92
- if col in df.columns:
93
- le = LabelEncoder()
94
- df[col] = le.fit_transform(df[col].astype(str))
95
- self.label_encoders[col] = le
96
-
97
- # Suppression des NaN (après feature engineering, certaines features peuvent être NaN)
98
- df = df.dropna(subset=feature_cols + [target_col])
99
-
100
- # Extraction X et y
101
- X = df[feature_cols].select_dtypes(include=[np.number])
102
  y = df[target_col]
103
- dates = df['date']
104
-
105
- # Split temporel (respecte l'ordre chronologique)
106
- n = len(X)
107
- train_end = int(n * (1 - test_size - val_size))
108
- val_end = int(n * (1 - test_size))
109
-
110
- X_train = X.iloc[:train_end]
111
- X_val = X.iloc[train_end:val_end]
112
- X_test = X.iloc[val_end:]
113
-
114
- y_train = y.iloc[:train_end]
115
- y_val = y.iloc[train_end:val_end]
116
- y_test = y.iloc[val_end:]
117
-
118
- dates_train = dates.iloc[:train_end]
119
- dates_val = dates.iloc[train_end:val_end]
120
- dates_test = dates.iloc[val_end:]
121
-
122
- # Stockage pour visualisations
123
- self.X_train = X_train
124
- self.X_val = X_val
125
- self.X_test = X_test
126
- self.y_train = y_train
127
- self.y_val = y_val
128
- self.y_test = y_test
129
- self.dates_train = dates_train
130
- self.dates_val = dates_val
131
- self.dates_test = dates_test
132
- self.feature_names = X_train.columns.tolist()
133
-
134
- return X_train, X_val, X_test, y_train, y_val, y_test, self.feature_names
135
-
136
- def train_xgboost(self, X_train, X_val, y_train, y_val):
137
- """
138
- Entraînement XGBoost avec early stopping.
139
- XGBoost est le champion des compétitions Kaggle pour données tabulaires.
140
- """
141
- model = xgb.XGBRegressor(
142
- n_estimators=500,
143
- learning_rate=0.05,
144
- max_depth=6,
145
- min_child_weight=3,
146
- subsample=0.8,
147
- colsample_bytree=0.8,
148
- gamma=0.1,
149
- random_state=42,
150
- n_jobs=-1,
151
- early_stopping_rounds=50,
152
- eval_metric='rmse'
153
- )
154
-
155
- model.fit(
156
- X_train, y_train,
157
- eval_set=[(X_val, y_val)],
158
- verbose=False
159
- )
160
 
161
- self.models['XGBoost'] = model
162
- return model
163
-
164
- def train_lightgbm(self, X_train, X_val, y_train, y_val):
165
- """
166
- Entraînement LightGBM.
167
- Plus rapide que XGBoost, souvent meilleur sur grandes données.
168
- """
169
- model = lgb.LGBMRegressor(
170
- n_estimators=500,
171
- learning_rate=0.05,
172
- num_leaves=31,
173
- max_depth=6,
174
- min_child_samples=20,
175
- subsample=0.8,
176
- colsample_bytree=0.8,
177
- random_state=42,
178
- n_jobs=-1,
179
- force_col_wise=True,
180
- verbose=-1
181
- )
182
-
183
- model.fit(
184
- X_train, y_train,
185
- eval_set=[(X_val, y_val)],
186
- eval_metric='rmse',
187
- callbacks=[lgb.early_stopping(stopping_rounds=50, verbose=False)]
188
- )
189
-
190
- self.models['LightGBM'] = model
191
- return model
192
-
193
- def train_random_forest(self, X_train, y_train):
194
- """
195
- Entraînement Random Forest.
196
- Baseline robuste, très interprétable, résistant au surapprentissage.
197
- """
198
- model = RandomForestRegressor(
199
- n_estimators=300,
200
- max_depth=15,
201
- min_samples_split=10,
202
- min_samples_leaf=5,
203
- max_features='sqrt',
204
- random_state=42,
205
- n_jobs=-1,
206
- verbose=0
207
- )
208
-
209
- model.fit(X_train, y_train)
210
-
211
- self.models['Random Forest'] = model
212
- return model
213
-
214
- def evaluate_model(self, model_name, model, X_test, y_test):
215
- """
216
- Évaluation complète d'un modèle avec toutes les métriques.
217
- """
218
- y_pred = model.predict(X_test)
219
-
220
- # Métriques de régression
221
- rmse = np.sqrt(mean_squared_error(y_test, y_pred))
222
- mae = mean_absolute_error(y_test, y_pred)
223
- r2 = r2_score(y_test, y_pred)
224
-
225
- # MAPE (Mean Absolute Percentage Error)
226
- # Gestion du cas où y_test contient des 0
227
- mask = y_test != 0
228
- if mask.sum() > 0:
229
- mape = np.mean(np.abs((y_test[mask] - y_pred[mask]) / y_test[mask])) * 100
230
- else:
231
- mape = np.nan
232
-
233
- # Stockage des prédictions
234
- self.predictions[model_name] = y_pred
235
 
236
- # Stockage des métriques
237
- self.metrics[model_name] = {
238
- 'RMSE': rmse,
239
- 'MAE': mae,
240
- 'R²': r2,
241
- 'MAPE': mape
242
- }
243
-
244
- return rmse, mae, r2, mape
245
-
246
- def train_all_models(self):
247
- """Pipeline complet d'entraînement des 3 modèles"""
248
-
249
- st.info("🚀 **Préparation des données avec split temporel...**")
250
- X_train, X_val, X_test, y_train, y_val, y_test, feature_names = self.prepare_data()
251
-
252
- progress_bar = st.progress(0)
253
- status_text = st.empty()
254
 
255
- # 1. XGBoost
256
- status_text.text("⚡ Entraînement XGBoost (Gradient Boosting Optimisé)...")
257
- progress_bar.progress(10)
258
- self.train_xgboost(X_train, X_val, y_train, y_val)
259
- self.evaluate_model('XGBoost', self.models['XGBoost'], X_test, y_test)
260
- progress_bar.progress(40)
261
 
262
- # 2. LightGBM
263
- status_text.text("🌟 Entraînement LightGBM (Gradient Boosting Rapide)...")
264
- self.train_lightgbm(X_train, X_val, y_train, y_val)
265
- self.evaluate_model('LightGBM', self.models['LightGBM'], X_test, y_test)
266
- progress_bar.progress(70)
267
 
268
  # 3. Random Forest
269
- status_text.text("🌲 Entraînement Random Forest (Baseline Robuste)...")
270
- self.train_random_forest(X_train, y_train)
271
- self.evaluate_model('Random Forest', self.models['Random Forest'], X_test, y_test)
272
- progress_bar.progress(100)
273
-
274
- status_text.text("✅ Entraînement terminé!")
275
-
276
- # Détermination du meilleur modèle
277
- self._select_best_model()
278
-
279
- return self.metrics
280
-
281
- def _select_best_model(self):
282
- """
283
- Sélection automatique du meilleur modèle basée sur un score composite.
284
 
285
- Logique de scoring :
286
- - RMSE : poids 40% (critique pour éviter grandes erreurs en santé)
287
- - MAE : poids 30% (robustesse)
288
- - R² : poids 20% (variance expliquée)
289
- - MAPE : poids 10% (compréhension business)
290
-
291
- Plus le score est BAS, mieux c'est.
292
- """
293
  scores = {}
294
-
295
- for model_name, metrics in self.metrics.items():
296
- # Normalisation des métriques (min-max scaling)
297
- # Pour RMSE, MAE, MAPE : plus bas = mieux
298
- # Pour R² : plus haut = mieux
299
-
300
- rmse_norm = metrics['RMSE']
301
- mae_norm = metrics['MAE']
302
- r2_norm = 1 - metrics['R²'] # Inversé car on veut minimiser
303
- mape_norm = metrics['MAPE'] if not np.isnan(metrics['MAPE']) else 100
304
 
305
- # Score composite
306
- composite_score = (
307
- 0.40 * rmse_norm +
308
- 0.30 * mae_norm +
309
- 0.20 * r2_norm +
310
- 0.10 * mape_norm
311
- )
312
 
313
- scores[model_name] = composite_score
314
-
315
- # Sélection du modèle avec le score le plus bas
316
- self.best_model_name = min(scores, key=scores.get)
317
- self.composite_scores = scores
318
-
319
- def get_best_model_recommendation(self):
320
- """
321
- Génère une recommandation détaillée du meilleur modèle avec justification.
322
- """
323
- if not self.best_model_name:
324
- return "Aucun modèle entraîné."
325
-
326
- best_metrics = self.metrics[self.best_model_name]
327
-
328
- recommendation = f"""
329
- ### 🏆 MODÈLE RECOMMANDÉ : **{self.best_model_name}**
330
-
331
- #### 📊 Performances sur Test Set :
332
- - **RMSE** : {best_metrics['RMSE']:.2f} (Erreur quadratique moyenne)
333
- - **MAE** : {best_metrics['MAE']:.2f} (Erreur absolue moyenne)
334
- - **R²** : {best_metrics['R²']:.4f} ({best_metrics['R²']*100:.2f}% de variance expliquée)
335
- - **MAPE** : {best_metrics['MAPE']:.2f}% (Erreur en pourcentage)
336
-
337
- #### 🎯 Pourquoi ce modèle ?
338
 
339
- """
340
-
341
- # Justification spécifique selon le modèle
342
- if self.best_model_name == 'XGBoost':
343
- recommendation += """
344
- **Avantages de XGBoost :**
345
- - 🥇 **Champion des compétitions Kaggle** sur données tabulaires
346
- - 🎯 **Excellente précision** grâce au gradient boosting optimisé
347
- - 🛡️ **Robuste au surapprentissage** avec régularisation L1/L2
348
- - 🔍 **Gère bien les interactions complexes** entre features (hospitalières)
349
- - ⚡ **Early stopping** : arrêt automatique dès que validation cesse de s'améliorer
350
-
351
- **Idéal pour :** Prédictions critiques nécessitant la meilleure précision possible.
352
- """
353
-
354
- elif self.best_model_name == 'LightGBM':
355
- recommendation += """
356
- **Avantages de LightGBM :**
357
- - ⚡ **Rapidité exceptionnelle** : 10-20x plus rapide que XGBoost
358
- - 📈 **Excellent sur grandes données** grâce à l'algorithme leaf-wise
359
- - 💾 **Faible consommation mémoire** : idéal pour production
360
- - 🎯 **Précision comparable à XGBoost** avec moins de ressources
361
- - 🌳 **Moins de surapprentissage** naturellement
362
-
363
- **Idéal pour :** Déploiement en production avec contraintes de ressources.
364
- """
365
-
366
- elif self.best_model_name == 'Random Forest':
367
- recommendation += """
368
- **Avantages de Random Forest :**
369
- - 🌲 **Très robuste** : résiste bien au surapprentissage
370
- - 📖 **Facilement interprétable** : importance des features claire
371
- - 🛡️ **Stable** : moins sensible aux hyperparamètres que boosting
372
- - 🎲 **Gère bien les outliers** grâce au vote d'ensemble
373
- - 🔧 **Facile à maintenir** : peu de tuning nécessaire
374
 
375
- **Idéal pour :** Baseline solide et modèle de référence stable.
376
- """
377
-
378
- # Comparaison avec les autres modèles
379
- recommendation += "\n#### 📉 Comparaison avec les autres modèles :\n\n"
380
-
381
- for model_name, metrics in self.metrics.items():
382
- if model_name != self.best_model_name:
383
- rmse_diff = ((metrics['RMSE'] - best_metrics['RMSE']) / best_metrics['RMSE']) * 100
384
- mae_diff = ((metrics['MAE'] - best_metrics['MAE']) / best_metrics['MAE']) * 100
385
-
386
- recommendation += f"- **{model_name}** : RMSE {rmse_diff:+.1f}%, MAE {mae_diff:+.1f}% vs {self.best_model_name}\n"
387
-
388
- # Niveau de confiance
389
- best_r2 = best_metrics['R²']
390
- if best_r2 > 0.85:
391
- confidence = "🟢 EXCELLENTE"
392
- elif best_r2 > 0.70:
393
- confidence = "🟡 BONNE"
394
- else:
395
- confidence = "🔴 MODÉRÉE"
396
-
397
- recommendation += f"\n#### 🎖️ Niveau de Confiance : {confidence}\n"
398
- recommendation += f"Le modèle explique **{best_r2*100:.1f}%** de la variance des admissions.\n"
399
-
400
- return recommendation
401
-
402
- def plot_predictions_comparison(self):
403
- """Visualisation des prédictions des 3 modèles sur le test set"""
404
-
405
- fig = go.Figure()
406
-
407
- # Valeurs réelles
408
- fig.add_trace(go.Scatter(
409
- x=self.dates_test,
410
- y=self.y_test,
411
- mode='lines+markers',
412
- name='Réel',
413
- line=dict(color='black', width=3),
414
- marker=dict(size=6)
415
- ))
416
-
417
- # Prédictions de chaque modèle
418
- colors = {'XGBoost': '#1f77b4', 'LightGBM': '#ff7f0e', 'Random Forest': '#2ca02c'}
419
-
420
- for model_name, y_pred in self.predictions.items():
421
- is_best = (model_name == self.best_model_name)
422
-
423
- fig.add_trace(go.Scatter(
424
- x=self.dates_test,
425
- y=y_pred,
426
- mode='lines',
427
- name=f'{model_name}{" 🏆" if is_best else ""}',
428
- line=dict(
429
- color=colors.get(model_name, 'gray'),
430
- width=3 if is_best else 1.5,
431
- dash='solid' if is_best else 'dash'
432
- ),
433
- opacity=1.0 if is_best else 0.6
434
- ))
435
-
436
- fig.update_layout(
437
- title="📈 Comparaison des Prédictions sur Test Set",
438
- xaxis_title="Date",
439
- yaxis_title="Nombre d'Admissions",
440
- hovermode='x unified',
441
- template='plotly_white',
442
- height=500,
443
- legend=dict(
444
- yanchor="top",
445
- y=0.99,
446
- xanchor="left",
447
- x=0.01
448
- )
449
- )
450
-
451
- return fig
452
-
453
- def plot_metrics_comparison(self):
454
- """Graphique radar comparant les métriques des 3 modèles"""
455
-
456
- # Normalisation des métriques pour le radar (0-100 scale)
457
- metrics_normalized = {}
458
-
459
- all_rmse = [m['RMSE'] for m in self.metrics.values()]
460
- all_mae = [m['MAE'] for m in self.metrics.values()]
461
- all_r2 = [m['R²'] for m in self.metrics.values()]
462
- all_mape = [m['MAPE'] for m in self.metrics.values() if not np.isnan(m['MAPE'])]
463
 
464
- max_rmse = max(all_rmse)
465
- max_mae = max(all_mae)
466
- max_mape = max(all_mape) if all_mape else 100
467
 
468
- for model_name, metrics in self.metrics.items():
469
- # Pour RMSE, MAE, MAPE : inverser car plus bas = mieux
470
- rmse_score = (1 - metrics['RMSE'] / max_rmse) * 100
471
- mae_score = (1 - metrics['MAE'] / max_mae) * 100
472
- mape_score = (1 - metrics['MAPE'] / max_mape) * 100 if not np.isnan(metrics['MAPE']) else 0
473
- r2_score = metrics['R²'] * 100 # Déjà entre 0-1
474
-
475
- metrics_normalized[model_name] = {
476
- 'RMSE': rmse_score,
477
- 'MAE': mae_score,
478
- 'R²': r2_score,
479
- 'MAPE': mape_score
480
- }
481
-
482
- # Création du radar chart
483
- categories = ['RMSE', 'MAE', 'R²', 'MAPE']
484
-
485
- fig = go.Figure()
486
-
487
- colors = {'XGBoost': '#1f77b4', 'LightGBM': '#ff7f0e', 'Random Forest': '#2ca02c'}
488
-
489
- for model_name, norm_metrics in metrics_normalized.items():
490
- values = [norm_metrics[cat] for cat in categories]
491
- values.append(values[0]) # Fermeture du radar
492
-
493
- is_best = (model_name == self.best_model_name)
494
-
495
- fig.add_trace(go.Scatterpolar(
496
- r=values,
497
- theta=categories + [categories[0]],
498
- name=f'{model_name}{" 🏆" if is_best else ""}',
499
- fill='toself',
500
- line=dict(
501
- color=colors.get(model_name, 'gray'),
502
- width=3 if is_best else 2
503
- ),
504
- opacity=0.8 if is_best else 0.4
505
- ))
506
-
507
- fig.update_layout(
508
- polar=dict(
509
- radialaxis=dict(
510
- visible=True,
511
- range=[0, 100]
512
- )
513
- ),
514
- showlegend=True,
515
- title="🎯 Radar de Performance des Modèles (Score sur 100)",
516
- height=500
517
- )
518
-
519
- return fig
520
-
521
- def plot_feature_importance(self, top_n=15):
522
- """Importance des features pour le meilleur modèle"""
523
-
524
- best_model = self.models[self.best_model_name]
525
-
526
- if hasattr(best_model, 'feature_importances_'):
527
- importances = best_model.feature_importances_
528
-
529
- # Création du DataFrame d'importance
530
- importance_df = pd.DataFrame({
531
- 'Feature': self.feature_names,
532
- 'Importance': importances
533
- }).sort_values('Importance', ascending=False).head(top_n)
534
-
535
- fig = px.bar(
536
- importance_df,
537
- x='Importance',
538
- y='Feature',
539
- orientation='h',
540
- title=f"🔍 Top {top_n} Features Importantes - {self.best_model_name}",
541
- color='Importance',
542
- color_continuous_scale='Blues'
543
- )
544
-
545
- fig.update_layout(
546
- height=500,
547
- yaxis={'categoryorder': 'total ascending'},
548
- template='plotly_white'
549
- )
550
-
551
- return fig
552
- else:
553
- return None
554
-
555
- def plot_shap_summary(self, max_display=15):
556
- """SHAP summary plot pour le meilleur modèle"""
557
-
558
- if not SHAP_AVAILABLE:
559
- st.warning("⚠️ SHAP n'est pas installé. Installer avec : `pip install shap`")
560
- return None
561
 
562
  try:
563
- best_model = self.models[self.best_model_name]
564
-
565
- # Sélection d'un échantillon pour SHAP (trop lent sur gros datasets)
566
- sample_size = min(500, len(self.X_test))
567
- X_sample = self.X_test.sample(n=sample_size, random_state=42)
568
-
569
- # Calcul des SHAP values
570
- if self.best_model_name in ['XGBoost', 'LightGBM']:
571
- explainer = shap.TreeExplainer(best_model)
572
- else:
573
- explainer = shap.Explainer(best_model.predict, X_sample)
574
-
575
- shap_values = explainer(X_sample)
576
-
577
- # Création du summary plot (version matplotlib)
578
- import matplotlib.pyplot as plt
579
-
580
- fig, ax = plt.subplots(figsize=(10, 8))
581
- shap.summary_plot(
582
- shap_values.values if hasattr(shap_values, 'values') else shap_values,
583
- X_sample,
584
- max_display=max_display,
585
- show=False
586
- )
587
-
588
- st.pyplot(fig)
589
- plt.close()
590
-
591
- return True
592
-
593
- except Exception as e:
594
- st.error(f"Erreur SHAP : {str(e)}")
595
- return None
596
-
597
- def plot_residuals(self):
598
- """Analyse des résidus pour le meilleur modèle"""
599
-
600
- best_pred = self.predictions[self.best_model_name]
601
- residuals = self.y_test - best_pred
602
-
603
- # Création de subplots
604
- fig = make_subplots(
605
- rows=1, cols=2,
606
- subplot_titles=('Distribution des Résidus', 'Résidus vs Prédictions')
607
- )
608
-
609
- # Histogramme des résidus
610
- fig.add_trace(
611
- go.Histogram(
612
- x=residuals,
613
- name='Résidus',
614
- marker_color='steelblue',
615
- nbinsx=30
616
- ),
617
- row=1, col=1
618
- )
619
-
620
- # Scatter résidus vs prédictions
621
- fig.add_trace(
622
- go.Scatter(
623
- x=best_pred,
624
- y=residuals,
625
- mode='markers',
626
- name='Résidus',
627
- marker=dict(color='coral', size=5, opacity=0.6)
628
- ),
629
- row=1, col=2
630
- )
631
-
632
- # Ligne horizontale à 0
633
- fig.add_hline(y=0, line_dash="dash", line_color="red", row=1, col=2)
634
-
635
- fig.update_xaxes(title_text="Résidus", row=1, col=1)
636
- fig.update_xaxes(title_text="Prédictions", row=1, col=2)
637
- fig.update_yaxes(title_text="Fréquence", row=1, col=1)
638
- fig.update_yaxes(title_text="Résidus", row=1, col=2)
639
-
640
- fig.update_layout(
641
- title_text=f"📊 Analyse des Résidus - {self.best_model_name}",
642
- height=400,
643
- showlegend=False,
644
- template='plotly_white'
645
- )
646
-
647
- return fig
648
 
 
 
649
 
650
  # ============================================================================
651
- # INTERFACE STREAMLIT (GOTHAM UI)
652
  # ============================================================================
653
 
654
- def show_forecasting_module():
655
- """
656
- Interface Streamlit pour le module de forecasting.
657
- Affiche l'entraînement, la comparaison et la recommandation des modèles.
658
- """
659
-
660
- st.markdown("## 🎯 FORECASTING ENGINE")
661
- st.caption("Entraînement, comparaison et sélection automatique du meilleur modèle ML.")
662
- st.divider()
663
-
664
- # Vérification des features dans session_state
665
  if 'ml_features' not in st.session_state:
666
- st.error("❌ **Aucun dataset ML détecté.**")
667
- st.info("💡 Veuillez d'abord exécuter le module **Feature Engineering** pour générer les features.")
668
-
669
- if st.button("🔄 Aller au Feature Engineering"):
670
- st.switch_page("pages/03_Features.py") # Adapter selon votre structure
671
  return
672
 
673
- # Récupération du dataset
674
- ml_dataset = st.session_state['ml_features']
675
-
676
- # Informations sur le dataset
677
- st.success(f"✅ Dataset ML chargé : **{len(ml_dataset):,}** échantillons")
678
-
679
- col1, col2, col3 = st.columns(3)
680
- col1.metric("Features Disponibles", len(ml_dataset.columns))
681
- col2.metric("Période", f"{ml_dataset['date'].min().strftime('%Y-%m-%d')} → {ml_dataset['date'].max().strftime('%Y-%m-%d')}")
682
- col3.metric("Services", ml_dataset['service_code'].nunique() if 'service_code' in ml_dataset.columns else "N/A")
683
-
684
- st.divider()
685
-
686
- # Configuration
687
- with st.expander("⚙️ Configuration de l'Entraînement"):
688
  col1, col2 = st.columns(2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
689
 
690
- with col1:
691
- target_var = st.selectbox(
692
- "Variable Cible",
693
- options=['nb_admissions', 'taux_occupation', 'tension_personnel', 'lits_occupes'],
694
- index=0,
695
- help="Variable à prédire"
696
- )
697
-
698
- with col2:
699
- test_size = st.slider(
700
- "Taille Test Set (%)",
701
- min_value=10,
702
- max_value=30,
703
- value=15,
704
- step=5,
705
- help="Proportion des données pour le test final"
706
- )
707
-
708
- # Bouton d'entraînement
709
- if st.button("🚀 **LANCER L'ENTRAÎNEMENT DES 3 MODÈLES**", type="primary", use_container_width=True):
710
-
711
- # Initialisation du forecaster
712
- forecaster = HospitalForecaster(ml_dataset)
713
-
714
- # Entraînement
715
- with st.spinner("🔥 Entraînement en cours..."):
716
- metrics = forecaster.train_all_models()
717
-
718
- # Stockage dans session_state
719
- st.session_state['forecaster'] = forecaster
720
- st.session_state['metrics'] = metrics
721
-
722
- st.success("✅ **Entraînement terminé avec succès !**")
723
- st.balloons()
724
-
725
- # Affichage des résultats si déjà entraîné
726
- if 'forecaster' in st.session_state:
727
- forecaster = st.session_state['forecaster']
728
- metrics = st.session_state['metrics']
729
-
730
- st.divider()
731
-
732
- # ========== RECOMMANDATION DU MEILLEUR MODÈLE ==========
733
- st.markdown("---")
734
- recommendation = forecaster.get_best_model_recommendation()
735
- st.markdown(recommendation)
736
- st.markdown("---")
737
-
738
- # ========== TABLEAU COMPARATIF ==========
739
- st.subheader("📊 Tableau Comparatif des Performances")
740
-
741
- metrics_df = pd.DataFrame(metrics).T
742
- metrics_df = metrics_df.round(4)
743
-
744
- # Mise en évidence du meilleur modèle
745
- def highlight_best(s):
746
- if s.name == forecaster.best_model_name:
747
- return ['background-color: #90EE90'] * len(s)
748
- else:
749
- return [''] * len(s)
750
-
751
- styled_metrics = metrics_df.style.apply(highlight_best, axis=1)
752
- st.dataframe(styled_metrics, use_container_width=True)
753
-
754
- # KPIs du meilleur modèle
755
- st.subheader(f"🏆 KPIs - {forecaster.best_model_name}")
756
- kpi1, kpi2, kpi3, kpi4 = st.columns(4)
757
-
758
- best_metrics = metrics[forecaster.best_model_name]
759
- kpi1.metric("RMSE", f"{best_metrics['RMSE']:.2f}", help="Root Mean Squared Error")
760
- kpi2.metric("MAE", f"{best_metrics['MAE']:.2f}", help="Mean Absolute Error")
761
- kpi3.metric("R²", f"{best_metrics['R²']:.4f}", help="Coefficient de détermination")
762
- kpi4.metric("MAPE", f"{best_metrics['MAPE']:.2f}%", help="Mean Absolute Percentage Error")
763
-
764
- st.divider()
765
 
766
- # ========== VISUALISATIONS ==========
 
 
 
 
 
767
 
768
- # 1. Comparaison des prédictions
769
- st.subheader("📈 Visualisation des Prédictions")
770
- fig_pred = forecaster.plot_predictions_comparison()
771
- st.plotly_chart(fig_pred, use_container_width=True)
772
 
773
- # 2. Radar de performance
774
- col1, col2 = st.columns(2)
775
 
776
- with col1:
777
- st.subheader("🎯 Radar de Performance")
778
- fig_radar = forecaster.plot_metrics_comparison()
779
- st.plotly_chart(fig_radar, use_container_width=True)
 
 
780
 
781
- with col2:
782
- st.subheader("📊 Analyse des Résidus")
783
- fig_residuals = forecaster.plot_residuals()
784
- st.plotly_chart(fig_residuals, use_container_width=True)
 
785
 
786
- # 3. Feature Importance
787
- st.subheader("🔍 Importance des Features")
788
- fig_importance = forecaster.plot_feature_importance(top_n=15)
789
- if fig_importance:
790
- st.plotly_chart(fig_importance, use_container_width=True)
791
 
792
- # 4. SHAP Analysis
793
- if SHAP_AVAILABLE:
794
- st.subheader("🧠 SHAP Analysis - Interprétabilité des Prédictions")
795
- st.caption("Les SHAP values montrent l'impact de chaque feature sur les prédictions du modèle.")
796
-
797
- with st.spinner("Calcul des SHAP values... (peut prendre quelques secondes)"):
798
- forecaster.plot_shap_summary(max_display=15)
799
 
800
- # ========== TÉLÉCHARGEMENT DES RÉSULTATS ==========
801
- st.divider()
802
- st.subheader("💾 Téléchargement des Résultats")
803
 
804
- col1, col2 = st.columns(2)
 
 
 
 
805
 
806
- with col1:
807
- # Export des métriques
808
- metrics_csv = pd.DataFrame(metrics).T.to_csv(index=True).encode('utf-8')
809
- st.download_button(
810
- label="📥 Télécharger les Métriques (CSV)",
811
- data=metrics_csv,
812
- file_name='model_metrics_comparison.csv',
813
- mime='text/csv'
814
- )
815
 
816
- with col2:
817
- # Export des prédictions
818
- predictions_df = pd.DataFrame({
819
- 'date': forecaster.dates_test,
820
- 'reel': forecaster.y_test,
821
- 'XGBoost': forecaster.predictions.get('XGBoost', []),
822
- 'LightGBM': forecaster.predictions.get('LightGBM', []),
823
- 'Random_Forest': forecaster.predictions.get('Random Forest', [])
824
- })
825
 
826
- predictions_csv = predictions_df.to_csv(index=False).encode('utf-8')
827
- st.download_button(
828
- label="📥 Télécharger les Prédictions (CSV)",
829
- data=predictions_csv,
830
- file_name='model_predictions_test_set.csv',
831
- mime='text/csv'
832
- )
833
-
834
- st.success("✅ Module Forecasting complet !")
835
-
836
-
837
- # Point d'entrée si appelé directement
838
- if __name__ == "__main__":
839
- show_forecasting_module()
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ MODULE: FORECASTING ENGINE (GOTHAM UI - EXPERT EDITION)
3
+ =======================================================
4
+ Responsabilité : Entraînement ML, Audit Comparatif (3 Modèles) et Prédiction.
5
+ Fonctionnalités : Comparaison Visuelle, SHAP, et Projection Multi-Horizon.
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  """
7
 
8
  import pandas as pd
9
  import numpy as np
10
  import streamlit as st
 
11
  import plotly.graph_objects as go
12
+ from datetime import timedelta
 
13
  import warnings
14
+ import copy
15
 
16
  # ML Models
17
  from sklearn.ensemble import RandomForestRegressor
18
  from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
 
19
  import xgboost as xgb
20
  import lightgbm as lgb
21
 
22
+ # Sécurité SHAP (Graphiques d'interprétabilité)
23
  try:
24
  import shap
25
+ import matplotlib.pyplot as plt # Requis pour certains plots SHAP
26
  SHAP_AVAILABLE = True
27
  except ImportError:
28
  SHAP_AVAILABLE = False
29
 
30
+ warnings.filterwarnings('ignore')
31
 
32
  class HospitalForecaster:
 
 
33
  def __init__(self, ml_dataset):
34
  self.df = ml_dataset.copy()
35
+ self.df['date'] = pd.to_datetime(self.df['date'])
36
  self.models = {}
 
37
  self.metrics = {}
38
+ self.test_predictions = {} # Pour stocker les courbes des 3 modèles
39
  self.best_model_name = None
40
+ self.last_train_date = self.df['date'].max()
41
 
42
+ def prepare_data(self, target_col='nb_admissions', test_days=60): # 60 jours pour bien voir la comparaison
43
+ """Split Temporel pour validation"""
44
+ df = self.df.sort_values('date').reset_index(drop=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
+ exclude_cols = [target_col, 'date', 'stay_id', 'patient_id']
47
+ feature_cols = [c for c in df.columns if c not in exclude_cols and np.issubdtype(df[c].dtype, np.number)]
48
 
49
+ split_idx = len(df) - test_days
 
50
 
51
+ X = df[feature_cols]
 
 
 
 
 
 
 
 
 
 
52
  y = df[target_col]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
+ self.X_train = X.iloc[:split_idx]
55
+ self.X_test = X.iloc[split_idx:]
56
+ self.y_train = y.iloc[:split_idx]
57
+ self.y_test = y.iloc[split_idx:]
58
+ self.dates_test = df['date'].iloc[split_idx:]
59
+ self.feature_names = feature_cols
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
+ return self.X_train, self.X_test, self.y_train, self.y_test
62
+
63
+ def train_models(self):
64
+ """Entraîne les 3 champions et stocke leurs prédictions pour comparaison"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
+ # 1. LightGBM
67
+ lgb_model = lgb.LGBMRegressor(n_estimators=300, learning_rate=0.05, verbose=-1, random_state=42)
68
+ lgb_model.fit(self.X_train, self.y_train)
69
+ self.models['LightGBM'] = lgb_model
 
 
70
 
71
+ # 2. XGBoost
72
+ xgb_model = xgb.XGBRegressor(n_estimators=300, learning_rate=0.05, n_jobs=-1, random_state=42)
73
+ xgb_model.fit(self.X_train, self.y_train)
74
+ self.models['XGBoost'] = xgb_model
 
75
 
76
  # 3. Random Forest
77
+ rf_model = RandomForestRegressor(n_estimators=100, max_depth=10, n_jobs=-1, random_state=42)
78
+ rf_model.fit(self.X_train, self.y_train)
79
+ self.models['Random Forest'] = rf_model
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
+ # Évaluation Comparative
 
 
 
 
 
 
 
82
  scores = {}
83
+ for name, model in self.models.items():
84
+ # On stocke la prédiction sur le test set pour le graphique
85
+ pred = model.predict(self.X_test)
86
+ self.test_predictions[name] = pred
 
 
 
 
 
 
87
 
88
+ # Métriques
89
+ rmse = np.sqrt(mean_squared_error(self.y_test, pred))
90
+ mae = mean_absolute_error(self.y_test, pred)
91
+ r2 = r2_score(self.y_test, pred)
 
 
 
92
 
93
+ self.metrics[name] = {'RMSE': rmse, 'MAE': mae, 'R2': r2}
94
+ scores[name] = rmse
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
+ self.best_model_name = min(scores, key=scores.get)
97
+ return self.metrics
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
+ def compute_shap(self):
100
+ """Calcule l'importance des features via SHAP (sur le meilleur modèle)"""
101
+ if not SHAP_AVAILABLE: return None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
+ model = self.models[self.best_model_name]
 
 
104
 
105
+ # On échantillonne pour la vitesse (max 200 points)
106
+ X_sample = self.X_test.sample(n=min(200, len(self.X_test)), random_state=42)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
  try:
109
+ # TreeExplainer est optimisé pour XGB/LGBM/RF
110
+ explainer = shap.TreeExplainer(model)
111
+ shap_values = explainer.shap_values(X_sample)
112
+ return explainer, shap_values, X_sample
113
+ except:
114
+ return None, None, None
115
+
116
+ def generate_future_forecast(self, months=3):
117
+ """Prédiction Récursive vers le Futur"""
118
+ days = months * 30
119
+ future_dates = [self.last_train_date + timedelta(days=i) for i in range(1, days + 1)]
120
+ model = self.models[self.best_model_name]
121
+
122
+ # Reconstruction simplifiée des vecteurs futurs (Vectorisée pour vitesse)
123
+ future_df = pd.DataFrame({'date': future_dates})
124
+ future_df['jour_semaine'] = future_df['date'].dt.dayofweek
125
+ future_df['mois'] = future_df['date'].dt.month
126
+ future_df['mois_sin'] = np.sin(2 * np.pi * future_df['mois']/12)
127
+ future_df['mois_cos'] = np.cos(2 * np.pi * future_df['mois']/12)
128
+
129
+ # Moyenne des autres features (Hypothèse de stabilité)
130
+ mean_values = self.df[self.feature_names].mean()
131
+ X_future = pd.DataFrame([mean_values] * len(future_df), columns=self.feature_names)
132
+
133
+ # Update temporel
134
+ for col in ['jour_semaine', 'mois_sin', 'mois_cos']:
135
+ if col in X_future.columns:
136
+ X_future[col] = future_df[col].values
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
+ y_future = model.predict(X_future)
139
+ return future_dates, y_future
140
 
141
  # ============================================================================
142
+ # INTERFACE STREAMLIT (UPDATE : NON-TECH FRIENDLY)
143
  # ============================================================================
144
 
145
+ def show_forecasting_module(client, sheet_name):
146
+ st.markdown("## 🔮 ORACLE : FORECASTING & AUDIT")
147
+ st.caption("Intelligence Artificielle de prédiction des flux hospitaliers.")
148
+
 
 
 
 
 
 
 
149
  if 'ml_features' not in st.session_state:
150
+ st.error("❌ Données ML absentes. Allez dans 'Features Eng.' d'abord.")
 
 
 
 
151
  return
152
 
153
+ df = st.session_state['ml_features']
154
+
155
+ # --- 1. CONFIGURATION RAPIDE ---
156
+ with st.expander("⚙️ Paramètres", expanded=False):
 
 
 
 
 
 
 
 
 
 
 
157
  col1, col2 = st.columns(2)
158
+ target = col1.selectbox("Cible à Prédire", ['nb_admissions', 'taux_occupation'], index=0)
159
+ test_days = col2.slider("Période de Test (Jours)", 30, 90, 60)
160
+
161
+ # --- 2. ENTRAÎNEMENT (AUTO) ---
162
+ if 'forecaster' not in st.session_state:
163
+ forecaster = HospitalForecaster(df)
164
+ with st.spinner("🧠 Le système compare 3 intelligences (XGBoost, LightGBM, RF)..."):
165
+ forecaster.prepare_data(target_col=target, test_days=test_days)
166
+ metrics = forecaster.train_models()
167
+ st.session_state['forecaster'] = forecaster
168
+ st.session_state['metrics'] = metrics
169
+ st.rerun()
170
+
171
+ forecaster = st.session_state['forecaster']
172
+ metrics = st.session_state['metrics']
173
+ best = forecaster.best_model_name
174
+ best_mae = metrics[best]['MAE']
175
+
176
+ # --- 3. AFFICHAGE PAR ONGLETS ---
177
+ tab_future, tab_audit = st.tabs(["🔮 Le Futur (Décision)", "⚖️ Le Verdict (Preuve)"])
178
+
179
+ # === ONGLET 1 : LE FUTUR ===
180
+ with tab_future:
181
+ st.markdown(f"### 🚀 Projections : {target}")
182
 
183
+ # Sélecteur d'horizon
184
+ horizon_label = st.radio("Voir dans...", ["3 Mois", "6 Mois", "9 Mois", "12 Mois"], horizontal=True)
185
+ months_map = {"3 Mois": 3, "6 Mois": 6, "9 Mois": 9, "12 Mois": 12}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
 
187
+ with st.spinner("Calcul de la trajectoire optimale..."):
188
+ f_dates, f_values = forecaster.generate_future_forecast(months_map[horizon_label])
189
+
190
+ # Graphique Futur
191
+ fig = go.Figure()
192
+ hist = df.iloc[-90:] # 3 derniers mois historiques
193
 
194
+ # Zone Historique
195
+ fig.add_trace(go.Scatter(x=hist['date'], y=hist[target], name="Passé Réel", line=dict(color='gray'), opacity=0.5))
 
 
196
 
197
+ # Zone Futur
198
+ fig.add_trace(go.Scatter(x=f_dates, y=f_values, name="Projection IA", line=dict(color='#00E676', width=3)))
199
 
200
+ # Ligne verticale "Aujourd'hui"
201
+ fig.add_vline(x=hist['date'].max(), line_width=1, line_dash="dash", line_color="white")
202
+ fig.add_annotation(x=hist['date'].max(), y=hist[target].mean(), text="Aujourd'hui", showarrow=True, arrowhead=1)
203
+
204
+ fig.update_layout(title=f"Horizon {horizon_label}", template="plotly_dark", height=450, hovermode="x unified")
205
+ st.plotly_chart(fig, use_container_width=True)
206
 
207
+ st.caption(f"Prédiction générée par le modèle **{best}** (Fiabilité : {metrics[best]['R2']*100:.0f}%)")
208
+
209
+ # === ONGLET 2 : LE VERDICT (Pour les non-techs) ===
210
+ with tab_audit:
211
+ st.markdown("### 🏆 Qui est le meilleur prédicteur ?")
212
 
213
+ # --- A. LE VERDICT EN FRANÇAIS ---
214
+ st.info(f"""
215
+ **Résultat du match :** L'intelligence artificielle **{best}** a battu les autres modèles.
 
 
216
 
217
+ 🔎 **Pourquoi ?** Sur les {test_days} derniers jours, elle a suivi la courbe réelle avec le plus de précision.
218
+ En moyenne, elle se trompe de seulement **{best_mae:.1f}** unités par jour (contre {metrics['Random Forest']['MAE']:.1f} pour le moins bon).
219
+ """)
220
+
221
+ # --- B. PREUVE VISUELLE ---
222
+ st.markdown("#### 👁️ Preuve Visuelle : Modèles vs Réalité")
223
+ st.caption("Regardez quelle ligne colorée suit le mieux la ligne blanche pointillée (La Réalité).")
224
 
225
+ fig_comp = go.Figure()
 
 
226
 
227
+ # 1. La Réalité (Blanche, très visible)
228
+ fig_comp.add_trace(go.Scatter(
229
+ x=forecaster.dates_test, y=forecaster.y_test,
230
+ name="🔴 LA RÉALITÉ", line=dict(color='white', width=4, dash='dot')
231
+ ))
232
 
233
+ # 2. Le Vainqueur (Vert fluo)
234
+ best_pred = forecaster.test_predictions[best]
235
+ fig_comp.add_trace(go.Scatter(
236
+ x=forecaster.dates_test, y=best_pred,
237
+ name=f"🏆 {best} (Gagnant)",
238
+ line=dict(color='#00E676', width=3),
239
+ opacity=0.9
240
+ ))
 
241
 
242
+ # 3. Les Perdants (Grisés pour ne pas polluer)
243
+ for name, pred in forecaster.test_predictions.items():
244
+ if name != best:
245
+ fig_comp.add_trace(go.Scatter(
246
+ x=forecaster.dates_test, y=pred,
247
+ name=name,
248
+ line=dict(color='gray', width=1),
249
+ opacity=0.4
250
+ ))
251
 
252
+ fig_comp.update_layout(template="plotly_dark", height=450)
253
+ st.plotly_chart(fig_comp, use_container_width=True)
254
+
255
+ # --- C. SHAP (Bonus Explicatif) ---
256
+ col_why, col_reset = st.columns([3, 1])
257
+ with col_why:
258
+ if SHAP_AVAILABLE:
259
+ with st.expander("🧠 Comprendre le cerveau de l'IA (Pourquoi ces chiffres ?)"):
260
+ st.write("Ce graphique montre ce qui influence le plus la hausse ou la baisse des admissions :")
261
+ if st.button("Calculer les Facteurs d'Influence"):
262
+ with st.spinner("Analyse..."):
263
+ explainer, shap_vals, X_samp = forecaster.compute_shap()
264
+ if shap_vals is not None:
265
+ fig_shap, ax = plt.subplots(figsize=(8, 5))
266
+ fig_shap.patch.set_facecolor('#0e1117')
267
+ ax.set_facecolor('#0e1117')
268
+ shap.summary_plot(shap_vals, X_samp, max_display=8, plot_type="bar", show=False)
269
+ ax.tick_params(colors='white', labelsize=10)
270
+ ax.xaxis.label.set_color('white')
271
+ st.pyplot(fig_shap)
272
+
273
+ with col_reset:
274
+ if st.button("♻️ Reset"):
275
+ del st.session_state['forecaster']
276
+ st.rerun()