ITSkillsCenter
Blog

Séries temporelles avec pandas et statsmodels : ARIMA et Prophet pas à pas

12 min de lecture

Les séries temporelles ont leurs propres règles. Une succession de mesures dans le temps n’est pas un jeu tabulaire ordinaire : l’ordre compte, la corrélation entre observations consécutives existe et doit être modélisée, les saisonnalités (hebdomadaires, annuelles) s’empilent, et la validation croisée naïve mélange passé et futur en produisant des résultats faussement bons. Ce tutoriel construit pas-à-pas une analyse complète : décomposition, ARIMA avec statsmodels, Prophet pour les séries opérationnelles avec saisonnalités multiples, validation par TimeSeriesSplit.

Pour le contexte global, voir le guide principal sur la stack data 2026. Ce tutoriel se concentre sur la modélisation prédictive d’une série univariée — typiquement un volume de ventes journalier ou un trafic horaire.

Prérequis

  • Python 3.10+, pandas 3.0+, numpy, matplotlib
  • statsmodels 0.14+, prophet 1.1+
  • Bases pandas (DatetimeIndex, resample) — voir l’analyse exploratoire
  • Notions de base sur autocorrélation et stationnarité
  • Temps estimé : 2 heures

Étape 1 — Installer et préparer l’environnement

Prophet a la particularité de dépendre d’une bibliothèque de modélisation bayésienne (cmdstanpy depuis la version 1.1) qui peut nécessiter un compilateur C++ à l’installation sous Linux et Windows. Sous macOS et Linux récents, le wheel précompilé règle le problème. Si l’installation échoue, l’erreur est généralement explicite et la documentation officielle de Prophet liste les correctifs par OS.

pip install pandas numpy matplotlib statsmodels prophet

Après installation, un import rapide vérifie que statsmodels et prophet sont disponibles. statsmodels affiche son numéro de version au premier import, c’est rassurant ; Prophet imprime parfois un message lié à cmdstanpy qu’on peut ignorer en production.

Étape 2 — Charger la série et garantir un index temporel régulier

Toute analyse de série temporelle commence par un DatetimeIndex régulier. Si certaines dates manquent, il faut décider : combler par 0 (cas du trafic web où l’absence vaut zéro), interpoler (cas d’une mesure physique continue), ou laisser les NaN et choisir un modèle qui les gère. Le asfreq de pandas force la régularité et révèle immédiatement les trous.

import pandas as pd

df = pd.read_parquet("ventes_quotidiennes.parquet")
df["date"] = pd.to_datetime(df["date"], errors="coerce")
df = df.dropna(subset=["date"])    # on jette les lignes à date invalide
serie = df.set_index("date")["volume"].sort_index()

# Forcer une fréquence quotidienne, combler les jours manquants par 0
serie = serie.asfreq("D", fill_value=0)
print(serie.head())
print(f"Période couverte : {serie.index.min().date()} → {serie.index.max().date()}")
print(f"Nombre de jours : {len(serie)}")

L’argument "D" représente la fréquence quotidienne. Pour un pas horaire on passe à "h", hebdomadaire "W", mensuel "MS" (début de mois) ou "ME" (fin de mois). Le fill_value=0 n’est légitime que si l’absence métier signifie zéro — sinon préférer method="ffill" (report de la dernière valeur) ou laisser les NaN apparents.

Étape 3 — Visualiser et décomposer

Avant de modéliser, on observe. Le tracé brut révèle les tendances longues et les pics. La décomposition saisonnière sépare la série en trois composantes : tendance lente, saisonnalité régulière, et résidu. Cette décomposition oriente le choix du modèle : si la saisonnalité est forte et stable, ARIMA saisonnier ou Prophet sont indiqués ; si elle est faible, un ARIMA simple peut suffire.

import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import STL

fig, ax = plt.subplots(figsize=(12, 4))
serie.plot(ax=ax, lw=0.8)
ax.set_title("Volume quotidien")
ax.set_xlabel("Date"); ax.set_ylabel("Volume")
plt.tight_layout(); plt.savefig("serie_brute.png", dpi=120); plt.show()

# Décomposition saisonnière (STL)
stl = STL(serie, period=7, robust=True).fit()
fig = stl.plot()
fig.set_size_inches(12, 8)
plt.tight_layout(); plt.savefig("decomposition.png", dpi=120); plt.show()

STL (Seasonal-Trend decomposition using Loess) est l’option moderne et robuste de statsmodels. L’argument period=7 indique une saisonnalité hebdomadaire pour des données quotidiennes ; pour de l’horaire avec saisonnalité quotidienne ce serait 24, pour du mensuel avec saisonnalité annuelle ce serait 12. Le résultat affiche quatre panneaux : observation, tendance, saisonnalité, résidu. Si le résidu n’est pas centré autour de zéro et stable, on a manqué une composante.

Étape 4 — Tester la stationnarité

ARIMA suppose la série stationnaire — ses propriétés statistiques (moyenne, variance) ne doivent pas dépendre du temps. Une tendance linéaire viole cette hypothèse, une saisonnalité aussi. Le test ADF (Augmented Dickey-Fuller) tranche : si la p-value est inférieure à 0,05, on rejette l’hypothèse de non-stationnarité. Sinon, il faut différencier la série autant de fois que nécessaire.

from statsmodels.tsa.stattools import adfuller

resultat = adfuller(serie.dropna())
print(f"Statistique ADF : {resultat[0]:.3f}")
print(f"p-value : {resultat[1]:.4f}")

# Si non stationnaire, différencier d'ordre 1
serie_diff = serie.diff().dropna()
resultat2 = adfuller(serie_diff)
print(f"Après différenciation, p-value : {resultat2[1]:.4f}")

Une p-value à 0,30 dit clairement « non stationnaire ». Après une différenciation d’ordre 1 (diff()), la p-value chute généralement sous 0,01 et on est bon. C’est cet ordre de différenciation qu’on retiendra comme paramètre d dans ARIMA(p,d,q). Pour les saisonnalités, on utilise une différenciation saisonnière supplémentaire — serie.diff(7) pour une période hebdomadaire.

Étape 5 — Identifier ARIMA via ACF et PACF

Les graphiques d’autocorrélation (ACF) et d’autocorrélation partielle (PACF) sont les outils classiques pour choisir les ordres p (auto-régressif) et q (moyenne mobile) d’ARIMA. ACF qui décroît lentement et PACF qui se coupe nettement après p retards suggère un AR(p). L’inverse (ACF coupée, PACF décroissante) suggère MA(q). Cette lecture demande de l’expérience ; en pratique, beaucoup de praticiens préfèrent désormais une recherche automatique.

from statsmodels.graphics.tsaplots import plot_acf, plot_pacf

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 6))
plot_acf(serie_diff, lags=40, ax=ax1)
plot_pacf(serie_diff, lags=40, ax=ax2, method="ywm")
plt.tight_layout(); plt.savefig("acf_pacf.png", dpi=120); plt.show()

Sur la série différenciée, les barres dépassant les bandes bleues indiquent des autocorrélations significatives. Si l’ACF montre des pics aux multiples de 7 (ou 24, ou 12 selon la fréquence), il y a une saisonnalité résiduelle non captée — il faut donc passer à SARIMA. La méthode "ywm" de PACF est l’option recommandée par statsmodels en 2026.

Étape 6 — Entraîner un modèle SARIMA

SARIMA (Seasonal ARIMA) combine ARIMA classique et composante saisonnière. Sa notation est SARIMA(p,d,q)(P,D,Q,s) où le bloc saisonnier capture les corrélations à période s. Pour des ventes quotidiennes avec saisonnalité hebdomadaire, un point de départ raisonnable est SARIMA(1,1,1)(1,1,1,7) — un AR et un MA classique, un AR et un MA saisonnier hebdomadaire.

from statsmodels.tsa.statespace.sarimax import SARIMAX

# Découpage : on garde les 60 derniers jours pour la validation
train = serie.iloc[:-60]
test = serie.iloc[-60:]

modele = SARIMAX(
    train,
    order=(1, 1, 1),
    seasonal_order=(1, 1, 1, 7),
    enforce_stationarity=False,
    enforce_invertibility=False,
)
fit = modele.fit(disp=False)
print(fit.summary().tables[1])

Le tableau de résumé liste les coefficients estimés et leur p-value. Les coefficients dont la p-value dépasse 0,1 ne sont pas significatifs et suggèrent un modèle plus parcimonieux. Les options enforce_*=False évitent l’arrêt prématuré quand l’optimiseur explore des zones de l’espace des paramètres mal conditionnées — utile en pratique, le résultat reste valide.

Étape 7 — Prévoir et tracer l’intervalle de confiance

L’intérêt d’un modèle statistique comme SARIMA réside autant dans la prévision ponctuelle que dans son intervalle de confiance. Un modèle qui prévoit 100 ± 5 est très différent d’un modèle qui prévoit 100 ± 80. La méthode get_forecast renvoie l’objet complet avec moyennes et bornes.

prevision = fit.get_forecast(steps=60)
moyenne = prevision.predicted_mean
intervalle = prevision.conf_int(alpha=0.05)

fig, ax = plt.subplots(figsize=(12, 5))
train.iloc[-180:].plot(ax=ax, label="Historique", color="black", lw=0.8)
test.plot(ax=ax, label="Réel", color="green", lw=0.8)
moyenne.plot(ax=ax, label="Prévision", color="orange")
ax.fill_between(intervalle.index, intervalle.iloc[:, 0], intervalle.iloc[:, 1], color="orange", alpha=0.2)
ax.legend(); ax.set_title("Prévision SARIMA — 60 jours")
plt.tight_layout(); plt.savefig("sarima_forecast.png", dpi=120); plt.show()

La bande orange représente l’intervalle de confiance à 95 %. Plus l’horizon s’éloigne, plus la bande s’élargit — c’est attendu et c’est honnête : prédire un jour à l’avance est plus précis que prédire dans deux mois. Si la courbe verte (réel) sort de la bande sur plus de 5 % des points, le modèle sous-estime l’incertitude et il faut revenir à l’identification.

Étape 8 — Modèle alternatif : Prophet

Prophet, développé initialement par Meta et désormais maintenu sur GitHub par la communauté, vise un cas d’usage différent : les séries opérationnelles avec saisonnalités multiples, événements ponctuels (jours fériés, promotions), tendances changeantes. Il demande beaucoup moins de réglages qu’ARIMA pour donner un résultat raisonnable, au prix d’une moindre flexibilité statistique. C’est le bon choix pour une mise en production rapide quand l’expertise ARIMA n’est pas disponible.

from prophet import Prophet

# Prophet exige les colonnes nommées 'ds' et 'y'
df_p = train.reset_index().rename(columns={"date": "ds", "volume": "y"})

modele_p = Prophet(weekly_seasonality=True, yearly_seasonality=True, daily_seasonality=False)
modele_p.fit(df_p)

# Prévision sur 60 jours
futur = modele_p.make_future_dataframe(periods=60, freq="D")
prevision_p = modele_p.predict(futur)
print(prevision_p[["ds", "yhat", "yhat_lower", "yhat_upper"]].tail())

Prophet impose ses conventions de nommage de colonnes : ds pour la date et y pour la cible. C’est une contrainte mineure, qu’on règle avec rename. Les saisonnalités hebdomadaire et annuelle sont activées par défaut sur des données quotidiennes ; la saisonnalité quotidienne (intra-journée) est utile pour des données horaires. La méthode make_future_dataframe construit le DataFrame d’horizon à prévoir.

Étape 9 — Ajouter des jours fériés à Prophet

Une force de Prophet est sa gestion native des événements ponctuels. On peut fournir un DataFrame des jours fériés ou des dates de promotion, et le modèle apprend leur effet — y compris en généralisant sur des dates futures. Pour les fêtes mobiles (Aïd el-Fitr, Aïd el-Adha) qui suivent le calendrier hégirien, on saisit explicitement les dates correspondant aux années d’historique et de prévision.

jours_feries = pd.DataFrame({
    "holiday": "evenement",
    "ds": pd.to_datetime([
        "2024-04-10", "2024-06-16",
        "2025-03-30", "2025-06-06",
        "2026-03-20", "2026-05-27",
    ]),
    "lower_window": 0,
    "upper_window": 1,
})

modele_p = Prophet(holidays=jours_feries, weekly_seasonality=True, yearly_seasonality=True)
modele_p.fit(df_p)

Les colonnes lower_window et upper_window permettent d’étendre l’effet sur plusieurs jours autour de l’événement — utile quand un jour férié influence aussi la veille et le lendemain. Le modèle réestimé prendra ces dates en compte automatiquement et leur impact sera visible dans la décomposition retournée par Prophet.

Étape 10 — Validation par TimeSeriesSplit

La validation croisée classique (K-fold) mélange passé et futur et fausse complètement l’évaluation d’un modèle temporel. TimeSeriesSplit de scikit-learn implémente la validation expanding window : à chaque pli, le train est tout l’historique avant un point T, et le test est l’horizon suivant. C’est la méthode correcte pour mesurer la performance prédictive d’un modèle de série temporelle.

from sklearn.model_selection import TimeSeriesSplit
import numpy as np

tscv = TimeSeriesSplit(n_splits=5, test_size=30)
rmse_par_pli = []
for idx_train, idx_test in tscv.split(serie):
    s_tr = serie.iloc[idx_train]
    s_te = serie.iloc[idx_test]
    fit_pli = SARIMAX(s_tr, order=(1,1,1), seasonal_order=(1,1,1,7),
                      enforce_stationarity=False, enforce_invertibility=False).fit(disp=False)
    prev = fit_pli.forecast(steps=len(s_te))
    rmse = np.sqrt(((prev.values - s_te.values) ** 2).mean())
    rmse_par_pli.append(rmse)

print(f"RMSE par pli : {[round(r,2) for r in rmse_par_pli]}")
print(f"RMSE moyen : {np.mean(rmse_par_pli):.2f} ± {np.std(rmse_par_pli):.2f}")

Cinq plis donnent une estimation honnête. L’écart-type entre plis est souvent élevé sur les séries temporelles parce que la difficulté de prévision varie selon les périodes (un événement exceptionnel dans un pli plombe son RMSE). Documenter cet écart-type plutôt que de cacher la moyenne sous le tapis est essentiel pour communiquer la confiance dans le modèle.

Erreurs fréquentes

ErreurCauseSolution
K-fold standard donne d’excellents scores trompeursMélange passé/futur, fuiteTimeSeriesSplit obligatoirement
SARIMAX ne converge pasSérie trop bruitée ou ordres mal choisisenforce_*=False et essayer d’autres ordres
Index temporel sans fréquenceDates dupliquées ou trousasfreq(« D ») avec drop_duplicates au préalable
Prophet plante à l’install WindowsCompilateur C++ manquantSuivre le guide install officiel ou utiliser conda
Saisonnalité annuelle invisibleMoins de 2 ans d’historiqueDésactiver yearly_seasonality ou attendre plus de données
Intervalle de confiance trop largeSur-paramétrage du modèleRéduire les ordres p, q, P, Q
Pic de fin d’année non capturéPas de jour férié déclaréFournir le DataFrame holidays à Prophet

Tutoriels associés

Ressources officielles

FAQ

SARIMA ou Prophet : lequel choisir ?

SARIMA quand on a l’expertise statistique et qu’on veut maîtriser chaque paramètre. Prophet quand on veut un résultat correct rapidement, en particulier avec des jours fériés et des saisonnalités multiples. Les deux donnent souvent des performances comparables.

Combien d’historique faut-il ?

Pour capturer une saisonnalité annuelle, au moins deux ans, idéalement trois. Pour une saisonnalité hebdomadaire, six mois minimum. En dessous de trois mois d’historique quotidien, les modèles statistiques classiques perdent en pertinence.

Faut-il toujours différencier ?

Pour ARIMA classique, oui, sauf si la série est déjà stationnaire (test ADF avec p < 0,05 sans différenciation). Prophet n’a pas cette contrainte parce qu’il modélise explicitement la tendance.

Comment évaluer une prévision ?

RMSE pour les écarts pénalisés au carré, MAE pour des écarts proportionnels, MAPE pour comparer en pourcentage entre séries d’échelles différentes. Toujours évaluer en validation expanding window, jamais sur le train.

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité