stephmnt commited on
Commit
047370a
·
verified ·
1 Parent(s): 2dd3322

Sync from GitHub Actions

Browse files
Files changed (1) hide show
  1. app/gradio_app.py +123 -3
app/gradio_app.py CHANGED
@@ -254,6 +254,56 @@ def blend_with_type_history(
254
  return blended
255
 
256
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  def apply_transfers(
258
  counts: Dict[str, int],
259
  total_inscrits: int,
@@ -1114,6 +1164,10 @@ class PredictorBackend:
1114
  target_type: str,
1115
  target_year: int,
1116
  inscrits_override: float | None = None,
 
 
 
 
1117
  ) -> Tuple[Dict[str, object] | None, str, str]:
1118
  feature_df, _ = self._get_features_and_refs(target_type, target_year)
1119
  if feature_df.empty:
@@ -1136,6 +1190,7 @@ class PredictorBackend:
1136
  preds_by_cat = {cat: float(preds_share[idx]) for idx, cat in enumerate(CANDIDATE_CATEGORIES)}
1137
  preds_by_cat = blend_with_type_history(preds_by_cat, row.iloc[0], target_type)
1138
  ordered = ordered_categories()
 
1139
  share_vec = np.array([preds_by_cat.get(cat, 0.0) for cat in ordered], dtype=float)
1140
 
1141
  stats = self.event_stats[self.event_stats["code_bv"] == code_bv].sort_values("date_scrutin")
@@ -1207,6 +1262,15 @@ class PredictorBackend:
1207
  turnout_rate = pick_rate("turnout_pct")
1208
  blancs_rate = pick_rate("blancs_pct")
1209
  nuls_rate = pick_rate("nuls_pct")
 
 
 
 
 
 
 
 
 
1210
  if blancs_rate + nuls_rate > turnout_rate and (blancs_rate + nuls_rate) > 0:
1211
  scale = turnout_rate / (blancs_rate + nuls_rate)
1212
  blancs_rate *= scale
@@ -1259,12 +1323,20 @@ class PredictorBackend:
1259
  target_type: str,
1260
  target_year: int,
1261
  inscrits_override: float | None = None,
 
 
 
 
1262
  ) -> Tuple[pd.DataFrame, str, str]:
1263
  details, backend_label, meta = self.predict_bureau_details(
1264
  code_bv,
1265
  target_type,
1266
  target_year,
1267
  inscrits_override,
 
 
 
 
1268
  )
1269
  if details is None:
1270
  return pd.DataFrame(), backend_label, ""
@@ -1359,6 +1431,29 @@ def create_interface() -> gr.Blocks:
1359
  bureau_dd = gr.Dropdown(choices=bureau_labels, value=default_bv, label="Bureau de vote")
1360
  target_dd = gr.Dropdown(choices=target_labels, value=default_target, label="Élection cible (type année)")
1361
  inscrits_in = gr.Number(value=None, label="Inscrits (optionnel)", precision=0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1362
  predict_btn = gr.Button("Prédire")
1363
  source_box = gr.Markdown(value=f"Source des données : {backend_label}")
1364
  output_df = gr.Dataframe(
@@ -1454,7 +1549,15 @@ def create_interface() -> gr.Blocks:
1454
  sim_chart = gr.Plot()
1455
  opportunity_df = gr.Dataframe(headers=OPPORTUNITY_OUTPUT_COLUMNS, label="Bureaux à potentiel (trié)")
1456
 
1457
- def _predict(bv_label: str, target_label: str, inscrits_override: float | None):
 
 
 
 
 
 
 
 
1458
  if not bv_label or not target_label:
1459
  return pd.DataFrame(), "Entrée invalide", None
1460
  code_bv = bureau_map.get(bv_label)
@@ -1465,7 +1568,22 @@ def create_interface() -> gr.Blocks:
1465
  target_type, target_year = parts[0].lower(), int(parts[1])
1466
  except Exception:
1467
  target_type, target_year = "municipales", 2026
1468
- df, backend_label, meta = backend.predict_bureau(code_bv, target_type, target_year, inscrits_override)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1469
  plot = build_bar_chart(
1470
  df,
1471
  value_col="nombre",
@@ -1650,7 +1768,9 @@ def create_interface() -> gr.Blocks:
1650
  opp_df = opp_df.sort_values(["bascule", "gain_cible"], ascending=[False, False])
1651
  return sim_table, sim_plot, opp_df
1652
 
1653
- predict_btn.click(_predict, inputs=[bureau_dd, target_dd, inscrits_in], outputs=[output_df, source_box, chart])
 
 
1654
  history_btn.click(
1655
  _history,
1656
  inputs=[history_bureau_dd, history_election_dd],
 
254
  return blended
255
 
256
 
257
+ def _normalize_override_pct(value: float | None) -> float | None:
258
+ if value is None:
259
+ return None
260
+ try:
261
+ val = float(value)
262
+ except (TypeError, ValueError):
263
+ return None
264
+ if np.isnan(val):
265
+ return None
266
+ return float(np.clip(val, 0.0, 100.0))
267
+
268
+
269
+ def apply_share_overrides(
270
+ preds_by_cat: Dict[str, float],
271
+ overrides_pct: Dict[str, float] | None,
272
+ ordered: list[str],
273
+ ) -> Dict[str, float]:
274
+ if not overrides_pct:
275
+ return preds_by_cat
276
+ fixed = {}
277
+ for cat, pct in overrides_pct.items():
278
+ if cat not in ordered:
279
+ continue
280
+ norm = _normalize_override_pct(pct)
281
+ if norm is None:
282
+ continue
283
+ fixed[cat] = norm / 100.0
284
+ if not fixed:
285
+ return preds_by_cat
286
+ fixed_sum = sum(fixed.values())
287
+ if fixed_sum >= 1.0:
288
+ scaled = {cat: (val / fixed_sum) for cat, val in fixed.items() if fixed_sum > 0}
289
+ return {cat: float(scaled.get(cat, 0.0)) for cat in ordered}
290
+ remaining = 1.0 - fixed_sum
291
+ residual_cats = [cat for cat in ordered if cat not in fixed]
292
+ base_sum = sum(float(preds_by_cat.get(cat, 0.0)) for cat in residual_cats)
293
+ if base_sum <= 0 and residual_cats:
294
+ per_cat = remaining / len(residual_cats)
295
+ base_alloc = {cat: per_cat for cat in residual_cats}
296
+ else:
297
+ base_alloc = {
298
+ cat: (float(preds_by_cat.get(cat, 0.0)) / base_sum) * remaining
299
+ for cat in residual_cats
300
+ }
301
+ merged = {cat: float(base_alloc.get(cat, 0.0)) for cat in ordered}
302
+ for cat, val in fixed.items():
303
+ merged[cat] = float(val)
304
+ return merged
305
+
306
+
307
  def apply_transfers(
308
  counts: Dict[str, int],
309
  total_inscrits: int,
 
1164
  target_type: str,
1165
  target_year: int,
1166
  inscrits_override: float | None = None,
1167
+ share_overrides: Dict[str, float] | None = None,
1168
+ abstention_override_pct: float | None = None,
1169
+ blancs_override_pct: float | None = None,
1170
+ nuls_override_pct: float | None = None,
1171
  ) -> Tuple[Dict[str, object] | None, str, str]:
1172
  feature_df, _ = self._get_features_and_refs(target_type, target_year)
1173
  if feature_df.empty:
 
1190
  preds_by_cat = {cat: float(preds_share[idx]) for idx, cat in enumerate(CANDIDATE_CATEGORIES)}
1191
  preds_by_cat = blend_with_type_history(preds_by_cat, row.iloc[0], target_type)
1192
  ordered = ordered_categories()
1193
+ preds_by_cat = apply_share_overrides(preds_by_cat, share_overrides, ordered)
1194
  share_vec = np.array([preds_by_cat.get(cat, 0.0) for cat in ordered], dtype=float)
1195
 
1196
  stats = self.event_stats[self.event_stats["code_bv"] == code_bv].sort_values("date_scrutin")
 
1262
  turnout_rate = pick_rate("turnout_pct")
1263
  blancs_rate = pick_rate("blancs_pct")
1264
  nuls_rate = pick_rate("nuls_pct")
1265
+ abstention_override = _normalize_override_pct(abstention_override_pct)
1266
+ if abstention_override is not None:
1267
+ turnout_rate = float(np.clip(1.0 - (abstention_override / 100.0), 0.0, 1.0))
1268
+ blancs_override = _normalize_override_pct(blancs_override_pct)
1269
+ if blancs_override is not None:
1270
+ blancs_rate = float(blancs_override / 100.0)
1271
+ nuls_override = _normalize_override_pct(nuls_override_pct)
1272
+ if nuls_override is not None:
1273
+ nuls_rate = float(nuls_override / 100.0)
1274
  if blancs_rate + nuls_rate > turnout_rate and (blancs_rate + nuls_rate) > 0:
1275
  scale = turnout_rate / (blancs_rate + nuls_rate)
1276
  blancs_rate *= scale
 
1323
  target_type: str,
1324
  target_year: int,
1325
  inscrits_override: float | None = None,
1326
+ share_overrides: Dict[str, float] | None = None,
1327
+ abstention_override_pct: float | None = None,
1328
+ blancs_override_pct: float | None = None,
1329
+ nuls_override_pct: float | None = None,
1330
  ) -> Tuple[pd.DataFrame, str, str]:
1331
  details, backend_label, meta = self.predict_bureau_details(
1332
  code_bv,
1333
  target_type,
1334
  target_year,
1335
  inscrits_override,
1336
+ share_overrides,
1337
+ abstention_override_pct,
1338
+ blancs_override_pct,
1339
+ nuls_override_pct,
1340
  )
1341
  if details is None:
1342
  return pd.DataFrame(), backend_label, ""
 
1431
  bureau_dd = gr.Dropdown(choices=bureau_labels, value=default_bv, label="Bureau de vote")
1432
  target_dd = gr.Dropdown(choices=target_labels, value=default_target, label="Élection cible (type année)")
1433
  inscrits_in = gr.Number(value=None, label="Inscrits (optionnel)", precision=0)
1434
+ override_inputs: Dict[str, gr.Number] = {}
1435
+ with gr.Accordion("Imputation manuelle (optionnel)", open=False):
1436
+ gr.Markdown("Abstention / blancs / nuls en % des inscrits.")
1437
+ with gr.Row():
1438
+ abstention_in = gr.Number(value=40, label="Abstention (% inscrits)", precision=1)
1439
+ blancs_in = gr.Number(value=None, label="Blancs (% inscrits)", precision=1)
1440
+ nuls_in = gr.Number(value=None, label="Nuls (% inscrits)", precision=1)
1441
+ gr.Markdown("Nuances politiques en % des exprimés (laisser vide pour garder le modèle).")
1442
+ cats = ordered_categories()
1443
+ with gr.Row():
1444
+ for cat in cats[:4]:
1445
+ override_inputs[cat] = gr.Number(
1446
+ value=None,
1447
+ label=DISPLAY_CATEGORY_LABELS.get(cat, cat),
1448
+ precision=1,
1449
+ )
1450
+ with gr.Row():
1451
+ for cat in cats[4:]:
1452
+ override_inputs[cat] = gr.Number(
1453
+ value=None,
1454
+ label=DISPLAY_CATEGORY_LABELS.get(cat, cat),
1455
+ precision=1,
1456
+ )
1457
  predict_btn = gr.Button("Prédire")
1458
  source_box = gr.Markdown(value=f"Source des données : {backend_label}")
1459
  output_df = gr.Dataframe(
 
1549
  sim_chart = gr.Plot()
1550
  opportunity_df = gr.Dataframe(headers=OPPORTUNITY_OUTPUT_COLUMNS, label="Bureaux à potentiel (trié)")
1551
 
1552
+ def _predict(
1553
+ bv_label: str,
1554
+ target_label: str,
1555
+ inscrits_override: float | None,
1556
+ abstention_override: float | None,
1557
+ blancs_override: float | None,
1558
+ nuls_override: float | None,
1559
+ *cat_overrides: float,
1560
+ ):
1561
  if not bv_label or not target_label:
1562
  return pd.DataFrame(), "Entrée invalide", None
1563
  code_bv = bureau_map.get(bv_label)
 
1568
  target_type, target_year = parts[0].lower(), int(parts[1])
1569
  except Exception:
1570
  target_type, target_year = "municipales", 2026
1571
+ share_overrides: Dict[str, float] = {}
1572
+ for cat, value in zip(ordered_categories(), cat_overrides):
1573
+ norm = _normalize_override_pct(value)
1574
+ if norm is None:
1575
+ continue
1576
+ share_overrides[cat] = norm
1577
+ df, backend_label, meta = backend.predict_bureau(
1578
+ code_bv,
1579
+ target_type,
1580
+ target_year,
1581
+ inscrits_override,
1582
+ share_overrides=share_overrides if share_overrides else None,
1583
+ abstention_override_pct=abstention_override,
1584
+ blancs_override_pct=blancs_override,
1585
+ nuls_override_pct=nuls_override,
1586
+ )
1587
  plot = build_bar_chart(
1588
  df,
1589
  value_col="nombre",
 
1768
  opp_df = opp_df.sort_values(["bascule", "gain_cible"], ascending=[False, False])
1769
  return sim_table, sim_plot, opp_df
1770
 
1771
+ predict_inputs = [bureau_dd, target_dd, inscrits_in, abstention_in, blancs_in, nuls_in]
1772
+ predict_inputs += [override_inputs[cat] for cat in ordered_categories() if cat in override_inputs]
1773
+ predict_btn.click(_predict, inputs=predict_inputs, outputs=[output_df, source_box, chart])
1774
  history_btn.click(
1775
  _history,
1776
  inputs=[history_bureau_dd, history_election_dd],