SPB Git forge

spb/qc-election

Public
20commits 1branches 0releases
4.9 MBsize
maindefault branch
20 days agolast push
Python 66.6% HTML 24.8% CSS 4.9% JavaScript 3.6%
2.2 KB · 62 lines python
Raw Blame History
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