feat(v36): ML Lab — les manques qu'on avait délibérément laissés ouverts - #54
Conversation
Chaque point avait été descopé par son nom dans une vague antérieure, pas oublié. Les livrer ensemble garde les descopes visibles au lieu de les laisser devenir permanents en silence. 1) DÉSÉQUILIBRE DE CLASSES, descopé nommément en V16. Deux mécanismes, chacun NOMMÉ par famille plutôt que caché derrière un mot : la perte est pondérée là où la perte nous appartient (régression logistique, gradient boosting — le gradient ET la hessienne sont mis à l'échelle, car ne pondérer que le gradient gonfle les valeurs de feuilles au lieu de rééquilibrer), et un rééchantillonnage équilibré seedé sert aux familles ml-cart (arbre, forêt) qui n'acceptent aucun poids. La minorité est sur-échantillonnée jusqu'à la taille de la majorité, jamais l'inverse : équilibrer en rognant la classe commune jette de vraies observations pour corriger un ratio. Désactivé par défaut — sur une cible équilibrée ça ne change rien, et un bouton qui ne fait rien est pire que pas de bouton. 2) LA MÉTRIQUE DE CLASSEMENT DEVIENT UN CHOIX. Accuracy et RMSE étaient imposées, ce qui est le mauvais critère sur un problème déséquilibré : un modèle qui ne prédit jamais la classe rare peut dominer un classement par accuracy et être inutile. Classer sur F1, rappel, précision ou ROC-AUC change réellement l'ordre ; un modèle incapable de produire la métrique choisie descend en dernier plutôt que d'être écarté. 3) SEUILS MULTICLASSES, ouverts depuis V16, lus en un-contre-tous. Ce que le panneau refuse de laisser croire : ce n'est pas une règle de décision multiclasse complète — deux classes peuvent dépasser leur seuil et rien ici ne dit laquelle l'emporte. Il le dit. 4) ENSEMBLE DES MEILLEURS : la moyenne du top 3, construite à partir de modèles déjà entraînés. La baseline n'en est jamais membre, les membres sont choisis par la règle de classement de V35, et les membres probabilistes sont préférés — une mesure en cours de route a montré l'ensemble gagnant sur iris avec un simple vote parce que k-NN était dans le top 3, ce qui fermait en silence les panneaux seuil, calibration et effets par mot sur le champion. Refusés explicitement : une dixième famille de modèles, un mode AutoML, le deep learning tabulaire. Tests dont les comptes de lignes encodaient l'ancien zoo mis à jour en explicitant le pourquoi (8 familles + ensemble ; 7 + ensemble en régression). Vérifié : 387 tests unitaires, 69 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
There are confirmed correctness issues that can crash or misrepresent leaderboard output (non-finite delta formatting) and an inconsistency where summary.ensemble can be recorded even when the ensemble build failed.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR delivers the V36 ML Lab feature set by extending the training pipeline, worker protocol, UI panels, and tests to support class-imbalance handling, user-selectable leaderboard ranking metrics, one-vs-rest multiclass threshold analysis, and a “top-3” ensemble model built from already-trained families.
Changes:
- Add optional class balancing (loss weighting for logistic/GBDT; seeded balanced resampling for tree/forest) and expose imbalance signals in run summaries and the UI.
- Make the leaderboard rank metric user-selectable (accuracy/F1/recall/precision/ROC-AUC for classification; RMSE/MAE/R² for regression) while keeping ranking logic centralized.
- Add one-vs-rest multiclass threshold analysis and introduce an ensemble model (top 3, probabilistic-first) added to results/artifacts and surfaced across UI + i18n + tests.
File summaries
| File | Description |
|---|---|
| src/locales/fr.json | Adds V36 UI strings (ranking metric, ensemble, weighting, one-vs-rest threshold copy). |
| src/locales/en.json | English equivalents for new V36 UI strings. |
| src/features/ml/worker-protocol.ts | Extends threshold-analysis request with optional focusClass. |
| src/features/ml/train/v36.test.ts | New unit tests covering weighting, ranking metric ordering, ensemble planning/building, multiclass thresholds. |
| src/features/ml/train/types.ts | Adds RankingMetric, classWeighting config, and summary fields for imbalance + ensemble. |
| src/features/ml/train/trainer.ts | Computes class weights, emits ensemble result, and records imbalance/ensemble in summary. |
| src/features/ml/train/trainer.test.ts | Updates expected model counts and asserts ensemble presence/behavior. |
| src/features/ml/train/threshold-analysis.ts | Generalizes threshold analysis to multiclass via one-vs-rest (focusClass). |
| src/features/ml/train/ranking.ts | Adds metric-direction map, rankable metrics, and metric-based sorting/gap logic. |
| src/features/ml/train/models.ts | Introduces weighted fitting via resampling for ml-cart families; passes weights into GBDT. |
| src/features/ml/train/gbdt.ts | Adds row-weighted gradients+hessians and weighted prior for binary booster; supports per-class weights. |
| src/features/ml/train/ensemble.ts | New module to plan/build ensemble (probabilities/vote/mean) from trained models. |
| src/features/ml/train/class-weight.ts | New module implementing balanced weights, imbalance detection, and seeded balanced resampling. |
| src/features/ml/lab-store.ts | Adds UI state/actions for class weighting, ranking metric, and threshold focus class. |
| src/features/ml/data/parse.worker.ts | Passes focusClass through to threshold analysis in the worker. |
| src/features/ml/components/TrainPanel.tsx | Adds class-weighting toggle (with imbalance indicator). |
| src/features/ml/components/ThresholdPanel.tsx | Adds multiclass selector + one-vs-rest explanatory note. |
| src/features/ml/components/LeaderboardTable.tsx | Adds rank-metric selector, imbalance hint, weighting + ensemble run-info display. |
| src/features/ml/components/Leaderboard.tsx | Wires rank-metric state into LeaderboardTable. |
| README.md | Updates stated principles and test counts to include V36 behavior. |
| PLAN.md | Reformats/updates plan section to mark V36 delivered and adjust ordering text. |
| e2e/train.spec.ts | Updates expected leaderboard row counts to include ensemble. |
| e2e/projects.spec.ts | Updates expected stored/shared run leaderboard counts to include ensemble. |
| e2e/offline.spec.ts | Updates expected offline leaderboard row count to include ensemble. |
| e2e/imbalance.spec.ts | New e2e coverage for ranking-metric choice, ensemble, weighting announcement, and multiclass thresholds. |
Review details
Suppressed comments (1)
src/features/ml/components/LeaderboardTable.tsx:96
rankingValue()can return ±Infinity when a model can’t produce the selected ranking metric (e.g. AUC with no probabilities). Indelta(), that propagates intovalue.toFixed(3), which throws aRangeErroron non-finite numbers and can crash the leaderboard render.
Guard against non-finite deltas and render an em dash instead.
const value = higherWins
? rankingValue(result, metric) - rankingValue(baseline, metric)
: rankingValue(baseline, metric) - rankingValue(result, metric);
const sign = value > 0 ? '+' : '';
return `${sign}${value.toFixed(3)}`;
- Files reviewed: 25/25 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.
| imbalanced: majorityShare(trainY, classes.length) >= IMBALANCE_THRESHOLD, | ||
| }), | ||
| ...(weights !== undefined && { classWeighting: 'balanced' as const }), | ||
| ...(plan !== null && { ensemble: plan }), |
| // V36: rank on the chosen metric — accuracy is the wrong criterion on an | ||
| // imbalanced target, and the order genuinely changes with the choice. | ||
| const metric = rankMetric ?? undefined; | ||
| const sorted = sortResults(results, taskType, metric); |
V36 lancée sur ta commande. Chaque point avait été descopé par son nom dans une vague antérieure, pas oublié — les livrer ensemble garde les descopes visibles au lieu de les laisser devenir permanents en silence.
1. Déséquilibre de classes (descopé nommément en V16)
Deux mécanismes, chacun nommé par famille plutôt que caché derrière un mot :
Deux décisions que je veux souligner :
Désactivé par défaut : sur une cible équilibrée ça ne change rien, et un bouton qui ne fait rien est pire que pas de bouton. Le run annonce la part de la classe majoritaire et le leaderboard le signale au-delà de 60 %.
2. La métrique de classement devient un choix
Accuracy et RMSE étaient imposées — le mauvais critère sur un problème déséquilibré, où un modèle qui ne prédit jamais la classe rare peut dominer le classement et être inutile. Classer sur F1, rappel, précision ou ROC-AUC change réellement l'ordre. Un modèle incapable de produire la métrique choisie descend en dernier plutôt que d'être écarté : il a tourné, la table le dit.
Le classement reste dans le module unique de V35, donc leaderboard, historique, comparaison, rapport HTML et modèle auto-inspecté bougent ensemble.
3. Seuils multiclasses (ouverts depuis V16)
Lus en un-contre-tous : on choisit une classe, on la score contre toutes les autres, mêmes courbes PR et de calibration. Ce que le panneau refuse de laisser croire : ce n'est pas une règle de décision multiclasse complète — deux classes peuvent dépasser leur seuil et rien ici ne dit laquelle l'emporte. Il le dit, plutôt que d'impliquer une règle qu'il ne fournit pas.
4. Ensemble des meilleurs
La moyenne du top 3, construite à partir de modèles déjà entraînés — une passe de plus sur le jeu de test, rien d'autre. La baseline n'en est jamais membre (moyenner un prédicteur constant tire vers la classe majoritaire), et les membres sont choisis par la règle de classement de V35.
Un défaut trouvé par la mesure en cours de route : l'ensemble gagnait sur iris avec un simple vote, parce que k-NN était dans le top 3 et n'a pas de probabilités — ce qui fermait en silence les panneaux seuil, calibration et effets par mot sur le champion. Les membres probabilistes sont désormais préférés ; le vote ne sert que si moins de deux modèles savent exprimer une confiance.
Refusés explicitement
Une dixième famille de modèles, un mode AutoML « on s'occupe de tout », le deep learning tabulaire — les trois sont nommés dans le PLAN avec leur raison.
Vérifications
npm run testnpx playwright testnpm run format:checknpm run lintmainnpm run typechecknpm run buildLes tests dont les comptes de lignes encodaient l'ancien zoo sont mis à jour en explicitant le pourquoi (8 familles + ensemble ; 7 + ensemble en régression), pas contournés.
Generated by Claude Code