Prédire une durée de traitement depuis un CSV avec XGBoost

200 tâches synthétiques de test : RMSE de 3,39 secondes pour XGBoost et 64,93 secondes pour la moyenne.

Comment estimer le temps d’une tâche avant son exécution ? Ce projet Python part d’un CSV de tâches déjà terminées, entraîne XGBoost et exporte les prédictions sur des tâches gardées à l’écart. Vous obtenez les données, le script complet et les résultats de référence pour reproduire chaque étape.

Le scénario est pédagogique : les 1 000 tâches et leurs durées sont entièrement synthétiques. Elles n’ont pas été relevées sur un serveur. L’objectif est d’apprendre à construire et vérifier le parcours CSV → modèle → résultat, sans promettre une précision sur une infrastructure réelle.

Télécharger le projet complet gratuit (ZIP, 26 Ko) : script, CSV, dépendances, guide de lancement, générateur des données et résultats de référence. Aucun compte n’est nécessaire pour ce téléchargement.

Exécuté le 14 septembre 2026 avec Python 3.12.14, XGBoost 3.4.1, scikit-learn 1.9.1 et NumPy 2.5.3. Le calcul utilise le CPU et fonctionne localement après installation des dépendances.

Ce que le programme va produire

Le CSV contient des caractéristiques connues avant une tâche et sa durée observée après exécution. Le script apprend sur 600 lignes, utilise 200 autres lignes pour l’arrêt anticipé et mesure le résultat sur les 200 dernières lignes réservées au test.

Le dossier de sortie contient predictions_test.csv, rapport.json et modele.json. Vous pourrez retrouver chaque tâche grâce à son identifiant et comparer XGBoost à un premier repère : prédire toujours la durée moyenne de l’apprentissage.

Ce cas complète le guide XGBoost en régression. Le guide explique l’algorithme ; ce projet s’occupe aussi du contrat de fichier, des erreurs de saisie et de la conservation des résultats.

1. Comprendre les colonnes du CSV

Colonne Signification Rôle
id Identifiant unique d’une tâche Traçabilité ; exclu des entrées du modèle.
taille_mo Taille des données d’entrée en Mo Variable explicative positive.
nb_fichiers Nombre de fichiers à traiter Entier positif.
compression Compression prévue, 0 ou 1 Variable explicative binaire.
workers Nombre de workers configurés avant la tâche Entier positif.
duree_s Durée observée en secondes Cible à apprendre, jamais une entrée.

Le fichier fourni commence ainsi :

id,taille_mo,nb_fichiers,compression,workers,duree_s
T0001,423.260,52,0,8,23.799
T0002,923.823,49,1,2,101.062
T0003,1527.678,38,1,2,170.201
T0004,1842.213,129,0,2,164.168

Le générateur invente une relation avec interactions entre taille, compression et workers, plus du bruit. Il produit des tailles entre 10 et 2 000 Mo, 1 à 150 fichiers et un nombre de workers parmi 1, 2, 4 et 8. Il ne simule pas fidèlement un logiciel particulier. La formule complète se trouve dans generer_donnees.py ; le CSV est déjà prêt à utiliser.

Le lecteur exige ces six colonnes, des identifiants uniques, au moins 50 lignes et des valeurs numériques finies. Il refuse les cellules vides et les champs supplémentaires. Le seuil de 50 lignes sert uniquement à garder des ensembles non minuscules dans cet exercice ; il ne prouve pas qu’un jeu réel est suffisant.

2. Installer les dépendances et lancer le script

Extrayez le ZIP et ouvrez PowerShell dans le dossier contenant predire_durees.py. Avec Python installé, lancez :

python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
.\.venv\Scripts\python.exe predire_durees.py taches_synthetiques.csv resultats

Sous macOS ou Linux, créez l’environnement avec python3 -m venv .venv, puis utilisez .venv/bin/python à la place de .\.venv\Scripts\python.exe. Une connexion sert à installer les paquets ; le script ne transmet ensuite pas votre fichier à une API.

Le nom resultats doit désigner un nouveau dossier. Pour un second essai, utilisez resultats_2. Cette règle protège les résultats précédents et le CSV d’origine.

3. Garder un test indépendant de l’apprentissage

Le découpage aléatoire fixe est adapté aux observations indépendantes de notre générateur. La validation guide l’arrêt anticipé, tandis que le test reste en dehors de fit. La liste des variables FEATURES exclut explicitement l’identifiant et la durée cible.

Le modèle de référence DummyRegressor apprend uniquement la moyenne des durées de l’apprentissage. Il sert à vérifier si les variables permettent de faire mieux qu’une constante. C’est un point de départ, pas un comparatif complet : une régression linéaire et d’autres méthodes mériteraient une évaluation séparée avant un choix métier.

XGBoost dispose d’un plafond de 800 arbres. La configuration d’arrêt anticipé laisse 30 itérations sans amélioration de la RMSE de validation. Dans l’exécution fournie, 757 arbres sont retenus. Le script ne choisit aucun paramètre en regardant les scores du test.

4. Consulter le script complet

Le code ci-dessous est identique au fichier de l’archive. Vous pouvez le déplier pour étudier le contrôle des entrées, la séparation des indices et l’export :

Afficher le script Python complet
"""Projet pédagogique : évaluer une durée en secondes sur un CSV étiqueté."""
import argparse
import csv
import hashlib
import json
from pathlib import Path

import numpy as np
import sklearn
import xgboost
from sklearn.dummy import DummyRegressor
from sklearn.metrics import mean_absolute_error, root_mean_squared_error
from sklearn.model_selection import train_test_split
from xgboost import XGBRegressor

FEATURES = ("taille_mo", "nb_fichiers", "compression", "workers")
COLUMNS = ("id", *FEATURES, "duree_s")


def lire_csv(path):
    with Path(path).open(encoding="utf-8-sig", newline="") as f:
        reader = csv.DictReader(f)
        if reader.fieldnames is None or len(reader.fieldnames) != len(COLUMNS) or set(reader.fieldnames) != set(COLUMNS):
            raise ValueError("Colonnes attendues : " + ", ".join(COLUMNS))
        rows = list(reader)
    if len(rows) < 50:
        raise ValueError("Au moins 50 lignes sont nécessaires pour cet exercice.")
    ids, values, seen = [], [], set()
    for line, row in enumerate(rows, start=2):
        if None in row or any(v is None or not v.strip() for v in row.values()):
            raise ValueError(f"Ligne {line} : cellule vide ou nombre de champs incorrect.")
        ident = row["id"]
        if ident.strip() in seen:
            raise ValueError(f"Ligne {line} : identifiant dupliqué.")
        seen.add(ident.strip())
        try:
            vals = [float(row[c]) for c in (*FEATURES, "duree_s")]
        except ValueError as exc:
            raise ValueError(f"Ligne {line} : nombre invalide.") from exc
        size, files, compression, workers, duration = vals
        if not np.isfinite(vals).all() or min(size, files, workers, duration) <= 0:
            raise ValueError(f"Ligne {line} : valeurs finies et positives requises.")
        if files != int(files) or workers != int(workers) or compression not in (0, 1):
            raise ValueError(f"Ligne {line} : fichiers/workers entiers, compression 0 ou 1.")
        ids.append(ident)
        values.append(vals)
    values = np.asarray(values, dtype=np.float64)
    return ids, values[:, :-1], values[:, -1]


def separer(n):
    trainval, test = train_test_split(np.arange(n), test_size=0.2, random_state=42)
    train, val = train_test_split(trainval, test_size=0.25, random_state=42)
    return train, val, np.sort(test)


def entrainer(X, y, train, val):
    baseline = DummyRegressor(strategy="mean").fit(X[train], y[train])
    model = XGBRegressor(
        objective="reg:squarederror", n_estimators=800, learning_rate=0.05,
        max_depth=3, min_child_weight=5, subsample=0.9, colsample_bytree=1.0,
        reg_lambda=1.0, tree_method="hist", n_jobs=1, random_state=42,
        eval_metric="rmse", early_stopping_rounds=30,
    )
    model.fit(X[train], y[train], eval_set=[(X[val], y[val])], verbose=False)
    return model, baseline


def mesures(y, pred):
    return {"mae_s": float(mean_absolute_error(y, pred)),
            "rmse_s": float(root_mean_squared_error(y, pred))}


def executer(source, destination):
    source, destination = Path(source), Path(destination)
    if destination.exists():
        raise FileExistsError("Choisissez un nouveau dossier de sortie.")
    ids, X, y = lire_csv(source)
    train, val, test = separer(len(y))
    model, baseline = entrainer(X, y, train, val)
    pred, ref = model.predict(X[test]), baseline.predict(X[test])
    report = {
        "donnees": "Origine à documenter ; le CSV fourni est entièrement synthétique.",
        "source_sha256": hashlib.sha256(source.read_bytes()).hexdigest(),
        "features_ordre": list(FEATURES), "target": "duree_s",
        "effectifs": {"apprentissage": len(train), "validation": len(val), "test": len(test)},
        "ids_separation": {k: [ids[i] for i in indices] for k, indices in
                           [("apprentissage", train), ("validation", val), ("test", test)]},
        "arbres_retenus": model.best_iteration + 1,
        "iterations_executees": len(model.evals_result()["validation_0"]["rmse"]),
        "test": {"moyenne_apprentissage": mesures(y[test], ref), "xgboost": mesures(y[test], pred)},
        "baseline_constante_s": float(baseline.constant_.ravel()[0]),
        "versions": {"numpy": np.__version__, "scikit-learn": sklearn.__version__, "xgboost": xgboost.__version__},
    }
    destination.mkdir(parents=True, exist_ok=False)
    model.save_model(destination / "modele.json")
    (destination / "rapport.json").write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    with (destination / "predictions_test.csv").open("x", encoding="utf-8", newline="") as f:
        writer = csv.writer(f)
        writer.writerow(["id", "duree_reelle_s", "prediction_xgb_s", "prediction_baseline_s", "erreur_xgb_s"])
        for i, prediction, constant in zip(test, pred, ref):
            writer.writerow([ids[i], f"{y[i]:.6f}", f"{prediction:.6f}", f"{constant:.6f}", f"{prediction-y[i]:.6f}"])
    print("Lignes :", len(y), "| apprentissage / validation / test :", len(train), len(val), len(test))
    for name, metrics in report["test"].items():
        print(f"{name} : MAE {metrics['mae_s']:.3f} s ; RMSE {metrics['rmse_s']:.3f} s")
    print("Arbres retenus :", report["arbres_retenus"])
    print("Résultats :", destination)
    return report


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("csv", type=Path)
    parser.add_argument("sortie", type=Path)
    args = parser.parse_args()
    try:
        executer(args.csv, args.sortie)
    except (ValueError, OSError) as exc:
        parser.exit(2, f"Erreur : {exc}\n")

5. Lire le résultat obtenu

Lignes : 1000 | apprentissage / validation / test : 600 200 200
moyenne_apprentissage : MAE 52.538 s ; RMSE 64.925 s
xgboost : MAE 2.595 s ; RMSE 3.386 s
Arbres retenus : 757

Sur ces 200 tâches de test, l’erreur absolue moyenne de XGBoost est d’environ 2,595 secondes. Sa RMSE atteint 3,386 secondes ; les erreurs importantes y pèsent davantage. La référence constante a ici une RMSE de 64,925 secondes. Ces résultats illustrent une relation synthétique construite pour être apprenable. Ils ne démontrent ni un gain économique ni une précision attendue en production.

Prédictions et durées synthétiques des 200 tâches de test ; comparaison des RMSE 64,93 et 3,39 secondes.
À gauche, les tâches de test et la diagonale d’une prédiction exacte. À droite, les RMSE sur ces mêmes tâches. Graphique produit à partir des fichiers de référence.

Le fichier exporté commence ainsi :

id,duree_reelle_s,prediction_xgb_s,prediction_baseline_s,erreur_xgb_s
T0011,146.102000,147.857452,94.452168,1.755452
T0024,110.169000,115.237511,94.452168,5.068511
T0026,136.848000,128.107864,94.452168,-8.740136
T0031,115.985000,117.729912,94.452168,1.744912

erreur_xgb_s est la prédiction moins la durée réelle : une valeur positive signifie une surestimation. Les 200 lignes conservent l’ordre relatif du CSV source. Il n’y a pas de prédictions exportées pour les lignes d’apprentissage ou de validation, ce qui évite de mélanger leurs résultats avec le test.

Le rapport garde les identifiants des trois ensembles, les métriques, la moyenne de référence, l’ordre des variables, les versions et l’empreinte du CSV. La sauvegarde du modèle permet de le recharger avec XGBRegressor.load_model. L’ordre des variables doit rester identique lors de futures prédictions ; les réglages complets du programme restent documentés dans le script.

6. Comprendre les erreurs courantes

  • « Colonnes attendues » : vérifiez la première ligne et le séparateur virgule. Un export avec des points-virgules doit être converti explicitement.
  • « Nombre invalide » : utilisez un point décimal et retirez les unités textuelles des cellules numériques.
  • « Cellule vide » : corrigez l’entrée ou définissez un traitement des absences avant d’adapter ce projet. Le script ne supprime aucune ligne silencieusement.
  • « Identifiant dupliqué » : chaque tâche doit pouvoir être retrouvée sans ambiguïté. Deux exécutions distinctes ont besoin de deux identifiants.
  • « Nouveau dossier de sortie » : changez le nom de destination pour préserver l’essai précédent. Après une erreur d’écriture, un dossier peut être incomplet ; choisissez aussi un nouveau nom.

7. Trois exercices pour aller plus loin

  1. Ouvrez predictions_test.csv et retrouvez la tâche dont l’erreur absolue est la plus élevée. Comparez ses caractéristiques à celles du reste du test.
  2. À partir de rapport.json, vérifiez que les listes d’identifiants d’apprentissage, de validation et de test sont disjointes et couvrent les 1 000 tâches.
  3. Préparez un nouveau jeu final tenu à l’écart, puis comparez une autre configuration ou une régression linéaire. Choisissez les réglages sur la validation et utilisez le nouveau test seulement à la fin.

Le projet a été contrôlé notamment sur la conservation des identifiants, la reproductibilité, le refus des données invalides et le rechargement du modèle. Un test change fortement les durées du jeu de test : cela ne modifie ni les prédictions apprises ni l’itération retenue. L’archive extraite reproduit aussi les sorties de référence.

Adapter le projet à de vraies tâches

Avant d’utiliser des historiques réels, définissez le moment exact où la prédiction doit être disponible. Excluez les informations connues seulement après la tâche. Si plusieurs lignes proviennent du même serveur ou d’exécutions proches dans le temps, un découpage aléatoire peut donner une image trop favorable : réservez une période future ou des groupes réellement séparés.

Vérifiez aussi les tâches annulées, les changements de matériel et les nouvelles charges. Ce script attend une durée observée pour évaluer chaque ligne ; il ne fournit pas encore une commande destinée aux tâches futures sans étiquette. Une extension devra définir leur schéma, contrôler les plages et surveiller les erreurs après exécution.

Enfin, une relation entre workers et durée ne prouve pas qu’ajouter des workers accélérera une tâche réelle : charge concurrente, disque et mémoire peuvent intervenir. La régression prédit dans un contexte observé ; elle ne remplace pas une expérience permettant de mesurer l’effet d’un changement.

Continuer le parcours CSV et Python

Télécharger les fichiers gratuits de ce projet, puis retrouver les explications des paramètres dans le guide XGBoost régression.

Pour apprendre à examiner des lignes inhabituelles sans cible à prédire, essayez le projet CSV avec Isolation Forest. Notre sélection de ressources Python et de livres par niveau complète les prérequis.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. Découvrez comment les données de vos commentaires sont traitées.