Site web (Flask, app.py) qui lit une base SQLite (ranking.db) calculée par build_db.py à partir des résultats CIVL. Il est hébergé sur Render (voir DEPLOY_RENDER.md).
Le site propose :
- un classement par mois (état des niveaux à la fin de chaque mois) ;
- une page par compétition qui montre, pilote par pilote, le calcul : rang réel, rang attendu, niveau avant et après, écart ;
- une page par pilote : rang mondial, victoires/top 10/place médiane, graphique de l'évolution du niveau compétition par compétition et tableau des résultats (tous les pilotes, sauf ceux retirés à leur demande) ;
- les pilotes inactifs (plus de 18 mois sans compétition) sortent du classement : ils n'ont pas de rang et ne prennent la place de personne ; si on décoche « Masquer les inactifs » (
?actifs=0), ils apparaissent grisés et sans numéro à la place que leur niveau leur donnerait ; ils redeviennent « provisoires » (K relevé) à leur retour.
Chaque compétition est un ensemble de duels : chaque pilote est comparé à tous les autres (1 = devant, 0,5 = ex æquo, 0 = derrière).
Pour un duel entre niveaux A et B, la probabilité que A gagne est 1 / (1 + 10^((B − A)/640)). Le niveau d'un pilote varie de
K × (score obtenu − score attendu) (moyenne sur ses duels). Battre des pilotes faibles rapporte peu ; battre des pilotes forts rapporte
beaucoup ; perdre contre des pilotes faibles coûte cher. K = 64 × (1 + 4·e^(−n/10)) : les nouveaux pilotes bougent vite.
Après une longue absence (> 18 mois), le nombre de compétitions « effectif » n est divisé par 2 tous les 18 mois, ce qui relève K.
C'est du calcul maison (une vingtaine de lignes dans pgrank/engine.py, vérifié par un test contre un calcul à la main) ; il ne nécessite pas OpenSkill.
Choix des réglages (K, boost) sur les mêmes données — part des duels bien prédits entre pilotes ayant ≥ 3 compétitions :
| Moteur | Duels bien prédits |
|---|---|
| OpenSkill BradleyTerryPart (fenêtre 10, τ 0,5) | 71,4 % |
| Elo K = 16 | 71,4 % |
| Elo K = 32, boost 2 | 72,9 % |
| Elo K = 64, boost 4 (retenu) | 74,3 % |
| Elo K = 128, boost 4 | 74,4 % (plus réactif, favorise trop les pilotes récents) |
L'échelle (640, soit 400 × 1,6 avec K = 64 × 1,6 : mêmes classements qu'en (400, 64) mais étirés pour que les meilleurs approchent 2800) est une simple unité : (échelle 300, K 48) donne exactement le même classement que (400, 64) ; seul le rapport K / échelle compte. La resserrer ne rend donc pas les favoris plus « sûrs » à elle seule ; avec K/échelle plus grand, le rang attendu de Lambert à Gemona passe de 22 à 19–20 sans gain de prévision (74,3 → 74,4 %). Les pages de compétition montrent, par pilote, la part de duels gagnés et la part attendue (rang attendu au survol).
L'ancien moteur OpenSkill reste dans pgrank/engine.py (Config(engine="openskill"), utilisé par les tests ; nécessite pip install "openskill>=6.1.1", Python ≥ 3.10).
Le moteur Elo ne calcule pas d'incertitude ; elle est ajoutée à l'affichage, sans changer le classement. Pour chaque pilote :
σ = √(115² + 215²/(n + 0,5) + 20² × t), avec n le nombre de compétitions « effectif » (celui qui sert à K) et t les mois depuis la
dernière compétition : ±210 après une compétition, ±133 après dix, jamais moins de ±115. Le rang probable est le rang qu'aurait le pilote
avec rating − σ et rating + σ, les autres restant en place (actifs seulement ; colonnes sd, rank_lo, rank_hi de monthly).
Calage par simulation : des pilotes fictifs au niveau connu (niveau de départ = rating final réel, puis marche aléatoire de 20 points par mois)
courent exactement les 2 241 compétitions de la base ; chaque résultat = niveau + bruit normal de 290 points. Ce réglage reproduit les
74,9 % de duels bien prédits des vraies données et l'écart type des variations de rating sur 12 mois des vétérans (44 points simulés,
46 réels). On mesure ensuite l'écart entre le rating calculé et le vrai niveau, et on ajuste la formule (erreur relative < 5 %).
Le plancher de ±115 vient du K élevé (102) : même un vétéran bouge beaucoup à chaque compétition. Réglages : elo_sd dans pgrank/config.py.
pip install -r requirements.txt
python build_db.py --db data/civl_data.db --out ranking.db # calcule et écrit la base (~1 min, ~500 Mo)
python app.py --db ranking.db # http://127.0.0.1:5000La CIVL publie un nouveau classement WPRS le 1er de chaque mois. Après cette date, une seule commande :
pip install -r requirements-wprs.txt # une fois (requests, openpyxl)
python update.py --dry-run # facultatif : liste ce qui manque, sans rien écrire
python update.py # importe, télécharge le WPRS du mois, recalcule ranking.db
git add data && git commit -m "Mise à jour des résultats" && git push # Render redéploie le siteupdate.py lit sur civlcomps la liste des compétitions du dernier classement publié (3 dernières années),
importe dans data/civl_data.db celles qui manquent, réimporte celles dont civlcomps a mis à jour les résultats
(colonne « Results Updated », mémorisée dans la table sync_state), télécharge le classement WPRS du mois dans
data/wprs/, puis relance build_db.py. Une page à la fois, 8 s entre deux pages (--pause) ; une page incomplète
(nombre de pilotes différent de celui annoncé) est refusée et reprise au passage suivant. Autres options :
--refetch ID… (réimporter des compétitions), --no-wprs, --no-build.
Automatique : le workflow GitHub .github/workflows/mise-a-jour.yml lance update.py le 2 de chaque mois à 5 h (UTC)
et ouvre une PR « Mise à jour des résultats (civlcomps) » avec les nouvelles données ; la fusionner met le site à jour.
On peut aussi le lancer à la main (onglet Actions → « Mise à jour mensuelle » → Run workflow). Il faut autoriser une fois
Settings → Actions → General → Allow GitHub Actions to create and approve pull requests. Si une compétition n'a pas pu
être importée, la PR est quand même ouverte avec le reste et le workflow finit en échec (relancer plus tard).
Tout est recalculé depuis zéro (déterministe) : une compétition ajoutée avec retard se répercute
correctement sur les mois suivants. Les compétitions d'un mois apparaissent dans le classement publié le 1er du mois suivant,
comme pour le WPRS. Le genre des nouveaux pilotes absents de data/genders.csv est pris dans les classements WPRS téléchargés.
- Inactivité : au-delà de
inactive_after_months(18) sans compétition, le pilote sort du classement (pas de rang, grisé s'il est affiché) ; avant son prochain calcul, σ est relevé deidle_tau² par mois au-delà du seuil (plafonné au σ initial du modèle). - Sélection : compétitions de moins de
min_field(5) pilotes ignorées ; un pilote à 0 point est classé ex æquo dernier. - Nettoyage des données (
pgrank/load.py) : datesApr 01, 2010, libellé « Sport » collé aux noms (10 604 lignes), lignes sanscivlid(7) ignorées. Les pilotes sont identifiés parcivlid.
Réglages dans pgrank/config.py ; options de build_db.py : python build_db.py --help.
Constaté sur les données CIVL (plateaux de 50 à 150 pilotes) :
- σ reste proche de sa valeur initiale (médiane 8,31 pour 8,33 chez les pilotes à 30 compétitions ou plus) : la relance de σ après inactivité a donc très peu d'effet visible ;
- la mise à jour dépend surtout de la place d'arrivée, peu de l'écart avec le rang attendu (sur un plateau de 130, le 1ᵉʳ attendu 32ᵉ et le 6ᵉ attendu 92ᵉ gagnent tous deux +0,6) ; μ monte à chaque bon résultat, et le rating favorise les pilotes qui ont couru beaucoup de compétitions.
Autres modèles OpenSkill essayés avec le même moteur (Config(model=...)), sur les mêmes données :
| Modèle | Duels bien prédits | Entre les 30 meilleurs | σ médian (vétérans) | Échelle de μ (top 300) |
|---|---|---|---|---|
| PlackettLuce | 69,5 % | 63,8 % | 8,31 | 40 à 98 |
| BradleyTerryPart | 69,5 % | 65,8 % | 3,72 | 25 à 38 |
| ThurstoneMostellerPart | 68,9 % | 65,5 % | 5,80 | 25 à 44 |
| BradleyTerryFull | 65,2 % | 61,2 % | 0,56 | 191 à 560 |
| ThurstoneMostellerFull | 62,0 % | 58,5 % | 0,47 | diverge |
Les variantes Full sont à écarter (elles divergent sur de grands plateaux). BradleyTerryPart prédit aussi
bien, mieux entre élites, rend σ significatif (donc l'inactivité utile) et ne récompense plus le volume de
compétitions : c'est le défaut.
python tests/test_engine.py # moteur : inactivité, statuts mensuels, ex æquo, égalité avec des appels directs à openskill
python tests/test_wprs.py # comparaison WPRS : choix du classement du mois, pilotes sans points
python tests/test_update.py # mise à jour : lecture des pages civlcomps, pages incomplètes refuséesLes classements WPRS mensuels publiés par la CIVL sont dans data/wprs/ : un fichier AAAA-MM.csv.gz par mois
(classement mondial complet : rang, CIVL ID, nom, sexe, nation, points), de novembre 2023 à octobre 2026, et
rankings.csv (identifiant civlcomps de chaque mois publié depuis mars 2007).
pip install -r requirements-wprs.txt
python tools/fetch_wprs.py --since 2023-11 # télécharge les mois manquants (export Excel de civlcomps.org)
python tools/compare_wprs.py --since 2023-12 --until 2026-09 --csv comparaison.csvLe téléchargement fait une demande à la fois et attend 20 s entre deux mois pour ménager le site. Pour chaque épreuve, la comparaison prend le dernier classement publié au plus tard le jour du début de l'épreuve (au plus 45 jours avant), et le rating Elo juste avant l'épreuve ; un pilote absent du WPRS a 0 point. Résultat (458 épreuves, décembre 2023 à septembre 2026) :
| Duels | Elo | WPRS | Écart | Duels comptés | Épreuves où l'Elo fait mieux |
|---|---|---|---|---|---|
| Pilotes ≥ 3 épreuves | 76,1 % | 72,5 % | +3,6 pt | 1 067 485 | 365 / 458 |
| Tous | 74,8 % | 72,3 % | +2,5 pt | 1 490 006 | 321 / 458 |
| Classés WPRS et ≥ 3 épreuves | 76,1 % | 72,5 % | +3,5 pt | 1 040 168 | 360 / 457 |
| 30 meilleurs de l'épreuve (≥ 10 épreuves) | 67,4 % | 65,0 % | +2,4 pt | 132 862 | 269 / 444 |
La dernière ligne ne garde que les pilotes qui ont des points WPRS : l'écart ne vient pas des pilotes non classés. En moyenne, 13,6 % des pilotes d'une épreuve n'ont aucun point WPRS.
/exports/classement_<mois>.csv (classement d'un mois, ?sex=F pour le classement féminin) et
/exports/competition_<id>.csv (calcul d'une compétition : niveau avant et après, duels gagnés et attendus, rang attendu).
Testés sur les mêmes données (BradleyTerryPart) :
| Réglage | Duels bien prédits | Entre les 30 meilleurs | Compét. moyennes du top 20 |
|---|---|---|---|
| fenêtre 4, μ − 3σ (défaut d'OpenSkill) | 69,5 % | 65,8 % | 72 |
| fenêtre 10, μ − 3σ | 71,4 % | 66,6 % | 68 |
| fenêtre 10, μ − 2σ (retenu) | 71,4 % | 66,6 % | 60 |
| fenêtre 25, μ − 3σ | 72,5 % | 67,1 % | 68 (mais moins de victoires dans le top 20) |
La fenêtre agit sur la prévision (μ) ; z agit seulement sur l'affichage : plus z est grand, plus la baisse de σ (qui vient avec chaque compétition courue) pèse dans le rating, donc plus le volume est récompensé.
OpenSkill ajoute τ² à σ² avant chaque compétition. Avec τ = 25/300 (défaut), σ d'un vétéran se bloque vers 2,3 et la mise à jour devient minuscule : Andreas Malecki, 113ᵉ à Gemona (attendu 31ᵉ) puis 110ᵉ à l'Ikarus Open, ne perdait que 0,06 et 0,13 de μ. Avec τ = 0,5, σ se stabilise vers 3,8, il perd 0,2 et 0,34 et passe du 35ᵉ au 80ᵉ rang mondial après ses résultats récents. La prévision reste la même (duels bien prédits : 71,4 % → 71,4 %, entre les 30 meilleurs : 66,6 % → 66,7 %), donc ce réglage change la réactivité sans dégrader le modèle.
- Pages sans extension :
/pilot/40219,/competition/123,/competitions,/method,/wprs,/legal,/compare. - Classement d'un mois, paginé par 100 :
/ranking/2007-12,/ranking/2007-12-p6(les anciennes adresses en.htmlredirigent en 301 vers ces adresses). - Saut direct à un rang :
/ranking/2007-12?rank=600(formulaire « Aller au rang » sur chaque page)./ranking/2007ouvre le dernier mois de l'année. - Recherche sur tous les pilotes et toutes les compétitions (sans accents) :
/search?q=garcia. - Femmes : onglet « Féminin » (
?sex=F) sur les classements. Le genre vient dedata/genders.csv(civlid,gender), extrait de la liste CIVL ;build_db.py --gendersaccepte aussi directement un .xlsx de la liste CIVL. Les pilotes dont le genre n'est pas connu n'apparaissent pas dans l'onglet. Pour en ajouter, écrire une lignecivlid,F(ouM) dansdata/genders_manual.csv, qui est prioritaire, puis relancerbuild_db.py. Le niveau Elo est celui du classement général : le rang « féminin » est le rang de la pilote parmi les pilotes féminines.
Le site choisit la langue du navigateur (en-tête Accept-Language) et la mémorise dans un cookie ; les liens « Français / English / Deutsch / Español » en pied de page
(?lang=en) la changent à la main. Langues disponibles : français (texte source), anglais, allemand et espagnol. Quand la langue du navigateur n'est pas disponible, c'est l'anglais.
- Les textes sont les textes français écrits dans les gabarits :
_("…")pour un texte simple,_h("…")pour un texte avec HTML,_n("singulier", "pluriel", n). Les traductions sont danspgrank/i18n/<langue>.json(clé = texte français). Une clé absente retombe sur le français. - Les textes du JavaScript sont les clés
js.*du même fichier. Les formats de nombres et de dates dépendent de la langue (pgrank/i18n.py). - Ajouter une langue : copier
pgrank/i18n/en.jsonen<code>.json, le traduire, puis ajouter le code dansLANGS(pgrank/i18n.py).
- Serveur de production :
pip install -r requirements.txt, puisRANKING_DB=/chemin/ranking.db RANKING_PROXY=1 gunicorn -w 3 -b 127.0.0.1:8000 wsgi:application, derrière Caddy ou nginx (HTTPS).RANKING_PROXY=1(ou--proxy) à n'activer que derrière un reverse proxy de confiance : l'adresse du visiteur est lue dansX-Forwarded-For. - Mise à jour :
python update.py(voir « Mise à jour mensuelle »), puis copierranking.dbsur le serveur et redémarrer gunicorn. - Cache : les pages sont servies avec
Cache-Control: private(la langue dépend d'un cookie) ; ne pas les mettre en cache partagé (CDN) sans exclure le cookielangde la clé de cache, sinon une langue serait servie à tous. Les fichiers/static/sont versionnés et peuvent l'être. - Protections : en-têtes de sécurité (CSP, nosniff, anti-framing), limitation de débit par adresse (400 requêtes/min, 120 pour la recherche, par processus),
/_debugsupprimé, base ouverte en lecture seule. Pages pilotes, recherche et API :noindex.robots.txtetsitemap.xmlsont générés. - Aucune ressource tierce : plus de polices Google (police système de repli). Pour retrouver Archivo et Source Serif 4, les télécharger
(fontsource, Google Fonts) et les servir depuis
pgrank/static/avec des@font-face. - Mentions légales : page
/legal(4 langues). Renseignereditor_nameethost_infodanspgrank/config.py(affichés s'ils sont remplis) ;contact_emailest déjà réglé. - Retrait d'un pilote (droit d'effacement / d'opposition) : ajouter son numéro CIVL à
data/removals.csv, puis relancerbuild_db.py. Son nom, sa nation et son genre disparaissent, sa page devient introuvable ; sa ligne reste anonyme (« — ») pour garder les rangs intacts. - Déploiement sur Render : voir
DEPLOY_RENDER.md.
Le code de ce projet est un logiciel libre, distribué sous la licence GNU Affero General Public License v3.0 (AGPL-3.0) : voir le fichier LICENSE.
Vous pouvez l'utiliser, l'étudier, le modifier et le redistribuer. Si vous mettez en ligne une version modifiée (site web ou API),
vous devez en publier le code source sous la même licence.
Les résultats de compétition (data/civl_data.db) proviennent de la CIVL (FAI) et ne sont pas couverts par cette licence.