pi-ratings et rang HLTV : changer d'objectif, changer d'univers¶
Les quatre premiers chapitres ont tous modélisé la même chose : la probabilité qu'une équipe batte l'autre. Ce chapitre en présente deux qui rompent avec cette habitude, chacune à sa manière.
Les pi-ratings changent d'objectif : au lieu de prédire qui gagne, ils prédisent de combien. Le rang mondial HLTV change d'univers : c'est une horloge que nous n'avons pas construite, qui vit ailleurs, avec une mémoire bien plus longue que la nôtre — et qu'il a fallu aller déterrer dans les info-bulles de graphiques JavaScript.
La seconde a rapporté à elle seule tout le gain de l'étape squeeze de la campagne.
pi-ratings : prédire un écart, pas un vainqueur¶
Les pi-ratings viennent des travaux d'Anthony Constantinou et Norman Fenton sur le football (2013). Leur point de départ est une critique d'Elo qui prolonge celle du chapitre 3 : si la marge porte de l'information, pourquoi la traiter comme une correction du modèle de victoire plutôt que comme la quantité à modéliser ?
D'où le renversement : le rating d'une équipe n'est plus une position sur une échelle abstraite, c'est directement un nombre de manches.
Intuition
Un rating Elo de 1620 ne veut rien dire tout seul — il ne prend sens que par comparaison. Un pi-rating de \(+1{,}4\) veut dire quelque chose de concret : « cette équipe marque en moyenne 1,4 manche de plus que la moyenne ». L'écart entre deux pi-ratings est l'écart de manches attendu. C'est une échelle interprétable, et c'est rare.
La formule¶
L'écart de manches attendu entre \(A\) et \(B\) est simplement la différence de leurs ratings :
Après le match, on observe l'écart réel \(g\) et on calcule l'erreur \(\text{err} = g - e\). La mise à jour ne se fait pas proportionnellement à cette erreur, mais à sa version amortie :
Les symboles :
- \(\pi_A\), \(\pi_B\) : les pi-ratings, exprimés en manches. Tout le monde part de \(0\) ;
- \(g\) : l'écart de manches observé, orienté du point de vue de \(A\), et borné à \(\pm 13\) dans notre implémentation ;
- \(\text{err} = g - e\) : l'erreur de prédiction, en manches ;
- \(\psi\) : la fonction d'amortissement logarithmique ;
- \(\lambda\) : la vitesse d'apprentissage, l'équivalent du \(K\) d'Elo ;
- le facteur \(3\) : une constante d'échelle de Constantinou & Fenton, conservée telle quelle.
L'amortissement joue exactement le rôle du logarithme du chapitre 3 : il empêche qu'un 13-1 aberrant ne détruise un rating construit sur quinze matchs.
| \(|\text{err}|\) (manches) | \(\psi\) | |---|---| | 0,5 | 0.528 | | 2 | 1.431 | | 7 | 2.709 | | 13 | 3.438 |
Une erreur 26 fois plus grande produit une correction 6,5 fois plus grande seulement. Le code :
# pi-ratings : erreur sur l'écart de rounds, amortie en log
g = max(-13.0, min(13.0, float(s1 - s2)))
for k, lam in PI_LAMBDAS.items():
e = self.pi[k][a] - self.pi[k][b]
err = g - e
psi = math.copysign(3.0 * math.log10(1 + abs(err)), err)
self.pi[k][a] += lam * psi
self.pi[k][b] -= lam * psi
Exemple numérique¶
Supposons \(\pi_A = 1{,}2\) et \(\pi_B = -0{,}8\). L'horloge attend donc :
Le match se solde par un 13-4 pour \(A\), donc \(g = +9\) :
Avec la version lente, \(\lambda = 0{,}06\) :
Le nouvel écart attendu passe de \(2{,}00\) à \(2{,}325\) manches. Avec la version rapide, \(\lambda = 0{,}15\), il passerait à \(2{,}813\) — l'horloge croit bien plus vite ce qu'elle vient de voir.
Notre implémentation fait tourner les deux vitesses en parallèle, sans trancher a priori :
# pi-ratings : deux vitesses d'apprentissage, le choix se fera en validation interne
PI_LAMBDAS = {"pi_diff_a": 0.06, "pi_diff_b": 0.15}
La mesure¶
Les pi-ratings sont calculés à la granularité map (chaque map jouée met à jour
les ratings) et entrent dans le modèle comme pi_diff_a et pi_diff_b. Résultat en
validation interne, par-dessus les 32 features du modèle final/ :
| variante | Brier (validation) |
|---|---|
| base, 32 features | 0.2285 |
+ elo_mov_diff (Elo MOV à la 538) |
0.2279 |
+ mapelo (Elo par map, blend 50/50) |
0.2280 |
+ pi_diff_b (\(\lambda = 0{,}15\)) |
0.2274 |
+ pi_diff_a (\(\lambda = 0{,}06\)) |
0.2272 |
Les pi-ratings sont donc la meilleure des trois horloges map-granularité testées,
devant l'Elo MOV à la FiveThirtyEight et devant l'Elo par map. pi_diff_a a été
retenu dans les 40 features du modèle push/, qui a pris la tête de la campagne à
0.2172 de Brier.
La version lente bat la version rapide. C'est le même enseignement que le \(K = 90\) du chapitre 1 : sur un signal bruité, l'horloge prudente calibre mieux.
À retenir
Les pi-ratings modélisent l'écart de manches attendu plutôt que la victoire, avec une mise à jour amortie en logarithme de l'erreur. Chez nous, \(\lambda = 0{,}06\) en granularité map : 0.2272 en validation contre 0.2285 sans, la meilleure des trois horloges à marge de manches essayées.
Notre implémentation est volontairement une version simplifiée de la méthode publiée. Le lecteur curieux de l'écart trouvera le détail ci-dessous ; les autres peuvent passer directement à la seconde moitié du chapitre.
Pour aller plus loin — ce qu'on n'a pas repris de Constantinou & Fenton
L'article original distingue le rating à domicile et le rating à
l'extérieur de chaque équipe, avec un paramètre de diffusion \(\gamma\) qui
transfère partiellement l'apprentissage de l'un à l'autre. C'est essentiel au
football, où l'avantage du terrain est massif et hétérogène. En CS2, l'équivalent
naturel serait LAN contre online — un axe que notre modèle traite ailleurs, par
une feature lan et par la segmentation du modèle final. La séparation n'a pas
été essayée sur les pi-ratings ; c'est une piste ouverte, non mesurée.
Le rang mondial HLTV : une horloge externe¶
Changeons complètement de registre.
Toutes les horloges vues jusqu'ici ont un défaut commun, et il est structurel : elles sont construites sur notre fenêtre de données. Le moteur de Valve travaille sur six mois glissants, et nos balayages aussi. Une équipe qui dominait il y a huit mois et qui revient d'une pause n'existe pas, pour nous, autrement que comme une équipe à 1500 points.
Or il existe, à l'extérieur, une horloge qui n'a pas ce problème : le classement mondial HLTV. Il est calculé chaque semaine depuis plus de dix ans, avec sa propre méthodologie, sur l'historique complet du jeu. Si on pouvait le lire à la date de chaque match, on disposerait d'un résumé de tout ce que notre fenêtre ne voit pas.
L'histoire des info-bulles fusioncharts¶
Le problème est évidemment temporel : lire le classement HLTV d'aujourd'hui pour prédire un match d'il y a trois mois serait une fuite du futur caractérisée, et invaliderait toute l'évaluation.
Il fallait donc du rang daté. Il se trouve que chaque page d'équipe de HLTV
affiche un graphique de l'évolution de son classement. Ce graphique est rendu par la
bibliothèque JavaScript FusionCharts, et sa configuration est intégrée dans le HTML
sous forme d'un attribut data-fusionchart-config contenant du JSON échappé. Chaque
point de la courbe y porte une info-bulle (tooltext) qui contient — et c'est le
petit miracle de cette histoire — la date en toutes lettres et les deux rangs, le
rang HLTV et le rang Valve :
_HRANK = re.compile(r'h-rank">#(\d+)')
_VRANK = re.compile(r'v-rank-tooltip">#(\d+)')
def team_history(page):
"""[(unix, hltv_rank, valve_rank|None)] dédupliqué par date. Les tooltips des
charts fusioncharts portent les DEUX rangs (classes h-rank / v-rank-tooltip)."""
Le pipeline complet : parser le JSON de configuration du graphique, en extraire chaque point, lire la date (« 23rd June 2025 ») dans le sous-titre de l'info-bulle, lire les rangs dans les classes CSS, dédupliquer par date.
Aucune requête réseau n'a été faite pour cela — les pages d'équipe étaient déjà dans le cache HTML local du projet, collectées pour d'autres raisons. C'est, comme la marge du chapitre 3, de l'information déjà payée.
La récolte¶
| grandeur | valeur |
|---|---|
| équipes couvertes | 439 |
| points de classement datés | 42 302 |
| période couverte | octobre 2015 → août 2026 |
| matchs de notre pool avec les deux équipes datées | 5 088 / 6 062, soit 84 % |
Onze ans d'histoire du classement mondial, reconstruits depuis des info-bulles de graphiques. La densité augmente avec le temps — 924 points pour 2016, 8 715 pour les huit premiers mois de 2026 — ce qui est cohérent avec la croissance du nombre d'équipes suivies.
La validation croisée par les archives hebdomadaires¶
Une donnée arrachée à des info-bulles mérite d'être vérifiée avant qu'on ne construise
quoi que ce soit dessus. Une seconde source, indépendante, a été extraite du même
cache : les pages d'archives hltv.org/ranking/teams/<année>/<mois>/<jour>,
publiées chaque lundi. Trente semaines de février à août 2026, environ 249 équipes
par semaine, avec rang et points.
Le croisement des deux sources donne 99,8 % d'accord exact sur les rangs. C'est le genre de contrôle qui ne prend pas longtemps et qui autorise à faire confiance à tout le reste.
Erreur fréquente
Se contenter d'une source unique quand elle a été obtenue par un chemin inhabituel. Une expression régulière sur un JSON échappé dans un attribut HTML peut se tromper de mille façons silencieuses — décalage d'un point, mauvaise série, fuseau horaire. Ici la seconde source coûtait une demi-heure de parsing et a transformé « probablement correct » en « vérifié à 99,8 % ».
La feature¶
Le rang n'entre pas brut dans le modèle. Il entre en logarithme :
Positif quand \(A\) est mieux classée (un rang plus petit est un meilleur rang). Le logarithme est indispensable, et l'exemple le montre :
| confrontation | écart brut de rangs | hr_rank_diff |
|---|---|---|
| #3 contre #25 | 22 | 2.120 |
| #100 contre #122 | 22 | 0.199 |
Le même écart nominal de 22 places vaut dix fois plus en haut du classement qu'au milieu. C'est exactement ce qu'on veut : la distance entre la 3ᵉ et la 25ᵉ équipe mondiale est un gouffre, celle entre la 100ᵉ et la 122ᵉ un détail.
Une seconde feature capture la tendance sur 90 jours, sur le même principe :
Positive quand \(A\) progresse plus vite que \(B\). L'intégrité temporelle est assurée par construction : la fonction de lecture prend systématiquement le dernier point antérieur ou égal à la date du match, jamais le suivant.
La mesure : le rang HLTV est tout le gain de squeeze¶
L'étape squeeze de la campagne a testé quatre familles de features extraites du
cache existant. Voici le verdict complet, en validation interne, sur une base de
0.2263 :
| famille | contenu | Brier (validation) |
|---|---|---|
| A | H2H enrichi, pondéré par le recouvrement de roster | 0.2264 |
| B | boxes « Past matches » des pages équipe | 0.2261 |
| C | rang mondial HLTV daté + tendance 90 j | 0.2250 |
| D | archives hebdomadaires (rang + points) | 0.2258 |
| C + D | 0.2255 | |
| toutes | 0.2256 |
Et le détail par feature, qui est encore plus net :
| feature seule | Brier (validation) |
|---|---|
+ hr_rank_diff |
0.2251 |
+ hr_trend_diff |
0.2264 |
+ wk_rank_diff |
0.2258 |
+ wk_pts_share |
0.2260 |
La sélection gloutonne, lancée sur l'ensemble des candidates, n'en retient
qu'une seule : hr_rank_diff. La tendance 90 jours n'apporte rien. Les archives
hebdomadaires n'apportent rien en plus de l'historique long — elles sont
redondantes, ce qui est rassurant puisqu'elles mesurent la même chose sur une fenêtre
plus courte.
Sur le holdout, en une seule passe :
| modèle | accuracy | Brier | log-loss |
|---|---|---|---|
push (leader précédent, re-run) |
64,0 % | 0.2172 | 0.6210 |
push + famille A |
63,7 % | 0.2172 | 0.6208 |
push + famille B |
64,1 % | 0.2176 | 0.6216 |
squeeze = push + famille C |
64,6 % | 0.2154 | 0.6192 |
Toute l'étape squeeze tient dans une seule feature. Une expression régulière sur
des info-bulles de graphiques a produit 0,0018 de Brier — plus que le raffinement
FiveThirtyEight du chapitre 3, plus que TrueSkill par map du chapitre 4.
Pourquoi ça marche : la mémoire longue¶
L'explication est dans le journal de la campagne, et elle est nette :
L'horloge externe longue durée (le rang HLTV perce notre fenêtre de 6 mois) est le seul vrai gisement restant du cache actuel.
Nos horloges connaissent six mois. Le classement HLTV en connaît onze ans. Pour une équipe stable, la différence est nulle — nos six mois suffisent à la situer. Pour une équipe qui revient, qui vient de monter d'un tier, qui a raté une saison sur blessure ou visa, le rang HLTV contient une information que rien dans nos données ne peut reconstruire.
notre fenêtre le rang HLTV
├──────── 6 mois ────────┤ ├──────────── 11 ans ─────────────┤
équipe stable : suffisant redondant
équipe qui revient : aveugle ← la seule source
équipe promue : partiel ← le contexte manquant
C'est aussi, mot pour mot, l'annonce du chapitre 6 : le problème n'est pas la formule de l'horloge, c'est la longueur de sa mémoire.
Limite importante
Une horloge externe est une boîte noire. Nous ne savons pas exactement comment HLTV calcule son classement, ni s'il a changé de méthodologie sur onze ans (c'est arrivé au moins une fois publiquement). Nous ne pouvons pas la recalculer, la déboguer, ni garantir qu'elle sera disponible demain dans le même format. La dépendance est réelle, et c'est le prix du gain. Un modèle de production qui repose sur du parsing d'info-bulles JavaScript doit prévoir le jour où le graphique change de bibliothèque.
Une seconde limite, moins visible : la couverture est de 84 %. Pour 16 % des matchs,
hr_rank_diff est manquant. Le pipeline remplace alors la valeur absente par zéro
(np.nan_to_num avant la standardisation), ce qui revient à dire « les deux équipes
sont au même rang » — une valeur neutre, donc inoffensive, mais qui n'apporte rien.
Ces matchs sont typiquement ceux d'équipes obscures de tier 3 sans page d'équipe
fournie. Le gain de 0,0018 est donc un gain moyen qui cache un gain plus fort sur
les 84 % couverts et zéro sur le reste.
À retenir
Le rang mondial HLTV daté, extrait des info-bulles FusionCharts des pages
d'équipe (439 équipes, 42 302 points datés d'octobre 2015 à août 2026,
validés à 99,8 % par les archives hebdomadaires), entre dans le modèle sous forme
de \(\ln(\text{rang}_B) - \ln(\text{rang}_A)\). C'est la seule feature retenue
de l'étape squeeze, et elle en constitue tout le gain : holdout 0.2172 →
0.2154. Sa valeur vient de sa mémoire longue : elle perce la fenêtre de six
mois qui aveugle toutes nos horloges internes.
Chapitre suivant : le démarrage à froid, qui prend ce constat au sérieux et le transforme en le plus gros gain mesuré de la campagne.