Aller au contenu principal

Module 9 — Validation glissante et métriques adaptées

Les modules précédents ont ajusté cinq familles de modèles, chacune évaluée sur les 28 jours de janvier 2025. C'est un début mais c'est insuffisant : la performance sur une seule origine dépend beaucoup de la période choisie. Ce module généralise la validation par origine glissante et introduit les métriques adaptées aux séries temporelles, avec leurs pièges.

Pourquoi une seule origine ne suffit pas

Choisir « les 28 derniers jours » comme jeu de test place la validation sur un mois précis. Or ce mois peut être atypique — janvier vient après un pic de fêtes, il n'y a presque jamais de vagues épidémiques, la météo est stable. Un modèle qui excelle sur ce mois précis peut échouer en avril, en juillet ou en novembre. La validation par origine unique donne un score, mais pas une estimation fiable de la performance opérationnelle.

La solution est la validation glissante par origine mobile. On choisit plusieurs dates d'origine réparties dans le temps, on ajuste le modèle sur tout l'historique antérieur à chaque origine, on prévoit les 28 jours suivants, et on agrège les scores. C'est plus lent mais c'est la seule évaluation qui reflète l'usage.

import pandas as pd
import numpy as np

def valider_glissante(serie, modele_ajusteur, origines, horizon=28):
"""Applique une origine mobile. modele_ajusteur(train) doit renvoyer une
fonction prevoir(h) qui retourne un tableau de h valeurs."""
scores = []
for origine in origines:
train = serie.loc[:origine - pd.Timedelta(days=1)]
test = serie.loc[origine : origine + pd.Timedelta(days=horizon - 1)]
if len(test) < horizon:
continue
prevoir = modele_ajusteur(train)
y_prevu = prevoir(horizon)
scores.append({
"origine": origine,
"mae": mean_absolute_error(test.values, y_prevu),
"rmse": np.sqrt(mean_squared_error(test.values, y_prevu)),
})
return pd.DataFrame(scores)

origines = pd.date_range("2024-02-01", "2025-01-01", freq="MS") # 12 origines

Douze origines espacées d'un mois donnent un échantillonnage raisonnable. On peut aller plus loin — hebdomadaire — mais le coût de calcul explose. Une bonne règle : au moins 10 origines, réparties sur au moins 12 mois, pour capturer la variation saisonnière annuelle.

Deux stratégies : fenêtre glissante ou fenêtre étendue

Deux protocoles se distinguent. La fenêtre étendue (« expanding window ») accumule toutes les données historiques disponibles à chaque origine — plus l'origine est tardive, plus l'entraînement est long. La fenêtre glissante (« rolling window ») garde toujours la même longueur d'historique (par exemple 3 ans) et fait glisser train et test ensemble.

La première évalue « ce que le modèle sait faire aujourd'hui, avec toute l'histoire disponible ». La seconde évalue « comment le modèle se comporte s'il n'a que 3 ans d'historique à sa disposition ». Le bon choix dépend du régime de production : si vous réentraînerez périodiquement avec toute l'histoire, choisissez l'étendue ; si vous ne conservez qu'une fenêtre récente, choisissez le glissant.

MAE, RMSE : les métriques absolues

L'erreur absolue moyenne :

MAE=1ni=1nyiy^i\text{MAE} = \frac{1}{n} \sum_{i=1}^{n} |y_i - \hat{y}_i|

est la métrique de référence pour la plupart des usages métiers. Elle est directement interprétable — « en moyenne, on se trompe de 12 unités par jour » — et robuste aux valeurs aberrantes.

L'erreur quadratique moyenne :

RMSE=1ni=1n(yiy^i)2\text{RMSE} = \sqrt{\frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2}

pénalise plus lourdement les grosses erreurs. Elle est adaptée quand une erreur ponctuelle coûte disproportionnellement cher (rupture de stock brutale).

Une bonne pratique : rapporter les deux systématiquement. Un modèle qui gagne en MAE mais perd en RMSE fait des erreurs moyennes plus faibles avec quelques gros ratés — c'est un profil différent d'un modèle qui gagne partout.

MAPE et ses pièges

L'erreur pourcentage absolue moyenne :

MAPE=1ni=1nyiy^iyi×100\text{MAPE} = \frac{1}{n} \sum_{i=1}^{n} \left|\frac{y_i - \hat{y}_i}{y_i}\right| \times 100

exprime l'erreur en pourcentage, ce qui la rend comparable entre séries de niveaux différents. C'est sa force ; ses défauts sont sévères.

Division par zéro : dès que yiy_i vaut zéro, la MAPE est indéfinie. Sur des séries de ventes qui contiennent des jours de magasin fermé (vente = 0), la MAPE est inutilisable telle quelle. La parade classique — ajouter un epsilon au dénominateur — masque le problème plutôt que le résoudre.

Asymétrie : la MAPE pénalise davantage la sur-prévision que la sous-prévision. Une prévision de 200 pour une vraie valeur de 100 donne 100 % d'erreur ; une prévision de 0 pour une vraie valeur de 100 donne également 100 %, mais une prévision de 50 pour 100 donne 50 %, contre 100 % pour 50 → 100. Cette asymétrie biaise les optimisations qui minimisent la MAPE vers des prévisions systématiquement basses.

Deux alternatives corrigent partiellement : la sMAPE (symmetric MAPE) qui normalise par la moyenne de y|y| et y^|\hat{y}|, et la MdAPE (median APE) qui remplace la moyenne par la médiane, plus robuste. Aucune ne résout le problème du zéro.

MASE : la métrique de comparaison

Le Mean Absolute Scaled Error, introduit par Hyndman en 2006, résout élégamment les défauts de la MAPE. Il divise l'erreur du modèle par l'erreur d'une référence naïve (naïf saisonnier sur le train) :

MASE=1niyiy^i1ntrainst=s+1ntrainytyts\text{MASE} = \frac{\frac{1}{n} \sum_i |y_i - \hat{y}_i|}{\frac{1}{n_{\text{train}} - s} \sum_{t=s+1}^{n_{\text{train}}} |y_t - y_{t-s}|}

Interprétation directe : MASE=1\text{MASE} = 1 signifie « aussi bon que le naïf saisonnier », MASE=0,5\text{MASE} = 0{,}5 signifie « moitié moins d'erreur que le naïf saisonnier », MASE>1\text{MASE} > 1 signifie « pire que le naïf saisonnier ».

def mase(y_vrai, y_prevu, y_train, s=7):
erreurs_naives = np.abs(y_train[s:] - y_train[:-s])
denominateur = np.mean(erreurs_naives)
numerateur = np.mean(np.abs(np.asarray(y_vrai) - np.asarray(y_prevu)))
return numerateur / denominateur

score = mase(test.values, prevision_gbm, train["y"].values, s=7)
print(f"MASE = {score:.3f}") # < 1 = mieux que le naif saisonnier

Sa force : robustesse aux zéros, échelle indépendante, comparabilité entre séries. Sa limite : le choix de la référence naïve doit être honnête — utiliser un naïf simple s=1s = 1 sur une série saisonnière donne artificiellement un MASE très bas.

Évaluer les intervalles

Un modèle qui fournit des intervalles de prévision (SARIMA, Prophet, quantiles du gradient boosting du module 10) doit être évalué sur ses intervalles autant que sur sa moyenne. Deux métriques comptent.

La couverture empirique : quelle proportion des vraies valeurs se trouve dans l'intervalle à 95 % ? Un intervalle honnête à 95 % doit contenir environ 95 % des observations. En pratique, SARIMA affiche souvent 85-90 % de couverture réelle, Prophet est encore plus bas ; c'est un point à mesurer, pas à supposer.

La largeur moyenne : pour deux modèles à couverture comparable, celui aux intervalles les plus étroits est le meilleur — un intervalle qui couvre tout parce qu'il va de 0 à l'infini n'a aucune valeur. Un score combinant les deux, le pinball loss au quantile souhaité, permet une évaluation intégrée qui pénalise à la fois les intervalles trop étroits et trop larges.

MAPE = 0 ne prouve rien

Sur notre fil rouge, si le magasin est fermé le dimanche (vente = 0) et que le modèle prédit 0 ce jour-là, la MAPE saute à l'infini pour ce jour. Les moyennes qu'on en tirera dépendent de la façon dont on traite ces cas. La règle est simple : ne pas utiliser MAPE sur des séries pouvant valoir zéro ; MAE et MASE font le travail sans piège.

Décomposition par horizon

Un dernier raffinement utile : évaluer la MAE par horizon. Un modèle qui excelle à 1-7 jours et s'effondre à 15-28 jours n'a pas le même profil qu'un modèle stable sur tout l'horizon. Il faut donc calculer un vecteur MAE indexé par le pas.

def mae_par_pas(scores_glissants, horizon=28):
empilement = np.stack([s["erreurs_par_pas"] for s in scores_glissants])
return empilement.mean(axis=0)

# Exemple d'utilisation : trace de la MAE en fonction de l'horizon.
mae_par_h = mae_par_pas(scores_glissants)
import matplotlib.pyplot as plt
plt.plot(range(1, 29), mae_par_h)
plt.xlabel("horizon (jours)")
plt.ylabel("MAE moyen")

Sur notre fil rouge, cette courbe montre presque toujours une pente ascendante — plus l'horizon est loin, plus l'erreur monte — mais son taux d'accroissement distingue les modèles. Un modèle statistique bien ajusté a une pente douce ; un LSTM récursif mal conçu a une pente très raide.

Le protocole d'évaluation complet

Pour la comparaison finale entre modèles : (1) au moins 10 origines, réparties sur 12 mois ; (2) MAE et RMSE comme métriques principales ; (3) MASE pour comparer entre projets ou entre séries ; (4) éviter MAPE, ou au moins la doubler avec MAE ; (5) décomposition par horizon ; (6) couverture empirique et largeur des intervalles pour les modèles probabilistes. Ce protocole tient dans un rapport de deux pages et suffit à convaincre une direction technique.

En résumé

  • La validation par origine glissante évalue la performance sur plusieurs points de départ ; au moins 10 origines sur 12 mois.
  • MAE et RMSE absolues, MASE pour la comparabilité inter-séries ; la MAPE est piégée par les zéros et par l'asymétrie.
  • Les modèles à intervalles s'évaluent sur la couverture empirique et la largeur moyenne ; SARIMA et Prophet sur-vendent souvent leur couverture théorique.
  • La décomposition par horizon distingue les modèles stables des modèles qui dérivent ; c'est un critère décisif en production.

Module suivant : le projet final, prévision de demande avec intervalles et plan de réentraînement.