Séries temporelles Pandas : 10 astuces pour des analyses sans biais

·

·

15 min de lecture

Séries temporelles Pandas : 10 astuces pour des analyses sans biais

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.

Vous avez aimé cet article ?

Recevez les prochains directement dans votre boîte mail.