Un test peut influencer XGBoost sans jamais apparaître dans les données d’apprentissage. Il suffit de l’utiliser dans eval_set pour choisir le nombre d’arbres par arrêt anticipé. Le modèle est alors réglé avec des informations issues du test ; le score calculé sur ce même lot n’est plus une évaluation indépendante.
Le guide XGBoost régression présente le protocole recommandé. Cette expérience compare explicitement deux démarches sur dix jeux synthétiques fixés à l’avance. Elle conserve aussi un lot d’audit indépendant, afin de ne pas confondre le score annoncé avec le comportement sur de nouvelles données.
Exécuté le 18 septembre 2026 avec Python 3.12.14, XGBoost 3.4.1, scikit-learn 1.9.1 et NumPy 2.5.3. Télécharger le script, les résultats par graine et les tests. Aucun compte ni service payant n’est nécessaire.
Deux protocoles et un audit commun
| Lot | Lignes | Démarche correcte | Erreur pédagogique |
|---|---|---|---|
| Apprentissage | 600 | Construire les arbres | Construire les arbres |
| Validation | 200 | Choisir le nombre d’arbres | Non utilisée |
| Test annoncé | 200 | Évaluer après sélection | Choisir le nombre d’arbres puis annoncer le score sur le même lot |
| Audit | 800 | Évaluation supplémentaire indépendante | Évaluation supplémentaire indépendante |
Les quatre lots sont disjoints. Les données sont indépendantes et identiquement distribuées dans ce générateur ; un découpage par blocs convient donc ici. Cette justification ne s’étend pas automatiquement à un journal daté ou à plusieurs lignes du même client. Dans ces cas, la séparation doit respecter le temps ou les groupes.
Les paramètres, la plage de graines 0 à 9 et les tailles sont communs aux deux démarches. Seul le lot utilisé pour l’arrêt anticipé change. L’audit n’intervient dans aucune sélection. Il sert ici à expliquer l’évaluation ; répéter des choix en observant son résultat finirait également par le contaminer.
Pourquoi eval_set participe à la sélection
Dans cette expérience, XGBoost peut construire jusqu’à 1 200 arbres. Il s’arrête après 40 tours sans amélioration de la RMSE sur le lot de contrôle. La documentation de l’interface scikit-learn de XGBoost décrit ce fonctionnement et l’utilisation de la meilleure itération pour prédire avec l’estimateur.
Le nombre d’arbres retenu fait partie du modèle final. Si les cibles du test peuvent changer ce nombre, elles influencent la prédiction. Renommer le tableau X_test ou ne pas l’utiliser comme premier argument de fit ne protège donc pas l’évaluation. La règle de séparation des choix et du test est rappelée dans les pièges de fuite de données documentés par scikit-learn.
Exécuter la comparaison complète
Dans le dossier extrait, installez les dépendances avec python -m pip install -r requirements.txt, puis lancez python validation_xgboost.py. Le programme ci-dessous régénère les données ; il n’accède à aucun fichier personnel.
La branche test_consulte est volontairement incorrecte pour annoncer une performance de test indépendante. Elle est conservée uniquement pour l’exercice comparatif. Pour votre projet, utilisez la branche correcte, puis ouvrez le test après avoir figé vos décisions.
"""Expérience pédagogique : arrêt anticipé sur validation ou test consulté."""
import numpy as np
from sklearn.datasets import make_friedman1
from sklearn.metrics import root_mean_squared_error
from xgboost import XGBRegressor
def donnees(seed):
X, y = make_friedman1(n_samples=1800, n_features=10, noise=4.0, random_state=seed)
# Lignes iid synthétiques. En temporel ou par individu, adapter la séparation.
return X[:600], y[:600], X[600:800], y[600:800], X[800:1000], y[800:1000], X[1000:], y[1000:]
def entrainer(X_train, y_train, X_arret, y_arret, seed):
modele = XGBRegressor(n_estimators=1200, max_depth=3, learning_rate=0.05,
objective="reg:squarederror", eval_metric="rmse",
early_stopping_rounds=40, tree_method="hist",
n_jobs=1, random_state=seed)
modele.fit(X_train, y_train, eval_set=[(X_arret, y_arret)], verbose=False)
return modele
def experience(seeds=range(10)):
lignes = []
for seed in seeds:
Xt, yt, Xv, yv, Xe, ye, Xa, ya = donnees(seed)
for nom, X_arret, y_arret in [("correcte", Xv, yv), ("test_consulte", Xe, ye)]:
modele = entrainer(Xt, yt, X_arret, y_arret, seed)
rapporte = float(root_mean_squared_error(ye, modele.predict(Xe)))
audit = float(root_mean_squared_error(ya, modele.predict(Xa)))
lignes.append(dict(seed=seed, methode=nom, arbres=modele.best_iteration + 1,
rmse_rapporte=rapporte, rmse_audit=audit,
ecart=audit - rapporte))
return lignes
if __name__ == "__main__":
lignes = experience()
print("10 jeux synthétiques fixés à l'avance ; RMSE moyenne (unités de la cible)")
for nom in ("correcte", "test_consulte"):
selection = [r for r in lignes if r["methode"] == nom]
print(f"{nom} : RMSE rapportée={np.mean([r['rmse_rapporte'] for r in selection]):.3f} ; "
f"audit={np.mean([r['rmse_audit'] for r in selection]):.3f} ; "
f"écart moyen={np.mean([r['ecart'] for r in selection]):+.3f}")
print("Un écart positif indique une erreur rapportée plus faible que celle de l'audit.")
print("Une réalisation peut donner l'écart inverse : le protocole reste contaminé.")
Les chiffres obtenus, sans sélectionner la meilleure graine
10 jeux synthétiques fixés à l'avance ; RMSE moyenne (unités de la cible)
correcte : RMSE rapportée=4.461 ; audit=4.394 ; écart moyen=-0.067
test_consulte : RMSE rapportée=4.432 ; audit=4.383 ; écart moyen=-0.049
Un écart positif indique une erreur rapportée plus faible que celle de l'audit.
Une réalisation peut donner l'écart inverse : le protocole reste contaminé.
La démarche incorrecte affiche une RMSE moyenne légèrement plus faible sur le lot consulté : 4,432 au lieu de 4,461. Sur l’audit, les moyennes sont respectivement 4,383 et 4,394. Ces écarts sont modestes et descriptifs. Ils ne démontrent ni une supériorité statistique ni une amélioration réelle grâce à l’utilisation du test.

Dans ce résultat, l’erreur moyenne d’audit est même inférieure à l’erreur rapportée pour les deux démarches. C’est compatible avec la variabilité d’échantillonnage : le biais de sélection ne garantit pas un écart de signe donné dans une petite expérience. La contamination se prouve par le rôle du test dans la sélection, pas en exigeant que chaque graphique présente un grand écart positif.
Le fichier resultats-par-graine.csv fournit les vingt lignes de résultat, le nombre d’arbres retenu et les deux RMSE. Nous ne retenons pas uniquement une graine spectaculaire. La RMSE reste exprimée dans l’unité de la cible artificielle : aucun gain de temps ni gain financier métier n’en découle.
Un test qui révèle la dépendance aux mauvaises cibles
Les tests livrés dans test_validation.py remplacent les cibles du lot annoncé par des valeurs décalées de 1 000 pour la graine 0. Dans la démarche correcte, réentraîner sur l’apprentissage avec arrêt sur validation conserve exactement les mêmes prédictions d’audit. Le score de test change, puisqu’on l’évalue contre d’autres cibles.
Dans la démarche contaminée, les cibles modifiées pilotent l’arrêt : la meilleure itération et les prédictions changent. Ce contrôle montre directement le chemin par lequel le test entre dans le choix du modèle. Il n’est pas nécessaire d’inventer une catastrophe de performance pour expliquer l’erreur.
Lancez python -m unittest test_validation -v. Les six tests couvrent notamment la séparation des lots, un arrêt anticipé effectif, la meilleure itération utilisée pour prédire, les calculs de RMSE et cette perturbation des cibles.
Les autres façons de contaminer un test
- Comparer de nombreux paramètres sur le test et garder celui qui gagne.
- Calculer une imputation ou une sélection de variables sur toutes les lignes avant la séparation.
- Utiliser une colonne connue seulement après l’événement à prédire.
- Revoir sans cesse le protocole après lecture du score final, puis continuer d’appeler ce lot « test ».
Pour travailler proprement, séparez les rôles dès le début, ajustez le prétraitement sur l’apprentissage, choisissez les paramètres sur validation ou dans une validation croisée adaptée, puis évaluez la version figée. Si vous décidez de changer le modèle après le test, annoncez cette décision et prévoyez une nouvelle évaluation indépendante.
Exercice et suite du parcours
Vous passez eval_set=[(X_validation, y_validation), (X_test, y_test)] à un modèle avec arrêt anticipé. Le test est-il protégé parce que la validation figure en premier ?
Voir la réponse
Non. Avec cette interface et plusieurs lots, le dernier lot sert à l’arrêt anticipé. Ici le test influence donc le nombre d’arbres. Fournissez seulement le lot de validation pour cette sélection et réservez le test à l’évaluation finale.
Le projet CSV de prédiction de durées applique la séparation apprentissage/validation/test et exporte les prédictions avec leurs identifiants. La page de ressources Python propose les téléchargements gratuits pour poursuivre.

