J’analyse une série temporelle Python en respectant l’ordre du temps, pas comme un tableau classique. Le vrai sujet, c’est l’index temporel, la saisonnalité, les trous, les anomalies, puis une validation sans fuite de données. Sinon le modèle a l’air bon, mais il ment.
Pourquoi le temps change tout ?
Le temps change tout parce que les observations d’une série temporelle ne sont pas indépendantes, elles dépendent souvent de ce qui s’est passé avant.
Quand j’analyse des séries temporelles avec Python, je ne peux pas raisonner comme sur un tableau classique de machine learning où chaque ligne serait interchangeable. Dans une série de ventes, de consommation d’énergie, de transactions, de stocks ou de constantes vitales, l’ordre des lignes n’est pas décoratif. Si je mélange les lignes, je casse une partie de l’information.
C’est là qu’arrive la dépendance temporelle. Une valeur à 10h peut dépendre de celle à 9h. Les ventes de cette semaine peuvent être liées aux ventes de la semaine dernière. On parle souvent d’autocorrélation : ça veut simplement dire qu’une série peut être corrélée avec ses propres valeurs passées. Dit autrement, le passé laisse une trace.
Autre notion importante : la stationnarité. Une série est plus facile à modéliser quand sa moyenne, sa variance et sa structure statistique restent relativement stables dans le temps. Si la série change complètement de comportement tous les trois mois, le modèle va apprendre quelque chose qui ne sera peut-être plus vrai demain. Et ça, je l’ai vu chez un client retail : un modèle correct sur l’historique devenait médiocre dès qu’une nouvelle stratégie promo changeait le rythme des ventes.
Beaucoup de séries réelles ne sont pas stationnaires naturellement. Elles montent, elles descendent, elles changent de régime. Il peut y avoir une tendance longue, une saisonnalité, une crise, une rupture d’approvisionnement, une campagne marketing ou juste un changement d’usage.
La saisonnalité, ce n’est pas juste un motif joli sur un graphique. C’est souvent là que se cache la valeur métier. Un hôtel n’a pas le même taux d’occupation le lundi et le samedi. Un site e-commerce peut voir des pics le soir, le week-end, au moment des soldes ou en fin de mois. Si je rate ça, je rate une partie du signal.
Rob J Hyndman et George Athanasopoulos le rappellent très bien dans Forecasting Principles and Practice : avant de choisir un modèle, je dois comprendre le comportement temporel de la donnée. Python aide beaucoup, mais il ne remplace pas cette lecture.
| Notion | Ce que ça veut dire | Risque si on l’ignore |
| Dépendance temporelle | Les valeurs passées peuvent influencer les valeurs présentes | Perdre le signal en mélangeant les lignes |
| Stationnarité | La moyenne, la variance et la structure restent assez stables | Construire un modèle fragile hors historique |
| Tendance et saisonnalité | La série évolue ou répète des cycles dans le temps | Rater les pics, les creux et les vrais comportements métier |
Quel index pandas utiliser ?
J’utilise surtout DatetimeIndex quand je manipule des instants datés, et PeriodIndex quand je raisonne en périodes comme des mois, trimestres ou années.
Le bon index temporel évite beaucoup d’erreurs invisibles. Une colonne date laissée comme simple texte, c’est le début des ennuis : tris faux, agrégations bancales, jointures imprécises. J’ai déjà vu des tableaux où “2024-10” passait avant “2024-2”, juste parce que pandas lisait ça comme du texte. Ça paraît bête, mais derrière vous pouvez fausser tout un reporting.
Dans pandas, mon réflexe est simple : je convertis la colonne avec pd.to_datetime, je la passe en index avec set_index, puis je trie l’index. Après ça, je peux découper une période, changer la fréquence avec resample, ou calculer des statistiques locales avec rolling et expanding.
import pandas as pd
df = pd.DataFrame({
"date": ["2024-01-01", "2024-01-02", "2024-01-03", "2024-02-01"],
"valeur": [10, 15, 13, 30]
})
df["date"] = pd.to_datetime(df["date"])
df = df.set_index("date").sort_index()
janvier = df.loc["2024-01-01":"2024-01-31"]
mensuel = df.resample("M")["valeur"].mean()
df["moyenne_mobile_2j"] = df["valeur"].rolling(window=2).mean()Le downsampling, c’est quand je passe vers une fréquence plus large. Par exemple de l’heure au jour, ou du jour au mois. Là, je dois choisir une agrégation qui a du sens : somme pour un chiffre d’affaires, moyenne pour une température, maximum pour un pic de charge.
Le upsampling, c’est l’inverse. Je passe du jour à l’heure, donc je crée des points qui n’existaient pas. Ces points sont vides au départ. Je dois décider quoi en faire : laisser des valeurs manquantes, propager la dernière valeur connue, interpoler, ou revenir à la donnée source si elle existe.
Attention aux fenêtres roulantes. Elles doivent respecter l’ordre du temps. Si je calcule une moyenne avec des données futures, je crée une fuite de données. C’est un piège classique, j’en ai vu chez des clients sur des dashboards prédictifs : les scores étaient magnifiques, mais impossibles à reproduire en production.
| Élément | Usage |
| DatetimeIndex | Index pour des dates précises, avec heure possible. Idéal pour événements, mesures, logs, transactions. |
| PeriodIndex | Index pour des périodes comme mois, trimestres ou années. Pratique quand “janvier 2024” est une unité métier. |
| resample | Change la fréquence temporelle. Sert au downsampling et à l’upsampling. |
| rolling | Calcule une statistique sur une fenêtre glissante, comme une moyenne des 7 derniers jours. |
| expanding | Calcule une statistique cumulative depuis le début de la série jusqu’au point courant. |
Comment nettoyer une série temporelle ?
Je nettoie une série temporelle en séparant d’abord les timestamps manquants des valeurs manquantes, parce que ce n’est pas le même problème. Une date absente dans l’index, c’est un trou dans la grille temporelle. Une valeur NaN sur une date présente, c’est une observation connue comme manquante. Et un simple dropna peut casser toute l’analyse, surtout si vous faites ensuite une moyenne mobile, une décomposition saisonnière ou un modèle prédictif.
Ma méthode est assez simple. Je définis d’abord une grille temporelle canonique, avec la bonne fréquence. Horaire, quotidienne, mensuelle, ça dépend du signal. Je réindexe la série dessus, j’identifie les trous, puis je choisis l’imputation selon le métier. Une température peut souvent être interpolée. Un stock se propage plutôt avec un forward-fill, c’est-à-dire “la dernière valeur connue reste vraie jusqu’au prochain changement”. Une transaction manquante ne s’invente pas. Une tension artérielle non plus, en tout cas pas sans prudence.
import pandas as pd
# Série brute avec index datetime
s = raw["value"].copy()
s.index = pd.to_datetime(raw["timestamp"])
# Doublons : ici je garde la moyenne, mais ça dépend du métier
s = s.groupby(level=0).mean().sort_index()
# Grille canonique horaire
idx = pd.date_range(s.index.min(), s.index.max(), freq="h")
s_reg = s.reindex(idx)
# Séparer dates absentes et valeurs NaN déjà présentes
missing_timestamps = idx.difference(s.index)
nan_on_existing_dates = s.index[s.isna()]
# Interpolation temporelle, mais seulement pour petits trous
s_interp = s_reg.interpolate(method="time", limit=2, limit_area="inside")
# Forward-fill pour un état, limité pour éviter les grands remplissages aveugles
s_ffill = s_reg.ffill(limit=3)
# Dates qui restent manquantes après prudence
still_missing = s_interp[s_interp.isna()]
Pour les variations continues, l’interpolation temporelle marche souvent bien. Pour un état qui reste valable, forward-fill est plus logique. Pour des trous longs, je préfère souvent laisser manquant ou ajouter un flag “donnée imputée”. Quand la saisonnalité est très forte, une décomposition saisonnière peut aider, mais ce n’est pas magique. J’ai déjà vu un client lisser une rupture de capteur comme si c’était une vraie baisse d’activité. Le modèle était propre, mais faux.
Les outliers se jugent aussi dans le temps. Une valeur peut être normale pendant un pic et absurde en période creuse. Je regarde souvent des Z-scores roulants, donc un écart à la moyenne locale, ou un IQR sur fenêtre glissante, l’écart entre les quartiles locaux. Pour plusieurs capteurs ou variables, Isolation Forest peut aider à repérer des anomalies multivariées, mais je le garde comme signal d’alerte, pas comme vérité absolue.
L’alignement des fréquences compte autant que le nettoyage. Joindre des ventes quotidiennes avec une météo horaire ou un calendrier mensuel demande une règle claire. Moyenne météo du jour, maximum, dernière valeur connue, mois attaché à chaque jour ? Sans ça, on fabrique de la précision qui n’existe pas.
| Problème | Symptôme | Solution raisonnable | Piège à éviter |
| Dates absentes | Index irrégulier | Réindexer sur une fréquence canonique | Faire un dropna directement |
| Valeurs NaN | Date présente, valeur vide | Interpoler ou propager selon le signal | Remplir sans limite |
| Outliers locaux | Pic isolé ou rupture étrange | Z-score roulant, IQR glissant, contrôle métier | Comparer à une moyenne globale |
| Fréquences différentes | Jointures incohérentes | Agréger ou propager avec une règle claire | Créer une précision artificielle |
Comment créer des variables utiles ?
Je crée des variables utiles en transformant le passé disponible en signaux exploitables, jamais en utilisant une information du futur.
La date brute seule sert rarement à grand-chose. Un modèle ne comprend pas “2025-03-14” comme nous. Il a besoin d’indices plus parlants : l’heure, le jour de semaine, le mois, une période de vacances si on l’a, une valeur de la veille, une moyenne des derniers jours, une variation récente, un écart à une tendance.
Les variables de retard, ou lags, sont souvent les premières à créer. Avec shift, je décale la série pour donner au modèle ce qu’on connaissait déjà au moment de prédire. Par exemple lag_1 peut représenter la valeur de la veille, lag_7 celle de la semaine précédente, et lag_30 celle du mois précédent si la série est quotidienne.
Les statistiques roulantes ajoutent du contexte. Une moyenne mobile sur 7 jours dit si on est plutôt haut ou bas récemment. Une médiane résiste mieux aux valeurs extrêmes. Un écart-type mesure l’instabilité. Un min ou un max donne les bornes récentes. Le piège classique, c’est de calculer ces valeurs avec la cible du jour incluse. Là, on triche sans le vouloir.
import pandas as pd
df = df.sort_values("date").copy()
df["date"] = pd.to_datetime(df["date"])
# Variables calendrier
df["day_of_week"] = df["date"].dt.dayofweek
df["month"] = df["date"].dt.month
# Variables de retard
df["lag_1"] = df["y"].shift(1)
df["lag_7"] = df["y"].shift(7)
# Rolling calculé uniquement sur le passé connu
df["rolling_mean_7"] = df["y"].shift(1).rolling(window=7).mean()
df["rolling_std_7"] = df["y"].shift(1).rolling(window=7).std()
df = df.dropna()La différenciation peut aussi aider quand une série monte ou descend fortement dans le temps. Au lieu de prédire le niveau brut, je travaille sur la variation entre deux périodes. C’est utile quand la tendance écrase tout le reste. On enlève un peu le “mouvement de fond” pour mieux voir les changements réels.
Dans les projets réels, les meilleures variables viennent souvent d’un échange métier très simple. Pour un inventaire, une rupture de stock précédente explique parfois plus qu’un modèle sophistiqué. Pour une consommation d’énergie, la météo ou le calendrier peut tout changer. C’est rarement magique, c’est surtout du bon sens bien encodé.
| Famille de variables | Intérêt | Risque principal de fuite de données |
| Calendrier | Capte les cycles jour, semaine, mois, vacances | Utiliser un calendrier non disponible au moment réel de la prédiction |
| Retards | Donne au modèle les valeurs passées importantes | Décaler dans le mauvais sens et injecter le futur |
| Statistiques roulantes | Résume les dernières périodes avec moyenne, médiane, volatilité | Inclure la valeur cible du jour dans le calcul |
| Différenciation | Travaille sur les variations plutôt que sur les niveaux | Recomposer la prédiction finale avec des valeurs futures |
| Variables métier | Ajoute le contexte réel que la date ne porte pas | Utiliser une information connue seulement après coup |
Comment valider sans se tromper ?
Je valide une série temporelle en respectant la chronologie, jamais avec un découpage aléatoire classique.
Une fois que mes données sont comprises, indexées, nettoyées et enrichies, je peux tester un modèle. Mais le test doit ressembler à la vraie vie : j’entraîne sur le passé, je prédis le futur. Sinon, je me raconte une belle histoire.
Le train test split aléatoire est dangereux ici parce qu’il mélange des observations futures dans l’apprentissage. Le modèle voit indirectement le futur, puis il est évalué sur un passé qu’il connaît déjà un peu. Résultat classique : une performance trop optimiste, et une grosse déception en production. J’ai déjà vu ça chez un client retail, le modèle semblait excellent en notebook, puis il s’est écroulé dès qu’on l’a branché sur les ventes réelles.
Le découpage le plus simple reste chronologique. Je garde les premières lignes pour l’entraînement, les dernières pour le test.
import pandas as pd
# Série déjà triée par date avec une colonne cible "y"
df = df.sort_index()
split_point = int(len(df) * 0.8)
train = df.iloc[:split_point]
test = df.iloc[split_point:]
X_train = train.drop(columns=["y"])
y_train = train["y"]
X_test = test.drop(columns=["y"])
y_test = test["y"]
print(train.index.min(), train.index.max())
print(test.index.min(), test.index.max())Quand je veux une validation plus robuste, j’utilise une fenêtre glissante ou une fenêtre expansive. La fenêtre glissante garde une taille d’historique fixe. La fenêtre expansive ajoute progressivement plus de passé. TimeSeriesSplit de scikit-learn fait très bien ce travail.
from sklearn.model_selection import TimeSeriesSplit
X = df.drop(columns=["y"])
y = df["y"]
tscv = TimeSeriesSplit(n_splits=5)
for fold, (train_idx, test_idx) in enumerate(tscv.split(X), start=1):
X_train, X_test = X.iloc[train_idx], X.iloc[test_idx]
y_train, y_test = y.iloc[train_idx], y.iloc[test_idx]
print(f"Fold {fold}")
print("Train :", X_train.index.min(), "à", X_train.index.max())
print("Test :", X_test.index.min(), "à", X_test.index.max())Côté modèles, je reste pragmatique. Les modèles statistiques comme ARIMA ou le lissage exponentiel avec statsmodels marchent bien sur des séries propres. Les modèles machine learning fonctionnent aussi, à condition d’avoir de bonnes variables temporelles : retards, moyennes mobiles, jour de semaine, mois, saison. Les modèles avancés ont du sens quand le volume, la granularité et l’enjeu business le justifient. Le modèle vient après la préparation, pas avant.
Pour mesurer, j’utilise souvent la MAE, l’erreur moyenne absolue, facile à lire. La RMSE pénalise plus fort les grosses erreurs. Le MAPE parle en pourcentage, mais je fais attention si les valeurs sont proches de zéro, parce qu’il peut devenir instable.
Et après le déploiement, je surveille. Une série peut changer de régime. Un modèle bon aujourd’hui peut devenir mauvais demain. Je regarde l’erreur, les données entrantes, les trous, les ruptures, les changements de saisonnalité. C’est souvent là que le projet gagne ou perd sa vraie valeur business.
| Méthode de validation | Quand l’utiliser | Avantage | Limite |
| Découpage chronologique simple | Pour un premier test rapide | Simple et réaliste | Dépend beaucoup d’une seule période de test |
| Fenêtre glissante | Quand le passé récent compte plus que l’ancien | Reproduit bien un usage opérationnel | Peut ignorer des historiques utiles |
| Fenêtre expansive | Quand tout l’historique reste pertinent | Utilise de plus en plus de données | Moins adaptée si la série change fortement |
| TimeSeriesSplit | Pour automatiser plusieurs validations temporelles | Standard, pratique, compatible scikit-learn | Demande de bien choisir le nombre de splits |
Et maintenant, vous testez sur quelle série ?
Pour analyser correctement des séries temporelles Python, je pars toujours du temps lui-même. Je vérifie l’index, la fréquence, les trous, les anomalies, la tendance et la saisonnalité avant de parler modèle. Ensuite seulement, je crée des variables avec le passé disponible, je valide dans l’ordre chronologique, et je surveille le comportement dans le temps. C’est moins spectaculaire qu’un modèle posé trop vite, mais c’est beaucoup plus fiable. Le bénéfice pour vous est simple : des analyses plus justes, des prévisions moins fragiles, et des décisions business qui reposent sur des signaux propres.
FAQ
- Qu’est-ce qu’une série temporelle en Python ?
Une série temporelle est une suite de valeurs ordonnées dans le temps. En Python, je la manipule souvent avec pandas, en utilisant un index temporel comme DatetimeIndex. L’ordre des observations compte autant que les valeurs elles-mêmes. - Pourquoi ne pas utiliser un split aléatoire sur une série temporelle ?
Parce qu’un split aléatoire mélange le futur et le passé. Le modèle peut apprendre des informations qu’il n’aurait jamais eues au moment réel de la prédiction. Résultat : une performance trop belle, mais inutile en production. - Quelle est la différence entre DatetimeIndex et PeriodIndex ?
DatetimeIndex représente des instants précis, comme une date et une heure. PeriodIndex représente des périodes, comme un mois, un trimestre ou une année. Le choix dépend de la manière dont je veux analyser ou agréger la donnée. - Comment gérer les valeurs manquantes dans une série temporelle ?
Je commence par distinguer les timestamps absents des valeurs NaN. Ensuite, je choisis une méthode adaptée au signal : interpolation temporelle, forward-fill, réindexation sur une fréquence fixe ou traitement spécifique des trous longs. Il n’y a pas une seule bonne méthode universelle. - Quels outils Python utiliser pour analyser des séries temporelles ?
J’utilise surtout pandas pour structurer, rééchantillonner et préparer les données. Pour la validation, scikit-learn propose TimeSeriesSplit. Pour certains modèles statistiques, statsmodels est une référence courante. Le plus important reste la qualité de l’index, du nettoyage et de la validation.
A propos de l’auteur
Je suis Franck Scandolera, expert et formateur en tracking avancé server-side, Analytics Engineering, automatisation No/Low Code avec n8n, intégration de l’IA en entreprise et SEO/GEO. J’accompagne des équipes data, marketing et business sur des sujets où la donnée doit être fiable, exploitable et actionnable. J’ai travaillé avec des références comme Logis Hôtel, Yelloh Village, BazarChic, la Fédération Française de Football ou Texdecor. Je dirige l’agence webAnalyste et l’organisme Formations Analytics. Si vous voulez structurer vos données, automatiser vos analyses ou intégrer l’IA proprement dans vos process, contactez-moi.
⭐ Analytics engineer, Data Analyst et Automatisation IA indépendant ⭐
Ref clients : Logis Hôtel, Yelloh Village, BazarChic, Fédération Football Français, Texdecor…
Mon terrain de jeu :
Data Analyst & Analytics engineering : tracking avancé (GA4, Matomo, Piano, GTM server, Tealium, Commander Act, e-commerce, CAPI, RGPD), entrepôt de données (BigQuery, Snowflake, PostgreSQL, ClickHouse), modèles (Airflow, dbt, Dataform), dashboards décisionnels (Looker, Power BI, Metabase, SQL, Python).
Automatisation IA des taches Data, Marketing, RH, compta etc : conception de workflows intelligents robustes (n8n, App Script, scraping) connectés aux API de vos outils et LLM (OpenAI, Mistral, Claude…).
Engineering IA pour créer des applications et agent IA sur mesure : intégration de LLM (OpenAI, Mistral…), RAG, assistants métier, génération de documents complexes, APIs, backends Node.js/Python.





