Séries temporelles dans Pandas : 10 recettes pour éviter les pièges qui faussent tes analyses
En bref
- Un index datetime mal configuré peut biaiser tes resamplings sans que tu t’en aperçoives – un décalage horaire ou un gap invisible, et tes agrégations deviennent fausses.
rolling(« 7D »)≠rolling(7): cette nuance transforme un graphique trompeur en analyse fiable, surtout quand tes données ont des trous.- Les moyennes mobiles pondérées (
ewm) et les jointures temporelles tolérantes (merge_asof) sauvent tes dashboards IoT du bruit et des incohérences. - Une seule ligne de code mal placée peut faire basculer ton dashboard d’un outil de décision à une source de confusion – et personne ne te préviendra.
—
Pourquoi tes séries temporelles mentent (et comment les faire parler vrai)
Tu as déjà passé des heures à nettoyer des données, à resampler des séries, à calculer des moyennes mobiles… pour réaliser trop tard que ton « graphique propre » cachait une erreur silencieuse ? Un décalage horaire ignoré, une interpolation abusive, ou un rolling basé sur des comptes plutôt que sur le temps réel ?
Le vrai problème ? Les séries temporelles sont des pièges à clics. Un index non monotone, une timezone oubliée, ou un resampling mal ancré – et tes agrégations deviennent fausses. Sans aucun warning de Pandas.
La bonne nouvelle ? Avec ces 10 recettes testées en production, tu peux éviter 90% des pièges courants. Voici comment transformer tes données temporelles en analyses fiables, rapides et reproductibles – sans te faire avoir par les détails qui tuent.
—
1. L’index datetime : la base que 80% des gens bâclent (et le paient cher)
Le piège : Un index non datetime ou non trié, et tes resamplings/rollings deviennent imprévisibles. Pandas ne te préviendra pas.
La solution : Nettoie ton index dès l’import, et assure-toi qu’il est prêt pour les opérations temporelles.
import pandas as pd
# 1. Parse les timestamps dès la lecture (évite les surprises)
df = pd.read_csv("metrics.csv", parse_dates=["ts"])
# 2. Trie et définis l'index (monotonicité obligatoire)
df = df.sort_values("ts").set_index("ts")
# 3. Gère les timezones (évite les décalages DST)
df = df.tz_localize("UTC") if df.index.tz is None else df
# 4. (Optionnel) Infère la fréquence pour optimiser les calculs
try:
df = df.asfreq(pd.infer_freq(df.index))
except Exception:
pass # Pas toujours possible, c'est OK
👉 **Pourquoi c’est crucial** :
- Un index non trié ? `resample` et `rolling` donneront des résultats incohérents – **sans erreur**.
- Une timezone ignorée ? Tes agrégations horaires seront décalées pendant les changements d’heure (DST).
- Pas de fréquence inférée ? Pandas devra deviner, ce qui ralentit les calculs et peut fausser les interpolations.
**Exemple concret** :
Si tes données ont un timestamp à `2023-03-26 02:30:00` (heure d’été en Europe/Paris), mais que ton index est en UTC, Pandas va resampler comme si c’était `00:30:00`. **Résultat ?** Tes agrégations horaires seront décalées d’1h ce jour-là.
---
## **2. Resampling : comment agréger sans mentir (ni te mentir)**
### **Downsampling (réduire la fréquence)**
**Le piège** : Un resampling hebdomadaire par défaut (`"W"`) peut ancrer tes semaines sur des jours aléatoires – **et tu ne le verras pas.**
**La solution** : **Ancre explicitement** tes périodes (ex: `"W-MON"` pour des semaines commençant le lundi).
python
# Daily → Weekly (ancré au lundi, avec agrégations honnêtes)
weekly = df.resample("W-MON").agg(
value_sum=("value", "sum"), # Somme pour les comptages
value_mean=("value", "mean"), # Moyenne pour les mesures
count=("value", "size") # Nombre de points pour détecter les gaps
)
👉 **Le truc en plus** :
- `"W"` vs `"W-MON"` : la différence semble minime, mais elle change **tous tes calculs** si tes données ont des patterns hebdomadaires (ex: trafic web plus élevé le lundi).
- Pour les données financières, utilise `"B"` (business days) ou un calendrier personnalisé (voir recette 8).
**Cas pratique** :
Si tu resamples des ventes quotidiennes en semaines avec `"W"`, Pandas peut ancrer tes semaines sur un mercredi. **Résultat ?** Tes "semaines" commenceront un mercredi et finiront un mardi – ce qui fausse les comparaisons avec les données externes (ex: rapports marketing).
---
### **Upsampling (augmenter la fréquence)**
**Le piège** : Inventer des données avec `ffill` ou `interpolate` peut fausser tes analyses – **et personne ne te dira que tu inventes des chiffres.**
**La solution** : Choisis la méthode en fonction de la **nature de tes données** :
python
# 1. Forward-fill (pour les états discontinus, ex: changements de configuration)
minute_ffill = df.resample("T").ffill()
# 2. Interpolation linéaire (pour les signaux continus, ex: température)
minute_linear = df.resample("T").interpolate(method="time")
# 3. Distribuer un compte horaire en taux minute (ex: trafic web)
per_min_rate = (df["count"].resample("T")
.ffill()
.div(60)) # 1 count/h → 1/60 count/min
👉 **Règle d’or** :
- **`ffill`** → Données en escalier (états, événements). Ex: un capteur qui envoie "ON" ou "OFF".
- **`interpolate`** → Signaux physiques (température, pression). Ex: une courbe de température lissée.
- **Jamais d’interpolation sur des comptes** (ex: nombre de ventes) sans les convertir en taux. **Pourquoi ?** Parce que 10 ventes en 1h ne signifient pas 0,16 vente par minute – c’est une **moyenne**, pas une réalité.
**Exemple qui fait mal** :
Si tu upsamples des ventes horaires en minutes avec `interpolate`, Pandas va inventer des ventes fractionnaires (ex: 0,5 vente à 14h30). **Problème ?** Tes analyses de conversion ou de panier moyen seront faussées.
---
## **3. Rolling windows : temps vs. comptes (le piège qui change tout)**
**Le piège** : `rolling(7)` vs `rolling("7D")` donne des résultats **radicalement différents** si tes données ont des gaps. **Et tu ne le verras pas dans ton graphique.**
python
# ❌ Risqué : basé sur des comptes (7 lignes, même si elles couvrent 10 jours)
roll7rows = df["value"].rolling(7, min_periods=3).mean()
# ✅ Robuste : basé sur le temps (7 jours, même avec des gaps)
roll7d = df["value"].rolling("7D", min_periods=3, center=True).mean()
👉 **Quand utiliser quoi** :
| Cas d’usage | `rolling(7)` (compte) | `rolling("7D")` (temps) |
|---------------------------|-----------------------|-------------------------|
| Données **parfaitement régulières** (ex: logs toutes les heures) | ✅ OK | ✅ OK |
| Données **avec gaps** (ex: capteurs IoT) | ❌ Biaisé | ✅ Robuste |
| Données **à fréquence variable** (ex: transactions) | ❌ Incohérent | ✅ Fiable |
**Exemple concret** :
Imaginons un capteur qui envoie des données toutes les 5 minutes, mais avec des trous :
- `rolling(7)` va calculer une moyenne sur 7 points, même si ces 7 points couvrent 35 minutes ou 3 heures.
- `rolling("7D")` va calculer une moyenne sur **7 jours de données réelles**, même si certains jours ont 0 point.
**Résultat ?** `rolling(7)` peut donner une moyenne sur 3h de données, alors que `rolling("7D")` donnera une moyenne sur 7 jours – **deux interprétations radicalement différentes.**
---
## **4. Timezones : le piège invisible des DST (et comment l’éviter)**
**Le piège** : Convertir une timezone **après** un resampling décale tes bins. **Et Pandas ne te préviendra pas.**
**La solution** : **Convertis avant** de resampler.
python
# ❌ À éviter : resample d'abord, convertis après
df_utc = df.resample("H").mean()
df_local = df_utc.tz_convert("Europe/Paris") # Bins décalés pendant DST !
# ✅ Bon : convertis d'abord, resample après
df_local = df.tz_convert("Europe/Paris") # ou tz_localize si index naive
df_hourly = df_local.resample("H").mean() # Bins cohérents
👉 **Cas pratique** :
- Si tu analyses des données de trafic web en `UTC` mais que ton public est en `Europe/Paris`, **convertis avant le resampling** pour éviter des décalages d’1h pendant l’heure d’été.
- **Pourquoi ?** Parce que le 26 mars 2023 à 2h du matin, l’heure passe à 3h en Europe/Paris. Si tu resamples en UTC d’abord, Pandas va créer un bin à `01:00:00` qui n’existe pas en heure locale.
**Astuce** :
Utilise `df.index.tz_convert(None)` pour supprimer la timezone si tu veux travailler en "naive time" (mais attention aux ambiguïtés !).
---
## **5. Rolling pondéré : donne plus de poids aux données récentes (sans tricher)**
**Le piège** : Une moyenne mobile simple (`rolling.mean()`) traite tous les points de la même façon – **même les vieux.**
**La solution** : Utilise des poids pour privilégier les données récentes.
### **Option 1 : Moyenne pondérée manuelle**
python
import numpy as np
# Poids croissants (le plus récent a 5x plus de poids)
weights = np.array([1, 2, 3, 4, 5], dtype=float)
weights = weights / weights.sum()
def weighted_avg(x):
return np.dot(x, weights[-len(x):]) / weights[-len(x):].sum()
weighted_roll = df["value"].rolling(5, min_periods=3).apply(weighted_avg, raw=True)
### **Option 2 : Moyenne exponentielle (`ewm`)**
python
# Lissage exponentiel (halflife = 3 jours)
ewm_smooth = df["value"].ewm(halflife="3D", adjust=False).mean()
👉 **Quand utiliser quoi** :
| Méthode | Avantages | Inconvénients | Cas d’usage |
|-----------------------|------------------------------------|-----------------------------------|---------------------------------|
| **`ewm`** | Réactif, simple à paramétrer | Moins transparent | Cours de bourse, signaux bruyants |
| **Pondération manuelle** | Contrôle fin sur les poids | Plus complexe à implémenter | Données où tu veux un poids spécifique (ex: 80% sur les 3 derniers jours) |
**Exemple concret** :
Si tu analyses des ventes quotidiennes et que tu veux donner plus de poids aux derniers jours (car plus représentatifs des tendances actuelles), une pondération manuelle te permet de dire : *"Les 3 derniers jours comptent pour 70% de la moyenne."*
---
## **6. Jointures temporelles tolérantes avec `merge_asof` (le sauveur des données désynchronisées)**
**Le piège** : Joindre deux tables temporelles avec des timestamps légèrement décalés – **et obtenir des NaN partout.**
**La solution** : `merge_asof` avec une tolérance explicite.
python
# left_df : événements (ex: déploys)
# right_df : métriques (resamplées à la minute)
left_df = left_df.sort_values("ts")
right_df = right_df.sort_values("ts")
joined = pd.merge_asof(
left_df, right_df,
on="ts",
direction="backward", # Dernière métrique avant l'événement
tolerance=pd.Timedelta("5min") # Tolérance de 5 minutes
)
👉 **Pourquoi c’est puissant** :
- **Sans tolérance** : Si les timestamps ne matchent pas exactement, la jointure échoue (NaN).
- **Avec tolérance** : Tu peux associer des événements à la métrique la plus proche dans une fenêtre de temps.
- **Cas d’usage** :
- Associer des pics de latence à des déploys.
- Corréler des anomalies à des changements de configuration.
- Joindre des logs d’erreurs à des métriques système.
**Exemple concret** :
Si tu as un déploiement à `14:32:15` et des métriques toutes les minutes (`14:32:00`, `14:33:00`), `merge_asof` avec une tolérance de 5min va associer le déploiement à la métrique de `14:32:00` – **sans que tu aies à arrondir les timestamps manuellement.**
---
## **7. Séries temporelles robustes : détecter et gérer les outliers (sans tout casser)**
**Le piège** : Un pic de données fausse ta moyenne mobile – **et ton dashboard devient trompeur.**
**La solution** : Utilise des statistiques robustes (médiane + MAD).
python
# 1. Calcul de la médiane mobile
med = df["value"].rolling("7D", min_periods=3).median()
# 2. Calcul de l'écart médian absolu (MAD)
mad = (df["value"] - med).abs().rolling("7D", min_periods=3).median() * 1.4826
# 3. Score Z robuste
z_robust = (df["value"] - med) / mad.replace(0, np.nan)
# 4. Filtrage des outliers (seuil à 3)
clean = df.loc[z_robust.abs() <= 3, "value"]
# 5. Moyenne mobile sur les données nettoyées
trend = clean.rolling("7D", min_periods=3).mean()
👉 **Quand utiliser ça** :
- Données bruyantes (capteurs IoT, logs d’erreurs).
- Séries avec des pics rares (ex: coûts cloud avec des pics ponctuels).
- **Alternative** : `ewm` pour un lissage plus doux si tes données sont très volatiles.
**Pourquoi la médiane + MAD ?**
- La moyenne et l’écart-type sont sensibles aux outliers.
- La **médiane** et le **MAD** (Median Absolute Deviation) sont **robustes** – un pic de données ne les fera pas dérailler.
**Exemple concret** :
Si un capteur envoie soudainement une valeur 100x plus élevée à cause d’un bug, une moyenne mobile classique va être **fortement biaisée**. La médiane + MAD, elle, va ignorer ce pic et continuer à donner une tendance fiable.
---
## **8. Calendriers business-day : évite les weekends dans tes moyennes (et tes mauvaises décisions)**
**Le piège** : Des zéros le week-end faussent tes agrégations – **et tu prends des décisions sur des données erronées.**
**La solution** : Utilise un calendrier personnalisé avec `CustomBusinessDay`.
python
from pandas.tseries.holiday import AbstractHolidayCalendar, Holiday
from pandas.tseries.offsets import CustomBusinessDay
# 1. Définis tes jours fériés
class MyHolidays(AbstractHolidayCalendar):
rules = [Holiday("FoundersDay", month=4, day=14)]
# 2. Crée un calendrier business-day
biz = CustomBusinessDay(calendar=MyHolidays())
# 3. Applique-le à tes données
biz_df = df.asfreq(biz) # Gaps uniquement pour les jours ouvrés
biz_daily = df.resample(biz).mean() # Moyenne sur les jours ouvrés
👉 **Cas d’usage** :
- Données financières (évite de moyenner sur des weekends).
- Métriques opérationnelles (ex: trafic web, commandes).
- **Astuce** : Combine avec `ffill` pour remplir les gaps si nécessaire.
**Exemple concret** :
Si tu analyses des ventes quotidiennes et que tu veux une moyenne **uniquement sur les jours ouvrés**, `CustomBusinessDay` va :
1. Ignorer les weekends et jours fériés.
2. Calculer une moyenne sur **5 jours** au lieu de 7 – ce qui donne une vision plus réaliste de ton activité.
**Pour aller plus loin** :
Tu peux aussi utiliser `CustomBusinessHour` pour les données horaires (ex: métriques de production en usine).
---
## **9. Cas pratique : transformer 500 capteurs IoT bruyants en un dashboard fiable (sans se faire avoir)**
**Le problème** :
- 500 capteurs qui envoient des données toutes les 40-70 secondes (avec des gaps).
- L’équipe ops veut un dashboard **quotidien** avec une tendance lissée sur 7 jours.
- La direction veut un résumé **business-day** (sans weekends).
- **Le piège** : Les données sont irrégulières, bruyantes, et avec des timezones différentes.
**La solution** :
python
# 1. Nettoie l'index et convertis en timezone locale
df = df.sort_values("ts").set_index("ts").tz_convert("Asia/Kolkata")
# 2. Upsample en minute avec interpolation (températures = signal continu)
df_minute = df.resample("T").interpolate(method="time")
# 3. Agrège par jour et par capteur
daily = (df_minute
.reset_index()
.groupby(["id", pd.Grouper(key="ts", freq="D")])
.mean())
# 4. Lissage 7 jours (time-aware)
daily["trend_7d"] = daily.groupby("id")["value"].rolling("7D", min_periods=3).mean()
# 5. Vue business-day pour la direction
biz = CustomBusinessDay(calendar=MyHolidays())
biz_daily = daily.groupby(pd.Grouper(key="ts", freq=biz)).mean()
# 6. Détection d'anomalies (robuste)
daily["z_robust"] = (daily["value"] - daily["value"].rolling("7D").median()) /
(daily["value"].rolling("7D").mad() * 1.4826)
daily["is_outlier"] = daily["z_robust"].abs() > 3
# 7. Jointure avec les événements (ex: déploys)
joined = pd.merge_asof(
daily.reset_index(),
deploys.sort_values("ts"),
on="ts",
direction="backward",
tolerance=pd.Timedelta("1D")
)
**Résultat** :
✅ Un dashboard quotidien **fiable**, même avec des données irrégulières.
✅ Une vue business-day **propre** pour la direction (sans weekends ni jours fériés).
✅ Des anomalies **marquées** pour investigation (sans faux positifs).
✅ Une jointure temporelle **tolérante** pour corréler les événements aux métriques.
**Leçon apprise** :
Avec des données IoT, **tout est une question de trade-offs** :
- **Interpolation** : Oui pour les signaux continus (température), non pour les comptes (nombre de messages).
- **Lissage** : `ewm` pour la réactivité, médiane mobile pour la robustesse.
- **Timezones** : Convertis **avant** le resampling, sinon tes agrégations seront décalées.
---
## **10. Les pièges courants (et comment les éviter – pour de bon)**
| Piège | Solution | Exemple concret |
|-------|----------|-----------------|
| `rolling(7)` sur des données irrégulières | Utilise `rolling("7D")` avec `min_periods` | Un capteur avec des gaps : `rolling(7)` donne une moyenne sur 3h, `rolling("7D")` sur 7 jours. |
| Resampling après conversion de timezone | Convertis **avant** de resampler | Resampler en UTC puis convertir en Europe/Paris décale les bins pendant le DST. |
| Interpolation sur des comptes | Convertis en taux (`div(60)` pour des comptes horaires → minute) | 10 ventes en 1h ≠ 0,16 vente par minute. |
| Weekends dans les moyennes financières | Utilise `CustomBusinessDay` | Une moyenne sur 7 jours inclut des weekends où l’activité est nulle. |
| Jointures temporelles strictes | `merge_asof` avec une tolérance | Associer un déploiement à 14:32:15 à une métrique à 14:32:00. |
| Outliers qui faussent les moyennes | Médiane + MAD ou `ewm` | Un pic de données biaise une moyenne mobile classique. |
---
## **Conclusion : une ligne de code peut tout changer (pour le meilleur ou pour le pire)**
Les séries temporelles sont **le domaine où les détails comptent le plus**. Un index mal configuré, un `rolling` basé sur des comptes plutôt que sur le temps, ou une timezone ignorée – et tes analyses deviennent **trompeuses sans que tu t’en rendes compte**.
**La bonne nouvelle ?** Avec ces 10 recettes, tu peux éviter 90% des pièges courants. **Teste-les sur tes données**, et observe la différence :
- Des graphiques **plus propres** et **plus fiables**.
- Des agrégations **cohérentes**, même avec des données irrégulières.
- Des dashboards **qui inspirent confiance** (et pas des maux de tête).
**Et maintenant ?** Prends ton dataset le plus problématique, applique ces recettes, et regarde comment tes analyses deviennent **plus précises en quelques lignes de code**.
**Dernier conseil** : La prochaine fois que tu vois un graphique de série temporelle, demande-toi :
- *"Est-ce que l’index est bien trié et en datetime ?"*
- *"Est-ce que le resampling est ancré sur une période logique ?"*
- *"Est-ce que les timezones sont gérées correctement ?"*
Si la réponse est non à l’une de ces questions, **ton analyse est probablement biaisée**. Et maintenant, tu sais comment la corriger.
---
**PS** : Tu as un dataset de séries temporelles qui te donne du fil à retordre ? Partage un extrait (anonymisé) en commentaire, et je te dirai **exactement** quelles recettes appliquer pour le dompter.



