SPB Git

spb/qc-election

Public
4.9 KB · 107 lines html
1<!DOCTYPE html>2<!--3  QC Élection Forecast — Plateforme de prévision électorale du Québec 20264  Auteur : Simon-Pierre Boucher5  Contact : contact@spboucher.ai6  https://www.qc-election.com7-->8<html lang="fr">9<head>10<script>document.documentElement.dataset.theme=localStorage.getItem("qce-theme")||"dark";</script>11<meta charset="utf-8">12<meta name="viewport" content="width=device-width, initial-scale=1">13<title>Backtesting &amp; calibration — QC Élection Forecast</title>14<link rel="stylesheet" href="/assets/style.css">15<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>16</head>17<body>18<header class="site"></header>19<div class="wrap">20  <div class="hero">21    <h1>Backtesting et calibration</h1>22    <p class="sub">Le modèle rejoué sur l'élection de 2022 avec uniquement l'information disponible à chaque date. La calibration prime sur la « bonne prédiction » ponctuelle.</p>23  </div>2425  <div class="grid two">26    <div class="card">27      <h3>Calibration des probabilités de victoire locales</h3>28      <div class="chart" id="calib"></div>29      <p class="chart-note">Chaque point : un bac de probabilité prédite (toutes circonscriptions × partis × horizons). Un modèle bien calibré suit la diagonale : parmi les cas annoncés à ~70 %, ~70 % se réalisent.</p>30    </div>31    <div class="card">32      <h3>Erreur du vote national selon l'horizon</h3>33      <div class="chart" id="mae"></div>34      <p class="chart-note">EAM (erreur absolue moyenne) des parts de vote prévues vs le résultat réel de 2022, en points de pourcentage.</p>35    </div>36  </div>3738  <h2 class="section">Détail par horizon</h2>39  <div class="card" style="overflow-x:auto"><table class="data" id="detail"></table></div>40  <p class="section-sub" id="bt-note" style="margin-top:10px"></p>41</div>42<footer class="site"></footer>4344<script src="/assets/app.js"></script>45<script>46(async () => {47  await initShell("/backtest");48  let rep;49  try { rep = await getJSON(`${API}/backtest`); }50  catch (e) {51    document.getElementById("detail").innerHTML =52      "<tr><td>Backtest non encore exécuté — lancez-le depuis l'administration.</td></tr>";53    return;54  }55  document.getElementById("bt-note").textContent = rep.note;5657  const cc = baseChart(document.getElementById("calib"));58  cc.setOption({59    grid: { left: 56, right: 20, top: 36, bottom: 44 },60    tooltip: { ...TOOLTIP, trigger: "item",61      formatter: pt => pt.data.bin ? `${pt.data.bin}<br>prédit ${fmtPct(pt.value[0])} · observé ${fmtPct(pt.value[1])}<br>n=${pt.data.n}` : "" },62    xAxis: { type: "value", min: 0, max: 1, name: "probabilité prédite", nameLocation: "middle",63      nameGap: 28, nameTextStyle: { color: MUTED }, ...AXIS_STYLE,64      axisLabel: { ...AXIS_STYLE.axisLabel, formatter: v => (v * 100) + " %" } },65    yAxis: { type: "value", min: 0, max: 1, name: "fréquence observée",66      nameTextStyle: { color: MUTED }, ...AXIS_STYLE,67      axisLabel: { ...AXIS_STYLE.axisLabel, formatter: v => (v * 100) + " %" } },68    series: [69      { type: "line", data: [[0, 0], [1, 1]], symbol: "none", silent: true,70        lineStyle: { color: GRID_COLOR, type: "dashed", width: 1 } },71      { type: "scatter", color: "#8fa3c8",72        symbolSize: (value, params) => 8 + Math.min(14, Math.sqrt(params.data.n) / 4),73        data: rep.calibration.map(b => ({ value: [b.predicted, b.observed], bin: b.bin, n: b.n })) },74    ],75  });7677  const hs = rep.horizons.filter(h => !h.error);78  const mc = baseChart(document.getElementById("mae"));79  mc.setOption({80    grid: { left: 44, right: 20, top: 20, bottom: 44 },81    tooltip: { ...TOOLTIP },82    xAxis: { type: "category", data: hs.map(h => "J−" + h.horizon), ...AXIS_STYLE,83      splitLine: { show: false }, name: "horizon avant le scrutin", nameLocation: "middle",84      nameGap: 30, nameTextStyle: { color: MUTED } },85    yAxis: { type: "value", ...AXIS_STYLE, name: "pp", nameTextStyle: { color: MUTED } },86    series: [{ name: "EAM vote", type: "bar", barWidth: 22, color: "#8fa3c8",87      itemStyle: { borderRadius: [4, 4, 0, 0] },88      label: { show: true, position: "top", color: INK2, fontSize: 11 },89      data: hs.map(h => h.national.mae_pp) }],90  });9192  document.getElementById("detail").innerHTML =93    `<tr><th>Horizon</th><th>Sondages</th><th>EAM vote (pp)</th><th>Couverture IC 95 %</th>94     <th>EAM sièges</th><th>Couverture sièges</th><th>Brier</th><th>Log loss</th>95     <th>Vainqueurs locaux corrects</th></tr>` +96    hs.map(h => `<tr>97      <td><b>J−${h.horizon}</b> <span style="color:var(--muted)">(${h.as_of})</span></td>98      <td>${h.n_polls}</td><td>${h.national.mae_pp}</td>99      <td>${fmtPct(h.national.coverage_95)}</td>100      <td>${h.seats.mae}</td><td>${fmtPct(h.seats.coverage_90)}</td>101      <td>${h.districts.brier}</td><td>${h.districts.log_loss}</td>102      <td>${fmtPct(h.districts.winner_accuracy)}</td></tr>`).join("");103})();104</script>105</body>106</html>107
Files · 37f4ccc0b3eb0e39b9cfda7f484dd925993656ce
Menu

Theme