Chronologie de la campagne¶
Cette campagne a duré un peu moins de vingt-cinq heures. Entre le premier Monte Carlo qui réclamait des probabilités de victoire (24 août, 08 h 36 UTC) et le chantier de clôture (25 août, 09 h 28 UTC), onze approches ont été construites, mesurées et classées, et quatre hypothèses ont été réfutées avec des chiffres.
Ce chapitre raconte comment. Pas pour l'anecdote : parce que le dispositif expérimental a produit plus de valeur que n'importe lequel des modèles. Si vous ne deviez retenir qu'une chose de la partie récit, ce serait le protocole décrit ici, pas le leader qui en est sorti.
Le point de départ : un simulateur qui réclamait des probabilités¶
Le projet ne visait pas la prédiction de matchs. Il visait la reproduction du Valve Regional Standings, le classement qui distribue les invitations au Major — un moteur porté fidèlement depuis le code officiel de Valve, puis corrigé pendant deux jours jusqu'à retrouver 32/32 slots Major et 0,39 point d'écart moyen sur le classement officiel.
Un simulateur de tournois restants a suivi : un Monte Carlo qui tire chaque match à venir et compte, sur 500 tirages, combien de fois chaque équipe décroche son invitation. Or un Monte Carlo a besoin d'une entrée qu'on n'avait pas : la probabilité qu'une équipe batte une autre.
La v1 utilisait le expected_score du Glicko de Valve. Ce Glicko a une
particularité : Valve fige le RD à 75 pour toutes les équipes. L'incertitude
est morte ; le rating ne sait plus dire « je ne connais pas cette équipe ». Sur
nos 1 146 matchs de test, ce prédicteur donne un Brier de 0.2422 et 56,0 %
d'exactitude — mieux qu'une pièce lancée (0.250 / 50 %), mais à peine.
Le 24 août à 18 h 40, un premier modèle d'apprentissage remplace ce Glicko : un XGBoost sur dix features d'équipe (écart de Glicko, forme sur 5 et 10 matchs, head-to-head, expérience, repos, stabilité de roster, LAN, enjeu, et l'écart d'Elo à marge). Résultat : 0.2330 / 60,4 %. C'est le « champion v2 », le point de départ du concours. La feature la plus utile du lot est l'Elo à marge — celui qui compte un 2-0 plus lourd qu'un 2-1.
Intuition — pourquoi un concours plutôt qu'une suite d'essais
Avec un seul expérimentateur, les idées s'essaient en série et la meilleure reste. Le problème, c'est que la deuxième idée est toujours testée par quelqu'un qui a déjà vu le résultat de la première : les choix se contaminent. Lancer six explorations en parallèle, à l'aveugle les unes des autres, sur le même jeu de test, donne six mesures indépendantes — et surtout, ça révèle quelles familles de signal sont réellement disjointes.
Le concours : six agents, un protocole, un holdout intouchable¶
Six explorations sont lancées en parallèle. Chacune reçoit le même document — un brief de protocole — et un dossier de travail isolé. Aucune ne voit les résultats des autres avant la clôture.
Le protocole tient en quatre règles, et ce sont elles qui rendent le classement final lisible.
Règle 1 — zéro accès réseau¶
Le site source (HLTV) bannit les IP qui l'interrogent en parallèle. Six collecteurs simultanés auraient tué le projet en une heure. Toutes les explorations travaillent hors ligne, sur un cache local d'environ 16 000 pages HTML déjà capturées.
Contrainte apparemment pénible, en réalité fertile : le cache contenait des tables jamais analysées — statistiques par joueur, veto de cartes, scores par manche avec les mi-temps CT et T. Deux des trois familles gagnantes du concours sont sorties de ce gisement dormant. La règle « pas de réseau » a forcé à regarder ce qu'on avait déjà.
Règle 2 — intégrité temporelle stricte¶
Une feature d'un match ne peut utiliser que de l'information antérieure à ce match. En pratique : un balayage chronologique obligatoire, où l'état de chaque équipe (rating, forme, statistiques glissantes, historique de confrontations) est calculé avant le match, puis mis à jour après.
temps ──────────────────────────────────────────────────────────►
match n match n+1 match n+2
│ │ │
┌──┴───┐ ┌──┴───┐ ┌──┴───┐
│ LIRE │ │ LIRE │ │ LIRE │ features = état courant
│ état │ │ état │ │ état │
└──┬───┘ └──┬───┘ └──┬───┘
│ prédire │ prédire │ prédire
▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐
│ MAJ │ │ MAJ │ │ MAJ │ l'état absorbe le résultat
│ état │ │ état │ │ état │
└──────┘ └──────┘ └──────┘
Inverser ces deux blocs — mettre à jour avant de lire — fait fuir le résultat du match dans ses propres features. Le modèle devient spectaculaire et faux. Toute fuite disqualifiait l'approche, sans discussion.
Règle 3 — un holdout commun, intouchable¶
Les 42 derniers jours de matchs sont mis de côté : 1 146 matchs. Personne ne s'entraîne dessus. Personne n'y règle un hyperparamètre. La coupure est calculée exactement de la même façon pour tout le monde, et les métriques sont calculées par la même fonction.
Chaque approche dispose donc de deux jeux :
┌───────────────────────── historique ─────────────────────────┐
│ │
│ FIT (entraînement) VAL (validation interne) │ HOLDOUT
│ 9 479 matchs 808 matchs │ 1 146
│ ◄──────────────────────► ◄──────────────────────► │ ◄──────►
│ │
└── tous les choix se font ici ────────────────────────────────┘ une seule
mesure
Les tailles ci-dessus sont celles de la fin de campagne (fenêtre d'entraînement étendue à douze mois) ; au début du concours, la fenêtre native de six mois donnait FIT 4 108 / VAL 808 / TRAIN 4 916, le holdout restant identique.
Règle 4 — une ligne de classement, écrite par soi-même¶
En fin de course, chaque approche ajoute une ligne à un fichier de classement partagé : approche, Brier, log-loss, exactitude, note. C'est tout. Pas de présentation, pas de plaidoyer. Le classement est la seule sortie qui compte.
À retenir — le holdout est un budget, pas un tableau de bord
Chaque regard sur le jeu de test consomme un peu de son honnêteté : plus on le consulte, plus on l'ajuste sans le savoir. Le protocole imposait une passe. Une approche a triché à moitié (deux passes, décision figée avant la seconde) et l'a écrit noir sur blanc dans son rapport. Écrire l'entorse vaut mieux que la cacher — mais ne pas la commettre vaut encore mieux.
Ce que le concours a trouvé : trois familles de signal¶
À la clôture, six rapports. Le classement, du meilleur au moins bon : fusion 0.2180, TrueSkill-joueur 0.2256, statistiques joueurs 0.2289, cartes/manches 0.2293, empilement 0.2306, régression logistique 0.2315 — contre 0.2330 pour le champion de départ.
Trois familles de signal réellement disjointes en sont sorties.
Les horloges de force. L'approche « bayes » ressuscite l'incertitude que
Valve avait figée : un Glicko-2 complet par équipe (avec son \(\sigma\) vivant,
gonflé par l'inactivité) et un TrueSkill par joueur, où les ratings suivent
les joueurs à travers les transferts et où l'équipe du jour est la somme de son
line-up. En prédicteurs purs : Glicko-2 0.2365, TrueSkill 0.2354, contre 0.2422
pour le Glicko de Valve. Injecté dans le modèle, ts_diff devient la feature
n° 1, devant toutes les features de départ.
Les statistiques individuelles. L'approche « stats » analyse 9 112 pages de
match du cache et en extrait des tables jamais lues : K-D, ADR, KAST, rating 3.0
par joueur. Moyennes glissantes sur 10 matchs, agrégées par équipe (moyenne,
star, écart-type, couverture). Résultat 0.2289, avec pkast_diff et
prat_mean_diff en 2ᵉ et 3ᵉ position d'importance.
Les cartes et les manches. L'approche « maps » extrait le veto complet, les
scores par carte avec mi-temps CT/T, et le head-to-head daté. Elle produit
round_share_diff — la part de manches gagnées sur les 30 dernières cartes —
qui devient la feature n° 1 absolue du modèle, devant l'écart d'Elo à marge.
Autrement dit : dominer les manches prédit mieux que gagner les séries.
Intuition — pourquoi la part de manches bat le score de série
Une série se résume à 2-0, 2-1 ou 1-2 : trois valeurs, beaucoup de hasard. Les manches, elles, se comptent par dizaines. Un 2-1 gagné 16-14, 10-16, 16-13 et un 2-1 gagné 16-2, 5-16, 16-4 racontent deux histoires différentes — et la part de manches les distingue là où le score de série les confond.
Deux approches n'ont pas produit de gain, et leur échec est instructif.
L'empilement (« stack ») construit cinq modèles de base et les combine par validation temporelle. Gain réel mais minuscule : 0.2329 → 0.2306. Le rapport donne la raison sans détour — les cinq bases partagent les mêmes features, donc elles se trompent aux mêmes endroits. Un ensemble ne rapporte que ce que ses membres ont de désaccords.
La régression logistique (« logreg ») code une logistique à la main sur 22 features transformées et fait 0.2315 — c'est-à-dire qu'elle bat le champion XGBoost avec un modèle sans un seul arbre. C'est le premier signe d'un résultat qui sera confirmé quatre fois : sur ces données, la frontière est presque linéaire.
La sixième approche : le silence de gbdt¶
Une approche du concours n'a jamais rendu de README : « gbdt », dont la mission était de pousser les arbres de décision au maximum. Elle a réglé XGBoost, LightGBM et CatBoost sur 50 essais chacun, puis mis en sac huit modèles, puis testé les calibrations.
Son report.json existe pourtant. Verdict : le meilleur candidat (CatBoost
réglé) fait 0.2331 sur le holdout — contre 0.2330 pour le champion XGBoost
non réglé qu'il devait battre. Six minutes de réglage automatique sur trois
bibliothèques d'arbres, pour un résultat identique à la virgule près.
Erreur fréquente — croire qu'un rapport absent est un résultat absent
Le classement mentionne « gbdt : tuning en cours à la clôture, rapport absent ». C'est vrai pour le README ; c'est faux pour la mesure. Le chiffre dormait dans un fichier JSON. Un résultat non raconté est un résultat perdu, même quand il est enregistré — et celui-là valait la peine d'être raconté.
Les fusions successives : six itérations, six leaders¶
Le concours terminé, la campagne devient une chaîne. Chaque étape part du leader précédent, y ajoute une famille, et ne garde que ce qui améliore la validation interne.
champion v2 → final → push → squeeze → assault → lastmile → segmodel
0.2330 0.2180 0.2172 0.2154 0.2054 0.2044 0.2041
10 feats 32 f 40 f 42 f 46 f 54 f 54 f + blend
XGBoost logreg logreg logreg logreg logreg 2 logregs
L2 L2 L2 L2 L2 blendées
final fusionne les trois familles du concours sur une timeline commune : 32 features, un seul balayage chronologique, les modules des familles rejoués tels quels. Deux décisions structurantes y sont prises, toutes deux en validation interne. D'abord : les sous-ensembles « top-k par importance » perdent contre les 32 features complètes — les familles se recouvrent peu, donc on garde tout. Ensuite : la logistique L2 bat XGBoost et bat leur mélange (0.2285 contre 0.2331 et 0.2293 en validation). Le champion arboré cède la place à un modèle linéaire. Holdout : 0.2180 / 64,2 %.
push applique tout ce que la littérature suggérait : Elo à marge de manches façon FiveThirtyEight, Elo par carte mélangé moitié-moitié avec l'Elo global (l'idée du tennis par surface), pi-ratings de Constantinou & Fenton, GBDT→LR de Facebook, TrueSkill mis à jour par carte (2,1 fois plus de mises à jour), momentum, fatigue, pondération par récence. Soixante configurations chiffrées. Ce qui passe : les horloges à granularité carte, les pi-ratings, le momentum, la demi-vie de 180 jours. Ce qui échoue : la recomposition de série à partir de modèles par carte (0.2303–0.2379 : le veto attendu est trop bruité), GBDT→LR (0.2405 et plus : les feuilles n'apportent que du bruit sur une frontière linéaire), XGBoost à contraintes monotones (0.2302), le label multinomial 2-0/2-1 (0.2292). Holdout : 0.2172 / 64,0 %.
squeeze presse le cache existant à quatre endroits. Trois ne donnent rien : le head-to-head enrichi et pondéré par le recouvrement de roster (déjà couvert par les features existantes), les boxes de forme (déjà couvertes par form5 et form10), les archives hebdomadaires de classement (redondantes). Le quatrième donne tout : le rang mondial HLTV au jour du match, reconstitué depuis l'historique daté embarqué dans les pages d'équipe — 439 équipes, 42 302 points, 84 % des matchs couverts, validé à 99,8 % contre les archives hebdomadaires. Pourquoi ça marche : ce rang est une horloge externe longue durée, qui perce notre fenêtre de six mois. Holdout : 0.2154 / 64,6 %.
assault est le tournant. Trois idées, dont une écrase les autres.
- Réchauffer les horloges. Jusqu'ici, chaque rating démarrait à froid au début de la fenêtre de six mois. En étendant la fenêtre native à douze mois, les états arrivent chauds au moment du test. Effet mesuré, à lui seul : 0.2153 → 0.2081, soit −0.0072. Le plus gros gain unitaire de toute la campagne, et il n'y a pas une ligne de modélisation dedans.
- Le préchauffage externe depuis un historique de 40 023 matchs remontant à janvier 2024 (Elo à marge, Glicko et pi-ratings rejoués par nom normalisé) : −0.0018.
- L'économie par manche — pistolets, éco et anti-éco, first kills, clutchs, efficacité au dollar — extraite de 209 482 manches : −0.0017.
Combiné, choisi en validation : 0.2054 / 66,8 %, 46 features, logistique L2.
lastmile reprend les chantiers qu'assault avait dû couper. Quatre pistes, une seule survit : un indicateur « les deux équipes sont bien connues » (\(\sigma\) total du TrueSkill sous la médiane) croisé avec les sept features les plus fortes. Les autres meurent proprement : l'empilement sur familles réellement disjointes ne bat jamais le modèle plat (0.2201 seul, 0.2178 avec base, 0.2170 en mélange, contre 0.2163 pour le plat), les statistiques de carrière archivées ne couvrent que 19 % du holdout et n'apportent rien, les transformations de features et la régularisation par groupe non plus. Holdout : 0.2044 / 67,5 %.
segmodel est né d'un aveu. Le rapport de lastmile signalait honnêtement qu'un candidat — un mélange entre un modèle entraîné sur le segment « informé » et le modèle global — avait fait 0.2149 en validation, le meilleur score jamais vu, mais que la logique d'adoption automatique du script ne l'avait pas retenu. Le chantier suivant l'a repris, exploré la grille (deux définitions de segment, trois seuils, trois poids de mélange, deux jeux de features : 40 combinaisons), et retenu le mélange 75 % segment / 25 % global au seuil médian. Holdout : 0.2041 / 67,9 %. C'est le leader final.
À retenir — écrire ses regrets dans le rapport
Le dernier gain de la campagne vient d'une phrase qu'un rapport n'était pas obligé d'écrire : « ce candidat était meilleur en validation, mon script ne l'a pas promu, la passe holdout est consommée, ça reste une piste ». Sans cet aveu, segmodel n'existe pas. Consigner ce qu'on n'a pas fait est une livraison à part entière.
Une hypothèse réfutée proprement : hardcore¶
Après segmodel, une question restait ouverte : sur le segment le plus difficile — les Bo3 en ligne entre équipes établies — un modèle plus simple ferait-il mieux que le modèle riche ? L'argument est classique : moins de features, moins de variance, meilleur compromis là où le signal est faible.
L'expérience est montée dans les règles : le leader reproduit à l'identique (contrôle exact, 0.2147 en validation, 0.2041 en holdout), plus une branche « noyau dur » entraînée sur le segment seul, avec 3, 5 ou 8 features fortes, une régularisation forte, un mélange avec le leader et un rétrécissement des probabilités vers 50 %. Cent dix combinaisons, toutes en validation.
En validation, l'hypothèse tenait : la branche à trois features (TrueSkill, Elo à marge, rang HLTV) dominait — 0.2135 global contre 0.2147, et 0.2221 sur le segment dur contre 0.2238. Le simple battait bien le riche.
En holdout, une seule passe : leader 0.2041, candidat 0.2042. Sur le segment dur lui-même : leader 0.2205, candidat 0.2207. Le gain de validation ne s'est pas transféré d'un iota.
Verdict : sur-ajustement à la fenêtre de validation (531 matchs durs), pas un effet biais-variance réel. Deux sous-résultats sont tombés au passage : le rétrécissement vers 50 % dégrade (le leader n'est donc pas surconfiant sur ce segment — ses probabilités y sont déjà honnêtes), et la régularisation est à plateau (±0.0001 sur un ordre de grandeur du paramètre). La porte « modélisation » du segment dur est fermée, avec des chiffres.
La phase finale : trois chantiers, zéro survivant¶
Après hardcore, la campagne a continué encore deux heures et demie. Trois chantiers de plus, dont deux disposaient d'un vrai gain de validation. Aucun n'a produit de ligne au classement — et c'est cette série de trois qui a servi de signal d'arrêt.
ultimate — l'économie recalculée sur douze mois¶
Entre-temps, la collecte externe avait été étendue vers l'arrière : la fenêtre août 2025 → février 2026 est venue s'ajouter, portant le gisement de manches à 299 961 manches sur 6 220 matchs, avec un taux d'appariement de 89,6 % sur la période nouvellement collectée. L'idée était évidente : rejouer les huit états d'économie du leader sur ce gisement unifié.
La couverture a effectivement doublé côté entraînement — de 3 041 à 6 211 matchs renseignés sur 9 479. Mais au moment du holdout, elle passe de 964 à 969 sur 1 146. Cinq matchs.
C'est le diagnostic entier : les états d'économie étaient déjà chauds en 2026. L'unification ne change en pratique que l'étiquetage des lignes d'entraînement anciennes, où des valeurs neutres ont été remplacées par des valeurs réelles.
Résultat en validation : le leader avec l'économie unifiée fait 0.2157 contre 0.2147 pour sa reproduction exacte. Une dégradation. Le plan prévoyait de n'étendre les features (conversion de pistolets, conversion de premiers kills) qu'en cas de gain sur la base : l'extension n'a jamais été déclenchée. Un re-réglage léger du poids de mélange et du seuil de segment ne fait pas mieux non plus (meilleur 0.2146, sous le seuil de bruit).
Le candidat retenu en validation est donc le leader lui-même, et la passe unique de holdout le confirme : contrôle 0.2041 / 67,9 %, candidat identique.
Intuition — pourquoi plus de données peut dégrader
Avant l'unification, les matchs de 2025 avaient des features d'économie absentes, remplacées par une valeur neutre. Après, ils portent leurs vraies valeurs. Cela déplace les coefficients appris — le modèle passe du temps à ajuster une famille de features qui, sur la période qui compte réellement, était déjà renseignée.
Plus de données n'aide que si elles arrivent là où il en manquait au moment de l'évaluation. Ici elles sont arrivées ailleurs.
lastcard — la dernière carte, et la validation ment une fois de plus¶
Un dernier candidat restait plausible. Le TrueSkill par carte (tsm), adopté par
push, démarre froid sur la timeline du concours — alors que le préchauffage
externe avait déjà spectaculairement payé pour l'Elo à marge. On applique donc la
même recette : réchauffer le TrueSkill par carte sur les 77 890 cartes de
l'historique externe remontant à 2024.
La construction est propre : 39 929 matchs traités, 70 815 mises à jour au niveau
carte, 5 039 replis au niveau match quand aucune carte n'est exploitable, lecture
strictement antérieure au match. La nouvelle horloge (wtsm) est corrélée à
l'ancienne à 0,64 sur le jeu d'entraînement — assez différente pour espérer
apporter quelque chose.
En validation, elle apporte : ajouter wtsm_diff et wtsm_sigma_sum aux 54
features donne 0.2135 contre 0.2147 pour le leader, soit −0.0012. Le seuil de
déclenchement de la passe holdout était fixé d'avance à 0.0005 ; il est franchi
largement.
Une passe de holdout, avec contrôle : leader 0.2041, candidat 0.2044.
Verdict : infirmé. La validation mentait. Exactement le même motif que hardcore, avec la même amplitude de gain apparent (−0.0012) et le même effondrement en holdout.
La clôture : quand trois validations d'affilée se font démentir¶
Trois chantiers en fin de campagne, trois échecs, et surtout trois formes d'échec différentes :
| Chantier | Gain en validation | Résultat en holdout | Nature |
|---|---|---|---|
| hardcore | −0.0012 | +0.0001 (0.2042 vs 0.2041) | le gain ne se transfère pas |
| ultimate | +0.0010 (dégradation) | 0.2041 = contrôle exact | l'idée meurt avant le holdout |
| lastcard | −0.0012 | +0.0003 (0.2044 vs 0.2041) | le gain ne se transfère pas |
Deux candidats avaient un gain de validation net, tous deux de −0.0012, tous deux sélectionnés parmi une grille. Les deux ont été exécutés par l'unique passe de holdout, dans le même sens et avec la même ampleur.
À retenir — le plancher de bruit est un résultat mesurable
Trois chantiers consécutifs où la validation propose et où le holdout dispose : ce n'est plus de la malchance, c'est un régime. Les gains que la validation détecte encore sont désormais du bruit d'échantillonnage, et le holdout le démontre à chaque fois.
C'est le signal d'arrêt de la campagne, et il est chiffré — pas ressenti. Tant que les gains de validation survivaient au holdout (final, push, squeeze, assault, lastmile, segmodel : six fois d'affilée), il fallait continuer. Dès qu'ils ont cessé de survivre trois fois de suite, le signal extractible de nos données était épuisé.
Le message du dernier commit de la campagne le dit sans détour : « troisième fois que l'unique passe holdout tranche contre elle en fin de campagne. Toutes les avenues de modélisation avec nos données sont épuisées. »
Une fuite attrapée en écrivant le cours¶
Un dernier événement mérite sa place, parce qu'il est arrivé pendant la rédaction de ces pages et non pendant la campagne.
En documentant le chapitre sur le gradient boosting, un détail du modèle de repli de production a sauté aux yeux : son arrêt anticipé se faisait sur le holdout. Autrement dit, l'entraînement s'arrêtait au moment le plus favorable au jeu de test, ce qui est une fuite du futur en bonne et due forme.
Portée réelle : limitée. Tous les modèles à partir de la fusion final
arrêtaient déjà correctement, sur une queue temporelle du train — la leçon avait
été tirée dès l'approche stack. Seul le modèle de repli embarqué en production
portait le défaut. Il a été corrigé et réentraîné.
Erreur fréquente — croire qu'un pipeline audité est un pipeline propre
Cette fuite a survécu à une campagne entière de mesures, de contrôles de reproduction et de rapports. Elle a été trouvée par l'exercice de l'expliquer à quelqu'un d'autre.
Écrire la documentation d'un système est une forme de relecture que rien ne remplace : elle force à justifier chaque ligne au lieu de la reconnaître.
Les vingt-quatre heures, heure par heure¶
| Heure (UTC) | Événement | État du leader |
|---|---|---|
| 24/08 08 h 36 | Monte Carlo des tournois restants : il faut des probabilités | Glicko 0.2422 |
| 24/08 18 h 40 | Premier modèle appris, benchmarké contre Glicko | 0.2330 |
| 24/08 18 h 55 | L'Elo à marge entre au moteur | 0.2330 |
| 24/08 20 h 05 | Clôture du concours : six approches, trois familles | 0.2180 |
| 24/08 20 h 33 | Missions scout (sources) et push (littérature) | 0.2172 |
| 25/08 03 h 57 | Assaut final : horloges réchauffées, économie, préchauffage | 0.2054 |
| 25/08 04 h 07 | lastmile + première carte des segments | 0.2044 |
| 25/08 04 h 54 | segmodel confirmé en holdout | 0.2041 |
| 25/08 05 h 35 | Le leader passe en production dans le simulateur | — |
| 25/08 05 h 55 | Monte Carlo complet rejoué avec le nouveau modèle | — |
| 25/08 06 h 05 | Correctif : continuité des pages de résultats | — |
| 25/08 06 h 59 | hardcore : hypothèse réfutée, chiffrée | — |
| 25/08 07 h 05 | Sentinelle anti-patchwork : un cache troué lève une erreur | — |
| 25/08 07 h 41 | Correctif : fuite d'arrêt anticipé du modèle de repli | — |
| 25/08 08 h 11 | Manches unifiées sur douze mois : 299 961 manches, 6 220 matchs | — |
| 25/08 08 h 14 | ultimate : économie recalculée, infirmée en validation | 0.2041 |
| 25/08 08 h 23 | Exclusion des 13 vainqueurs discordants de l'état d'économie | — |
| 25/08 09 h 28 | lastcard : wtsm infirmé en holdout — clôture de la campagne |
0.2041 |
Un peu moins de vingt-cinq heures, du premier besoin à la clôture. La partie purement modélisation — de 18 h 40 à 09 h 28 — tient en quinze heures.
Trois incidents formateurs¶
Les trois échecs les plus coûteux de la campagne n'ont rien à voir avec les modèles. Ils portent tous sur l'infrastructure de mesure, et chacun a laissé une règle derrière lui.
Le run mort sans log¶
Le premier passage d'assault était ambitieux : grille complète, empilement par familles disjointes, mélanges par régime. Il tournait dans un terminal dont la sortie standard n'allait nulle part ailleurs. Le terminal a disparu ; le run avec. Aucune trace de ce qui avait déjà été évalué. Heures de calcul perdues, et pire : impossible de savoir lesquelles.
La reprise a été bornée — moins d'ambition, mais un journal horodaté sur disque et un rapport JSON écrit de façon incrémentale, chaque évaluation mémoïsée par son nom. Les croisements coupés (empilement par familles disjointes, mélanges par régime, carrières archivées) ont été explicitement listés comme « non testés » dans le rapport, et repris par le chantier suivant, lastmile — qui les a tous réfutés, ce qui rend la perte moins amère.
Le mécanisme de reprise se voit dans les journaux : au redémarrage, la première
ligne écrite est reprise : 40 évals déjà faites. Rien n'est recalculé.
Limite importante — un calcul long sans journal n'existe pas
La règle qui en sort n'est pas « utiliser nohup ». Elle est : tout run de plus de quelques minutes écrit son résultat après chaque étape, dans un fichier, sous une clé qui permet de reprendre. Le coût est de dix lignes ; le bénéfice est de ne jamais recommencer à zéro.
Le crash du tri de dicts¶
Le préchauffage externe rejoue 40 023 matchs historiques dans l'ordre
chronologique. Le code initial construisait des tuples
(date, match, équipe1, équipe2) et les triait tels quels.
Python trie les tuples élément par élément. Tant que les dates diffèrent, tout
va bien. Dès que deux matchs partagent la même date, Python passe au deuxième
élément pour départager — et le deuxième élément est un dictionnaire. Les
dictionnaires ne sont pas ordonnables :
TypeError: '<' not supported between instances of 'dict' and 'dict'.
Sur 40 000 matchs de listings esport, les collisions de date sont garanties : les matchs d'un même tournoi commencent à la même heure. Le crash est certain, mais il n'apparaît qu'après la phase de chargement — c'est-à-dire après plusieurs minutes de travail utile.
Le bug était latent sur les deux fenêtres de collecte : il a fallu la
collecte étendue de la fin de campagne pour que la cause racine soit corrigée à
la source, dans le collecteur lui-même (commit adfd8bc).
Le correctif tient en huit caractères, et il est visible aux endroits où ce tri existe :
bo3.sort(key=lambda x: x[0]) # le tri ne regarde QUE la date
La leçon dépasse Python : quand vous triez des enregistrements composites, nommez explicitement la clé de tri. Un tri qui « marche » parce qu'il n'y a pas encore eu d'égalité est une bombe à retardement, et elle explose au moment précis où le jeu de données grandit.
Le patchwork de pagination¶
Le troisième incident est le plus subtil, et c'est celui qui a le plus abîmé la confiance dans les mesures.
Symptôme. Un test de non-régression qui compare notre classement au classement publié tourne chaque jour. Le 24 août au soir, les journées du 19 au 22 août affichaient 0,63 / 0,69 / 1,11 / 1,10 point d'écart moyen — excellent. Le lendemain matin, sans qu'une ligne de code n'ait changé, les mêmes journées donnaient 6,75 / 6,32 / 6,09 / 4,83.
Cause. Le listing de résultats du site est paginé par décalage : la page 0 contient les 100 matchs les plus récents, la page 100 les suivants, etc. À chaque nouveau match terminé, tout glisse d'un cran. Deux pages mises en cache par deux collectes différentes ne se raccordent donc plus : une bande de matchs tombe entre les deux, sur aucune page, et devient invisible.
L'état du cache ce matin-là, reconstitué par les dates de modification : les pages 0 à 600 dataient de 06 h 02, les pages 700 à 6000 du 23 août à 18 h 23. Entre les deux générations, 35 matchs manquants, joués les 30 et 31 juillet — en plein cœur des fenêtres de six mois des journées rejouées.
Preuve. Trois éléments, dans l'ordre. D'abord la reproduction à l'identique du chiffre du matin. Ensuite une guérison accidentelle observée : le démon de collecte a redémarré à 06 h 06, sa première marche longue a réécrit les pages 0 à 6000 en une seule génération cohérente, et le même test est retombé à 1,18 — alors qu'aucune page de match ni d'événement n'avait changé entre les deux runs. Enfin, une simulation : on ampute artificiellement le listing sain de la bande de 35 matchs et on rejoue.
| Jour | Mesuré le matin | Simulé (listing amputé) | Sain (après recollage) |
|---|---|---|---|
| 19/08 | 6,75 | 6,78 | 0,63 |
| 21/08 | 6,09 | 6,09 | 1,12 |
| 22/08 | 4,83 | 4,84 | 1,12 |
La bande explique l'intégralité de l'écart, au centième près sur le 21 août.
Correctif. Une sentinelle : si deux pages consécutives du cache viennent de générations différentes et laissent un trou temporel de plus de 12 heures à plus de 14 jours du bord de la fenêtre, le calcul lève une erreur au lieu de produire un chiffre. Les deux seuils sont justifiés par la donnée : la plus longue accalmie réelle mesurée est de 9,9 heures, et à 14 jours du bord le poids d'âge borne l'impact à environ 0,5 point.
Limite importante — un instrument de mesure se corrompt en silence
Le point qui fait mal : pendant plusieurs heures, l'outil de vérification produisait des chiffres. Pas d'erreur, pas d'avertissement, 32 slots sur 32 tenus. Juste faux. On a d'abord cherché la régression dans le moteur, puis dans les données du site. Elle était dans l'état du cache — une entrée invisible qu'aucun rapport ne mentionnait.
La règle : l'état de l'entrée fait partie du résultat. Un chiffre de test n'est interprétable qu'accompagné de la description de ce qu'il a lu. Depuis, cet état est vérifié à chaque construction du jeu de données, et le calcul refuse de tourner sur un cache incohérent.
Ce que cette conduite d'expériences enseigne¶
Six éléments, en résumé, qui n'ont rien de spécifique au jeu vidéo.
Le protocole d'abord, les modèles ensuite. Le brief de quatre règles a été écrit avant la première ligne de code de modélisation. C'est lui qui rend les onze chiffres du classement comparables ; sans lui, on aurait onze résultats incomparables et aucun classement.
Paralléliser l'exploration, sérialiser la fusion. Six approches en parallèle pour découvrir les familles de signal ; puis une chaîne strictement séquentielle où chaque étape part du leader mesuré. L'exploration parallèle trouve la diversité ; la fusion séquentielle la capitalise.
Un budget de mesure, pas un tableau de bord. Une passe holdout par chantier. Cette discipline a un coût — le candidat segmodel a failli être perdu parce que la passe était consommée — et un bénéfice : quand hardcore a échoué, on a su que c'était un vrai échec et pas un artefact d'usure du jeu de test.
Reproduire le leader à chaque étape. Chaque chantier commence par re-mesurer le résultat du précédent et vérifie qu'il retombe sur le même chiffre. Assault reproduit squeeze à 0.2153 contre 0.2154 publié ; lastmile reproduit assault à 0.2054 exact ; hardcore reproduit segmodel à 0.2041 exact. Sans ce contrôle, on ne saurait jamais si une amélioration vient de l'idée ou d'un changement silencieux du pipeline.
Écrire les échecs avec leurs chiffres. GBDT→LR à 0.2405, la recomposition par carte à 0.2379, l'empilement disjoint à 0.2201, les carrières archivées à 0.2166, hardcore à +0.0001 : ces nombres valent autant que le 0.2041, parce qu'ils ferment des portes définitivement. Un échec non chiffré sera retenté.
Traiter l'infrastructure de mesure comme du code de production. Les trois incidents de cette campagne sont tous des défauts d'outillage, pas de modèle. Ils ont coûté plus cher que n'importe quelle mauvaise idée de features.
Chapitre précédent : Introduction de la partie · Chapitre suivant : Le palmarès · Glossaire