spb/qc-election
Public
Python 66.6%
HTML 24.8%
CSS 4.9%
JavaScript 3.6%
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 & calibration — QC Élection Forecast</title>14<link rel="icon" type="image/svg+xml" href="/assets/img/favicon.svg">15<link rel="icon" type="image/png" sizes="64x64" href="/assets/img/favicon-64.png">16<link rel="apple-touch-icon" href="/assets/img/apple-touch-icon.png">17<meta property="og:site_name" content="QC Élection Forecast">18<meta property="og:type" content="website">19<meta property="og:title" content="Backtesting & calibration — QC Élection Forecast">20<meta property="og:description" content="Prévision probabiliste de l'élection générale québécoise du 5 octobre 2026 : 127 circonscriptions, modèle bayésien calibré, sources traçables.">21<meta property="og:image" content="https://www.qc-election.com/assets/img/og.png">22<meta property="og:image:width" content="1200">23<meta property="og:image:height" content="630">24<meta property="og:locale" content="fr_CA">25<meta name="twitter:card" content="summary_large_image">26<meta name="twitter:title" content="Backtesting & calibration — QC Élection Forecast">27<meta name="twitter:image" content="https://www.qc-election.com/assets/img/og.png">28<meta name="theme-color" content="#0a0d14">29<link rel="stylesheet" href="/assets/style.css">30<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>31</head>32<body>33<header class="site"></header>34<div class="wrap">35 <div class="hero">36 <h1>Backtesting et calibration</h1>37 <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>38 </div>3940 <div class="grid two">41 <div class="card">42 <h3>Calibration des probabilités de victoire locales</h3>43 <div class="chart" id="calib"></div>44 <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>45 </div>46 <div class="card">47 <h3>Erreur du vote national selon l'horizon</h3>48 <div class="chart" id="mae"></div>49 <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>50 </div>51 </div>5253 <h2 class="section">Détail par horizon</h2>54 <div class="card" style="overflow-x:auto"><table class="data" id="detail"></table></div>55 <p class="section-sub" id="bt-note" style="margin-top:10px"></p>56</div>57<footer class="site"></footer>5859<script src="/assets/app.js"></script>60<script>61(async () => {62 await initShell("/backtest");63 let rep;64 try { rep = await getJSON(`${API}/backtest`); }65 catch (e) {66 document.getElementById("detail").innerHTML =67 "<tr><td>Backtest non encore exécuté — lancez-le depuis l'administration.</td></tr>";68 return;69 }70 document.getElementById("bt-note").textContent = rep.note;7172 const cc = baseChart(document.getElementById("calib"));73 cc.setOption({74 grid: { left: 56, right: 20, top: 36, bottom: 44 },75 tooltip: { ...TOOLTIP, trigger: "item",76 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}` : "" },77 xAxis: { type: "value", min: 0, max: 1, name: "probabilité prédite", nameLocation: "middle",78 nameGap: 28, nameTextStyle: { color: MUTED }, ...AXIS_STYLE,79 axisLabel: { ...AXIS_STYLE.axisLabel, formatter: v => (v * 100) + " %" } },80 yAxis: { type: "value", min: 0, max: 1, name: "fréquence observée",81 nameTextStyle: { color: MUTED }, ...AXIS_STYLE,82 axisLabel: { ...AXIS_STYLE.axisLabel, formatter: v => (v * 100) + " %" } },83 series: [84 { type: "line", data: [[0, 0], [1, 1]], symbol: "none", silent: true,85 lineStyle: { color: GRID_COLOR, type: "dashed", width: 1 } },86 { type: "scatter", color: "#8fa3c8",87 symbolSize: (value, params) => 8 + Math.min(14, Math.sqrt(params.data.n) / 4),88 data: rep.calibration.map(b => ({ value: [b.predicted, b.observed], bin: b.bin, n: b.n })) },89 ],90 });9192 const hs = rep.horizons.filter(h => !h.error);93 const mc = baseChart(document.getElementById("mae"));94 mc.setOption({95 grid: { left: 44, right: 20, top: 34, bottom: 44 },96 tooltip: { ...TOOLTIP },97 xAxis: { type: "category", data: hs.map(h => "J−" + h.horizon), ...AXIS_STYLE,98 splitLine: { show: false }, name: "horizon avant le scrutin", nameLocation: "middle",99 nameGap: 30, nameTextStyle: { color: MUTED } },100 yAxis: { type: "value", ...AXIS_STYLE, name: "pp", nameTextStyle: { color: MUTED } },101 series: [{ name: "EAM vote", type: "bar", barWidth: 22, color: "#8fa3c8",102 itemStyle: { borderRadius: [4, 4, 0, 0] },103 label: { show: true, position: "top", color: INK2, fontSize: 11 },104 data: hs.map(h => h.national.mae_pp) }],105 });106107 document.getElementById("detail").innerHTML =108 `<tr><th>Horizon</th><th>Sondages</th><th>EAM vote (pp)</th><th>Couverture IC 95 %</th>109 <th>EAM sièges</th><th>Couverture sièges</th><th>Brier</th><th>Log loss</th>110 <th>Vainqueurs locaux corrects</th></tr>` +111 hs.map(h => `<tr>112 <td><b>J−${h.horizon}</b> <span style="color:var(--muted)">(${h.as_of})</span></td>113 <td>${h.n_polls}</td><td>${h.national.mae_pp}</td>114 <td>${fmtPct(h.national.coverage_95)}</td>115 <td>${h.seats.mae}</td><td>${fmtPct(h.seats.coverage_90)}</td>116 <td>${h.districts.brier}</td><td>${h.districts.log_loss}</td>117 <td>${fmtPct(h.districts.winner_accuracy)}</td></tr>`).join("");118})();119</script>120</body>121</html>122