Le protocole¶
Ce chapitre est le plus important du cours. Pas le plus spectaculaire — il ne contient aucun modèle — mais celui dont dépend la validité de tous les autres.
Un chiffre de performance est une affirmation sur l'avenir : « ce modèle se trompera de cette quantité-là sur des matchs qu'il n'a jamais vus ». Rien dans le calcul du Brier ne garantit cette affirmation. Ce qui la garantit, c'est le protocole : la discipline avec laquelle on sépare ce qui a servi à construire le modèle de ce qui sert à le juger.
Trois règles ont encadré toute la campagne. Elles sont énoncées ici, puis justifiées par ce qui s'est passé quand on a été tenté de les contourner.
- Un holdout temporel intouchable, mesuré une seule fois.
- Une validation temporelle interne pour absolument toutes les décisions.
- Une intégrité temporelle stricte des features : aucune information postérieure au match ne peut entrer dans sa prédiction.
Règle 1 — le holdout temporel intouchable¶
La coupure¶
Les 42 derniers jours de données sont mis de côté. Aucun modèle n'y est entraîné, aucun hyperparamètre n'y est réglé, aucune décision n'y est prise.
La coupure est définie mécaniquement, sans intervention humaine :
| Symbole | Signification |
|---|---|
| \(t_i\) | Horodatage de début du match \(i\), en secondes |
| \(\max_i(t_i)\) | Le match exploitable le plus récent des données |
| \(86\,400\) | Secondes dans une journée |
| \(t_{\text{cut}}\) | Frontière : avant, entraînement ; à partir de là, holdout |
Le maximum est pris sur les matchs exploitables : cinq joueurs de chaque côté, marqués ranked par Valve depuis 2025, avec un vainqueur et deux identifiants d'équipe connus. C'est exactement la même définition qu'au chapitre 1, et exactement la même que celle qu'utilise le modèle de production — un détail qui a son importance, on y revient plus bas.
Résultat : 1 146 matchs de holdout, identiques pour les onze approches de la campagne.
La partition complète¶
Le train lui-même est redécoupé, pour laisser de quoi décider :
◄──────────────────── 12 mois de données ─────────────────────►
┌──────────────────────────────────┬─────────┬──────────────────┐
│ FIT │ VAL │ HOLDOUT │
│ 9 479 │ 808 │ 1 146 │
│ entraînement des candidats │ décide │ mesuré 1 fois │
└──────────────────────────────────┴─────────┴──────────────────┘
◄──────────── TRAIN = 10 287 ───────────────►◄── 42 jours ────►
▲
t_cut ─┘
le temps s'écoule de gauche à droite ; rien ne remonte le courant
Trois blocs, trois rôles strictement disjoints :
- FIT (9 479 matchs) : on y ajuste les coefficients des modèles candidats.
- VAL (808 matchs) : on y compare les candidats entre eux et on choisit.
- HOLDOUT (1 146 matchs) : on y mesure, une fois, le candidat retenu.
Le bloc TRAIN — FIT + VAL, soit 10 287 matchs — est celui sur lequel le modèle final est ré-entraîné une fois le choix arrêté.
Pourquoi temporel et pas aléatoire¶
Le découpage standard en apprentissage automatique est aléatoire : on tire 20 % des lignes au hasard. Ici, ce serait une faute grave, pour deux raisons distinctes.
Raison 1 : la fuite par l'état partagé. Les variables du modèle ne sont pas des propriétés indépendantes de chaque ligne. Ce sont des états d'équipe — rating Glicko, forme sur cinq matchs, bilan des confrontations directes — qui se propagent d'un match au suivant. Si un match de mars est dans le test et qu'un match d'avril impliquant la même équipe est dans l'entraînement, le second porte en lui le résultat du premier. Le modèle a « vu » la réponse par la bande.
Raison 2 : le réalisme du déploiement. En production, on prédit toujours vers l'avant. On ne dispose jamais d'un échantillon aléatoire du futur ; on dispose du passé, et le futur arrive. Une évaluation aléatoire répond à une question qu'on ne se pose jamais.
Intuition
Un découpage aléatoire mesure la capacité à interpoler dans une période connue. Un découpage temporel mesure la capacité à extrapoler vers une période inconnue. Seule la seconde correspond à l'usage.
Le prix à payer est réel : le holdout temporel est plus dur, et les chiffres qu'il produit sont moins flatteurs. C'est précisément ce qu'on lui demande.
« Mesuré une fois »¶
C'est la partie que la discipline rend difficile. Chaque approche disposait
d'une seule passe sur le holdout, à la toute fin de son travail — le champ
holdout_passes: 1 figure dans les rapports machine de la campagne, et il vaut
1 partout.
La raison est arithmétique. Le holdout ne cesse d'être un ensemble de test qu'au moment où une décision est prise en le regardant. Si l'on essaie dix variantes et qu'on garde la meilleure sur le holdout, on a entraîné sur le holdout, avec un canal d'information étroit (un chiffre par essai) mais réel. Le score rapporté devient alors optimiste, et d'autant plus qu'on a essayé de variantes.
Deux conséquences pratiques ont structuré la campagne.
Les tableaux de décomposition sont pré-enregistrés. L'approche assault
voulait savoir combien rapportait chacune de ses briques — fenêtre étendue,
famille de statistiques économiques, warm-up historique. Elle n'a pas mesuré les
variantes une par une sur le holdout : la liste des lignes à produire a été
figée avant, puis toutes calculées dans la même passe unique. On mesure
plusieurs choses en une fois ; on ne décide rien entre les mesures.
Les contrôles de reproduction sont exigés. Chaque nouvelle approche
re-mesure le leader précédent dans sa propre passe, et le chiffre doit tomber
exactement. lastmile retrouve le 0,2054 / 66,8 % d'assault au chiffre près ;
segmodel retrouve le 0,2044 de lastmile ; hardcore retrouve le 0,2041 de
segmodel. Ces contrôles ne sont pas de la cérémonie : un écart signalerait que
le pipeline de données a bougé, et invaliderait la comparaison.
Erreur fréquente
« J'ai regardé le holdout juste pour vérifier que le code tournait. » Le canal est ouvert dès qu'un chiffre est lu. La seule protection est procédurale : on écrit à l'avance ce qu'on va mesurer, on le mesure, on l'écrit dans un rapport horodaté, et on n'y revient pas.
Règle 2 — toute décision se prend en validation¶
Ce qu'on appelle une décision¶
Tout ce qui n'est pas déterminé par les données d'entraînement seules : le choix des variables, la force de la régularisation, la demi-vie de la pondération temporelle, l'architecture, les poids d'un mélange de modèles, les seuils d'un découpage en segments. Absolument tout cela s'arbitre sur VAL, jamais sur le holdout.
Les campagnes de la période donnent une idée du volume :
| Approche | Combinaisons évaluées | Toutes en validation |
|---|---|---|
logreg |
grille de variables × \(\lambda \in [1;\,3000]\) × cible, sur 3 replis temporels de 3 semaines | oui |
segmodel |
40 combinaisons (segment × poids de mélange × jeu de variables) | oui |
hardcore |
110 combinaisons (variables × régularisation × mélange × rétrécissement) | oui |
Aucune de ces centaines d'évaluations n'a touché les 1 146 matchs du holdout.
La validation aussi est temporelle¶
Le bloc VAL n'est pas un échantillon aléatoire du train : c'est la fin du
train, juste avant le holdout. Certaines approches vont plus loin et emploient
plusieurs replis temporels — logreg découpe trois fenêtres de trois semaines
en fin d'entraînement et moyenne les scores.
L'intérêt est double : on reproduit en petit le scénario du holdout, et on lisse le bruit d'une fenêtre unique. L'inconvénient est que 808 matchs, c'est peu. Assez peu pour que le prochain paragraphe soit possible.
Le sur-ajustement de validation, pris la main dans le sac¶
C'est l'exemple le plus instructif de toute la campagne, parce qu'il ne s'agit pas d'une erreur : c'est le protocole qui fonctionne exactement comme prévu.
L'hypothèse. Sur le segment le plus difficile — matchs en ligne, format Bo3, entre équipes bien connues des systèmes de rating — un modèle plus simple devrait mieux se comporter qu'un modèle riche. L'intuition est classique et sérieuse : quand le signal est faible, un modèle à trois variables souffre moins de variance qu'un modèle à cinquante-quatre.
Le dispositif. L'approche hardcore reproduit le leader à l'identique, puis
lui greffe une branche « noyau dur » : une régression logistique entraînée sur
la seule intersection FIT ∩ segment dur, avec \(K \in \{3, 5, 8\}\) variables
fortes, une régularisation L2 sévère, un mélange de logits avec le leader, et un
rétrécissement optionnel. Au total 110 combinaisons, toutes évaluées en
validation, sur les 531 matchs durs de VAL.
Le résultat en validation. L'hypothèse tient, et bien.
| Modèle | Brier VAL global | Brier VAL segment dur |
|---|---|---|
| Leader seul (référence) | 0,2147 | 0,2238 |
| Branche simple, \(K = 3\) | 0,2135 | 0,2221 |
| Même branche mais riche (54 variables) | 0,2148 | — |
Un gain de \(0{,}2147 - 0{,}2135 = 0{,}0012\) de Brier. Mieux : la version riche de la même branche fait moins bien, ce qui confirme le mécanisme annoncé — le simple bat le riche au sein de la branche. Tout concorde. À ce stade, il est très difficile de ne pas y croire.
Le résultat en holdout. Une passe, un chiffre.
| Modèle | Brier holdout | Log-loss | Accuracy |
|---|---|---|---|
Leader segmodel |
0,2041 | 0,5916 | 67,89 % |
Candidat hardcore |
0,2042 | 0,5923 | 67,98 % |
Le gain de \(-0{,}0012\) observé en validation devient \(+0{,}0001\) en holdout. Il n'a pas rétréci : il a disparu et changé de signe. Sur le segment dur lui-même, où l'effet était censé se produire, le leader fait 0,2205 et le candidat 0,2207.
Le diagnostic. 110 combinaisons évaluées sur 531 matchs. La meilleure d'entre elles n'a pas été sélectionnée parce qu'elle capturait un vrai compromis biais-variance, mais parce qu'elle épousait le mieux le bruit particulier de cette fenêtre de validation. C'est du sur-ajustement de validation : le choix a été payé au prix fort par la sélection elle-même.
Le verdict est enregistré tel quel dans le rapport : hypothèse infirmée, pas de ligne au classement, le leader reste en place. Un écart de 0,0001 sur 1 146 matchs n'est pas une défaite mesurable — c'est une absence d'effet.
À retenir
Sans holdout intouchable, cette expérience aurait produit une ligne de classement, un gain publié de 0,0012, et une conviction fausse installée pour le reste du projet. Le protocole n'a pas empêché l'erreur : il l'a rendue visible, ce qui est tout ce qu'on peut lui demander.
Le pendant : quand la discipline coûte cher¶
L'inverse est arrivé, et il faut le raconter aussi. L'approche lastmile avait
exploré un mélange 50/50 entre un modèle entraîné sur le segment « informé » et
un modèle global. En validation, ce candidat sortait premier, à 0,2149 —
mais la logique d'adoption automatique du script ne l'a pas retenu, et la passe
holdout de l'approche était déjà consommée.
Le rapport le consigne honnêtement comme « piste de validation non mesurée ».
L'approche suivante, segmodel, l'a reprise : elle a d'abord reproduit le 0,2149
de validation à l'identique — contrôle de reproduction — avant de mesurer sur le
holdout 0,2041, nouveau leader. Le protocole a donc coûté un cycle complet
sur un gain réel. C'est le prix, et il reste inférieur à celui d'un classement
pollué par des effets inexistants.
Règle 3 — l'intégrité temporelle des features¶
Le balayage chronologique¶
Une variable calculée pour un match ne peut contenir que de l'information antérieure à ce match. Toute violation, même minuscule, invalide la mesure — et la mesure ne s'en plaint pas : elle devient simplement meilleure, sans prévenir.
Le mécanisme qui l'assure est unique dans tout le projet : un balayage chronologique (sweep). Les matchs sont triés par date, puis parcourus une seule fois, dans l'ordre. Pour chaque match, l'ordre des opérations est inviolable :
trier les matchs par date croissante
état ← vide
pour chaque match m, du plus ancien au plus récent :
┌─────────────────────────────────────────┐
│ 1. LIRE l'état ──► features(m) │ ← ne voit que le passé
│ 2. enregistrer (date, features, label) │
│ 3. METTRE À JOUR l'état avec le résultat│ ← le futur, pour les suivants
└─────────────────────────────────────────┘
inverser 1 et 3 = fuite du futur = mesure mensongère
L'état contient tout ce qui est cumulatif : les ratings Glicko et Elo à marge, les dix derniers résultats de chaque équipe, la date du dernier match, le dernier cinq aligné, le nombre de matchs joués, le bilan des confrontations directes de chaque paire. Il est lu avant, écrit après. Jamais l'inverse.
Un seul balayage sert à tout : construction du jeu d'entraînement, du jeu de validation, du holdout, et production de l'état « d'aujourd'hui » que le simulateur Monte Carlo clone pour ses tirages. C'est le même code, donc la même définition — il n'y a pas de version « évaluation » et de version « production » susceptibles de diverger.
Les fuites classiques, et comment elles ont été évitées¶
La fuite par agrégat global. Calculer la moyenne d'une statistique de joueur sur toute la période, puis l'utiliser comme variable pour un match de janvier : la moyenne contient décembre. Le balayage l'interdit par construction, puisque l'agrégat est maintenu incrémentalement.
La fuite par donnée non datée. Le classement mondial HLTV est publié
hebdomadairement, mais une page d'équipe consultée aujourd'hui montre le rang
d'aujourd'hui. L'utiliser pour un match d'il y a quatre mois serait une fuite
massive : le rang actuel intègre le résultat de ce match. L'approche squeeze a
donc reconstruit un historique daté du rang, validé à 99,8 % contre les
archives hebdomadaires, et n'utilise pour chaque match que le rang en vigueur ce
jour-là.
La fuite par état simulé. Le cas est plus subtil et concerne le Monte Carlo. Quand le simulateur joue un bracket, certaines variables doivent vivre à travers les matchs simulés — l'équipe qui gagne son quart doit arriver en demi-finale avec un rating mis à jour. D'autres ne le peuvent pas : des statistiques de joueur ou un rang mondial ne se recalculent pas à partir d'un résultat inventé. Le modèle de production tranche explicitement : seules les horloges de force réellement simulables évoluent pendant un tirage, les autres variables sont gelées à leur valeur du jour, et cet arbitrage est documenté dans le code plutôt qu'implicite.
La fuite par frontière de holdout. Elle a failli arriver pour une raison purement logistique. Les données étant collectées en continu, deux approches lancées à deux jours d'écart ne voient pas le même « dernier match », donc pas la même coupure à 42 jours, donc pas le même holdout — et leurs Brier ne sont plus comparables. La parade a été de figer le holdout sur les 1 146 matchs d'une approche de référence : six matchs disparus des listings ont été réinjectés depuis un cache antérieur, six matchs postérieurs à la coupure ont été exclus, et une reproduction à froid a confirmé que l'approche de référence retrouvait bien son chiffre (0,2153 contre 0,2154 publié).
Limite importante
Une fuite du futur ne se manifeste par aucune erreur, aucune exception, aucun avertissement. Elle se manifeste par un excellent score. C'est la raison pour laquelle l'intégrité temporelle ne peut pas être vérifiée après coup à partir des résultats : un modèle qui fuit ressemble exactement à un modèle qui marche. Elle doit être garantie par la structure du code — ici, l'ordre lire / enregistrer / mettre à jour du balayage.
La fragilité des instruments de mesure¶
Il reste une catégorie de problème que les trois règles ne couvrent pas : le cas où l'instrument lui-même est faussé. La campagne en a offert un cas d'école, sur un instrument de contrôle qui vérifiait quotidiennement que le moteur de classement reproduisait bien les chiffres officiels de Valve.
Le symptôme¶
Un soir, l'instrument rend des écarts moyens propres pour quatre journées consécutives : 0,63 / 0,69 / 1,11 / 1,10 point, très en dessous du seuil d'alerte fixé à 3,0.
Le lendemain matin, les mêmes journées, rejouées avec le même code, donnent 6,75 / 6,32 / 6,09 / 4,83. Un facteur dix. Et pourtant tous les contrôles structurels passent : 32 slots sur 32 attribués correctement. Re-télécharger les pages de classement n'y change rien.
Un instrument de mesure qui rend deux réponses différentes à la même question, sans que rien de mesuré n'ait bougé, ne mesure plus rien.
La cause¶
Elle n'a rien à voir avec les modèles. Elle tient entièrement à la pagination du site source.
La liste des résultats est paginée par décalage : page 0 = matchs 0 à 99, page 100 = matchs 100 à 199, et ainsi de suite. Or la liste est vivante : à chaque nouveau match terminé, tout glisse d'un cran vers le fond. Deux pages mises en cache à deux moments différents ne se raccordent donc plus.
collecte A (23/08 à 18h23) collecte B (25/08 à 06h02)
┌──────────────────────────┐ ┌──────────────────────────┐
│ page 700 : matchs 700-799│ │ page 600 : matchs 600-699│
└──────────────────────────┘ └──────────────────────────┘
▲ ▲
│ 35 matchs joués entre les │
└─────── deux collectes glissent ──┘
dans le vide
cache résultant = pages 0-600 (génération B) + pages 700-6000 (génération A)
──► une bande de 35 matchs n'apparaît sur AUCUNE page : elle a disparu
Le cache était donc un patchwork de générations. Les 35 matchs manquants avaient été joués fin juillet, en plein cœur des fenêtres de six mois des journées rejouées, avec un poids d'ancienneté d'environ 0,88 — presque le poids maximal. Ils comptaient beaucoup, et ils étaient invisibles.
S'ajoutait un détail cruel : un contrôle de continuité existait déjà, mais il ne s'exécutait qu'en ligne. Les rejeux hors-ligne — c'est-à-dire tous les instruments de mesure — avalaient le patchwork en silence.
La preuve¶
Le diagnostic n'a pas été validé par raisonnement mais par trois faits indépendants.
Reproduction. Relancer l'instrument sur la journée du 21/08 redonne 6,09, exactement la valeur suspecte du matin. Le problème est déterministe, donc attaquable.
Auto-guérison observée. Le démon de collecte a redémarré à 06:06:37, et son initialisation effectue une marche longue sur six mois. Cette marche a réécrit les pages 0 à 6000 en une génération cohérente entre 06:07:00 et 06:09:30. À 06:11, le même test rend 1,18. Aucune page de match ni d'événement n'avait changé entre les deux exécutions : seule la liste avait bougé.
Simulation contrôlée. On prend la liste saine du jour, on lui retire artificiellement la bande de 35 matchs identifiée, et on rejoue :
| Journée | Mesuré le matin | Simulé (liste \(-\) bande) | 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 |
L'amputation reproduit le symptôme au centième près sur le 21/08. La bande explique l'intégralité du résidu — il ne reste rien à expliquer, ce qui est la signature d'une cause racine et non d'une cause partielle.
Le correctif¶
Une sentinelle a été ajoutée au chemin hors-ligne. Si deux pages consécutives proviennent 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, la journée est refusée bruyamment avec un message « cache incomplet », au lieu d'être mesurée faux.
Les deux seuils sont justifiés par des mesures, pas choisis au jugé : la plus longue accalmie réelle jamais observée dans le calendrier CS2 est de 9,9 heures (d'où les 12 heures), et à 14 jours du bord de fenêtre la pondération d'ancienneté borne l'impact d'une bande manquante à environ 0,5 point. Une seconde frontière de générations, découverte plus loin dans la pagination, a été recollée à la main ; coût réseau total de l'enquête : 13 requêtes.
Validation finale : 21 journées consécutives rejouées, pire écart moyen 1,30 point contre un seuil de 3,0, 32 slots sur 32 partout, et les quatre journées litigieuses retrouvent leurs chiffres propres : 0,63 / 0,71 / 1,12 / 1,12.
La leçon¶
À retenir
Un chiffre produit par un instrument n'est interprétable qu'accompagné de l'état de ses entrées. Ici, la génération des pages en cache faisait partie de l'entrée au même titre que les résultats des matchs — sauf que personne ne l'avait écrit nulle part, donc personne ne la vérifiait.
La correction durable n'est pas d'avoir recollé le cache : c'est d'avoir rendu cet état vérifié automatiquement à chaque construction. Une hypothèse implicite sur les données est une hypothèse qui finira par être fausse un matin, en silence.
Cette histoire concerne un instrument de contrôle, pas le concours de modèles. Mais elle vaut pour lui aussi. Le holdout de 1 146 matchs est lui-même reconstruit depuis ce cache. Si un patchwork avait affecté sa fenêtre, tous les Brier de la campagne auraient été comparables entre eux — même erreur pour tout le monde — mais faux dans l'absolu, sans qu'aucune règle du protocole ne le détecte. C'est pour cette raison que les contrôles de reproduction d'une approche à l'autre sont exigés au chiffre près : ils sont le seul garde-fou contre un déplacement silencieux du sol.
Récapitulatif opérationnel¶
Ce que le protocole exige, sous forme de liste de contrôle :
| Question | Réponse exigée |
|---|---|
| Où le modèle est-il entraîné ? | FIT (9 479), ou TRAIN (10 287) pour le modèle final |
| Où les choix sont-ils faits ? | VAL (808), ou replis temporels internes au FIT |
| Où la performance est-elle mesurée ? | HOLDOUT (1 146), une seule passe |
| Le découpage est-il temporel ? | Oui, coupure à \(\max(t_i) - 42\) jours |
| Les features voient-elles le futur ? | Non — balayage chronologique, lire puis mettre à jour |
| Le leader précédent est-il re-mesuré ? | Oui, au chiffre près, dans la même passe |
| Les mesures sont-elles pré-enregistrées ? | Oui, la liste des lignes est figée avant la passe |
| L'état des données est-il vérifié ? | Oui, sentinelle de continuité à chaque construction |
Et ce qu'il faut retenir des deux histoires de ce chapitre : un gain de \(0{,}0012\) obtenu sur 110 essais en validation peut valoir exactement zéro en holdout, et un instrument peut rendre deux réponses différentes à la même question pour une raison qui n'a aucun rapport avec ce qu'il mesure. Les deux ont le même remède — rendre explicite ce qui était implicite, et le vérifier automatiquement.
Cette partie est terminée : le vocabulaire, les métriques et les règles du jeu sont posés. Pour un rappel de terme, voir le glossaire ; pour la nature du livrable, le chapitre 1 ; pour les formules, le chapitre 2.