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.
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 %.
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.
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.
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
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 |
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.
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.
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.
# 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.
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())
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 |
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é.
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]}
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
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.
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
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é
- Le surbooking est un problème de prédiction + optimisation, et ces deux briques doivent rester séparées.
- Cadrez-le en classification binaire par passager, pas en régression par vol.
- La calibration des probabilités compte plus que l’AUC brute.
- Le split temporel et la chasse aux fuites de données déterminent si votre modèle tiendra en production.
- Les coûts asymétriques pilotent la décision finale — ce sont des paramètres métier, pas techniques.
- 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