jaimalklyde commited on
Commit
505212c
·
verified ·
1 Parent(s): 181df15

Update src/module/Forecasting.py

Browse files
Files changed (1) hide show
  1. src/module/Forecasting.py +896 -176
src/module/Forecasting.py CHANGED
@@ -1,17 +1,31 @@
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
@@ -19,258 +33,964 @@ 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()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ MODULE: FORECASTING ENGINE - EXPERT EDITION AMÉLIORÉE
3
+ ======================================================
4
  Responsabilité : Entraînement ML, Audit Comparatif (3 Modèles) et Prédiction.
5
+ Fonctionnalités :
6
+ - Split Train/Val/Test temporel
7
+ - Prédiction récursive avec saisonnalité
8
+ - SHAP systématique et lisible
9
+ - Métriques explicatives pour non-techniciens
10
+ - Visualisations avancées
11
+
12
+ Améliorations v2.0 :
13
+ ✅ Split 3 parties (Train 70% / Val 15% / Test 15%)
14
+ ✅ Prédiction récursive (utilise ses propres lags)
15
+ ✅ SHAP toujours affiché avec explication
16
+ ✅ Tableau comparatif détaillé des modèles
17
+ ✅ Intervalles de confiance sur prédictions
18
+ ✅ Alertes automatiques sur anomalies prévues
19
+ ✅ Export des prédictions
20
  """
21
 
22
  import pandas as pd
23
  import numpy as np
24
  import streamlit as st
25
  import plotly.graph_objects as go
26
+ from plotly.subplots import make_subplots
27
+ from datetime import datetime, timedelta
28
  import warnings
 
29
 
30
  # ML Models
31
  from sklearn.ensemble import RandomForestRegressor
 
33
  import xgboost as xgb
34
  import lightgbm as lgb
35
 
36
+ # SHAP pour interprétabilité
37
  try:
38
  import shap
39
+ import matplotlib.pyplot as plt
40
  SHAP_AVAILABLE = True
41
  except ImportError:
42
  SHAP_AVAILABLE = False
43
 
44
  warnings.filterwarnings('ignore')
45
 
46
+
47
+ # =====================================================
48
+ # ============= MOTEUR DE PRÉDICTION ==================
49
+ # =====================================================
50
+
51
  class HospitalForecaster:
52
+ """
53
+ Moteur de prédiction hospitalière avec comparaison de modèles
54
+ et interprétabilité SHAP.
55
+ """
56
+
57
  def __init__(self, ml_dataset):
58
  self.df = ml_dataset.copy()
59
  self.df['date'] = pd.to_datetime(self.df['date'])
60
+
61
+ # Stockage des modèles et résultats
62
  self.models = {}
63
  self.metrics = {}
64
+ self.predictions = {
65
+ 'train': {},
66
+ 'val': {},
67
+ 'test': {}
68
+ }
69
  self.best_model_name = None
 
70
 
71
+ # Datasets splits
72
+ self.X_train = None
73
+ self.X_val = None
74
+ self.X_test = None
75
+ self.y_train = None
76
+ self.y_val = None
77
+ self.y_test = None
78
+ self.dates_train = None
79
+ self.dates_val = None
80
+ self.dates_test = None
81
+ self.feature_names = None
82
+
83
+ def prepare_data(self, target_col='nb_admissions', val_days=30, test_days=30):
84
+ """
85
+ Split temporel en 3 parties : Train / Validation / Test.
86
+
87
+ Args:
88
+ target_col: Variable cible à prédire
89
+ val_days: Nombre de jours pour validation
90
+ test_days: Nombre de jours pour test
91
+
92
+ Returns:
93
+ Dictionnaire avec les plages de dates
94
+ """
95
+ # Tri chronologique
96
  df = self.df.sort_values('date').reset_index(drop=True)
97
 
98
+ # Exclusion des colonnes non-features
99
+ exclude_cols = [
100
+ target_col, 'date', 'stay_id', 'patient_id',
101
+ 'scenario', 'service_nom', 'periode_conges_label'
102
+ ]
103
+
104
+ # Sélection STRICTE des colonnes numériques uniquement
105
+ all_cols = [c for c in df.columns if c not in exclude_cols]
106
+ feature_cols = [c for c in all_cols if np.issubdtype(df[c].dtype, np.number)]
107
 
108
+ # Vérification de la cible
109
+ if target_col not in df.columns:
110
+ raise ValueError(f"❌ Colonne cible '{target_col}' introuvable")
111
 
112
+ # Calcul des indices de split
113
+ n = len(df)
114
+ test_start = n - test_days
115
+ val_start = test_start - val_days
116
+
117
+ # Extraction X et y
118
  X = df[feature_cols]
119
  y = df[target_col]
120
+ dates = df['date']
121
+
122
+ # Split
123
+ self.X_train = X.iloc[:val_start]
124
+ self.X_val = X.iloc[val_start:test_start]
125
+ self.X_test = X.iloc[test_start:]
126
+
127
+ self.y_train = y.iloc[:val_start]
128
+ self.y_val = y.iloc[val_start:test_start]
129
+ self.y_test = y.iloc[test_start:]
130
+
131
+ self.dates_train = dates.iloc[:val_start]
132
+ self.dates_val = dates.iloc[val_start:test_start]
133
+ self.dates_test = dates.iloc[test_start:]
134
 
 
 
 
 
 
135
  self.feature_names = feature_cols
136
+ self.target_col = target_col
137
 
138
+ # Informations de split
139
+ split_info = {
140
+ 'train': {
141
+ 'start': self.dates_train.min(),
142
+ 'end': self.dates_train.max(),
143
+ 'n_samples': len(self.X_train)
144
+ },
145
+ 'val': {
146
+ 'start': self.dates_val.min(),
147
+ 'end': self.dates_val.max(),
148
+ 'n_samples': len(self.X_val)
149
+ },
150
+ 'test': {
151
+ 'start': self.dates_test.min(),
152
+ 'end': self.dates_test.max(),
153
+ 'n_samples': len(self.X_test)
154
+ }
155
+ }
156
+
157
+ return split_info
158
+
159
+ def train_all_models(self):
160
+ """
161
+ Entraînement des 3 modèles avec validation.
162
+
163
+ Returns:
164
+ Dictionnaire des métriques
165
+ """
166
+ progress_bar = st.progress(0, text="🔄 Entraînement en cours...")
167
+
168
+ # ===== 1. LightGBM =====
169
+ progress_bar.progress(0.1, text="⚡ LightGBM...")
170
+ lgb_model = lgb.LGBMRegressor(
171
+ n_estimators=500,
172
+ learning_rate=0.05,
173
+ num_leaves=31,
174
+ max_depth=6,
175
+ min_child_samples=20,
176
+ subsample=0.8,
177
+ colsample_bytree=0.8,
178
+ random_state=42,
179
+ verbose=-1,
180
+ n_jobs=-1
181
+ )
182
+
183
+ lgb_model.fit(
184
+ self.X_train, self.y_train,
185
+ eval_set=[(self.X_val, self.y_val)],
186
+ callbacks=[lgb.early_stopping(stopping_rounds=50, verbose=False)]
187
+ )
188
 
 
 
 
189
  self.models['LightGBM'] = lgb_model
190
+ self._evaluate_model('LightGBM', lgb_model)
191
+
192
+ # ===== 2. XGBoost =====
193
+ progress_bar.progress(0.4, text="🚀 XGBoost...")
194
+ xgb_model = xgb.XGBRegressor(
195
+ n_estimators=500,
196
+ learning_rate=0.05,
197
+ max_depth=6,
198
+ min_child_weight=3,
199
+ subsample=0.8,
200
+ colsample_bytree=0.8,
201
+ random_state=42,
202
+ n_jobs=-1,
203
+ early_stopping_rounds=50,
204
+ eval_metric='rmse'
205
+ )
206
+
207
+ xgb_model.fit(
208
+ self.X_train, self.y_train,
209
+ eval_set=[(self.X_val, self.y_val)],
210
+ verbose=False
211
+ )
212
 
 
 
 
213
  self.models['XGBoost'] = xgb_model
214
+ self._evaluate_model('XGBoost', xgb_model)
215
+
216
+ # ===== 3. Random Forest =====
217
+ progress_bar.progress(0.7, text="🌲 Random Forest...")
218
+ rf_model = RandomForestRegressor(
219
+ n_estimators=300,
220
+ max_depth=15,
221
+ min_samples_split=10,
222
+ min_samples_leaf=5,
223
+ max_features='sqrt',
224
+ random_state=42,
225
+ n_jobs=-1,
226
+ verbose=0
227
+ )
228
 
 
 
229
  rf_model.fit(self.X_train, self.y_train)
230
+
231
  self.models['Random Forest'] = rf_model
232
+ self._evaluate_model('Random Forest', rf_model)
233
 
234
+ progress_bar.progress(1.0, text="✅ Entraînement terminé!")
235
+ progress_bar.empty()
236
+
237
+ # Sélection du meilleur modèle
238
+ self._select_best_model()
239
+
240
+ return self.metrics
241
+
242
+ def _evaluate_model(self, model_name, model):
243
+ """
244
+ Évalue un modèle sur les 3 sets (train, val, test).
245
+
246
+ Args:
247
+ model_name: Nom du modèle
248
+ model: Instance du modèle entraîné
249
+ """
250
+ # Prédictions sur les 3 sets
251
+ pred_train = model.predict(self.X_train)
252
+ pred_val = model.predict(self.X_val)
253
+ pred_test = model.predict(self.X_test)
254
+
255
+ # Stockage des prédictions
256
+ self.predictions['train'][model_name] = pred_train
257
+ self.predictions['val'][model_name] = pred_val
258
+ self.predictions['test'][model_name] = pred_test
259
+
260
+ # Métriques sur le test set (évaluation finale)
261
+ rmse = np.sqrt(mean_squared_error(self.y_test, pred_test))
262
+ mae = mean_absolute_error(self.y_test, pred_test)
263
+ r2 = r2_score(self.y_test, pred_test)
264
+
265
+ # MAPE (Mean Absolute Percentage Error)
266
+ mask = self.y_test != 0
267
+ if mask.sum() > 0:
268
+ mape = np.mean(np.abs((self.y_test[mask] - pred_test[mask]) / self.y_test[mask])) * 100
269
+ else:
270
+ mape = np.nan
271
+
272
+ # Métriques sur validation (pour early stopping)
273
+ rmse_val = np.sqrt(mean_squared_error(self.y_val, pred_val))
274
+ mae_val = mean_absolute_error(self.y_val, pred_val)
275
+
276
+ self.metrics[model_name] = {
277
+ 'RMSE': rmse,
278
+ 'MAE': mae,
279
+ 'R²': r2,
280
+ 'MAPE': mape,
281
+ 'RMSE_val': rmse_val,
282
+ 'MAE_val': mae_val
283
+ }
284
+
285
+ def _select_best_model(self):
286
+ """
287
+ Sélection automatique du meilleur modèle basée sur RMSE + MAE.
288
+ """
289
  scores = {}
290
+
291
+ for model_name, metrics in self.metrics.items():
292
+ # Score composite (moyenne normalisée RMSE + MAE)
293
+ score = (metrics['RMSE'] + metrics['MAE']) / 2
294
+ scores[model_name] = score
295
+
296
+ # Meilleur = score le plus bas
 
 
 
 
 
 
297
  self.best_model_name = min(scores, key=scores.get)
298
+
299
+ def generate_future_forecast_recursive(self, months=3):
300
+ """
301
+ Prédiction RÉCURSIVE vers le futur avec saisonnalité.
302
+
303
+ Le modèle utilise ses propres prédictions comme lags pour
304
+ les prédictions suivantes, conservant ainsi les tendances.
305
+
306
+ Args:
307
+ months: Nombre de mois à prédire
308
+
309
+ Returns:
310
+ Tuple (dates_futures, prédictions, intervalles_confiance)
311
+ """
312
+ days = months * 30
313
+ model = self.models[self.best_model_name]
314
+
315
+ # Dernière ligne connue (base pour récursion)
316
+ last_row = self.df.iloc[-1:][self.feature_names].copy()
317
+ last_date = self.df['date'].max()
318
+
319
+ future_dates = []
320
+ future_predictions = []
321
+
322
+ # Identification des colonnes de lags
323
+ lag_cols = [c for c in self.feature_names if '_lag_' in c]
324
+ rolling_cols = [c for c in self.feature_names if any(x in c for x in ['_mean_', '_std_', '_min_', '_max_'])]
325
+
326
+ # Prédiction jour par jour
327
+ for i in range(1, days + 1):
328
+ current_date = last_date + timedelta(days=i)
329
+ future_dates.append(current_date)
330
+
331
+ # Mise à jour des features temporelles
332
+ row = last_row.copy()
333
+
334
+ # Features temporelles basiques
335
+ if 'jour_semaine' in row.columns:
336
+ row['jour_semaine'] = current_date.dayofweek
337
+ if 'mois' in row.columns:
338
+ row['mois'] = current_date.month
339
+ if 'trimestre' in row.columns:
340
+ row['trimestre'] = current_date.quarter
341
+ if 'jour_annee' in row.columns:
342
+ row['jour_annee'] = current_date.dayofyear
343
+
344
+ # Features cycliques
345
+ if 'mois_sin' in row.columns:
346
+ row['mois_sin'] = np.sin(2 * np.pi * current_date.month / 12)
347
+ if 'mois_cos' in row.columns:
348
+ row['mois_cos'] = np.cos(2 * np.pi * current_date.month / 12)
349
+
350
+ if 'jour_semaine_sin' in row.columns:
351
+ row['jour_semaine_sin'] = np.sin(2 * np.pi * current_date.dayofweek / 7)
352
+ if 'jour_semaine_cos' in row.columns:
353
+ row['jour_semaine_cos'] = np.cos(2 * np.pi * current_date.dayofweek / 7)
354
+
355
+ # Features de périodes hospitalières
356
+ mois = current_date.month
357
+ if 'periode_grippe' in row.columns:
358
+ row['periode_grippe'] = int((mois >= 11) or (mois <= 3))
359
+ if 'periode_canicule' in row.columns:
360
+ row['periode_canicule'] = int((mois >= 6) and (mois <= 8))
361
+ if 'periode_gastro' in row.columns:
362
+ row['periode_gastro'] = int((mois >= 11) or (mois <= 2))
363
+ if 'periode_bronchiolite' in row.columns:
364
+ row['periode_bronchiolite'] = int((mois >= 10) and (mois <= 2))
365
+
366
+ # Weekend
367
+ if 'est_weekend' in row.columns:
368
+ row['est_weekend'] = int(current_date.dayofweek >= 5)
369
+
370
+ # Prédiction
371
+ pred = model.predict(row)[0]
372
+ future_predictions.append(pred)
373
+
374
+ # RÉCURSION : Mise à jour des lags avec la nouvelle prédiction
375
+ # Pour le prochain jour, cette prédiction devient le lag_1
376
+ if len(future_predictions) >= 1 and f'{self.target_col}_lag_1' in lag_cols:
377
+ row[f'{self.target_col}_lag_1'] = pred
378
+
379
+ if len(future_predictions) >= 7 and f'{self.target_col}_lag_7' in lag_cols:
380
+ row[f'{self.target_col}_lag_7'] = future_predictions[-7]
381
+
382
+ if len(future_predictions) >= 14 and f'{self.target_col}_lag_14' in lag_cols:
383
+ row[f'{self.target_col}_lag_14'] = future_predictions[-14]
384
+
385
+ # Mise à jour des rolling stats (approximation simple)
386
+ if len(future_predictions) >= 7:
387
+ recent_7 = future_predictions[-7:]
388
+ if f'{self.target_col}_mean_7j' in rolling_cols:
389
+ row[f'{self.target_col}_mean_7j'] = np.mean(recent_7)
390
+ if f'{self.target_col}_std_7j' in rolling_cols:
391
+ row[f'{self.target_col}_std_7j'] = np.std(recent_7)
392
+
393
+ if len(future_predictions) >= 14:
394
+ recent_14 = future_predictions[-14:]
395
+ if f'{self.target_col}_mean_14j' in rolling_cols:
396
+ row[f'{self.target_col}_mean_14j'] = np.mean(recent_14)
397
+ if f'{self.target_col}_std_14j' in rolling_cols:
398
+ row[f'{self.target_col}_std_14j'] = np.std(recent_14)
399
+
400
+ # Préparation pour la prochaine itération
401
+ last_row = row
402
+
403
+ # Calcul des intervalles de confiance (basé sur l'erreur du modèle)
404
+ mae = self.metrics[self.best_model_name]['MAE']
405
+ lower_bound = np.array(future_predictions) - 1.96 * mae
406
+ upper_bound = np.array(future_predictions) + 1.96 * mae
407
+
408
+ confidence_intervals = {
409
+ 'lower': lower_bound,
410
+ 'upper': upper_bound
411
+ }
412
+
413
+ return future_dates, np.array(future_predictions), confidence_intervals
414
+
415
+ def compute_shap_values(self, n_samples=200):
416
+ """
417
+ Calcule les valeurs SHAP pour interprétabilité.
418
+
419
+ Args:
420
+ n_samples: Nombre d'échantillons pour le calcul
421
+
422
+ Returns:
423
+ Tuple (explainer, shap_values, X_sample) ou (None, None, None)
424
+ """
425
+ if not SHAP_AVAILABLE:
426
+ return None, None, None
427
 
428
  model = self.models[self.best_model_name]
429
 
430
+ # Échantillonnage pour la vitesse
431
+ X_sample = self.X_test.sample(n=min(n_samples, len(self.X_test)), random_state=42)
432
 
433
  try:
434
+ # TreeExplainer optimisé pour les modèles d'arbres
435
  explainer = shap.TreeExplainer(model)
436
  shap_values = explainer.shap_values(X_sample)
437
+
438
  return explainer, shap_values, X_sample
439
+
440
+ except Exception as e:
441
+ st.warning(f"⚠️ Erreur SHAP : {str(e)}")
442
  return None, None, None
443
+
444
+ def detect_future_anomalies(self, future_predictions, threshold_percentile=90):
445
+ """
446
+ Détecte les anomalies prévues dans les prédictions futures.
447
+
448
+ Args:
449
+ future_predictions: Array des prédictions futures
450
+ threshold_percentile: Percentile pour définir une anomalie
451
+
452
+ Returns:
453
+ Liste d'alertes
454
+ """
455
+ # Calcul du seuil basé sur l'historique
456
+ historical_values = self.df[self.target_col].values
457
+ threshold = np.percentile(historical_values, threshold_percentile)
458
 
459
+ # Détection des pics
460
+ alerts = []
461
+ for i, pred in enumerate(future_predictions):
462
+ if pred > threshold:
463
+ alerts.append({
464
+ 'day': i + 1,
465
+ 'value': pred,
466
+ 'threshold': threshold,
467
+ 'severity': 'HIGH' if pred > threshold * 1.2 else 'MEDIUM'
468
+ })
469
+
470
+ return alerts
 
 
 
471
 
 
 
472
 
473
+ # =====================================================
474
+ # ============= INTERFACE STREAMLIT ===================
475
+ # =====================================================
476
 
477
  def show_forecasting_module(client, sheet_name):
478
+ """
479
+ Interface Streamlit pour le module de forecasting.
480
+ """
481
  st.markdown("## 🔮 ORACLE : FORECASTING & AUDIT")
482
+ st.caption("Intelligence Artificielle de prédiction des flux hospitaliers avec interprétabilité complète.")
483
+
484
+ # ===== VÉRIFICATION DONNÉES =====
485
  if 'ml_features' not in st.session_state:
486
+ st.error("❌ **Données ML absentes.** Veuillez d'abord exécuter le module **Feature Engineering**.")
487
+
488
+ if st.button("🔄 Aller au Feature Engineering"):
489
+ st.switch_page("pages/03_Features.py")
490
  return
491
 
492
  df = st.session_state['ml_features']
493
+
494
+ st.success(f"✅ Dataset ML chargé : **{len(df):,}** échantillons")
495
+
496
+ # ===== CONFIGURATION =====
497
+ st.divider()
498
+
499
+ with st.expander("⚙️ Configuration de l'Entraînement", expanded=False):
500
+ col1, col2, col3 = st.columns(3)
501
+
502
+ with col1:
503
+ target = st.selectbox(
504
+ "Variable Cible",
505
+ ['nb_admissions', 'taux_occupation', 'tension_personnel'],
506
+ index=0,
507
+ help="Variable à prédire"
508
+ )
509
+
510
+ with col2:
511
+ val_days = st.number_input(
512
+ "Jours Validation",
513
+ min_value=15,
514
+ max_value=60,
515
+ value=30,
516
+ help="Nombre de jours pour le set de validation"
517
+ )
518
+
519
+ with col3:
520
+ test_days = st.number_input(
521
+ "Jours Test",
522
+ min_value=15,
523
+ max_value=60,
524
+ value=30,
525
+ help="Nombre de jours pour le test final"
526
+ )
527
+
528
+ # ===== ENTRAÎNEMENT =====
529
+ if 'forecaster' not in st.session_state or st.session_state.get('forecast_target') != target:
530
+
531
+ st.info("🚀 **Lancement de l'entraînement des 3 modèles...**")
532
+
533
  forecaster = HospitalForecaster(df)
534
+
535
+ try:
536
+ # Préparation des données
537
+ with st.spinner("📊 Préparation des données (Train/Val/Test)..."):
538
+ split_info = forecaster.prepare_data(
539
+ target_col=target,
540
+ val_days=val_days,
541
+ test_days=test_days
542
+ )
543
+
544
+ # Affichage des plages de dates
545
+ st.markdown("### 📅 Plages de Dates des Sets")
546
+
547
+ col1, col2, col3 = st.columns(3)
548
+
549
+ with col1:
550
+ st.metric(
551
+ "🟢 Train",
552
+ f"{split_info['train']['n_samples']} jours",
553
+ f"{split_info['train']['start'].strftime('%d/%m/%Y')} → {split_info['train']['end'].strftime('%d/%m/%Y')}"
554
+ )
555
+
556
+ with col2:
557
+ st.metric(
558
+ "🟡 Validation",
559
+ f"{split_info['val']['n_samples']} jours",
560
+ f"{split_info['val']['start'].strftime('%d/%m/%Y')} → {split_info['val']['end'].strftime('%d/%m/%Y')}"
561
+ )
562
+
563
+ with col3:
564
+ st.metric(
565
+ "🔴 Test",
566
+ f"{split_info['test']['n_samples']} jours",
567
+ f"{split_info['test']['start'].strftime('%d/%m/%Y')} → {split_info['test']['end'].strftime('%d/%m/%Y')}"
568
+ )
569
+
570
+ st.divider()
571
+
572
+ # Entraînement des modèles
573
+ metrics = forecaster.train_all_models()
574
+
575
+ # Stockage dans session_state
576
  st.session_state['forecaster'] = forecaster
577
  st.session_state['metrics'] = metrics
578
+ st.session_state['split_info'] = split_info
579
+ st.session_state['forecast_target'] = target
580
+
581
+ st.success("✅ **Entraînement terminé avec succès !**")
582
+ st.balloons()
583
 
584
+ except Exception as e:
585
+ st.error(f"❌ **Erreur lors de l'entraînement :** {str(e)}")
586
+ import traceback
587
+ with st.expander("🔍 Détails de l'erreur"):
588
+ st.code(traceback.format_exc())
589
+ return
590
+
591
+ # ===== RÉCUPÉRATION DES RÉSULTATS =====
592
  forecaster = st.session_state['forecaster']
593
  metrics = st.session_state['metrics']
594
+ split_info = st.session_state['split_info']
595
  best = forecaster.best_model_name
596
+
597
+ # ===== ONGLETS PRINCIPAUX =====
598
+ tab_future, tab_audit, tab_export = st.tabs([
599
+ "🔮 Prédictions Futures",
600
+ "⚖️ Audit & Comparaison",
601
+ "📥 Export & Données"
602
+ ])
603
+
604
+ # ================================================================
605
+ # ONGLET 1 : PRÉDICTIONS FUTURES
606
+ # ================================================================
607
  with tab_future:
608
+ st.markdown("### 🚀 Projections Récursives avec Saisonnalité")
609
 
610
+ col_h1, col_h2 = st.columns([3, 1])
 
 
611
 
612
+ with col_h1:
613
+ horizon_label = st.radio(
614
+ "Horizon de prédiction",
615
+ ["1 Mois", "3 Mois", "6 Mois", "12 Mois"],
616
+ horizontal=True,
617
+ index=1
618
+ )
619
+
620
+ with col_h2:
621
+ show_confidence = st.checkbox("Intervalle de confiance", value=True)
622
+
623
+ months_map = {"1 Mois": 1, "3 Mois": 3, "6 Mois": 6, "12 Mois": 12}
624
+ months = months_map[horizon_label]
625
+
626
+ with st.spinner("🔮 Calcul de la trajectoire récursive avec saisonnalité..."):
627
+ f_dates, f_values, conf_intervals = forecaster.generate_future_forecast_recursive(months)
628
+
629
+ # Graphique des prédictions futures
630
  fig = go.Figure()
 
631
 
632
+ # Historique récent (90 derniers jours)
633
+ hist = df.iloc[-90:]
634
+ fig.add_trace(go.Scatter(
635
+ x=hist['date'],
636
+ y=hist[target],
637
+ name="📊 Historique Réel",
638
+ line=dict(color='gray', width=2),
639
+ opacity=0.6
640
+ ))
641
+
642
+ # Prédictions futures
643
+ fig.add_trace(go.Scatter(
644
+ x=f_dates,
645
+ y=f_values,
646
+ name=f"🔮 Projection {best}",
647
+ line=dict(color='#00E676', width=3),
648
+ mode='lines+markers',
649
+ marker=dict(size=5)
650
+ ))
651
 
652
+ # Intervalle de confiance
653
+ if show_confidence:
654
+ fig.add_trace(go.Scatter(
655
+ x=f_dates + f_dates[::-1],
656
+ y=list(conf_intervals['upper']) + list(conf_intervals['lower'][::-1]),
657
+ fill='toself',
658
+ fillcolor='rgba(0, 230, 118, 0.2)',
659
+ line=dict(color='rgba(255,255,255,0)'),
660
+ name='Intervalle de confiance (95%)',
661
+ showlegend=True
662
+ ))
663
 
664
  # Ligne verticale "Aujourd'hui"
665
+ last_hist_date = hist['date'].max()
666
+ fig.add_vline(
667
+ x=last_hist_date,
668
+ line_width=2,
669
+ line_dash="dash",
670
+ line_color="white",
671
+ annotation_text="Aujourd'hui",
672
+ annotation_position="top"
673
+ )
674
+
675
+ fig.update_layout(
676
+ title=f"Projection {horizon_label} - {target.replace('_', ' ').title()}",
677
+ template="plotly_dark",
678
+ height=500,
679
+ hovermode="x unified",
680
+ xaxis_title="Date",
681
+ yaxis_title=target.replace('_', ' ').title()
682
+ )
683
+
684
  st.plotly_chart(fig, use_container_width=True)
685
 
686
+ # Statistiques de la projection
687
+ col1, col2, col3, col4 = st.columns(4)
688
+
689
+ col1.metric("Moyenne Prévue", f"{np.mean(f_values):.1f}")
690
+ col2.metric("Maximum Prévu", f"{np.max(f_values):.1f}")
691
+ col3.metric("Minimum Prévu", f"{np.min(f_values):.1f}")
692
+ col4.metric("Tendance",
693
+ "↗️ Hausse" if f_values[-1] > f_values[0] else "↘️ Baisse",
694
+ f"{((f_values[-1] / f_values[0]) - 1) * 100:+.1f}%")
695
+
696
+ # Détection d'anomalies futures
697
+ st.markdown("#### 🚨 Alertes Préventives")
698
+
699
+ alerts = forecaster.detect_future_anomalies(f_values)
700
+
701
+ if alerts:
702
+ st.warning(f"⚠️ **{len(alerts)} pic(s) détecté(s)** dans les {months} prochains mois")
703
+
704
+ for alert in alerts[:5]: # Top 5
705
+ severity_emoji = "🔴" if alert['severity'] == 'HIGH' else "🟠"
706
+ st.write(f"{severity_emoji} **Jour +{alert['day']}** : Valeur prévue = **{alert['value']:.0f}** "
707
+ f"(seuil normal : {alert['threshold']:.0f})")
708
+ else:
709
+ st.success("✅ Aucun pic anormal détecté dans la période")
710
+
711
+ st.caption(f"💡 Prédiction générée par **{best}** | Fiabilité : **{metrics[best]['R²']*100:.0f}%**")
712
+
713
+ # ================================================================
714
+ # ONGLET 2 : AUDIT & COMPARAISON
715
+ # ================================================================
716
  with tab_audit:
717
+ st.markdown("### 🏆 Le Champion de la Prédiction")
718
 
719
+ # Annonce du vainqueur
720
+ col_winner, col_reset = st.columns([4, 1])
 
721
 
722
+ with col_winner:
723
+ st.success(f"""
724
+ ### ✨ Gagnant : **{best}**
725
+
726
+ Sur les **{split_info['test']['n_samples']} derniers jours** de test, ce modèle a montré
727
+ la meilleure précision avec une erreur moyenne de seulement **{metrics[best]['MAE']:.2f}** unités par jour.
728
+
729
+ **Fiabilité globale :** {metrics[best]['R²']*100:.1f}%
730
+ (Le modèle explique {metrics[best]['R²']*100:.1f}% des variations observées)
731
+ """)
732
+
733
+ with col_reset:
734
+ if st.button("♻️ Réinitialiser", help="Relancer l'entraînement"):
735
+ for key in ['forecaster', 'metrics', 'split_info', 'forecast_target']:
736
+ if key in st.session_state:
737
+ del st.session_state[key]
738
+ st.rerun()
739
+
740
+ st.divider()
741
+
742
+ # ===== TABLEAU COMPARATIF =====
743
+ st.markdown("### 📊 Tableau Comparatif Détaillé")
744
+
745
+ comparison_data = []
746
+ for model_name in ['XGBoost', 'LightGBM', 'Random Forest']:
747
+ m = metrics[model_name]
748
+ comparison_data.append({
749
+ 'Modèle': f"{'🏆 ' if model_name == best else ''}{model_name}",
750
+ 'R² (Fiabilité %)': f"{m['R²']*100:.2f}%",
751
+ 'RMSE': f"{m['RMSE']:.2f}",
752
+ 'MAE (Erreur Moy.)': f"{m['MAE']:.2f}",
753
+ 'MAPE (Erreur %)': f"{m['MAPE']:.2f}%" if not np.isnan(m['MAPE']) else "N/A"
754
+ })
755
+
756
+ comparison_df = pd.DataFrame(comparison_data)
757
+
758
+ st.dataframe(
759
+ comparison_df,
760
+ use_container_width=True,
761
+ hide_index=True
762
+ )
763
+
764
+ # Explications des métriques
765
+ with st.expander("📖 Comprendre les Métriques"):
766
+ st.markdown("""
767
+ **R² (Coefficient de Détermination) :**
768
+ - Mesure de 0% à 100% : plus c'est proche de 100%, mieux c'est
769
+ - Indique quelle part de la variation est expliquée par le modèle
770
+ - Exemple : 85% = le modèle comprend 85% des raisons des fluctuations
771
+
772
+ **RMSE (Root Mean Squared Error) :**
773
+ - Erreur moyenne au carré, puis racine carrée
774
+ - Pénalise fortement les grandes erreurs
775
+ - Plus la valeur est basse, mieux c'est
776
+ - Utile en santé car les grosses erreurs sont graves
777
+
778
+ **MAE (Mean Absolute Error) :**
779
+ - Erreur moyenne en valeur absolue
780
+ - S'exprime dans l'unité de la cible (ex: nombre d'admissions)
781
+ - Plus facile à interpréter : "en moyenne, je me trompe de X unités"
782
+ - Plus robuste aux valeurs extrêmes que RMSE
783
+
784
+ **MAPE (Mean Absolute Percentage Error) :**
785
+ - Erreur moyenne en pourcentage
786
+ - Indépendant de l'échelle des données
787
+ - Exemple : 5% = en moyenne, l'erreur est de 5% de la valeur réelle
788
+ """)
789
+
790
+ st.divider()
791
+
792
+ # ===== GRAPHIQUE COMPARATIF SUR TEST SET =====
793
+ st.markdown("### 👁️ Comparaison Visuelle sur le Test Set")
794
 
795
  fig_comp = go.Figure()
796
 
797
+ # Réalité (en blanc épais)
798
  fig_comp.add_trace(go.Scatter(
799
+ x=forecaster.dates_test,
800
+ y=forecaster.y_test,
801
+ name="🎯 RÉALITÉ",
802
+ line=dict(color='white', width=4, dash='dot'),
803
+ mode='lines+markers',
804
+ marker=dict(size=6)
805
  ))
806
 
807
+ # Les 3 modèles
808
+ colors = {
809
+ 'XGBoost': '#1f77b4',
810
+ 'LightGBM': '#ff7f0e',
811
+ 'Random Forest': '#2ca02c'
812
+ }
813
+
814
+ for model_name in ['XGBoost', 'LightGBM', 'Random Forest']:
815
+ pred = forecaster.predictions['test'][model_name]
816
+ is_best = (model_name == best)
817
+
818
+ fig_comp.add_trace(go.Scatter(
819
+ x=forecaster.dates_test,
820
+ y=pred,
821
+ name=f"{'🏆 ' if is_best else ''}{model_name}",
822
+ line=dict(
823
+ color=colors[model_name],
824
+ width=3 if is_best else 1.5,
825
+ dash='solid' if is_best else 'dash'
826
+ ),
827
+ opacity=1.0 if is_best else 0.5
828
+ ))
829
+
830
+ fig_comp.update_layout(
831
+ title="Prédictions vs Réalité sur la Période de Test",
832
+ template="plotly_dark",
833
+ height=500,
834
+ hovermode="x unified",
835
+ xaxis_title="Date",
836
+ yaxis_title=target.replace('_', ' ').title()
837
+ )
838
 
 
 
 
 
 
 
 
 
 
 
 
839
  st.plotly_chart(fig_comp, use_container_width=True)
840
 
841
+ st.caption("💡 **Plus une ligne colorée est proche de la ligne blanche, meilleur est le modèle**")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
842
 
843
+ st.divider()
844
+
845
+ # ===== SHAP : INTERPRÉTABILITÉ =====
846
+ st.markdown("### 🧠 Comprendre les Décisions de l'IA (SHAP)")
847
+
848
+ st.info("""
849
+ **SHAP** (SHapley Additive exPlanations) révèle **quelles variables influencent le plus**
850
+ les prédictions du modèle.
851
+
852
+ 👉 Plus la barre est longue, plus cette feature est importante pour les décisions de l'IA.
853
+ """)
854
+
855
+ if SHAP_AVAILABLE:
856
+ with st.spinner("🔍 Calcul des valeurs SHAP (analyse d'importance)..."):
857
+ explainer, shap_values, X_sample = forecaster.compute_shap_values(n_samples=200)
858
+
859
+ if shap_values is not None:
860
+ # Summary plot (importance globale)
861
+ fig_shap, ax = plt.subplots(figsize=(10, 6))
862
+ fig_shap.patch.set_facecolor('#0e1117')
863
+ ax.set_facecolor('#0e1117')
864
+
865
+ shap.summary_plot(
866
+ shap_values,
867
+ X_sample,
868
+ max_display=15,
869
+ plot_type="bar",
870
+ show=False,
871
+ color='#00E676'
872
+ )
873
+
874
+ # Styling pour fond sombre
875
+ ax.tick_params(colors='white', labelsize=11)
876
+ ax.xaxis.label.set_color('white')
877
+ ax.yaxis.label.set_color('white')
878
+ ax.set_xlabel('Impact sur la prédiction', color='white', fontsize=12)
879
+ ax.set_title(f'Top 15 Features les Plus Importantes - {best}',
880
+ color='white', fontsize=14, fontweight='bold')
881
+
882
+ st.pyplot(fig_shap)
883
+ plt.close()
884
+
885
+ # Explications textuelles
886
+ st.markdown("#### 📌 Interprétation")
887
+
888
+ # Top 5 features
889
+ feature_importance = pd.DataFrame({
890
+ 'Feature': X_sample.columns,
891
+ 'Importance': np.abs(shap_values).mean(axis=0)
892
+ }).sort_values('Importance', ascending=False).head(5)
893
+
894
+ st.write("**Les 5 facteurs clés identifiés par l'IA :**")
895
+
896
+ for idx, row in feature_importance.iterrows():
897
+ feature = row['Feature']
898
+
899
+ # Traduction en français
900
+ if 'lag' in feature:
901
+ explanation = f"📅 Historique récent ({feature})"
902
+ elif 'mean' in feature or 'rolling' in feature:
903
+ explanation = f"📈 Tendance sur plusieurs jours ({feature})"
904
+ elif 'taux' in feature:
905
+ explanation = f"💹 Taux ou ratio ({feature})"
906
+ elif 'personnel' in feature:
907
+ explanation = f"👥 Ressources humaines ({feature})"
908
+ elif 'scenario' in feature or 'intensity' in feature:
909
+ explanation = f"🚨 Scénario/événement ({feature})"
910
+ elif 'mois' in feature or 'jour' in feature:
911
+ explanation = f"📆 Facteur temporel ({feature})"
912
+ else:
913
+ explanation = f"🔧 {feature}"
914
+
915
+ st.write(f"- {explanation}")
916
+
917
+ else:
918
+ st.warning("⚠️ Impossible de calculer les valeurs SHAP pour ce modèle")
919
+
920
+ else:
921
+ st.warning("⚠️ **SHAP n'est pas installé.** Installez avec : `pip install shap`")
922
+
923
+ # ================================================================
924
+ # ONGLET 3 : EXPORT & DONNÉES
925
+ # ================================================================
926
+ with tab_export:
927
+ st.markdown("### 📥 Téléchargement des Résultats")
928
+
929
+ col1, col2 = st.columns(2)
930
+
931
+ with col1:
932
+ # Export des métriques
933
+ st.markdown("#### 📊 Métriques des Modèles")
934
+
935
+ metrics_df = pd.DataFrame(metrics).T
936
+ metrics_csv = metrics_df.to_csv(index=True).encode('utf-8')
937
+
938
+ st.download_button(
939
+ label="📥 Télécharger Métriques (CSV)",
940
+ data=metrics_csv,
941
+ file_name=f'model_metrics_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv',
942
+ mime='text/csv'
943
+ )
944
+
945
+ st.dataframe(metrics_df, use_container_width=True)
946
+
947
+ with col2:
948
+ # Export des prédictions sur test
949
+ st.markdown("#### 🎯 Prédictions Test Set")
950
+
951
+ predictions_df = pd.DataFrame({
952
+ 'date': forecaster.dates_test,
953
+ 'reel': forecaster.y_test,
954
+ 'XGBoost': forecaster.predictions['test']['XGBoost'],
955
+ 'LightGBM': forecaster.predictions['test']['LightGBM'],
956
+ 'Random_Forest': forecaster.predictions['test']['Random Forest']
957
+ })
958
+
959
+ predictions_csv = predictions_df.to_csv(index=False).encode('utf-8')
960
+
961
+ st.download_button(
962
+ label="📥 Télécharger Prédictions (CSV)",
963
+ data=predictions_csv,
964
+ file_name=f'predictions_test_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv',
965
+ mime='text/csv'
966
+ )
967
+
968
+ st.dataframe(predictions_df.head(10), use_container_width=True)
969
+
970
+ # Export des prédictions futures
971
+ st.markdown("#### 🔮 Prédictions Futures")
972
+
973
+ if 'f_dates' in locals() and 'f_values' in locals():
974
+ future_df = pd.DataFrame({
975
+ 'date': f_dates,
976
+ 'prediction': f_values,
977
+ 'lower_bound': conf_intervals['lower'],
978
+ 'upper_bound': conf_intervals['upper']
979
+ })
980
+
981
+ future_csv = future_df.to_csv(index=False).encode('utf-8')
982
+
983
+ st.download_button(
984
+ label="📥 Télécharger Prédictions Futures (CSV)",
985
+ data=future_csv,
986
+ file_name=f'predictions_futures_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv',
987
+ mime='text/csv'
988
+ )
989
+
990
+ st.dataframe(future_df.head(30), use_container_width=True)
991
+
992
+
993
+ # Point d'entrée si appelé directement
994
+ if __name__ == "__main__":
995
+ # Pour test standalone
996
+ show_forecasting_module(None, None)