spb/qc-election
Public
Python 66.6%
HTML 24.8%
CSS 4.9%
JavaScript 3.6%
1# QC Élection Forecast — Plateforme de prévision électorale du Québec 20262# Auteur : Simon-Pierre Boucher3# Contact : contact@spboucher.ai4# https://www.qc-election.com5"""Transformations compositionnelles (parts de vote ↔ espace log-ratio).67Les intentions de vote sont compositionnelles (somme = 100 %). On modélise8l'état latent en espace « additive log-ratio » (alr) : y_k = log(p_k / p_ref),9ce qui garantit des parts positives sommant à 1 après transformation inverse10et induit la corrélation négative naturelle entre partis.11"""12from __future__ import annotations1314import numpy as np1516FLOOR = 0.0025 # plancher de part (0,25 %) pour stabilité numérique171819def close(p: np.ndarray) -> np.ndarray:20 """Renormalise en composition stricte (somme 1), avec plancher."""21 p = np.asarray(p, dtype=float)22 p = np.clip(p, FLOOR, None)23 return p / p.sum(axis=-1, keepdims=True)242526def alr(p: np.ndarray, ref: int = 0) -> np.ndarray:27 """shares (…, K) → log-ratios (…, K-1), référence = composante `ref`."""28 p = close(p)29 others = [i for i in range(p.shape[-1]) if i != ref]30 return np.log(p[..., others]) - np.log(p[..., ref:ref + 1])313233def inv_alr(y: np.ndarray, ref: int = 0) -> np.ndarray:34 """log-ratios (…, K-1) → shares (…, K) via softmax."""35 y = np.asarray(y, dtype=float)36 K = y.shape[-1] + 137 full = np.zeros(y.shape[:-1] + (K,), dtype=float)38 others = [i for i in range(K) if i != ref]39 full[..., others] = y40 full -= full.max(axis=-1, keepdims=True)41 e = np.exp(full)42 return e / e.sum(axis=-1, keepdims=True)434445def alr_obs_cov(p: np.ndarray, n_eff: float, excess_pp: float = 0.0,46 ref: int = 0) -> np.ndarray:47 """Covariance d'observation en espace alr (méthode delta sur la multinomiale).4849 Cov(y_i, y_j) ≈ (1/n)·(δ_ij / p_i + 1 / p_ref) pour i,j ≠ ref,50 plus une variance excédentaire (erreur non-échantillonnale) diagonale.51 """52 p = close(p)53 others = [i for i in range(p.shape[-1]) if i != ref]54 po = p[others]55 pr = float(p[ref])56 n_eff = max(float(n_eff), 50.0)57 cov = (np.diag(1.0 / po) + 1.0 / pr) / n_eff58 if excess_pp > 0:59 e = excess_pp / 100.060 cov = cov + np.diag((e / po) ** 2 + (e / pr) ** 2)61 return cov62