feat(v35): ML Lab — le chiffre cesse de se flatter lui-même - #53
Conversation
Deux défauts de méthode dans du code livré, plus les deux ajouts qui en découlent. 1) LE GAGNANT ÉTAIT ÉLU SUR LE JEU DE TEST. Le leaderboard triait neuf modèles sur la métrique calculée sur test et couronnait sorted[0] : prendre le maximum de neuf tirages sur ~180 lignes biaise le chiffre vers le haut. Il y a désormais un troisième split — la validation est taillée dans le train (64/16/20 aux ratios par défaut), la sélection se fait sur validation, et la ligne du champion annonce les deux chiffres et l'écart : « choisi sur la validation à 0,974, obtient 0,917 sur le jeu de test intact ». Les indices de test sont identiques à l'octet près à ce que la même config produisait avant V35 — vérifié par un test — donc segments, seuils, incertitude et comparaison de lot voient exactement les mêmes lignes. Sous 60 lignes utilisables, le troisième split est refusé par son nom. Le classement vit maintenant dans UN module (ranking.ts) : leaderboard, historique, comparaison de runs, rapport HTML et sélection automatique du modèle inspecté. C'est précisément ce qui a cassé en cours de route — un quatrième site triait encore sur test et ouvrait un modèle différent de celui couronné. 2) LA DÉCOUPE ÉTAIT TOUJOURS ALÉATOIRE, MÊME SUR DONNÉES DATÉES. Découpe chronologique (les plus anciennes entraînent, lignes sans date exploitable écartées et comptées) et découpe par groupe (aucun groupe des deux côtés), proposées quand une colonne s'y prête et ANNONCÉES dans l'info du run. 3) DÉTECTEUR DE FUITE PRÉDICTIVE. V6 attrapait les colonnes qui MAPPENT sur la cible ; celui-ci attrape la colonne seulement prédictive — un stump à une colonne ajusté sur train, mesuré sur validation. Une colonne seule qui lit la cible à 99 % s'affiche en avertissement cuivre avec son score, jamais comme une victoire. 4) CLASSEMENT ROBUSTE À LA DEMANDE. Validation croisée 5×2 sur train+validation (pipeline réajusté dans chaque pli, jeu de test jamais touché) : moyenne, dispersion, et combien de fois le premier a réellement battu le second — « 10 plis sur 10 : l'ordre est stable » ou « 6 sur 10 : traitez-les comme à égalité ». DÉFAUT EXPOSÉ ET NOMMÉ PAR LA VAGUE. Avec le train plus petit, le Naive Bayes gaussien sur ~150 variables TF-IDF sature à exactement 0/1 : l'occlusion de V24 mesurait alors exactement zéro pour chaque mot et la carte disparaissait — ce qui se lit « aucun mot ne compte », et c'est faux. Mesuré (2 probabilités distinctes sur 48 lignes de test, contre 48 pour la régression logistique et le gradient boosting), la carte se refuse maintenant par son nom et indique un modèle capable de répondre. Les tests V25 qui encodaient l'arithmétique à deux voies sont mis à jour en préservant leur intention (le cap compte toujours les lignes utilisables ; les tailles sont ajustées pour que les caps par famille mordent encore). Vérifié : 369 tests unitaires, 65 e2e, format:check, lint (2 avertissements préexistants sur main), typecheck, build avec coquilles prérendues. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UKw6oNC8iZ9Kn7q6x4qom4
There was a problem hiding this comment.
🟡 Changes recommended
The UI can currently offer a group split option that the trainer explicitly refuses (fewer than 3 groups), which can hard-fail training and put the lab into a generic error state.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR upgrades the ML Lab’s evaluation methodology (V35) to prevent selection-on-test bias by introducing a three-way split and centralizing ranking/selection logic across the UI surfaces, while adding announced non-random splits, a predictive leakage warning, and an on-demand robust (5×2) cross-validation ranking.
Changes:
- Introduces a three-way split (train/validation/test), ranks on validation when available, and reports champion val→test gap via a shared
ranking.ts. - Adds announced chronological and group splitting modes plus a predictive single-column leakage detector.
- Adds an on-demand 5×2 robust ranking workflow (worker protocol, UI panels, artifacts) and makes word-effects “refuse by name” when probabilities are saturated.
File summaries
| File | Description |
|---|---|
| src/locales/fr.json | Adds FR strings for validation/test display, split modes, leak warnings, robust ranking, and word-effects refusal text. |
| src/locales/en.json | Adds EN strings for validation/test display, split modes, leak warnings, robust ranking, and word-effects refusal text. |
| src/features/ml/worker-protocol.ts | Extends worker request/response union with robust ranking messages and payload typing. |
| src/features/ml/train/v35.test.ts | New unit tests freezing V35 behavior (3-way split compatibility, splits, leakage scan, ranking, robust rank). |
| src/features/ml/train/types.ts | Extends training/result/insights/summary types with split choice, validation metrics, leak warnings, and words refusal. |
| src/features/ml/train/trainer.ts | Implements validation split, non-random split modes, leak scan integration, and propagates new summary/result fields. |
| src/features/ml/train/trainer.test.ts | Updates existing trainer test to account for (train+validation+test) usable-row accounting. |
| src/features/ml/train/sampling.test.ts | Updates sampling tests for the 3-way split and adjusted caps behavior. |
| src/features/ml/train/robust.ts | Adds robust 5×2 CV ranking implementation and result structure. |
| src/features/ml/train/ranking.ts | Centralizes ranking/crowning logic (validation-first) and champion gap computation. |
| src/features/ml/train/pipeline.text.test.ts | Updates word-effects tests for new return shape and adds saturated-model refusal coverage. |
| src/features/ml/train/leakage.ts | Adds predictive leakage scan using a one-column stump scored on validation. |
| src/features/ml/train/insights.ts | Changes wordEffects to return {words, refusal} and propagates refusal into insights payload. |
| src/features/ml/projects/types.ts | Adds robust ranking artifact to persisted run artifacts. |
| src/features/ml/projects/report.ts | Uses shared ranking rule in HTML report and adds leak + champion-gap notes. |
| src/features/ml/projects/compare.ts | Uses shared ranking rule so compare crowns the same model as the leaderboard while comparing test metrics. |
| src/features/ml/lab-store.ts | Adds robust ranking state/actions and passes split choice through to worker training/tuning/curve/robust calls. |
| src/features/ml/data/parse.worker.ts | Adds robust ranking execution and cancellation to the worker. |
| src/features/ml/components/TrainPanel.tsx | Adds split-mode selector and robust-rank panel to the ML Lab UI. |
| src/features/ml/components/RunsHistory.tsx | Uses shared ranking rule to pick the displayed “best model” consistently. |
| src/features/ml/components/RunArtifactsView.tsx | Renders robust ranking artifact in stored/shared run artifact view. |
| src/features/ml/components/RobustRankPanel.tsx | New UI panel to run/cancel robust ranking and display results. |
| src/features/ml/components/LeaderboardTable.tsx | Displays validation-ranked primary metric (with test beside it), leak warnings, and champion gap line. |
| src/features/ml/components/insights/WordEffects.tsx | Shows an explicit refusal card when word-effects are impossible due to saturated probabilities. |
| src/features/ml/components/insights/InsightsSection.tsx | Renders WordEffects when words exist or when refusal is present. |
| README.md | Updates project principles to reflect validation selection + test reporting, split modes, leak warning, and saturation refusal. |
| PLAN.md | Updates V35 “delivered” record and reflows the plan table accordingly. |
| e2e/text.spec.ts | Updates E2E expectations for word-effects refusal on saturated model and switching to a graded-probability model. |
| e2e/split.spec.ts | New E2E coverage for validation selection/test reporting, robust ranking, leakage warning, and chronological split announcement. |
Review details
- Files reviewed: 29/29 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| ...profiles | ||
| .filter( | ||
| (p) => p.type === 'id' && p.name !== target && p.cardinality < p.rowCount - p.missingCount, | ||
| ) | ||
| .map((p) => ({ mode: 'group' as const, column: p.name })), |
| import { TEST_RATIO, TRAIN_SEED, useLabStore } from '@/features/ml/lab-store'; | ||
| import { VALIDATION_RATIO } from '@/features/ml/train/trainer'; | ||
| import type { SplitChoice } from '@/features/ml/train/types'; |
V35 lancée sur ta commande. Deux défauts de méthode dans du code livré, plus les deux ajouts qui en découlent.
1. Le gagnant était élu sur le jeu de test
Le leaderboard triait neuf modèles sur la métrique calculée sur test et couronnait
sorted[0]. Prendre le maximum de neuf tirages sur ~180 lignes biaise le chiffre annoncé vers le haut.Il y a désormais un troisième split : la validation est taillée dans le train (64/16/20 aux ratios par défaut), la sélection se fait sur validation, et la ligne du champion annonce les deux chiffres et l'écart :
Garantie de compatibilité vérifiée par un test : les indices de test sont identiques à l'octet près à ce que la même config produisait avant V35. Segments, seuils, incertitude et comparaison de lot voient exactement les mêmes lignes qu'avant. Sous 60 lignes utilisables, le troisième split est refusé par son nom et le labo classe sur test comme avant.
Le classement vit maintenant dans un seul module (
ranking.ts), utilisé par le leaderboard, l'historique, la comparaison de runs, le rapport HTML et la sélection automatique du modèle inspecté.2. La découpe était toujours aléatoire, même sur données datées
Découpe chronologique (les plus anciennes entraînent, les plus récentes testent ; lignes sans date exploitable écartées et comptées) et découpe par groupe (aucun groupe des deux côtés — le même client dans train et test est la même fuite). Proposées quand une colonne s'y prête, et annoncées dans l'info du run.
3. Détecteur de fuite prédictive
V6 attrapait les colonnes qui mappent sur la cible. Celui-ci attrape la colonne seulement prédictive : un stump à une colonne ajusté sur train, mesuré sur validation. Une colonne seule qui lit la cible à ≥ 99 % s'affiche en avertissement cuivre avec son score mesuré — jamais comme une victoire.
4. Classement robuste à la demande
Validation croisée 5×2 sur train+validation (pipeline réajusté dans chaque pli, jeu de test jamais touché) : moyenne, dispersion, et combien de fois le premier a réellement battu le second — « 10 plis sur 10 : l'ordre est stable » ou « 6 sur 10 : traitez-les comme à égalité ».
Un défaut que la vague a exposé, et nommé
Avec le train plus petit, le Naive Bayes gaussien sur ~150 variables TF-IDF sature à exactement 0/1. L'occlusion de V24 mesurait alors exactement zéro pour chaque mot et la carte des effets par mot disparaissait en silence — ce qui se lit « aucun mot ne compte », et c'est faux.
Mesuré avant de conclure : 2 probabilités distinctes sur 48 lignes de test, contre 48 pour la régression logistique et le gradient boosting. La carte se refuse maintenant par son nom et indique un modèle capable de répondre.
J'ai aussi trouvé et corrigé une incohérence que j'avais introduite en cours de route : un quatrième site de classement triait encore sur test et ouvrait un modèle différent de celui couronné.
Tests mis à jour, pas contournés
Cinq tests V25 encodaient l'arithmétique du split à deux voies. Leur intention est préservée : le cap compte toujours les lignes utilisables (train + validation + test), et les tailles des jeux sont ajustées pour que les caps par famille mordent encore.
Vérifications
npm run testnpx playwright testnpm run format:checknpm run lintmain(badge.tsx,button.tsx)npm run typechecknpm run buildGenerated by Claude Code