Module 2 — Décomposition : tendance, saisonnalité, bruit
Le module précédent posait le vocabulaire ; celui-ci l'utilise pour séparer une série en trois signaux additionnels ou multiplicatifs. La décomposition est autant un outil de diagnostic qu'un point de départ pour la prévision : elle rend visible ce qu'un tracé unique cache, et elle est la première chose à faire après avoir regardé la figure globale.
Le modèle décomposé
L'idée est ancienne — Buys-Ballot la formule dès 1847 — et sa version moderne se lit ainsi. Pour une série :
où est la tendance, la saisonnalité (motif périodique) et le reste (résidu). Le choix entre additif et multiplicatif est simple à énoncer : additif quand l'amplitude de la saisonnalité est indépendante du niveau ; multiplicatif quand elle croît avec le niveau. Sur notre fil rouge, la vente du samedi dépasse la moyenne hebdomadaire d'environ 40 %, quel que soit le niveau annuel : c'est un signe multiplicatif. L'astuce classique consiste alors à passer au logarithme, ce qui ramène le multiplicatif à un cadre additif :
Sur une série strictement positive comme la demande, cette transformation est presque toujours utile ; elle stabilise en outre la variance, ce qui simplifiera les diagnostics de résidus et rendra les intervalles de prévision plus honnêtes.
STL, la décomposition qu'on utilise vraiment
Les décompositions naïves par moyennes mobiles centrées existent — c'est ce que fait seasonal_decompose de statsmodels — mais elles imposent une saisonnalité rigoureusement fixe et laissent des trous en début et fin de série. STL (Seasonal-Trend decomposition using Loess), publiée en 1990 par Cleveland, résout les deux défauts : elle autorise une saisonnalité qui évolue lentement, ne perd aucune observation et gère bien les valeurs aberrantes lorsqu'on demande la variante robuste.
from statsmodels.tsa.seasonal import STL
import numpy as np
import matplotlib.pyplot as plt
# Passage au log pour rendre la saisonnalite additive.
y = np.log(ventes["quantite"])
# STL avec periode hebdomadaire (7 jours), fenetre saisonniere large (79) pour
# permettre une lente evolution du profil de semaine sur 4 ans, et variante
# robuste pour ne pas etre tire par les jours de promotion massive.
resultat = STL(y, period=7, seasonal=79, robust=True).fit()
figure, axes = plt.subplots(4, 1, figsize=(10, 8), sharex=True)
axes[0].plot(y); axes[0].set_title("log(ventes)")
axes[1].plot(resultat.trend); axes[1].set_title("tendance")
axes[2].plot(resultat.seasonal); axes[2].set_title("saisonnalite hebdomadaire")
axes[3].plot(resultat.resid); axes[3].set_title("reste")
plt.tight_layout()
Deux paramètres méritent d'être compris. La période est le nombre d'observations d'un cycle : 7 pour une saisonnalité hebdomadaire au pas quotidien, 12 pour une saisonnalité annuelle au pas mensuel. La fenêtre saisonnière (seasonal) contrôle la vitesse d'évolution du motif : petite valeur = motif quasi identique d'année en année, grande valeur = motif qui bouge lentement. Le choix par défaut privilégie une évolution presque inexistante ; en pratique, on veut souvent la laisser respirer.
Saisonnalités multiples : ce que STL ne fait pas seul
Notre fil rouge cache deux saisonnalités superposées : une hebdomadaire (pic du samedi, creux du dimanche) et une annuelle (pic de vaccination en novembre, creux d'août). STL n'en gère qu'une seule à la fois. Deux stratégies coexistent.
La première consiste à appliquer STL en cascade : on retire la saisonnalité hebdomadaire, puis on applique une seconde STL sur le résidu pour la saisonnalité annuelle. C'est la solution la plus lisible, elle donne d'excellents diagnostics.
La seconde est l'algorithme MSTL (Multiple STL), disponible dans statsmodels, qui traite plusieurs périodes en une seule décomposition itérative. Sur notre série, il faut lui fournir periods=[7, 365]. Pour la saisonnalité annuelle sur données quotidiennes, l'entier 365 est une approximation : la période exacte est fractionnaire () à cause des années bissextiles, ce qui explique certains décalages en fin de série ; c'est en général sans conséquence pratique, mais c'est un point à connaître.
Lire les résidus, geste par geste
Le résidu de la décomposition est ce qui reste après avoir retiré tendance et saisonnalité. Un bon résidu doit ressembler à du bruit : moyenne nulle, variance stable, aucune autocorrélation visible. Trois défauts se lisent immédiatement.
Un résidu qui remonte à la fin signale une tendance mal captée : le fenêtrage de la loess est trop large. Un résidu dont la variance change au cours du temps trahit une hétéroscédasticité, souvent corrigée par le passage au log qu'on vient d'évoquer. Un résidu qui contient encore un motif périodique visible signale une saisonnalité oubliée — sur notre fil rouge, oublier la saisonnalité annuelle laisse une onde d'un an dans le résidu, aisément reconnaissable.
Ce que la décomposition apporte, ce qu'elle n'apporte pas
Elle est irremplaçable pour comprendre une série : quantifier l'ampleur relative des trois composantes, repérer les changements de régime, préparer les entretiens avec les experts métier. Elle est aussi la base d'une prévision très simple : prolonger la tendance, projeter la saisonnalité, et supposer un résidu de moyenne nulle. Cette approche, connue sous le nom de prévision par recomposition, sert souvent de premier baseline (module 4 y reviendra).
Elle ne remplace toutefois pas un modèle statistique ou d'apprentissage : la décomposition ne dit rien sur les intervalles de prévision ni sur les régresseurs externes, et elle n'exploite pas la structure d'autocorrélation résiduelle. Le module 3 y viendra.
Décomposer avant d'avoir traité les valeurs aberrantes contamine la tendance et la saisonnalité — un jour de promotion massive, un arrêt de système ou une donnée dupliquée tirent la loess dans une direction fausse. Utilisez la variante robust=True de STL, ou nettoyez explicitement les points hors norme, sinon vous décomposerez une série qui n'existe pas.
Enregistrer les composantes pour la suite
Une bonne hygiène consiste à sauvegarder les trois composantes séparément : elles servent au module 3 (le résidu, presque stationnaire, est le bon objet à tester pour ADF), au module 4 (la recomposition est une référence à battre) et au module 10 (l'analyse d'incertitude sépare l'incertitude du niveau de celle des variations saisonnières).
decomposition = pd.DataFrame({
"log_ventes": y,
"tendance": resultat.trend,
"saisonnalite_hebdo": resultat.seasonal,
"reste": resultat.resid,
}).dropna()
decomposition.to_parquet("decomposition_hebdo.parquet")
# Verification rapide : la reconstruction doit egaler la serie originale.
reconstruit = decomposition[["tendance", "saisonnalite_hebdo", "reste"]].sum(axis=1)
ecart_max = (reconstruit - decomposition["log_ventes"]).abs().max()
assert ecart_max < 1e-9, ecart_max
Cette assertion piège les erreurs de manipulation (log oublié, indexation décalée) et elle vaut la peine d'être écrite : la reconstruction parfaite est la seule preuve que l'on n'a rien perdu en route.
Toujours dans cet ordre : (1) tracer la série ; (2) log si l'amplitude croît avec le niveau ; (3) STL robuste sur la saisonnalité la plus courte ; (4) MSTL ou STL en cascade si une seconde saisonnalité est visible ; (5) inspection des résidus. Cet enchaînement fait apparaître les bonnes questions dans le bon ordre, alors qu'inverser une étape (par exemple modéliser avant de nettoyer les aberrations) coûte plusieurs journées de retour en arrière.
En résumé
- Une série se décompose en tendance, saisonnalité et reste, en additif ou en multiplicatif ; le log ramène l'un à l'autre.
- STL évolue lentement, ne perd rien et propose une variante robuste ; pour deux saisonnalités, MSTL ou une STL en cascade.
- Le résidu doit ressembler à du bruit ; toute structure visible signale une composante mal captée.
- La décomposition sert au diagnostic et à une prévision de premier niveau par recomposition, mais elle ne remplace pas un modèle statistique.
Module suivant : stationnarité, différenciation, ACF et PACF — les outils qui préparent le terrain pour ARIMA et SARIMA.