Prédire le surbooking aérien avec le Machine Learning : le processus complet

 

Machine Learning · Azure · Revenue Management

Comment une compagnie aérienne peut-elle savoir exactement combien de sièges survendre ? Ce guide déroule le processus complet — cadrage, choix de plateforme cloud, données, entraînement, évaluation, optimisation et industrialisation — sur un cas d’usage réel.

3–15 %de no-shows selon les routes
× 3,3coût d’un refus vs siège vide
13étapes détaillées
~20 minde lecture

1. Le problème métier

Sur un vol donné, une partie des passagers ayant réservé ne se présente pas à l’embarquement. On les appelle les no-shows. Selon les routes, les saisons et les typologies de clientèle, ce taux oscille généralement entre 3 % et 15 %.

🪑 Ne pas survendre Des sièges partent vides. Manque à gagner direct sur chaque rotation. 🚫 Survendre trop Refus d’embarquement, indemnisations UE 261, image de marque dégradée. 🎯 Survendre juste Revenu net maximisé, risque quantifié et maîtrisé. C’est l’objectif du modèle.
Le surbooking n’est pas un pari, c’est un arbitrage entre deux coûts asymétriques.

Le surbooking est donc un problème d’optimisation sous incertitude. Et le cœur de cette incertitude est une question de prédiction : combien de personnes vont réellement se présenter sur ce vol, à cette date, sur cette route ? C’est exactement le type de question auquel le Machine Learning répond bien.

2. Pourquoi le Machine Learning plutôt qu’une règle métier ?

Historiquement, le yield management fonctionnait avec des règles fixes : « sur la route CDG–LYS en semaine, on survend de 6 % ». Cette approche a trois limites.

  • Elle est grossière. Un taux moyen par route écrase la différence entre un tarif Flex remboursable acheté par un consultant et un billet promo non modifiable acheté six mois à l’avance. Le premier a un risque de no-show dix fois supérieur.
  • Elle ne s’adapte pas. Grèves, vacances scolaires, événements sportifs, météo : les règles statiques ne suivent pas.
  • Elle ne capte pas les interactions. Tarif, délai de réservation, canal de vente, correspondance et historique passager interagissent de manière non linéaire. Aucun humain ne peut écrire à la main les milliers de conditions correspondantes.
DU PASSAGER À LA DÉCISION 1 · PRÉDICTION une proba par réservation PNR ABC123 · Flex0.31 PNR DEF456 · Promo0.02 PNR GHI789 · Corpo0.18 …… 2 · AGRÉGATION distribution au niveau vol E[shows] = 172 ± 6 Poisson binomiale 3 · OPTIMISATION minimisation du coût espéré Capacité avion 180 sièges AUTORISATION DE VENTE 188 sièges (+8)
Le modèle ne décide jamais du surbooking : il produit des probabilités que la couche d’optimisation transforme en décision commerciale.
◆ Le bon cadrage

Ce n’est pas « prédire le nombre de sièges à survendre » — ce serait de la régression sur une cible qu’on n’observe jamais directement. C’est une classification binaire par passager (se présentera / ne se présentera pas) dont on exploite la probabilité. C’est le point que la majorité des projets ratent au démarrage.

3. Choisir sa plateforme : Azure, AWS ou Google ?

Les trois hyperscalers proposent une plateforme de Machine Learning managée couvrant l’ensemble du cycle de vie. Voici comment elles se comparent sur ce cas d’usage précis.

  Azure Machine Learning AWS SageMaker Google Vertex AI
AutoML Intégré, très bon sur données tabulaires Autopilot AutoML Tables
Feature Store Managed Feature Store Feature Store (le plus mature) Vertex AI Feature Store
Pipelines Azure ML Pipelines + Azure DevOps / GitHub Actions natif SageMaker Pipelines Vertex AI Pipelines (Kubeflow)
Dérive Data Drift Monitors intégrés Model Monitor Model Monitoring
Intégration SI Excellente si Microsoft 365 / Fabric / Power BI / Entra ID Excellente si écosystème AWS Excellente si BigQuery
Gouvernance Responsible AI Dashboard, RBAC Entra ID, Private Link IAM, VPC endpoints IAM, VPC-SC

Notre choix pour cet article : Azure Machine Learning. Pour quatre raisons pragmatiques : la majorité des compagnies européennes ont déjà un SI Microsoft où vivent les données de réservation ; le Responsible AI Dashboard apporte une analyse d’équité réglementairement utile ; l’intégration native avec Power BI évite de construire une couche de restitution ad hoc ; et la gouvernance Entra ID / Private Link simplifie la conformité RGPD.

✦ À retenir

Ce choix n’est pas exclusif. Le code Python ci-dessous est portable à 95 % vers SageMaker ou Vertex AI — seul l’enrobage change (SDK d’orchestration, format de déploiement). Ne laissez jamais le choix de plateforme précéder le cadrage du problème.

4. L’architecture cible

PSS / GDS Amadeus, Sabre Réservations, historique Azure Data Factory · batch quotidien Data Lake Gen2 bronze → silver → gold pseudonymisation ici Feature Store features versionnées Azure ML Workspace Pipelines d’entraînement Experiments & MLflow Model Registry Drift Monitors Managed Endpoint scoring batch nocturne Private Link uniquement Moteur d’optimisation Monte-Carlo + coûts métier → niveau d’autorisation Power BI / outil RM analyste = décision finale
Architecture de référence sur Azure. Notez la séparation stricte entre le modèle (probabilités) et le moteur de décision (coûts métier).
◆ Point d’architecture

Le modèle ML ne décide pas du surbooking. Il produit des probabilités. Une couche distincte les transforme en décision commerciale en intégrant les coûts. Séparer les deux est essentiel : les coûts d’indemnisation changent bien plus souvent que la physique du no-show.

5. Les données : ce qu’il faut et où le trouver

Granularité

Une ligne = une réservation (PNR passenger) sur un segment de vol donné, observée à l’instant de décision.

Les variables candidates

Famille Variables
Réservation Classe et famille tarifaire · prix payé · remboursabilité · modifiabilité · booking lead time · canal de vente (direct, appli, agence, OTA, GDS) · nombre de modifications du PNR · taille du groupe
Voyage Route · durée · heure de départ · jour de la semaine · aller simple ou aller-retour · correspondance en amont · compagnie opérante vs marketing
Passager Statut fidélité · historique de no-show sur 24 mois · nombre de vols sur la période
Contexte Vacances scolaires · jours fériés · événements majeurs · météo prévue à l’origine · taux de remplissage au moment de la prédiction
▲ Piège n°1 — la fuite de données

C’est l’erreur qui tue les projets ML en production. Toute variable non disponible au moment de la décision doit être exclue. Fuites classiques ici : le statut d’enregistrement en ligne, le nombre de bagages enregistrés, le taux de remplissage final du vol.

Règle d’or : construisez le jeu d’entraînement en vous plaçant strictement à l’instant de décision (par exemple J-1 à 18h00) et n’utilisez que ce qui était connu à cet instant.

▲ Piège n°2 — le déséquilibre des classes

Si 8 % des passagers sont no-show, un modèle qui prédit toujours « show » atteint 92 % d’exactitude pour une utilité nulle. L’accuracy est ici une métrique à bannir — voir la section 8.

6. Préparation des données

Le script de feature engineering, à exécuter comme composant d’un pipeline Azure ML.

 features.py
import pandas as pd
import numpy as np

def build_features(bookings: pd.DataFrame,
                   pax_history: pd.DataFrame,
                   calendar: pd.DataFrame) -> pd.DataFrame:
    """
    Construit le jeu de features au point de décision (J-1, 18h00 UTC).
    Aucune colonne postérieure à ce timestamp ne doit entrer ici.
    """
    df = bookings.copy()

    # --- Temporel ---
    df["booking_lead_days"] = (
        df["departure_datetime"] - df["booking_datetime"]
    ).dt.days.clip(0, 365)

    df["dep_hour"]      = df["departure_datetime"].dt.hour
    df["dep_dayofweek"] = df["departure_datetime"].dt.dayofweek
    df["is_weekend"]    = df["dep_dayofweek"].isin([5, 6]).astype(int)

    # Encodage cyclique : l'heure 23 est proche de l'heure 0
    df["dep_hour_sin"] = np.sin(2 * np.pi * df["dep_hour"] / 24)
    df["dep_hour_cos"] = np.cos(2 * np.pi * df["dep_hour"] / 24)

    # --- Tarifaire ---
    df["is_refundable"] = df["fare_rules"].str.contains("REF", na=False).astype(int)
    df["is_changeable"] = df["fare_rules"].str.contains("CHG", na=False).astype(int)
    df["price_per_km"]  = df["fare_paid"] / df["route_distance_km"].replace(0, np.nan)
    df["is_corporate"]  = (df["sales_channel"] == "CORPORATE").astype(int)

    # --- Historique passager (fenêtre glissante, sans fuite) ---
    hist = pax_history[
        pax_history["flight_date"] < df["departure_datetime"].min()
    ]
    agg = (hist.groupby("pax_id")
                .agg(hist_flights=("pax_id", "size"),
                     hist_noshows=("is_noshow", "sum"))
                .reset_index())
    agg["hist_noshow_rate"] = agg["hist_noshows"] / agg["hist_flights"]

    df = df.merge(agg, on="pax_id", how="left")
    df["hist_flights"]     = df["hist_flights"].fillna(0)
    df["hist_noshow_rate"] = df["hist_noshow_rate"].fillna(-1)   # -1 = inconnu

    # --- Contexte et charge du vol ---
    df = df.merge(calendar, left_on=df["departure_datetime"].dt.date,
                  right_on="date", how="left")
    df["is_holiday"] = df["is_holiday"].fillna(0).astype(int)
    df["load_factor_at_decision"] = (
        df["seats_sold_at_decision"] / df["capacity"]
    )

    return df

Sur la partition train/test, un point crucial : ne faites jamais de split aléatoire.

 split.py
# CORRECT : split temporel — reproduit les conditions réelles
train = df[df["departure_date"] <  "2025-10-01"]
valid = df[(df["departure_date"] >= "2025-10-01") &
           (df["departure_date"] <  "2026-01-01")]
test  = df[df["departure_date"] >= "2026-01-01"]

# INCORRECT : train_test_split(df, random_state=42)
# → des réservations du même vol se retrouvent des deux côtés,
#   le modèle "voit" le futur, les scores explosent artificiellement.

7. Entraînement du modèle sur Azure ML

Quel algorithme ?

Sur des données tabulaires hétérogènes avec beaucoup de variables catégorielles, le gradient boosting reste l’état de l’art. Le deep learning n’apporte rien ici et coûte plus cher. Notre recommandation : LightGBM, pour sa vitesse et sa gestion native des catégories et des valeurs manquantes.

 train.py
import argparse, mlflow, joblib
import lightgbm as lgb
import pandas as pd
from sklearn.metrics import roc_auc_score, average_precision_score, brier_score_loss
from sklearn.calibration import CalibratedClassifierCV

def main(args):
    mlflow.autolog()

    train = pd.read_parquet(args.train_data)
    valid = pd.read_parquet(args.valid_data)

    FEATURES = [c for c in train.columns
                if c not in ("is_noshow", "pnr", "pax_id", "departure_date")]
    CATEGORICAL = ["route", "fare_class", "sales_channel",
                   "aircraft_type", "loyalty_tier"]

    for c in CATEGORICAL:
        train[c] = train[c].astype("category")
        valid[c] = valid[c].astype("category")

    model = lgb.LGBMClassifier(
        objective="binary",
        n_estimators=2000,
        learning_rate=0.03,
        num_leaves=64,
        min_child_samples=200,
        subsample=0.8,
        colsample_bytree=0.8,
        reg_lambda=1.0,
        scale_pos_weight=1.0,   # ne pas gonfler : cela casserait la calibration
        random_state=42,
    )

    model.fit(
        train[FEATURES], train["is_noshow"],
        eval_set=[(valid[FEATURES], valid["is_noshow"])],
        eval_metric="average_precision",
        categorical_feature=CATEGORICAL,
        callbacks=[lgb.early_stopping(100), lgb.log_evaluation(100)],
    )

    # Calibration : indispensable, on veut des PROBABILITÉS fiables
    calibrated = CalibratedClassifierCV(model, method="isotonic", cv="prefit")
    calibrated.fit(valid[FEATURES], valid["is_noshow"])

    p = calibrated.predict_proba(valid[FEATURES])[:, 1]
    mlflow.log_metrics({
        "auc_roc": roc_auc_score(valid["is_noshow"], p),
        "auc_pr":  average_precision_score(valid["is_noshow"], p),
        "brier":   brier_score_loss(valid["is_noshow"], p),
    })

    mlflow.sklearn.log_model(calibrated, "model",
                             registered_model_name="noshow-predictor")

if __name__ == "__main__":
    p = argparse.ArgumentParser()
    p.add_argument("--train_data"); p.add_argument("--valid_data")
    p.add_argument("--model_output")
    main(p.parse_args())
 submit_job.py
from azure.ai.ml import MLClient, command, Input, Output
from azure.identity import DefaultAzureCredential

ml_client = MLClient(DefaultAzureCredential(),
                     subscription_id="<sub-id>",
                     resource_group_name="rg-airline-ml",
                     workspace_name="aml-revenue-mgmt")

job = command(
    code="./src",
    command=("python train.py --train_data ${{inputs.train}} "
             "--valid_data ${{inputs.valid}} "
             "--model_output ${{outputs.model}}"),
    inputs={
        "train": Input(type="uri_file", path="azureml:noshow_train:8"),
        "valid": Input(type="uri_file", path="azureml:noshow_valid:8"),
    },
    outputs={"model": Output(type="uri_folder")},
    environment="azureml://registries/azureml/environments/sklearn-1.5/labels/latest",
    compute="cpu-cluster-d8s",
    display_name="noshow-lgbm-v8",
    experiment_name="overbooking",
)

ml_client.jobs.create_or_update(job)

8. Évaluation : les bonnes métriques

Oubliez l’accuracy. Avec 8 % de no-shows, elle est structurellement trompeuse.

Métrique Ce qu’elle mesure Cible indicative
AUC-PR Discrimination sur une classe rare > 0,35 (base = 0,08)
AUC-ROC Discrimination générale 0,75 – 0,85
Brier score Qualité des probabilités elles-mêmes Le plus bas possible
Calibration Quand le modèle dit 20 %, observe-t-on 20 % ? La diagonale
MAE au niveau vol Écart no-shows prédits / réels par vol < 2 passagers
POURQUOI LA CALIBRATION COMPTE PLUS QUE L’AUC Probabilité prédite par le modèle Fréquence observée 0 % 50 % 100 % 0 % 50 % 100 % Modèle calibré exploitable directement Non calibré surestime les no-shows → refus d’embarquement en série Les deux modèles peuvent avoir la MÊME AUC.
Deux modèles à AUC identique peuvent donner des décisions radicalement différentes. Seule la calibration garantit que les probabilités sont sommables.
 evaluate.py
flight_level = (
    scored.groupby("flight_id")
          .agg(expected_noshows=("p_noshow", "sum"),
               actual_noshows=("is_noshow", "sum"),
               bookings=("pnr", "count"))
)
flight_level["error"] = (flight_level["expected_noshows"]
                         - flight_level["actual_noshows"])

print("MAE vol :", flight_level["error"].abs().mean())
print("Biais   :", flight_level["error"].mean())   # doit être proche de 0

Un biais non nul est plus grave qu’une MAE élevée : un modèle qui surestime systématiquement les no-shows provoquera des refus d’embarquement à répétition. Côté interprétabilité, le Responsible AI Dashboard d’Azure ML fournit SHAP, l’analyse d’erreur par cohorte et les métriques d’équité — vérifiez que le modèle ne se dégrade pas sur les passagers à mobilité réduite, les familles ou les groupes.

9. De la probabilité à la décision : la couche d’optimisation

Le modèle donne, pour un vol de capacité C avec n réservations, une distribution du nombre de présentés. Sous hypothèse d’indépendance, le nombre de shows suit une loi de Poisson binomiale. On l’approche par simulation Monte-Carlo, simple et robuste. On cherche le nombre de sièges à vendre qui minimise le coût espéré.

TROUVER L’OPTIMUM DE SURVENTESièges survendus au-delà de la capacitéCoût espéré (€)0510152025 OPTIMUM +9 sièges · 1 420 € espérés Coût des sièges vides Coût des refus d’embarquement Coût total espéré
L’asymétrie des coûts (600 € par refus contre 180 € de marge perdue) déplace naturellement l’optimum vers une position conservatrice.
 optimize.py
import numpy as np

def optimal_authorization(p_show: np.ndarray,
                          capacity: int,
                          cost_spoilage: float = 180,   # € de marge perdue
                          cost_denied: float = 600,     # € UE261 + réacheminement
                          max_extra: int = 30,
                          n_sim: int = 20000,
                          seed: int = 42) -> dict:
    rng = np.random.default_rng(seed)
    results = {}

    for extra in range(0, max_extra + 1):
        n_sell = capacity + extra
        probs  = np.resize(p_show, n_sell)
        shows  = rng.binomial(1, probs, size=(n_sim, n_sell)).sum(axis=1)

        spoilage = np.maximum(capacity - shows, 0).mean()
        denied   = np.maximum(shows - capacity, 0).mean()

        results[n_sell] = cost_spoilage * spoilage + cost_denied * denied

    best = min(results, key=results.get)
    return {"authorization_level": best,
            "overbooking_seats": best - capacity,
            "expected_cost": results[best]}
✦ Conseil terrain

Les paramètres cost_spoilage et cost_denied se calibrent avec le contrôle de gestion, jamais avec l’équipe data. Ce sont des chiffres métier, et c’est justement pour cela qu’ils vivent en dehors du modèle.

10. Déploiement

 deploy.py
from azure.ai.ml.entities import ManagedOnlineEndpoint, ManagedOnlineDeployment

endpoint = ManagedOnlineEndpoint(
    name="noshow-endpoint",
    auth_mode="aml_token",
    public_network_access="disabled",   # accès via Private Link uniquement
)
ml_client.online_endpoints.begin_create_or_update(endpoint).result()

deployment = ManagedOnlineDeployment(
    name="blue",
    endpoint_name="noshow-endpoint",
    model=ml_client.models.get("noshow-predictor", label="latest"),
    instance_type="Standard_DS3_v2",
    instance_count=2,
)
ml_client.online_deployments.begin_create_or_update(deployment).result()

# Déploiement progressif : 10 % du trafic sur la nouvelle version
endpoint.traffic = {"blue": 90, "green": 10}
ml_client.online_endpoints.begin_create_or_update(endpoint).result()

Pour ce cas d’usage, un batch endpoint exécuté chaque nuit sur l’ensemble des vols à J+1 est souvent plus pertinent et bien moins coûteux qu’un endpoint temps réel. Réservez le temps réel aux ajustements de dernière minute.

11. MLOps : la partie que tout le monde sous-estime

Un modèle de no-show se dégrade vite : nouvelle politique tarifaire, nouveau concurrent sur la route, période de grève, événement exceptionnel. À mettre en place dès le jour 1 :

  • Monitoring de dérive des données sur les distributions d’entrée : lead time, mix de canaux, mix tarifaire.
  • Monitoring de performance : comparaison hebdomadaire prédictions vs réalité, alerte si la MAE au niveau vol dépasse un seuil.
  • Réentraînement planifié : mensuel, sur fenêtre glissante de 24 mois, avec promotion automatique uniquement si le nouveau modèle bat l’ancien sur le test le plus récent.
  • Registre versionné et traçabilité complète des données d’entraînement.
  • Kill switch : un retour instantané aux règles statiques historiques. Indispensable.
 monitor.py
from azure.ai.ml.entities import (
    AlertNotification, MonitorSchedule, MonitorDefinition,
    ServerlessSparkCompute, DataDriftSignal
)

monitor = MonitorSchedule(
    name="noshow-drift-monitor",
    trigger={"type": "recurrence", "frequency": "week", "interval": 1},
    create_monitor=MonitorDefinition(
        compute=ServerlessSparkCompute(runtime_version="3.4",
                                       instance_type="standard_e4s_v3"),
        monitoring_signals={"data_drift": DataDriftSignal()},
        alert_notification=AlertNotification(
            emails=["revenue-mgmt@compagnie.com"]),
    ),
)
ml_client.schedules.begin_create_or_update(monitor).result()

12. Conformité et éthique : ne sautez pas cette étape

Ce projet traite des données personnelles de passagers. Quelques points non négociables :

  • Base légale RGPD : l’intérêt légitime est généralement invocable, mais il doit être documenté par une analyse de balance des intérêts.
  • Minimisation : pseudonymisez les identifiants passagers dès la couche silver. Le modèle n’a pas besoin de savoir qui, seulement quel profil.
  • Variables interdites : jamais de nationalité, origine, religion ou état de santé — ni directement, ni par proxy. Un champ « repas spécial » peut être un proxy religieux ou médical : excluez-le.
  • AI Act européen : ce cas d’usage relève a priori du risque limité, mais la classification doit être formalisée par la conformité, pas par l’équipe data.
  • Décision humaine : le système propose, l’analyste dispose. Conservez la trace des surcharges manuelles — c’est un signal d’apprentissage précieux.

13. Feuille de route réaliste

Cadrage & accès aux données 3–4 semaines · spécification, DPIA, accès PSS Exploration & baseline 3 semaines · notebook, baseline de la règle actuelle Modélisation & calibration 4–6 semaines · modèle enregistré, rapport d’évaluation Couche d’optimisation 2 semaines · validée par le contrôle de gestion Shadow mode ⚠ non optionnel 8–12 semaines · comparaison silencieuse vs règles en place Déploiement progressif 4 semaines · 10 routes pilotes, puis extension Industrialisation MLOps
Comptez six à neuf mois entre le cadrage et un déploiement maîtrisé sur les premières routes.
▲ Ne sautez pas le shadow mode

Faites tourner le modèle en parallèle du système existant, sans qu’il ne décide de quoi que ce soit, pendant au moins un trimestre complet. C’est le seul moyen d’obtenir la confiance des équipes opérationnelles — et de découvrir les fuites de données que vous aviez manquées.

En résumé

  1. Le surbooking est un problème de prédiction + optimisation, et ces deux briques doivent rester séparées.
  2. Cadrez-le en classification binaire par passager, pas en régression par vol.
  3. La calibration des probabilités compte plus que l’AUC brute.
  4. Le split temporel et la chasse aux fuites de données déterminent si votre modèle tiendra en production.
  5. Les coûts asymétriques pilotent la décision finale — ce sont des paramètres métier, pas techniques.
  6. Sans MLOps, un modèle de no-show devient obsolète en quelques mois.

Azure Machine Learning couvre l’ensemble de ce cycle, mais la plateforme n’est pas le sujet. Le sujet, c’est la rigueur du cadrage.

Passez à la pratique

Formations en français sur Azure Machine Learning, le MLOps et l’IA appliquée aux métiers, animées par un formateur certifié Microsoft avec plus de 30 ans d’expérience IT.

Découvrir les formations
Share:

You May Also Like

Mettre en place un agent conversationnel avec Microsoft Copilot Studio n’a jamais été aussi accessible. Que vous souhaitiez automatiser une...
  • juillet 18, 2026
Anthropic est une entreprise américaine spécialisée dans l’intelligence artificielle, connue pour sa famille de modèles de langage Claude. Conçus avec...
  • juillet 18, 2026
Gemini est la famille de modèles d’intelligence artificielle développée par Google DeepMind. Conçus pour comprendre et générer du texte, des...
  • juillet 18, 2026