madm96 commited on
Commit
2ecce3e
·
verified ·
1 Parent(s): 973f376

Upload notebook_manager.py

Browse files
Files changed (1) hide show
  1. notebook_manager.py +93 -385
notebook_manager.py CHANGED
@@ -1,6 +1,6 @@
1
  # ╔══════════════════════════════════════════════════════════════════════════════╗
2
- # ║ NOTEBOOK : Analyse Vacances - VERSION MANAGER
3
- # ║ IPython.display, graphes, tableaux markdown
4
  # ╚══════════════════════════════════════════════════════════════════════════════╝
5
 
6
  # ════════════════════════════════════════════════════════════════════════════════
@@ -12,25 +12,9 @@ from datetime import date
12
  import warnings
13
  warnings.filterwarnings("ignore")
14
 
15
- try:
16
- from IPython.display import display, Markdown
17
- IPYTHON_OK = True
18
- except ImportError:
19
- IPYTHON_OK = False
20
- def display(x): print(x)
21
- def Markdown(x): return x
22
-
23
- try:
24
- import matplotlib.pyplot as plt
25
- MATPLOTLIB_OK = True
26
- except ImportError:
27
- MATPLOTLIB_OK = False
28
- print("⚠️ matplotlib non installé → pas de graphes. pip install matplotlib")
29
-
30
  # ════════════════════════════════════════════════════════════════════════════════
31
- # CELLULE 2 — Calendrier vacances scolaires (2023-2027)
32
  # ════════════════════════════════════════════════════════════════════════════════
33
-
34
  VACANCES = {
35
  "2023-2024": {
36
  "A": [(date(2023,10,21),date(2023,11,5)), (date(2023,12,23),date(2024,1,7)),
@@ -67,38 +51,34 @@ VACANCES = {
67
  },
68
  "2026-2027": {
69
  "A": [(date(2026,10,17),date(2026,11,1)), (date(2026,12,19),date(2027,1,3)),
70
- (date(2027,2,14),date(2027,3,1)), (date(2027,4,4),date(2027,4,19)),
71
  (date(2027,7,3),date(2027,8,31))],
72
  "B": [(date(2026,10,17),date(2026,11,1)), (date(2026,12,19),date(2027,1,3)),
73
- (date(2027,2,21),date(2027,3,8)), (date(2027,4,11),date(2027,4,26)),
74
  (date(2027,7,3),date(2027,8,31))],
75
  "C": [(date(2026,10,17),date(2026,11,1)), (date(2026,12,19),date(2027,1,3)),
76
- (date(2027,2,7),date(2027,2,22)), (date(2027,3,28),date(2027,4,12)),
77
  (date(2027,7,3),date(2027,8,31))],
78
  },
79
  }
80
 
81
  DR_TO_ZONE = {
82
- "Besancon": "A", "Bordeaux": "A", "Clermont-Ferrand": "A",
83
- "Dijon": "A", "Grenoble": "A", "Lyon": "A", "Limoges": "A", "Poitiers": "A",
84
- "Aix-Marseille": "B", "Amiens": "B", "Caen": "B", "Lille": "B",
85
- "Nantes": "B", "Nice": "B", "Orleans-Tours": "B", "Reims": "B",
86
- "Rennes": "B", "Rouen": "B", "Strasbourg": "B",
87
- "Creteil": "C", "Montpellier": "C", "Nancy-Metz": "C",
88
- "Paris": "C", "Toulouse": "C", "Versailles": "C",
89
- "AFC": "C",
90
  }
91
 
92
  def get_zone(dr): return DR_TO_ZONE.get(dr, "C")
 
93
 
94
  def is_vacances(d, zone, vac):
95
  for debut, fin in vac.get(zone, []):
96
  if debut <= d <= fin: return True
97
  return False
98
 
99
- def get_annee_scolaire(d):
100
- return f"{d.year}-{d.year+1}" if d.month >= 9 else f"{d.year-1}-{d.year}"
101
-
102
  def get_periode_vacances(d, vac):
103
  for zone in ["A","B","C"]:
104
  for debut, fin in vac.get(zone, []):
@@ -117,387 +97,115 @@ def add_vacances(df):
117
  df["zone_vacances"] = df["DR"].apply(get_zone)
118
  df["annee_scolaire"] = df["Date"].apply(lambda d: get_annee_scolaire(d.date()))
119
  def _vac(row):
120
- d = row["Date"].date()
121
- return is_vacances(d, row["zone_vacances"], VACANCES.get(row["annee_scolaire"], {}))
122
  def _per(row):
123
- d = row["Date"].date()
124
- return get_periode_vacances(d, VACANCES.get(row["annee_scolaire"], {}))
125
  df["is_vacances_zone"] = df.apply(_vac, axis=1)
126
  df["periode_vacances"] = df.apply(_per, axis=1)
127
  return df
128
 
129
  # ════════════════════════════════════════════════════════════════════════════════
130
- # CELLULE 3 — Métriques (vocabulaire métier)
131
- # ════════════════════════════════════════════════════════════════════════════════
132
-
133
- def ecart_absolu(y_true, y_pred):
134
- return np.mean(np.abs(np.asarray(y_true) - np.asarray(y_pred)))
135
-
136
- def ecart_relatif_pct(y_true, y_pred):
137
- yt, yp = np.asarray(y_true), np.asarray(y_pred)
138
- return np.mean(np.abs((yt - yp) / np.maximum(yt, 1))) * 100
139
-
140
- # ════════════════════════════════════════════════════════════════════════════════
141
- # CELLULE 4 — Analyse globale avec IPython.display
142
  # ════════════════════════════════════════════════════════════════════════════════
143
 
144
- def display_md(text):
145
- """Affiche du texte/Markdown via IPython.display si dispo."""
146
- if IPYTHON_OK:
147
- display(Markdown(text))
148
- else:
149
- print(text)
150
-
151
- def display_df(df, title=None):
152
- """Affiche un DataFrame formaté via IPython.display."""
153
- if title:
154
- display_md(f"### {title}")
155
- if IPYTHON_OK:
156
- # Style pour mise en évidence
157
- styled = df.style.set_properties(**{'text-align': 'center'})
158
- styled = styled.set_table_styles([
159
- {'selector': 'th', 'props': [('text-align', 'center'), ('font-weight', 'bold'), ('background-color', '#f0f0f0')]}
160
- ])
161
- display(styled)
162
- else:
163
- print(df.to_string(index=False))
164
-
165
- def analyse_globale(df):
166
  dfp = df[(df["count"] > 0) & (df["prediction_XGB"].notna())].copy()
167
- if len(dfp) == 0:
168
- display_md("❌ **Aucune donnée passée avec prédiction valide.**")
169
- return None
170
-
171
- mask_v = dfp["is_vacances_zone"]
172
- mask_h = ~mask_v
173
-
174
- rows = []
175
- for mask, label in [(mask_v, "Vacances scolaires"), (mask_h, "Hors vacances")]:
176
- sub = dfp[mask]
177
- if len(sub) == 0: continue
178
- yt, yp = sub["count"].values, sub["prediction_XGB"].values
179
- rows.append({
180
- "Periode": label,
181
- "Nb_jours": len(sub),
182
- "Vol_reel": round(yt.mean(), 1),
183
- "Vol_pred": round(yp.mean(), 1),
184
- "Surprediction_%": round(((yp.mean() - yt.mean()) / max(yt.mean(), 1)) * 100, 1),
185
- "Ecart_Absolu": round(ecart_absolu(yt, yp), 1),
186
- "Ecart_Relatif_%": round(ecart_relatif_pct(yt, yp), 1),
187
- })
188
-
189
- df_res = pd.DataFrame(rows)
190
-
191
- display_md("""
192
- ## 📊 EFFET VACANCES SCOLAIRES — RÉSULTATS AVANT CORRECTION
193
-
194
- **Procédure :**
195
- 1. Identification des jours de vacances scolaires par zone (A/B/C)
196
- 2. Comparaison volume réel d'appels vs prédiction XGBoost
197
- 3. Métriques :
198
- - **Ecart_Absolu** = erreur moyenne en nombre d'appels/jour
199
- - **Ecart_Relatif_%** = erreur moyenne relative (% du volume réel)
200
- 4. Correction = ajustement multiplicatif uniquement sur jours de vacances
201
- """)
202
-
203
- display_df(df_res, "📋 TABLEAU RÉCAPITULATIF")
204
 
205
- if len(df_res) >= 2:
206
- row_v = df_res[df_res["Periode"] == "Vacances scolaires"].iloc[0]
207
- row_h = df_res[df_res["Periode"] == "Hors vacances"].iloc[0]
208
- baisse = ((row_v["Vol_reel"] - row_h["Vol_reel"]) / max(row_h["Vol_reel"], 1)) * 100
209
-
210
- display_md(f"""
211
- ## 📈 INTERPRÉTATION MÉTIER
212
-
213
- → Pendant les vacances scolaires, le volume **baisse de {abs(baisse):.1f}%**
214
- - **{row_v['Vol_reel']:.0f}** appels/jour en vacances
215
- - **{row_h['Vol_reel']:.0f}** appels/jour hors vacances
216
-
217
- → Le modèle {'**sur-prédit**' if row_v['Surprediction_%'] > 0 else '**sous-prédit**'}
218
- de **{abs(row_v['Surprediction_%']):.1f}%** en période de vacances
219
- → Il ne capte pas complètement cette baisse
220
-
221
- → **Ecart_Absolu** = **{row_v['Ecart_Absolu']:.1f}** appels/jour en vacances
222
- (marge d'erreur de **{row_v['Ecart_Relatif_%']:.1f}%** du volume réel)
223
- """)
224
-
225
- return df_res
226
-
227
- # ════════════════════════════════════════════════════════════════════════════════
228
- # CELLULE 5 — Analyse par sous-type d'accueil
229
- # ════════════════════════════════════════════════════════════════════════════════
230
-
231
- def analyse_par_sous_type(df):
232
- dfp = df[(df["count"] > 0) & (df["prediction_XGB"].notna())].copy()
233
 
234
- rows = []
235
- for st in sorted(dfp["sous_type_accueil"].dropna().unique()):
236
- for periode_label, mask_base in [
237
- ("Vacances", dfp["is_vacances_zone"]),
238
- ("Hors_vacances", ~dfp["is_vacances_zone"])
239
- ]:
240
- mask = mask_base & (dfp["sous_type_accueil"] == st)
241
- if mask.sum() < 5: continue
242
- sub = dfp[mask]
243
- yt, yp = sub["count"].values, sub["prediction_XGB"].values
244
- rows.append({
245
- "Sous_type": st,
246
- "Periode": periode_label,
247
- "Nb_jours": len(sub),
248
- "Vol_reel": round(yt.mean(), 1),
249
- "Vol_pred": round(yp.mean(), 1),
250
- "Surprediction_%": round(((yp.mean() - yt.mean()) / max(yt.mean(), 1)) * 100, 1),
251
- "Ecart_Absolu": round(ecart_absolu(yt, yp), 1),
252
- "Ecart_Relatif_%": round(ecart_relatif_pct(yt, yp), 1),
253
- })
254
 
255
- df_st = pd.DataFrame(rows)
256
 
257
- display_md("## 📊 ANALYSE PAR SOUS-TYPE D'ACCUEIL")
 
 
258
 
259
- if len(df_st) > 0:
260
- display_df(df_st, "📋 Détail par sous-type")
261
-
262
- # Tableau markdown aussi
263
- display_md("### 📋 Synthèse par sous-type (compact)")
264
- md_lines = ["| Sous-type | Baisse vacances | Ecart Absolu (vac) | Ecart Relatif (vac) |"]
265
- md_lines.append("|---|---|---|---|")
266
- for st in sorted(df_st["Sous_type"].unique()):
267
- sub = df_st[df_st["Sous_type"] == st]
268
- vac = sub[sub["Periode"] == "Vacances"]
269
- hors = sub[sub["Periode"] == "Hors_vacances"]
270
- if len(vac) > 0 and len(hors) > 0:
271
- baisse = ((vac.iloc[0]["Vol_reel"] - hors.iloc[0]["Vol_reel"])
272
- / max(hors.iloc[0]["Vol_reel"], 1)) * 100
273
- md_lines.append(
274
- f"| **{st}** | {baisse:+.1f}% | {vac.iloc[0]['Ecart_Absolu']:.1f} appels "
275
- f"| {vac.iloc[0]['Ecart_Relatif_%']:.1f}% |"
276
- )
277
- display_md("\n".join(md_lines))
278
- else:
279
- display_md("❌ Pas assez de données par sous-type.")
280
 
281
- return df_st
282
-
283
- # ════════════════════════════════════════════════════════════════════════════════
284
- # CELLULE 6 — Calcul facteurs + correction
285
- # ════════════════════════════════════════════════════════════════════════════════
286
-
287
- def calcule_facteurs(df):
288
- dfp = df[(df["count"] > 0) & (df["prediction_XGB"].notna())].copy()
289
- facteurs = {}
290
- m_v = dfp["is_vacances_zone"]
291
- if m_v.sum() > 0:
292
- facteurs[("GLOBAL", "ALL")] = dfp.loc[m_v, "count"].mean() / max(dfp.loc[m_v, "prediction_XGB"].mean(), 1)
293
- for zone in ["A", "B", "C"]:
294
- for st in dfp["sous_type_accueil"].dropna().unique():
295
- m = (dfp["zone_vacances"]==zone) & (dfp["sous_type_accueil"]==st) & dfp["is_vacances_zone"]
296
- if m.sum() < 3: continue
297
- f = dfp.loc[m, "count"].mean() / max(dfp.loc[m, "prediction_XGB"].mean(), 1)
298
- facteurs[(zone, st)] = f
299
- return facteurs
300
-
301
- def corrige_predictions(df, facteurs):
302
- df = df.copy()
303
- df["prediction_XGB_corrige"] = df["prediction_XGB"].astype(float)
304
- m_v = df["is_vacances_zone"]
305
- for zone in ["A", "B", "C"]:
306
- for st in df["sous_type_accueil"].dropna().unique():
307
- m = m_v & (df["zone_vacances"]==zone) & (df["sous_type_accueil"]==st)
308
- if not m.any(): continue
309
- f = facteurs.get((zone, st), facteurs.get(("GLOBAL","ALL"), 1.0))
310
- df.loc[m, "prediction_XGB_corrige"] = df.loc[m, "prediction_XGB"] * f
311
- return df
312
-
313
- # ════════════════════════════════════════════════════════════════════════════════
314
- # CELLULE 7 — Évaluation avant/après avec IPython.display
315
- # ════════════════════════════════════════════════════════════════════════════════
316
-
317
- def evalue_correction(df):
318
- dfp = df[(df["count"] > 0) & (df["prediction_XGB"].notna())].copy()
319
 
320
- rows = []
321
  for label, mask in [
322
- ("Toutes_periodes", pd.Series([True]*len(dfp), index=dfp.index)),
323
- ("Vacances", dfp["is_vacances_zone"]),
324
- ("Hors_vacances", ~dfp["is_vacances_zone"]),
325
  ]:
326
  if mask.sum() < 2: continue
327
- yt = dfp.loc[mask, "count"].values
328
- y_avant = dfp.loc[mask, "prediction_XGB"].values
329
- y_apres = dfp.loc[mask, "prediction_XGB_corrige"].values
330
-
331
- ea_avant = ecart_absolu(yt, y_avant)
332
- ea_apres = ecart_absolu(yt, y_apres)
333
- er_avant = ecart_relatif_pct(yt, y_avant)
334
- er_apres = ecart_relatif_pct(yt, y_apres)
335
- gain = ((ea_avant - ea_apres) / max(ea_avant, 1)) * 100
336
-
337
- rows.append({
338
- "Periode": label,
339
- "Nb_jours": mask.sum(),
340
- "Ecart_Absolu_avant": round(ea_avant, 2),
341
- "Ecart_Absolu_apres": round(ea_apres, 2),
342
- "Gain_Ecart_Absolu_%": round(gain, 1),
343
- "Ecart_Relatif_%_avant": round(er_avant, 1),
344
- "Ecart_Relatif_%_apres": round(er_apres, 1),
345
  })
346
 
347
- df_eval = pd.DataFrame(rows)
348
-
349
- display_md("## 📊 ÉVALUATION : AVANT vs APRÈS CORRECTION")
350
- display_df(df_eval, "📋 Résultats")
351
-
352
- vac_row = df_eval[df_eval["Periode"] == "Vacances"]
353
- if len(vac_row) > 0:
354
- gain_vac = vac_row.iloc[0]["Gain_Ecart_Absolu_%"]
355
- ea_av = vac_row.iloc[0]["Ecart_Absolu_avant"]
356
- ea_ap = vac_row.iloc[0]["Ecart_Absolu_apres"]
357
- display_md(f"""
358
- ## 📈 INTERPRÉTATION
359
-
360
- → Sur les jours de vacances :
361
- - **Ecart_Absolu** passe de **{ea_av:.2f}** → **{ea_ap:.2f}** appels/jour
362
- - **Gain de {gain_vac:.1f}%** sur la précision des prédictions en vacances
363
-
364
- → Hors vacances : **aucune modification**
365
- - La correction ne touche QUE les jours identifiés comme vacances
366
-
367
- → Le facteur correcteur est appliqué **sans re-entraîner** le modèle
368
- - Post-processing uniquement, aucun impact sur le modèle XGBoost
369
- """)
370
-
371
- return df_eval
372
-
373
- # ════════════════════════════════════════════════════════════════════════════════
374
- # CELLULE 8 — Graphes pour le manager
375
- # ════════════════════════════════════════════════════════════════════════════════
376
-
377
- def graphes_manager(df, dr_filtre=None, st_filtre=None):
378
- if not MATPLOTLIB_OK:
379
- display_md("❌ **matplotlib non installé.** `pip install matplotlib`")
380
- return
381
-
382
- dfp = df[(df["count"] > 0) & (df["prediction_XGB"].notna())].copy()
383
- if dr_filtre: dfp = dfp[dfp["DR"] == dr_filtre]
384
- if st_filtre: dfp = dfp[dfp["sous_type_accueil"] == st_filtre]
385
- dfp = dfp.sort_values("Date")
386
-
387
- if len(dfp) == 0:
388
- display_md("❌ **Pas de données pour ce filtre.**")
389
- return
390
-
391
- fig, axes = plt.subplots(3, 1, figsize=(14, 12))
392
- titre = f"DR={dr_filtre}, Type={st_filtre}" if (dr_filtre or st_filtre) else "Global"
393
-
394
- # --- GRAPHE 1 : Série temporelle ---
395
- ax1 = axes[0]
396
- ax1.plot(dfp["Date"], dfp["count"], label="Réel", color="black", linewidth=1.5)
397
- ax1.plot(dfp["Date"], dfp["prediction_XGB"], label="XGB avant", color="orange", alpha=0.8, linewidth=1)
398
- if "prediction_XGB_corrige" in dfp.columns:
399
- ax1.plot(dfp["Date"], dfp["prediction_XGB_corrige"], label="XGB corrigé", color="green", alpha=0.8, linewidth=1)
400
 
401
- vac = dfp[dfp["is_vacances_zone"]]
402
- if len(vac) > 0:
403
- for _, r in vac.iterrows():
404
- ax1.axvline(r["Date"], color="red", alpha=0.03)
 
405
 
406
- ax1.set_title(f"Volume d'appels — {titre}", fontsize=12, fontweight='bold')
407
- ax1.set_ylabel("Appels / jour")
408
- ax1.legend(loc="upper left")
409
- ax1.grid(True, alpha=0.3)
410
 
411
- # --- GRAPHE 2 : Boxplot par période ---
412
- ax2 = axes[1]
413
- data_box, labels_box, colors_box = [], [], []
414
- for periode in ["Toussaint", "Noel", "Hiver", "Printemps", "Ete", "Hors_vacances"]:
415
- mask = dfp["periode_vacances"] == periode
416
- if mask.sum() < 3: continue
417
- data_box.append(dfp.loc[mask, "count"].values)
418
- labels_box.append(periode)
419
- colors_box.append("lightcoral" if periode != "Hors_vacances" else "lightblue")
420
 
421
- bp = ax2.boxplot(data_box, labels=labels_box, patch_artist=True)
422
- for patch, color in zip(bp["boxes"], colors_box):
423
- patch.set_facecolor(color)
424
- ax2.set_title("Distribution des volumes par période", fontsize=12, fontweight='bold')
425
- ax2.set_ylabel("Appels / jour")
426
- ax2.grid(True, alpha=0.3, axis="y")
427
-
428
- # --- GRAPHE 3 : Erreur avant/après ---
429
- ax3 = axes[2]
430
- periodes, ea_avant, ea_apres = [], [], []
431
- for periode in ["Toussaint", "Noel", "Hiver", "Printemps", "Ete"]:
432
- mask = dfp["periode_vacances"] == periode
433
- if mask.sum() < 3: continue
434
  yt = dfp.loc[mask, "count"].values
435
- yp_av = dfp.loc[mask, "prediction_XGB"].values
436
- periodes.append(periode)
437
- ea_avant.append(ecart_absolu(yt, yp_av))
438
- if "prediction_XGB_corrige" in dfp.columns:
439
- yp_ap = dfp.loc[mask, "prediction_XGB_corrige"].values
440
- ea_apres.append(ecart_absolu(yt, yp_ap))
441
- else:
442
- ea_apres.append(ecart_absolu(yt, yp_av))
443
-
444
- x = np.arange(len(periodes))
445
- width = 0.35
446
- bars1 = ax3.bar(x - width/2, ea_avant, width, label="Avant correction", color="orange", alpha=0.8)
447
- bars2 = ax3.bar(x + width/2, ea_apres, width, label="Après correction", color="green", alpha=0.8)
448
- ax3.set_title("Ecart Absolu par période (avant vs après correction)", fontsize=12, fontweight='bold')
449
- ax3.set_ylabel("Ecart Absolu (appels/jour)")
450
- ax3.set_xticks(x)
451
- ax3.set_xticklabels(periodes)
452
- ax3.legend()
453
- ax3.grid(True, alpha=0.3, axis="y")
454
-
455
- # Valeurs sur barres
456
- for bar in bars1:
457
- height = bar.get_height()
458
- ax3.annotate(f'{height:.1f}', xy=(bar.get_x() + bar.get_width() / 2, height),
459
- xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=8)
460
- for bar in bars2:
461
- height = bar.get_height()
462
- ax3.annotate(f'{height:.1f}', xy=(bar.get_x() + bar.get_width() / 2, height),
463
- xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=8)
464
-
465
- plt.tight_layout()
466
- plt.show()
467
- display_md("💾 **Sauvegarde :** `plt.savefig('vacances_manager.png', dpi=150, bbox_inches='tight')`")
468
-
469
- # ════════════════════════════════════════════════════════════════════════════════
470
- # CELLULE 9 — Pipeline complet
471
- # ════════════════════════════════════════════════════════════════════════════════
472
-
473
- def pipeline_manager(df):
474
- display_md("""
475
- 🔵══════════════════════════════════════════════════════════════════════════════🔵
476
- ## ANALYSE VACANCES SCOLAIRES — RAPPORT MANAGER
477
- 🔵══════════════════════════════════════════════════════════════════════════════🔵
478
- """)
479
-
480
- df_global = analyse_globale(df)
481
- df_st = analyse_par_sous_type(df)
482
-
483
- if MATPLOTLIB_OK:
484
- display_md("### 📊 Génération des graphes...")
485
- graphes_manager(df)
486
 
487
- facteurs = calcule_facteurs(df)
488
- display_md(f"""
489
- ### 🔧 Facteur correcteur
490
- - **Global** = `{facteurs.get(('GLOBAL','ALL'), 1.0):.4f}`
491
- - Formule = Volume_reel_vacances / Volume_pred_vacances
492
- """)
493
 
494
- df = corrige_predictions(df, facteurs)
495
- df_eval = evalue_correction(df)
 
 
 
 
 
 
496
 
497
- return df, df_global, df_st, df_eval, facteurs
498
 
499
  # ════════════════════════════════════════════════════════════════════════════════
500
- # CELLULE 10 — Exécution
501
  # ════════════════════════════════════════════════════════════════════════════════
502
- # df = add_vacances(df)
503
- # df, global_res, st_res, eval_res, facteurs = pipeline_manager(df)
 
1
  # ╔══════════════════════════════════════════════════════════════════════════════╗
2
+ # ║ NOTEBOOK MANAGER Impact Vacances Scolaires sur Prédictions XGB
3
+ # ║ Version simple : avant/après correction à destination du manager
4
  # ╚══════════════════════════════════════════════════════════════════════════════╝
5
 
6
  # ════════════════════════════════════════════════════════════════════════════════
 
12
  import warnings
13
  warnings.filterwarnings("ignore")
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  # ════════════════════════════════════════════════════════════════════════════════
16
+ # CELLULE 2 — Calendrier vacances scolaires + mapping DR→zone
17
  # ════════════════════════════════════════════════════════════════════════════════
 
18
  VACANCES = {
19
  "2023-2024": {
20
  "A": [(date(2023,10,21),date(2023,11,5)), (date(2023,12,23),date(2024,1,7)),
 
51
  },
52
  "2026-2027": {
53
  "A": [(date(2026,10,17),date(2026,11,1)), (date(2026,12,19),date(2027,1,3)),
54
+ (date(2027,2,13),date(2027,2,28)), (date(2027,4,3),date(2027,4,18)),
55
  (date(2027,7,3),date(2027,8,31))],
56
  "B": [(date(2026,10,17),date(2026,11,1)), (date(2026,12,19),date(2027,1,3)),
57
+ (date(2027,2,20),date(2027,3,7)), (date(2027,4,10),date(2027,4,25)),
58
  (date(2027,7,3),date(2027,8,31))],
59
  "C": [(date(2026,10,17),date(2026,11,1)), (date(2026,12,19),date(2027,1,3)),
60
+ (date(2027,2,6),date(2027,2,21)), (date(2027,3,27),date(2027,4,11)),
61
  (date(2027,7,3),date(2027,8,31))],
62
  },
63
  }
64
 
65
  DR_TO_ZONE = {
66
+ "SIR": "A", "AUV": "A", "ALP": "A", "PCH": "A", "LIM": "A",
67
+ "AQN": "A", "PYL": "A", "BRG": "A", "AFC": "A",
68
+ "PIC": "B", "NPC": "B", "PAS": "B", "CAZ": "B", "CAR": "B",
69
+ "NOR": "B", "BRE": "B", "CEN": "B", "PDL": "B",
70
+ "LOR": "C", "MPS": "C", "LRO": "C", "NMP": "C",
71
+ "PAR": "C", "IFE": "C", "IFO": "C",
 
 
72
  }
73
 
74
  def get_zone(dr): return DR_TO_ZONE.get(dr, "C")
75
+ def get_annee_scolaire(d): return f"{d.year}-{d.year+1}" if d.month >= 9 else f"{d.year-1}-{d.year}"
76
 
77
  def is_vacances(d, zone, vac):
78
  for debut, fin in vac.get(zone, []):
79
  if debut <= d <= fin: return True
80
  return False
81
 
 
 
 
82
  def get_periode_vacances(d, vac):
83
  for zone in ["A","B","C"]:
84
  for debut, fin in vac.get(zone, []):
 
97
  df["zone_vacances"] = df["DR"].apply(get_zone)
98
  df["annee_scolaire"] = df["Date"].apply(lambda d: get_annee_scolaire(d.date()))
99
  def _vac(row):
100
+ return is_vacances(row["Date"].date(), row["zone_vacances"], VACANCES.get(row["annee_scolaire"], {}))
 
101
  def _per(row):
102
+ return get_periode_vacances(row["Date"].date(), VACANCES.get(row["annee_scolaire"], {}))
 
103
  df["is_vacances_zone"] = df.apply(_vac, axis=1)
104
  df["periode_vacances"] = df.apply(_per, axis=1)
105
  return df
106
 
107
  # ════════════════════════════════════════════════════════════════════════════════
108
+ # CELLULE 3 — Rapport simple pour le manager
 
 
 
 
 
 
 
 
 
 
 
109
  # ════════════════════════════════════════════════════════════════════════════════
110
 
111
+ def rapport_manager(df):
112
+ """
113
+ Rapport simple : montre la baisse des prédictions après post-processing.
114
+ """
115
+ df = add_vacances(df)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  dfp = df[(df["count"] > 0) & (df["prediction_XGB"].notna())].copy()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
+ if len(dfp) == 0:
119
+ print("❌ Aucune donnée passée avec prédiction valide.")
120
+ return df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
+ m_v = dfp["is_vacances_zone"]
123
+ if m_v.sum() == 0:
124
+ print("❌ Aucun jour de vacances trouvé.")
125
+ return df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
+ facteur_global = dfp.loc[m_v, "count"].mean() / max(dfp.loc[m_v, "prediction_XGB"].mean(), 1)
128
 
129
+ df["prediction_XGB_corrige"] = df["prediction_XGB"].astype(float)
130
+ m_v_all = df["is_vacances_zone"] & df["prediction_XGB"].notna()
131
+ df.loc[m_v_all, "prediction_XGB_corrige"] = df.loc[m_v_all, "prediction_XGB"] * facteur_global
132
 
133
+ print("=" * 65)
134
+ print("📊 IMPACT POST-PROCESSING VACANCES SCOLAIRES")
135
+ print("=" * 65)
136
+ print(f"\n📅 Données analysées : {len(dfp):,} jours passés")
137
+ print(f"🏖️ Jours en vacances : {m_v.sum():,}")
138
+ print(f"📚 Jours hors vacances : {(~m_v).sum():,}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
+ print(f"\n{'─'*65}")
141
+ print("📉 VOLUMES MOYENS PRÉDITS — AVANT vs APRÈS CORRECTION")
142
+ print(f"{'─'*65}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
+ res = []
145
  for label, mask in [
146
+ ("Vacances scolaires", dfp["is_vacances_zone"]),
147
+ ("Hors vacances", ~dfp["is_vacances_zone"]),
 
148
  ]:
149
  if mask.sum() < 2: continue
150
+ sub = dfp[mask]
151
+ avant = sub["prediction_XGB"].mean()
152
+ apres = sub["prediction_XGB_corrige"].mean()
153
+ baisse = ((apres - avant) / max(avant, 1)) * 100
154
+ res.append({
155
+ "Période": label,
156
+ "n jours": int(mask.sum()),
157
+ "Avant correction": round(avant, 1),
158
+ "Après correction": round(apres, 1),
159
+ "Différence": f"{baisse:+.1f}%"
 
 
 
 
 
 
 
 
160
  })
161
 
162
+ print(pd.DataFrame(res).to_string(index=False))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
+ if m_v.sum() > 0 and (~m_v).sum() > 0:
165
+ baisse_reelle = ((dfp.loc[m_v, "count"].mean() - dfp.loc[~m_v, "count"].mean())
166
+ / max(dfp.loc[~m_v, "count"].mean(), 1)) * 100
167
+ print(f"\n🔴 Baisse RÉELLE des volumes en vacances : {baisse_reelle:.1f}%")
168
+ print(f"💡 Correction appliquée : facteur ×{facteur_global:.4f} (baisse de {(1-facteur_global)*100:.1f}%)")
169
 
170
+ def mae(y_true, y_pred):
171
+ return np.mean(np.abs(np.asarray(y_true) - np.asarray(y_pred)))
 
 
172
 
173
+ print(f"\n{'─'*65}")
174
+ print("📈 PRÉCISION (MAE) — AVANT vs APRÈS")
175
+ print(f"{'─'*65}")
 
 
 
 
 
 
176
 
177
+ res_mae = []
178
+ for label, mask in [
179
+ ("Toutes périodes", pd.Series([True]*len(dfp), index=dfp.index)),
180
+ ("Vacances scolaires", dfp["is_vacances_zone"]),
181
+ ("Hors vacances", ~dfp["is_vacances_zone"]),
182
+ ]:
183
+ if mask.sum() < 2: continue
 
 
 
 
 
 
184
  yt = dfp.loc[mask, "count"].values
185
+ mae_avant = mae(yt, dfp.loc[mask, "prediction_XGB"].values)
186
+ mae_apres = mae(yt, dfp.loc[mask, "prediction_XGB_corrige"].values)
187
+ gain = ((mae_avant - mae_apres) / max(mae_avant, 1)) * 100
188
+ res_mae.append({
189
+ "Période": label,
190
+ "MAE avant": round(mae_avant, 2),
191
+ "MAE après": round(mae_apres, 2),
192
+ "Gain": f"{gain:+.1f}%"
193
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
+ print(pd.DataFrame(res_mae).to_string(index=False))
 
 
 
 
 
196
 
197
+ print(f"\n{'='*65}")
198
+ print("✅ RÉSUMÉ")
199
+ print(f"{'='*65}")
200
+ print(f" • Facteur correcteur : ×{facteur_global:.4f}")
201
+ print(f" • Appliqué sur : {m_v_all.sum():,} jours en vacances (passés + futurs)")
202
+ print(f" • Hors vacances : inchangé")
203
+ print(f" • Impact : les prédictions en vacances sont corrigées à la baisse")
204
+ print(f" pour refléter la baisse réelle observée sur le passé.")
205
 
206
+ return df
207
 
208
  # ════════════════════════════════════════════════════════════════════════════════
209
+ # CELLULE 4 — Exécution
210
  # ════════════════════════════════════════════════════════════════════════════════
211
+ # df = rapport_manager(df)