Aller au contenu

Le démarrage à froid : le plus gros gain de la campagne

Voici le chapitre qui relativise les cinq précédents.

Nous avons passé beaucoup d'énergie à comparer des formules : Elo contre Glicko, \(K\) contre \(K\), marge de série contre marge de manches, volatilité dynamique contre \(RD\) figé, joueurs contre équipes. Les gains mesurés vont de 0,001 à 0,007 de Brier.

Puis nous avons changé une chose qui n'est pas une formule du tout — l'état de départ des horloges — et gagné 0,0072 de Brier d'un coup, plus que n'importe quelle famille de features de tout le concours.

C'est le résultat le plus important de cette partie, et probablement du cours entier.

Le problème : tout le monde part de 1500

Regardez le code de production du modèle de victoire :

def rating(self, team_id):
    return self.glicko.setdefault(team_id, model.GlickoTeam(1500.0))

La première fois qu'une équipe apparaît dans le balayage chronologique, elle reçoit 1500. C'est vrai de toutes nos horloges, sans exception :

horloge valeur de départ signification
Glicko de Valve \(r = 1500\), \(RD = 75\) « équipe moyenne, et j'en suis sûr »
Elo à marge \(r = 1500\) « équipe moyenne »
Glicko-2 \(\mu = 0\), \(\phi = \phi_{\max}\) « équipe moyenne, et je n'en sais rien »
TrueSkill \(\mu = 25\), \(\sigma = 25/3\) idem, par joueur
pi-ratings \(\pi = 0\) « écart de manches nul »

Ce n'est pas un bug : c'est le seul choix raisonnable en l'absence d'information. Mais c'est un prior massivement faux pour à peu près toutes les équipes du pool. Une organisation du top 5 mondial ne vaut pas 1500. Une équipe de qualification régionale non plus. Et l'horloge met du temps à s'en apercevoir.

Limite importante

Le Glicko de Valve cumule ici les deux erreurs : il part d'un prior faux et il affirme en connaître la précision, puisque son \(RD\) vaut 75 dès le premier match. Il annonce donc « équipe moyenne, avec la même confiance qu'une équipe suivie depuis cent matchs ». Glicko-2 et TrueSkill, eux, partent au moins avec une incertitude maximale : ils savent qu'ils ne savent pas.

Combien de matchs pour se réchauffer ?

C'est une question à laquelle on peut répondre exactement.

Prenons une équipe dont la force vraie correspond à 1800, qui joue contre un pool calibré à 1500, avec \(K = 30\). Elle gagne donc en moyenne 84,9 % de ses matchs. À chaque match, son rating avance de \(K\) fois l'écart entre sa probabilité de victoire réelle et celle que l'horloge attend. Voici la trajectoire moyenne :

matchs joués rating part du chemin parcouru
1 1510,5 3,5 %
5 1548,0 16,0 %
10 1586,7 28,9 %
20 1643,9 48,0 %
30 1683,1 61,0 %
50 1731,5 77,2 %
100 1779,6 93,2 %
200 1797,9 99,3 %

La convergence est géométrique, avec une constante de temps d'environ 23 matchs. Il faut une vingtaine de matchs pour combler la moitié de l'écart, et plus de cent pour être vraiment arrivé.

Intuition

La lenteur vient d'un effet de ciseaux. Loin de l'équilibre, la surprise \(y - E\) est grande, donc le pas est grand — mais la sigmoïde est plate aux extrêmes, donc il faut beaucoup de points de rating pour changer \(E\) de peu. Près de l'équilibre, la sigmoïde est pentue mais la surprise est petite. Dans les deux régimes, l'approche est lente.

Le chiffre qui fait mal

Et maintenant, la question qui tue : combien de matchs les équipes de notre pool jouent-elles réellement dans la fenêtre de six mois ?

Mesure directe sur les 6 062 matchs du balayage (fichier scratch/ml/squeeze/squeeze_features.pkl) :

grandeur valeur
équipes distinctes 656
matchs par équipe, moyenne 18,5
matchs par équipe, médiane 7
équipes à 5 matchs ou moins 298, soit 45 %
équipes à 10 matchs ou moins 366, soit 56 %
équipes à 20 matchs ou moins 435, soit 66 %

La médiane est à sept matchs. Croisez ce chiffre avec le tableau de convergence : sept matchs, c'est environ 22 % du chemin parcouru. Pour la moitié des équipes de notre pool, l'horloge n'est jamais sortie du voisinage de 1500.

Ce n'est pas un problème marginal réservé aux figurants. Comptons les matchs plutôt que les équipes :

1 895 des 6 062 matchs — soit 31 % — opposent au moins une équipe qui joue 20 matchs ou moins dans toute la fenêtre.

Presque un match sur trois est prédit avec au moins une horloge non convergée. Le diagnostic du journal de la campagne — « la faiblesse n°1 était bien le démarrage à froid » — n'était pas une intuition : c'est ce que ce tableau dit.

   distribution des matchs par équipe sur 6 mois (656 équipes)

   ≤5    ████████████████████████████████████████████  298   (45 %)
   6-10  ██████████                                     68   (10 %)
   11-20 ██████████                                     69   (11 %)
   21-30 ████████                                       53    (8 %)
   31-50 █████████████                                  90   (14 %)
   >50   ███████████                                    78   (12 %)
                                     médiane = 7 matchs ↑

Erreur fréquente

Raisonner sur la moyenne (18,5 matchs) et conclure « ça devrait aller ». La distribution est très asymétrique : une poignée d'équipes tier 1 joue 80 à 120 matchs et tire la moyenne vers le haut, pendant que la moitié du pool en joue sept. Sur des distributions à queue lourde, la médiane raconte l'histoire, la moyenne la cache.

Ce que Valve fait, et que nous ne faisions pas

Ironie de l'affaire : le moteur de Valve, que nous avons critiqué pendant cinq chapitres, a une réponse au démarrage à froid — et elle est plutôt bonne.

Valve ne part pas de 1500. Il calcule d'abord un seeding à partir de quatre facteurs de mérite (gains en tournoi, adversaires distincts battus, réseau d'adversaires, participation LAN), puis remappe ce score sur l'intervalle \([400, 2000]\) :

MIN_SEEDED_RANK = 400
MAX_SEEDED_RANK = 2000
team.rank_value = remap_value_clamped(
    team.seed_value, min_seed, max_seed, MIN_SEEDED_RANK, MAX_SEEDED_RANK
)
team.glicko = GlickoTeam(team.rank_value)

L'horloge Glicko ne démarre donc pas au centre : elle démarre là où le palmarès de l'équipe la place, et elle ne fait que corriger ce placement match après match. C'est une réponse structurelle au démarrage à froid, et elle explique en partie pourquoi la baseline de Valve tient si bien face à nos horloges plates.

Nos horloges de prédiction, elles, n'avaient rien de tel. Elles partaient toutes de 1500, plat. C'est l'écart qu'il a fallu combler — par une autre voie.

Le réchauffage, première voie : douze mois natifs

L'idée est simple au point d'être vexante : au lieu de commencer le balayage chronologique au début de la fenêtre de six mois, le commencer six mois plus tôt. Les matchs supplémentaires ne servent pas à entraîner le modèle ; ils servent uniquement à faire tourner les horloges pour qu'elles arrivent chaudes au début de la période d'intérêt.

   avant (froid)
                          ┌──── fenêtre native 6 mois ────┐
   horloges à 1500  ─────►│ balayage + entraînement       │──► holdout
                          2026-02                    2026-07

   après (réchauffé)
        ┌── réchauffage 6 mois ──┬──── fenêtre native ────┐
        │ balayage SEUL          │ balayage + entraînement│──► holdout
        2025-08              2026-02                 2026-07

La distinction entre « faire tourner les horloges » et « entraîner le modèle » est essentielle, et le protocole de la campagne assault/ l'isole proprement. Le build_dataset.py produit deux jeux de features pour les mêmes matchs : feats (états réchauffés sur 12 mois) et feats_cold (états rejoués depuis 6 mois seulement). Même modèle, mêmes lignes d'entraînement, mêmes hyperparamètres : seule change la température des horloges.

La mesure

Holdout de 1 146 matchs, tableau pré-enregistré, une seule passe :

configuration accuracy Brier log-loss
squeeze reproduit, froid, train 6 mois 64,6 % 0.2153 0.6191
états réchauffés, train 6 mois 66,2 % 0.2081 0.6007
+ train étendu à 12 mois 66,3 % 0.2078 0.6002
\[ \Delta_{\text{Brier}} = 0{,}2081 - 0{,}2153 = -0{,}0072 \]

Et voici le détail qui rend le résultat concluant plutôt que simplement bon : étendre le jeu d'entraînement de 4 916 à 10 287 lignes n'apporte que 0,0003 de plus. Le gain ne vient donc pas de « plus de données pour apprendre », il vient uniquement de la température des horloges. C'est exactement l'expérience qu'il fallait faire, et elle a été faite dans le bon ordre.

À retenir

Réchauffer les horloges sur six mois supplémentaires — sans changer une seule ligne de formule, sans ajouter une seule feature, sans agrandir le jeu d'entraînement — vaut −0,0072 de Brier et +1,6 point d'accuracy. C'est le plus gros gain unitaire de toute la campagne.

Pour situer : la meilleure amélioration de formule des chapitres précédents (TrueSkill injecté dans le modèle) valait 0,0074, et elle avait demandé une horloge bayésienne complète, une bibliothèque externe, une grille d'hyperparamètres et un changement d'objet suivi. Le réchauffage a demandé de reculer une date.

Le réchauffage, deuxième voie : 40 023 matchs bo3.gg

Six mois de plus, c'est bien. Mais notre cache HLTV a lui-même une profondeur limitée, et la même logique pousse à aller chercher de l'historique ailleurs.

Le site bo3.gg publie des résultats de matchs CS2 remontant bien plus loin. La collecte industrialisée dans harvest/ a rapporté :

gisement volume période
bo3_history.pkl — résultats de matchs 40 023 2024-01 → 2026-08
maps et scores associés 77 890 idem
matchs avec les maps détaillées 39 375 idem

Ces 40 023 matchs ne rejoignent pas le jeu d'entraînement — ils ne portent ni nos identifiants d'équipe, ni nos rosters, ni nos poids d'event. Ils servent exclusivement à faire tourner les horloges avant le début de notre fenêtre.

Trois horloges sont réchauffées de cette façon, par nom d'équipe normalisé :

def bo3_warm_feats(recs):
    """Horloges réchauffées depuis 2024-01 sur bo3_history, par nom normalisé.
    Lues strictement avant t (les matchs bo3 simultanés ne sont pas appliqués)."""
  • wmelo_diff : l'Elo à marge du chapitre 3, réchauffé depuis janvier 2024 ;
  • wglicko_diff : le Glicko de Valve, idem ;
  • wpi_diff : les pi-ratings du chapitre 5, alimentés par les scores de manches map par map ;
  • wn_min : \(\ln(1 + \min(n_A, n_B))\), le nombre de matchs bo3.gg vus pour la moins observée des deux équipes. C'est une feature de confiance : elle dit au modèle à quel point les trois précédentes sont fiables.

Le point technique le plus délicat est l'intégrité temporelle. Les horloges bo3 sont avancées jusqu'à la date du match courant, strictement avant (bo3[j][0] < r["t"]), et jamais au-delà. Un match bo3.gg joué le même jour n'est pas appliqué.

La mesure

configuration accuracy Brier log-loss
réchauffé natif + train 12 mois 66,3 % 0.2078 0.6002
+ rounds-éco seul 66,7 % 0.2064 0.5970
+ réchauffage bo3 seul 66,4 % 0.2063 0.5974
assault — combinaison choisie en validation 66,8 % 0.2054 0.5954

Le réchauffage externe apporte 0,0015 de plus, à peu près à égalité avec la famille des features économiques (pistolets, anti-éco, clutchs). En validation interne, il faisait mieux encore — 0.2219 sans, 0.2184 avec, soit 0,0035 — mais le holdout est plus sévère. C'est un écart normal entre validation et test ; on rapporte les deux et on croit le second.

Une seule des quatre features de réchauffage externe a survécu à la sélection gloutonne : wmelo_diff, l'Elo à marge réchauffé. Le Glicko réchauffé et les pi-ratings réchauffés n'apportaient rien en plus. Retour du chapitre 3 : quand on n'a droit qu'à une horloge, c'est l'Elo à marge qu'il faut prendre.

Limite importante

L'appariement avec bo3.gg se fait par nom d'équipe normalisé, pas par identifiant. Deux organisations homonymes, une équipe qui change de nom, une académie qui porte le nom du club — chacun de ces cas produit un réchauffage faux. Le rapport de collecte documente 4 302 vainqueurs concordants sur 4 304 matchs appariés (les deux discordants sont explicitement exclus), et un appariement global à 85 %. C'est propre, mais c'est un point de fragilité réel que wn_min ne suffit pas à neutraliser.

La décomposition complète

Voici, dans l'ordre chronologique de la campagne, la cascade complète depuis la baseline de Valve jusqu'au leader final. La figure décomposition des gains la met en forme.

étape ce qui est ajouté Brier gain
baseline Valve Glicko \(RD\) figé, prédicteur pur 0.2422 —
champion v2 XGBoost 10 features, dont l'Elo à marge 0.2330 −0.0092
bayes TrueSkill par joueur 0.2256 −0.0074
push pi-ratings, TrueSkill par map, momentum 0.2172 −0.0084
squeeze rang mondial HLTV daté 0.2154 −0.0018
réchauffage natif 12 mois d'historique, mêmes formules 0.2081 −0.0072
assault + éco + réchauffage bo3 0.2054 −0.0027
segmodel segmentation « informé » + blend 0.2041 −0.0013

Le réchauffage figure au deuxième rang des gains unitaires, derrière le champion v2 (qui cumulait dix features) et à égalité avec TrueSkill et les pi-ratings — sauf qu'il n'a rien coûté en complexité de modèle. Zéro feature ajoutée, zéro paramètre supplémentaire.

Pour la comparaison des horloges elles-mêmes, en prédicteur pur, voir la figure horloges comparées.

Pourquoi c'est la faiblesse numéro un

Trois raisons, dans l'ordre de gravité.

Une horloge froide ne se trompe pas au hasard : elle se trompe dans un sens connu. Toutes les équipes non convergées sont tirées vers 1500, c'est-à-dire vers le centre. Les forts sont sous-estimés, les faibles surestimés, et l'horloge annonce systématiquement des matchs plus serrés qu'ils ne le sont. C'est un biais, pas du bruit — et un biais ne s'annule pas en moyennant sur 1 146 matchs.

L'erreur se propage à toutes les features dérivées. glicko_diff, melo_diff, ts_diff, pi_diff_a : toutes sont des différences d'horloges. Si les deux horloges sont froides, la différence est fausse, et le modèle apprend à faire confiance à une variable bruitée. Pire encore, il apprend un coefficient atténué pour compenser — ce qui dégrade aussi ses prédictions sur les matchs où les horloges étaient chaudes.

Ce sont les matchs les plus prévisibles qui sont ratés. Un match entre une équipe tier 1 établie et un qualifié régional obscur devrait être le match le plus facile du lot. Il est justement celui où l'horloge de l'outsider est froide, donc où la prédiction est le plus tirée vers 0,5. On perd de l'argent, si l'on peut dire, sur les matchs les plus faciles.

C'est cohérent avec la décomposition par segments du modèle final : le segment « horloges froides » atteint 0.1591 de Brier et 75,0 % d'accuracy une fois le problème corrigé, ce qui en fait l'un des segments les plus prévisibles du holdout. Le potentiel était là depuis le début ; il était juste inaccessible tant que les horloges partaient de 1500.

Ce qui reste à faire

Le réchauffage a été poussé aussi loin que les données disponibles le permettaient, mais deux angles n'ont pas été traités.

Le réchauffage par joueur. TrueSkill initialise tout nouveau joueur à \(\mu = 25\) — le prior neutre. Les pages hltv.org/player/<id>, identifiées mais jamais collectées, porteraient l'historique et le rating individuel de chaque joueur. Un joueur qui monte de tier 2 en tier 1 arriverait avec son vrai niveau au lieu de la moyenne. C'est exactement le même problème que celui de ce chapitre, un cran plus bas.

Le seeding par le mérite, comme Valve. Rien n'empêcherait d'initialiser nos horloges de prédiction avec le rank_value_seed du moteur VRS — qui est déjà calculé par notre propre port du modèle de Valve — au lieu de 1500 plat. L'idée n'a pas été essayée. Elle est gratuite en collecte et coûte quelques lignes.

Erreur fréquente

Chercher le gain dans la sophistication du modèle avant d'avoir vérifié la qualité de son état initial. La campagne a exploré Glicko-2, TrueSkill, factor graphs, XGBoost monotone, GBDT→LR, modèles map-level recomposés, labels multinomiaux — et le gain le plus net est venu d'un décalage de six mois sur la date de début du balayage. L'ordre inverse aurait fait gagner des semaines.

Résumé de la partie

Six chapitres, cinq horloges, une vingtaine de mesures. Si vous ne deviez retenir qu'un seul encadré de toute cette partie, ce serait celui-ci — parce qu'il porte le gain le plus important et qu'il ne parle pas de mathématiques.

À retenir

Toutes nos horloges partaient de 1500. La convergence d'un Elo de \(K=30\) demande une vingtaine de matchs pour la moitié du chemin et plus de cent pour arriver ; or la médiane de notre pool est de 7 matchs sur six mois, et 31 % des matchs opposent au moins une équipe à 20 matchs ou moins. Réchauffer les horloges sur douze mois natifs vaut −0,0072 de Brier à formules et features constantes ; y ajouter 40 023 matchs bo3.gg 2024-2026 en apporte 0,0015 de plus. C'est le plus gros gain unitaire de la campagne — et il ne concerne aucune formule.

Les six chapitres de cette partie se résument en une phrase : une horloge de force vaut par sa formule, mais elle vaut davantage encore par la longueur de sa mémoire. Le chapitre 5 le disait déjà avec le rang HLTV, qui perçait notre fenêtre de six mois. Celui-ci le dit avec nos propres horloges.

Retour à l'introduction de la partie, ou au sommaire du cours.