# Exercices ## Exercices Très Basiques (Application Directe) ### Exercice 1 : Manipulation Basique de Séries Temporelles avec Pandas Vous disposez d'un fichier CSV fictif `ventes_quotidiennes.csv` contenant les ventes journalières d'un produit. Le fichier a deux colonnes : `Date` (au format `AAAA-MM-JJ`) et `Ventes`. **Objectifs :** 1. Charger le fichier CSV dans un `DataFrame` Pandas. 2. Convertir la colonne `Date` en un index de type `DatetimeIndex`. 3. Rééchantillonner les données pour obtenir la somme des ventes mensuelles. > [!example] Contenu de `ventes_quotidiennes.csv` (exemple) > ```csv > Date,Ventes > 2023-01-01,100 > 2023-01-02,120 > 2023-01-03,110 > ... > 2023-02-01,150 > ... > ``` > *Pour l'exécution de l'exercice, vous pouvez simuler ce fichier ou créer un DataFrame directement en Python.* ### Exercice 2 : Calcul d'une Moyenne Mobile Simple Considérons la série temporelle suivante représentant des températures journalières : `temperatures = [20, 22, 21, 23, 25, 24, 26, 28, 27, 29]` **Objectifs :** 1. Calculer la moyenne mobile simple d'ordre 3 (SMA-3) pour cette série. 2. Expliquer l'utilité d'une moyenne mobile. > [!definition] Moyenne Mobile Simple (SMA) > La moyenne mobile simple d'ordre $k$ à l'instant $t$ est la moyenne arithmétique des $k$ observations précédentes, y compris l'observation courante : > $ SMA_k(t) = \frac{1}{k} \sum_{i=0}^{k-1} y_{t-i} $ ## Exercices de Niveau Normal (Combinaison de Concepts) ### Exercice 3 : Nettoyage et Ingénierie de Caractéristiques Temporelles Vous avez un `DataFrame` Pandas `df_data` avec un index `DatetimeIndex` et une colonne `Valeur`. Le `DataFrame` contient des valeurs manquantes et couvre une période d'au moins un an. **Objectifs :** 1. Simuler un `DataFrame` avec un `DatetimeIndex` et quelques valeurs manquantes. 2. Gérer les valeurs manquantes dans la colonne `Valeur` en utilisant l'interpolation linéaire. 3. Créer les caractéristiques de temps suivantes à partir de l'index : * `jour_semaine` (numéro du jour de la semaine) * `mois` (numéro du mois) * `annee` (année) 4. Créer une caractéristique de décalage (lagged feature) `Valeur_lag_1` qui représente la valeur de la série à l'instant $t-1$. ### Exercice 4 : Décomposition de Série Temporelle Considérons une série temporelle simulée `ts_data` qui présente une tendance et une saisonnalité. **Objectifs :** 1. Simuler une série temporelle avec une tendance linéaire et une saisonnalité additive (période de 12 observations). 2. Appliquer une décomposition de série temporelle pour séparer la tendance, la saisonnalité et le résidu. 3. Visualiser les composants de la décomposition. 4. Expliquer la différence entre une décomposition additive et multiplicative. ### Exercice 5 : Prédiction Naïve et Évaluation Vous disposez d'une série temporelle `data_series`. **Objectifs :** 1. Diviser `data_series` en un ensemble d'entraînement (80%) et un ensemble de test (20%). 2. Implémenter une stratégie de prédiction naïve où la prédiction pour le pas de temps $t$ est simplement la dernière valeur observée dans l'ensemble d'entraînement, $y_{train\_dernier}$. 3. Calculer l'Erreur Absolue Moyenne (MAE) et l'Erreur Quadratique Moyenne (RMSE) des prédictions sur l'ensemble de test. > [!definition] Métriques d'erreur > - **Erreur Absolue Moyenne (MAE)** : $ MAE = \frac{1}{n} \sum_{i=1}^{n} |y_i - \hat{y}_i| $ > - **Erreur Quadratique Moyenne (RMSE)** : $ RMSE = \sqrt{\frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2} $ > Où $y_i$ sont les valeurs réelles et $\hat{y}_i$ sont les prédictions. ### Exercice 6 : Préparation de Données pour Modèles Multivariés Vous avez une série temporelle univariée `sales_data` (représentant des ventes mensuelles) avec un `DatetimeIndex`. Pour améliorer les prévisions, vous souhaitez intégrer des variables exogènes. **Objectifs :** 1. Simuler une série `sales_data` sur 3 ans (mensuelle). 2. Créer les variables exogènes suivantes à partir de l'index temporel : * `mois_sin` et `mois_cos` pour capturer la saisonnalité mensuelle de manière cyclique. * `trimestre` (numéro du trimestre). 3. Simuler une variable exogène externe `promotion` (binaire : 0 ou 1) indiquant si une promotion a eu lieu ce mois-là. 4. Construire un `DataFrame` final prêt pour un modèle multivarié, contenant la série cible et toutes les variables exogènes. ## Exercices Plus Élaborés (Problème, Réflexion) ### Exercice 7 : Implémentation et Comparaison de Modèles de Lissage Exponentiel Vous travaillez sur les données de consommation électrique (simulées) d'une ville, disponibles mensuellement sur 5 ans. La série `consommation_electrique` présente clairement une tendance et une saisonnalité. **Objectifs :** 1. Simuler une série temporelle mensuelle de 5 ans avec une tendance croissante et une saisonnalité annuelle prononcée. 2. Diviser la série en un ensemble d'entraînement (les 4 premières années) et un ensemble de test (la dernière année). 3. Appliquer deux modèles de lissage exponentiel : * **Holt's Linear Trend** (pour la tendance uniquement, sans saisonnalité). * **Holt-Winters Seasonal Smoothing** (avec tendance et saisonnalité additive). 4. Effectuer des prédictions pour la période de test avec chaque modèle. 5. Évaluer les performances de chaque modèle en utilisant le RMSE et le MAPE (Mean Absolute Percentage Error) sur l'ensemble de test. 6. Discuter des résultats : quel modèle est le plus adapté et pourquoi ? > [!definition] Mean Absolute Percentage Error (MAPE) > $ MAPE = \frac{100}{n} \sum_{i=1}^{n} \left| \frac{y_i - \hat{y}_i}{y_i} \right| $ > Le MAPE est utile pour comparer la précision des prévisions entre différentes séries temporelles. ### Exercice 8 : Modèle de Régression Linéaire pour le Forecasting avec Caractéristiques Temporelles et Lagged Features Vous êtes chargé de prévoir la demande journalière d'un produit. Vous disposez d'un `DataFrame` `df_demande` avec la colonne `Demande` et un `DatetimeIndex` sur 2 ans. **Objectifs :** 1. Simuler un `DataFrame` `df_demande` avec une `Demande` journalière sur 2 ans, incluant une saisonnalité hebdomadaire et une tendance. 2. Créer des caractéristiques pertinentes pour la prévision : * `Demande_lag_1` (demande du jour précédent). * `Demande_lag_7` (demande de la même journée la semaine précédente). * `jour_semaine` (jour de la semaine, encodé en one-hot). * `jour_annee_sin` et `jour_annee_cos` (saisonnalité annuelle cyclique). 3. Diviser les données en entraînement (les 18 premiers mois) et test (les 6 derniers mois). 4. Entraîner un modèle de régression linéaire (`LinearRegression` de `scikit-learn`) en utilisant ces caractéristiques pour prédire la `Demande`. 5. Évaluer le modèle sur l'ensemble de test (RMSE, MAE). 6. Interpréter les coefficients du modèle de régression pour les caractéristiques créées. ### Exercice 9 : Analyse de Stationnarité et Différenciation pour ARIMA Vous étudiez une série temporelle `prix_action` qui semble non stationnaire, représentant l'évolution du prix d'une action sur plusieurs années. **Objectifs :** 1. Simuler une série temporelle `prix_action` sur 5 ans (journalière) avec une tendance forte et une volatilité croissante (rendant la variance non constante). 2. Visualiser la série et discuter visuellement de sa stationnarité. 3. Appliquer le test de Dickey-Fuller augmenté (ADF) pour tester la stationnarité. Interpréter les résultats. 4. Si la série est non stationnaire, appliquer une différenciation d'ordre 1 et réappliquer le test ADF. 5. Discuter de l'importance de la stationnarité pour les modèles ARIMA et de la manière dont la différenciation aide à l'atteindre. > [!theorem] Test de Dickey-Fuller Augmenté (ADF) > Le test ADF est un test d'hypothèse statistique pour déterminer si une série temporelle est stationnaire. > - **Hypothèse Nulle ($H_0$)** : La série temporelle a une racine unitaire (elle est non stationnaire). > - **Hypothèse Alternative ($H_1$)** : La série temporelle n'a pas de racine unitaire (elle est stationnaire). > Une p-value faible (typiquement lt; 0.05$) permet de rejeter $H_0$ et de conclure à la stationnarité. ### Exercice 10 : Chaîne de Forecasting Complète avec SARIMAX Vous êtes un consultant en Data Science et une entreprise vous demande de prévoir ses ventes mensuelles pour les 6 prochains mois. Vous disposez de 3 ans de données de ventes mensuelles `ventes_mensuelles` et d'une variable exogène `publicite` (budget marketing du mois précédent). **Objectifs :** 1. Simuler un `DataFrame` `df_ventes` sur 3 ans de données mensuelles avec une tendance, une saisonnalité annuelle et une influence positive de la variable `publicite`. 2. Effectuer un nettoyage de données rudimentaire (vérifier les NaN, les gérer si nécessaire). 3. Créer des caractéristiques temporelles pertinentes (e.g., mois encodé de manière cyclique). 4. Diviser les données en un ensemble d'entraînement (les 2.5 premières années) et un ensemble de test (les 6 derniers mois). 5. Identifier les ordres $p, d, q, P, D, Q, s$ pour un modèle SARIMAX (vous pouvez les choisir de manière heuristique ou par inspection des ACF/PACF si vous connaissez déjà ces concepts, sinon, utilisez des valeurs données). * *Suggestion :* $p=1, d=1, q=1$ pour la partie non saisonnière, $P=1, D=1, Q=1, s=12$ pour la partie saisonnière. 6. Entraîner un modèle `SARIMAX` de `statsmodels` en utilisant les variables exogènes créées. 7. Effectuer des prédictions pour la période de test et pour les 6 mois futurs (hors données). 8. Évaluer les performances du modèle sur l'ensemble de test (RMSE, MAE). 9. Visualiser les prédictions par rapport aux valeurs réelles et discuter des limites du modèle et des améliorations possibles. # Corrigés Détaillés Cette section fournit les solutions complètes et commentées pour chaque exercice. Il est fortement recommandé de tenter de résoudre les exercices par vous-même avant de consulter les corrigés. > [!warning] Attention > Les corrigés sont là pour vous guider. N'hésitez pas à expérimenter d'autres approches ou à adapter le code à vos propres besoins. L'apprentissage passe aussi par l'exploration ! ### Corrigé Exercice 1 : Manipulation Basique de Séries Temporelles avec Pandas **Objectifs :** 1. Charger le fichier CSV dans un `DataFrame` Pandas. 2. Convertir la colonne `Date` en un index de type `DatetimeIndex`. 3. Rééchantillonner les données pour obtenir la somme des ventes mensuelles. ```python import pandas as pd import numpy as np import matplotlib.pyplot as plt # 1. Simuler le fichier CSV pour l'exercice # Créons un DataFrame directement pour simuler le chargement dates = pd.date_range(start='2023-01-01', periods=60, freq='D') ventes = np.random.randint(80, 150, size=60) + np.sin(np.arange(60)/10) * 30 df_quotidien = pd.DataFrame({'Date': dates, 'Ventes': ventes}) # Sauvegarder en CSV pour simuler le chargement depuis un fichier df_quotidien.to_csv('ventes_quotidiennes.csv', index=False) # 1. Charger le fichier CSV # On utilise parse_dates pour convertir la colonne 'Date' directement en datetime # et index_col pour la définir comme index. df = pd.read_csv('ventes_quotidiennes.csv', parse_dates=['Date'], index_col='Date') print("DataFrame initial (premières lignes) :") print(df.head()) print("\nType de l'index initial :", df.index) # 2. Convertir la colonne 'Date' en un index de type DatetimeIndex # Cette étape est déjà gérée par parse_dates et index_col lors du chargement. # Si l'index n'était pas défini lors du chargement, on ferait : # df['Date'] = pd.to_datetime(df['Date']) # df = df.set_index('Date') # df.index = pd.to_datetime(df.index) # S'assurer que le type est bien DatetimeIndex print("\nType de l'index après conversion/chargement :", df.index) # 3. Rééchantillonner les données pour obtenir la somme des ventes mensuelles # 'M' signifie fin de mois. On utilise .sum() pour agréger les ventes. df_mensuel = df.resample('M').sum() print("\nDataFrame rééchantillonné (ventes mensuelles) :") print(df_mensuel) print("\nType de l'index du DataFrame mensuel :", df_mensuel.index) # Visualisation (optionnel mais recommandé) plt.figure(figsize=(12, 6)) plt.plot(df.index, df['Ventes'], label='Ventes Quotidiennes', alpha=0.7) plt.plot(df_mensuel.index, df_mensuel['Ventes'], label='Ventes Mensuelles (Somme)', marker='o', linestyle='--') plt.title('Ventes Quotidiennes vs. Mensuelles') plt.xlabel('Date') plt.ylabel('Ventes') plt.legend() plt.grid(True) plt.show() ``` > [!note] Explication > - `pd.read_csv(..., parse_dates=['Date'], index_col='Date')` est la méthode la plus efficace pour charger des données de séries temporelles depuis un CSV. Elle convertit la colonne spécifiée en `datetime` et la définit comme index directement. > - La méthode `.resample()` est spécifique aux `DatetimeIndex` et est extrêmement puissante pour changer la fréquence d'une série temporelle. `'M'` est un alias pour la fin de chaque mois. Nous appliquons `.sum()` pour agréger les ventes sur chaque période mensuelle. D'autres fonctions d'agrégation comme `.mean()`, `.min()`, `.max()` sont également possibles. ### Corrigé Exercice 2 : Calcul d'une Moyenne Mobile Simple **Objectifs :** 1. Calculer la moyenne mobile simple d'ordre 3 (SMA-3) pour cette série. 2. Expliquer l'utilité d'une moyenne mobile. ```python import pandas as pd import numpy as np # Non strictement nécessaire ici mais bonne pratique temperatures = [20, 22, 21, 23, 25, 24, 26, 28, 27, 29] df_temp = pd.DataFrame({'Temperature': temperatures}) print("Série originale :") print(df_temp) # 1. Calculer la moyenne mobile simple d'ordre 3 (SMA-3) # On utilise la méthode .rolling() de Pandas, puis .mean() df_temp['SMA_3'] = df_temp['Temperature'].rolling(window=3).mean() print("\nSérie avec SMA_3 :") print(df_temp) ``` > [!note] Explication du calcul > - Pour le premier point (index 0), il n'y a pas 3 observations précédentes, donc la SMA est `NaN`. > - Pour le deuxième point (index 1), il n'y a pas 3 observations précédentes, donc la SMA est `NaN`. > - Pour le troisième point (index 2), la SMA est `(20 + 22 + 21) / 3 = 21.0`. > - Pour le quatrième point (index 3), la SMA est `(22 + 21 + 23) / 3 = 22.0`. > - Et ainsi de suite... > [!definition] Utilité d'une Moyenne Mobile > Une moyenne mobile est utilisée pour **lisser les fluctuations** à court terme dans une série temporelle et **identifier les tendances** ou les cycles à plus long terme. > - Elle **réduit le bruit** : en faisant la moyenne sur une fenêtre, les variations aléatoires sont atténuées. > - Elle **met en évidence la tendance** : en éliminant le bruit, la direction générale de la série devient plus claire. > - Elle peut servir de **base pour des prévisions simples** : la dernière valeur de la moyenne mobile peut être utilisée comme prédiction pour le prochain pas de temps. > - Elle est un composant fondamental de certains modèles de lissage exponentiel. ### Corrigé Exercice 3 : Nettoyage et Ingénierie de Caractéristiques Temporelles **Objectifs :** 1. Simuler un `DataFrame` avec un `DatetimeIndex` et quelques valeurs manquantes. 2. Gérer les valeurs manquantes dans la colonne `Valeur` en utilisant l'interpolation linéaire. 3. Créer les caractéristiques de temps suivantes à partir de l'index : `jour_semaine`, `mois`, `annee`. 4. Créer une caractéristique de décalage `Valeur_lag_1`. ```python import pandas as pd import numpy as np import matplotlib.pyplot as plt # 1. Simuler un DataFrame avec un DatetimeIndex et quelques valeurs manquantes np.random.seed(42) # Pour la reproductibilité dates = pd.date_range(start='2022-01-01', periods=365, freq='D') values = np.random.rand(365) * 100 + np.sin(np.arange(365)/30) * 20 + 50 df_data = pd.DataFrame({'Valeur': values}, index=dates) # Introduire quelques NaN nan_indices = df_data.sample(frac=0.05).index # Sélectionne 5% des indices aléatoirement df_data.loc[nan_indices, 'Valeur'] = np.nan print("DataFrame initial avec NaN (premières lignes) :") print(df_data.head()) print("\nNombre de valeurs manquantes avant interpolation :", df_data['Valeur'].isnull().sum()) # 2. Gérer les valeurs manquantes par interpolation linéaire df_data['Valeur_interpolated'] = df_data['Valeur'].interpolate(method='linear') print("\nDataFrame après interpolation (premières lignes) :") print(df_data.head()) print("\nNombre de valeurs manquantes après interpolation :", df_data['Valeur_interpolated'].isnull().sum()) # Visualisation de l'interpolation plt.figure(figsize=(12, 6)) plt.plot(df_data.index, df_data['Valeur'], 'o', label='Valeurs originales (avec NaN)', alpha=0.6, markersize=4) plt.plot(df_data.index, df_data['Valeur_interpolated'], '-', label='Valeurs interpolées', color='red') plt.title('Gestion des Valeurs Manquantes par Interpolation Linéaire') plt.xlabel('Date') plt.ylabel('Valeur') plt.legend() plt.grid(True) plt.show() # 3. Créer des caractéristiques de temps à partir de l'index df_data['jour_semaine'] = df_data.index.dayofweek # Lundi=0, Dimanche=6 df_data['mois'] = df_data.index.month df_data['annee'] = df_data.index.year print("\nDataFrame avec caractéristiques de temps (premières lignes) :") print(df_data.head()) # 4. Créer une caractéristique de décalage (lagged feature) df_data['Valeur_lag_1'] = df_data['Valeur_interpolated'].shift(1) print("\nDataFrame avec Valeur_lag_1 (premières lignes) :") print(df_data.head()) # Vérifier les NaN introduits par le lag (le premier élément n'a pas de précédent) print("\nNombre de NaN dans Valeur_lag_1 :", df_data['Valeur_lag_1'].isnull().sum()) ``` > [!note] Explication > - **Interpolation :** La méthode `.interpolate(method='linear')` remplit les `NaN` en traçant une ligne droite entre les points de données valides adjacents. C'est une méthode simple et efficace pour les séries temporelles. D'autres méthodes existent comme `pad` (remplir avec la valeur précédente) ou `bfill` (remplir avec la valeur suivante). > - **Caractéristiques de temps :** L'objet `DatetimeIndex` de Pandas offre de nombreuses propriétés (`.year`, `.month`, `.dayofweek`, etc.) pour extraire des informations temporelles qui peuvent être cruciales pour les modèles de forecasting (saisonnalité, cycles). > - **Caractéristiques de décalage (Lagged Features) :** La méthode `.shift(n)` décale la série de `n` périodes. `df['col'].shift(1)` donne la valeur de `col` à l'instant `t-1`. Ces caractéristiques sont fondamentales pour les modèles de séries temporelles, car elles permettent au modèle de "se souvenir" des valeurs passées de la série cible. Le premier élément du `lag_1` sera `NaN` car il n'y a pas de valeur précédente. ### Corrigé Exercice 4 : Décomposition de Série Temporelle **Objectifs :** 1. Simuler une série temporelle avec une tendance linéaire et une saisonnalité additive (période de 12 observations). 2. Appliquer une décomposition de série temporelle pour séparer la tendance, la saisonnalité et le résidu. 3. Visualiser les composants de la décomposition. 4. Expliquer la différence entre une décomposition additive et multiplicative. ```python import pandas as pd import numpy as np import matplotlib.pyplot as plt from statsmodels.tsa.seasonal import seasonal_decompose # 1. Simuler une série temporelle avec tendance et saisonnalité additive np.random.seed(42) # Pour la reproductibilité n_points = 12 * 5 # 5 ans de données mensuelles dates = pd.date_range(start='2018-01-01', periods=n_points, freq='MS') # MS = Month Start # Tendance linéaire trend = np.linspace(50, 100, n_points) # Saisonnalité additive (cycle annuel) seasonal_pattern = np.array([5, 10, 15, 20, 18, 12, 5, -5, -10, -15, -10, 0]) seasonality = np.tile(seasonal_pattern, n_points // 12) # Bruit aléatoire noise = np.random.normal(0, 3, n_points) # Série temporelle additive = Tendance + Saisonnalité + Bruit ts_data = pd.Series(trend + seasonality + noise, index=dates) plt.figure(figsize=(14, 7)) plt.plot(ts_data) plt.title('Série Temporelle Simulé (Tendance + Saisonnalité Additive)') plt.xlabel('Date') plt.ylabel('Valeur') plt.grid(True) plt.show() # 2. Appliquer une décomposition de série temporelle # Pour une série additive, model='additive' decomposition = seasonal_decompose(ts_data, model='additive', period=12) # 3. Visualiser les composants de la décomposition fig = decomposition.plot() fig.set_size_inches(12, 8) plt.suptitle('Décomposition Additive de la Série Temporelle', y=1.02) # Titre global plt.tight_layout(rect=[0, 0, 1, 0.98]) # Ajuster pour le suptitle plt.show() print("\nComposants de la décomposition (premières lignes) :") print("Original :", decomposition.observed.head()) print("Tendance :", decomposition.trend.head()) print("Saisonnalité :", decomposition.seasonal.head()) print("Résidu :", decomposition.resid.head()) ``` > [!definition] Décomposition Additive vs. Multiplicative > La décomposition d'une série temporelle vise à séparer ses composants principaux : la tendance (T), la saisonnalité (S) et le résidu (R). > > - **Modèle Additif :** $ Y_t = T_t + S_t + R_t $ > - Utilisé lorsque l'amplitude des variations saisonnières et la variance du bruit restent **constantes** au fil du temps, indépendamment du niveau de la tendance. > - Les fluctuations saisonnières sont ajoutées à la tendance. > - *Exemple :* Une série de ventes où la variation saisonnière de 100 unités reste la même, que les ventes totales soient de 1000 ou 10000. > > - **Modèle Multiplicatif :** $ Y_t = T_t \times S_t \times R_t $ > - Utilisé lorsque l'amplitude des variations saisonnières et la variance du bruit **augmentent ou diminuent proportionnellement** au niveau de la tendance. > - Les fluctuations saisonnières sont exprimées en pourcentage ou en facteur de la tendance. > - *Exemple :* Une série de ventes où la variation saisonnière est de 10% des ventes totales ; si les ventes doublent, la variation saisonnière en unités doublera aussi. > > > [!tip] Comment choisir ? > > - Visualisez votre série : si l'amplitude des pics et des creux saisonniers augmente avec la tendance, un modèle multiplicatif est probablement plus approprié. > > - Si l'amplitude reste relativement constante, un modèle additif est préférable. > > - Si votre série contient des valeurs nulles ou négatives, vous ne pouvez pas utiliser un modèle multiplicatif directement (car la multiplication par zéro ou un négatif altérerait le sens). Dans ce cas, une transformation (ex: log) peut être nécessaire avant d'appliquer un modèle multiplicatif. ### Corrigé Exercice 5 : Prédiction Naïve et Évaluation **Objectifs :** 1. Diviser `data_series` en un ensemble d'entraînement (80%) et un ensemble de test (20%). 2. Implémenter une stratégie de prédiction naïve où la prédiction pour le pas de temps $t$ est simplement la dernière valeur observée dans l'ensemble d'entraînement, $y_{train\_dernier}$. 3. Calculer l'Erreur Absolue Moyenne (MAE) et l'Erreur Quadratique Moyenne (RMSE) des prédictions sur l'ensemble de test. ```python import pandas as pd import numpy as np from sklearn.metrics import mean_absolute_error, mean_squared_error import matplotlib.pyplot as plt # Simuler une série temporelle np.random.seed(42) # Pour la reproductibilité dates = pd.date_range(start='2020-01-01', periods=100, freq='D') data_series = pd.Series(np.random.rand(100) * 10 + np.sin(np.arange(100)/10) * 5 + 50, index=dates) plt.figure(figsize=(12, 6)) plt.plot(data_series) plt.title('Série Temporelle Simulé') plt.xlabel('Date') plt.ylabel('Valeur') plt.grid(True) plt.show() # 1. Diviser la série en entraînement (80%) et test (20%) train_size = int(len(data_series) * 0.8) train_data, test_data = data_series[0:train_size], data_series[train_size:] print(f"Taille de l'ensemble d'entraînement : {len(train_data)} observations") print(f"Taille de l'ensemble de test : {len(test_data)} observations") # 2. Implémenter une stratégie de prédiction naïve # La prédiction pour chaque point du test est la dernière valeur de l'entraînement. last_train_value = train_data.iloc[-1] predictions_naive = pd.Series(last_train_value, index=test_data.index) print(f"\nDernière valeur de l'entraînement : {last_train_value:.2f}") print("Prédictions naïves (premières lignes) :") print(predictions_naive.head()) # 3. Calculer le MAE et le RMSE mae = mean_absolute_error(test_data, predictions_naive) rmse = np.sqrt(mean_squared_error(test_data, predictions_naive)) print(f"\nMAE (Erreur Absolue Moyenne) : {mae:.2f}") print(f"RMSE (Erreur Quadratique Moyenne) : {rmse:.2f}") # Visualisation des prédictions plt.figure(figsize=(12, 6)) plt.plot(train_data.index, train_data, label='Données d\'entraînement') plt.plot(test_data.index, test_data, label='Données réelles (test)', color='orange') plt.plot(predictions_naive.index, predictions_naive, label='Prédictions naïves', linestyle='--', color='red') plt.title('Prédictions Naïves vs. Réalité') plt.xlabel('Date') plt.ylabel('Valeur') plt.legend() plt.grid(True) plt.show() ``` > [!note] Interprétation des métriques > - Le **MAE** représente la moyenne des erreurs absolues. Il est robuste aux valeurs aberrantes et facile à interpréter : en moyenne, nos prédictions sont à $X$ unités des valeurs réelles. > - Le **RMSE** est la racine carrée de la moyenne des erreurs quadratiques. Il pénalise davantage les erreurs importantes et est souvent utilisé lorsque les grandes erreurs sont particulièrement indésirables. Il est dans la même unité que la série cible. > - La prédiction naïve est un **modèle de référence (baseline)** très simple. Tout modèle de forecasting plus complexe devrait surpasser la performance de ce modèle naïf pour être considéré comme utile. ### Corrigé Exercice 6 : Préparation de Données pour Modèles Multivariés **Objectifs :** 1. Simuler une série `sales_data` sur 3 ans (mensuelle). 2. Créer les variables exogènes suivantes à partir de l'index temporel : `mois_sin`, `mois_cos`, `trimestre`. 3. Simuler une variable exogène externe `promotion` (binaire : 0 ou 1). 4. Construire un `DataFrame` final prêt pour un modèle multivarié. ```python import pandas as pd import numpy as np import matplotlib.pyplot as plt # 1. Simuler une série sales_data sur 3 ans (mensuelle) np.random.seed(42) # Pour la reproductibilité n_months = 3 * 12 # 3 ans dates = pd.date_range(start='2020-01-01', periods=n_months, freq='MS') # Tendance linéaire trend = np.linspace(100, 150, n_months) # Saisonnalité mensuelle # Ajuster la taille de seasonal_pattern_full si n_months n'est pas un multiple de 12 seasonal_pattern_full = np.tile( 10 * np.sin(2 * np.pi * np.arange(12) / 12) + 5 * np.cos(2 * np.pi * np.arange(12) / 6), # Deux cycles par an pour plus de complexité n_months // 12 ) if n_months % 12 != 0: seasonal_pattern_full = np.concatenate((seasonal_pattern_full, seasonal_pattern_full[:n_months % 12])) # Bruit noise = np.random.normal(0, 5, n_months) sales_data = pd.Series(trend + seasonal_pattern_full + noise, index=dates) df_multivari = pd.DataFrame({'Sales': sales_data}) print("DataFrame initial (Sales) :") print(df_multivari.head()) # 2. Créer les variables exogènes cycliques et trimestrielles df_multivari['mois'] = df_multivari.index.month df_multivari['mois_sin'] = np.sin(2 * np.pi * df_multivari['mois'] / 12) df_multivari['mois_cos'] = np.cos(2 * np.pi * df_multivari['mois'] / 12) df_multivari['trimestre'] = df_multivari.index.quarter # 3. Simuler une variable exogène externe 'promotion' # Disons qu'une promotion a lieu aléatoirement certains mois np.random.seed(43) # Pour la reproductibilité de la promotion df_multivari['promotion'] = np.random.choice([0, 1], size=n_months, p=[0.7, 0.3]) # 30% de chance de promotion # 4. Construire un DataFrame final prêt pour un modèle multivarié # On peut choisir les colonnes à inclure comme features (variables explicatives) features = ['mois_sin', 'mois_cos', 'trimestre', 'promotion'] df_final = df_multivari[['Sales'] + features] print("\nDataFrame final avec variables exogènes (premières lignes) :") print(df_final.head()) # Visualisation des variables exogènes cycliques plt.figure(figsize=(14, 7)) plt.subplot(3, 1, 1) plt.plot(df_final.index, df_final['mois_sin']) plt.title('Mois Sinus (Saisonnalité)') plt.grid(True) plt.subplot(3, 1, 2) plt.plot(df_final.index, df_final['mois_cos']) plt.title('Mois Cosinus (Saisonnalité)') plt.grid(True) plt.subplot(3, 1, 3) plt.plot(df_final.index, df_final['promotion'], marker='o', linestyle='None', alpha=0.6) plt.title('Variable Promotion') plt.grid(True) plt.tight_layout() plt.show() ``` > [!note] Explication > - **Encodage cyclique :** Pour les caractéristiques temporelles cycliques comme le mois ou le jour de la semaine, il est souvent préférable d'utiliser un encodage sin/cos plutôt qu'un encodage one-hot ou ordinal. Cela préserve la nature cyclique (décembre est proche de janvier) et évite d'introduire une fausse relation d'ordre. > - $ \text{sin_feature} = \sin(2 \pi \times \text{valeur} / \text{période}) $ > - $ \text{cos_feature} = \cos(2 \pi \times \text{valeur} / \text{période}) $ > - **Variables exogènes :** Ce sont des variables externes à la série cible qui peuvent influencer ses valeurs futures. Elles sont cruciales pour les modèles multivariés. Il est important de pouvoir les prévoir ou de les connaître à l'avance pour la période de prévision. > - Le `DataFrame` `df_final` est maintenant structuré avec la variable cible (`Sales`) et les variables explicatives (`features`), prêt à être utilisé dans un modèle de régression ou un modèle de séries temporelles multivarié (comme SARIMAX avec `exog`). ### Corrigé Exercice 7 : Implémentation et Comparaison de Modèles de Lissage Exponentiel **Objectifs :** 1. Simuler une série temporelle mensuelle de 5 ans avec une tendance croissante et une saisonnalité annuelle prononcée. 2. Diviser la série en un ensemble d'entraînement (les 4 premières années) et un ensemble de test (la dernière année). 3. Appliquer deux modèles de lissage exponentiel : Holt's Linear Trend et Holt-Winters Seasonal Smoothing. 4. Effectuer des prédictions pour la période de test avec chaque modèle. 5. Évaluer les performances de chaque modèle en utilisant le RMSE et le MAPE sur l'ensemble de test. 6. Discuter des résultats. ```python import pandas as pd import numpy as np import matplotlib.pyplot as plt from statsmodels.tsa.api import ExponentialSmoothing, SimpleExpSmoothing, Holt from sklearn.metrics import mean_squared_error, mean_absolute_percentage_error from math import sqrt # 1. Simuler une série temporelle mensuelle de 5 ans avec tendance et saisonnalité np.random.seed(42) # Pour la reproductibilité n_years = 5 n_months = n_years * 12 dates = pd.date_range(start='2018-01-01', periods=n_months, freq='MS') # Tendance linéaire croissante trend = np.linspace(100, 200, n_months) # Saisonnalité annuelle prononcée seasonal_pattern = np.array([10, 20, 30, 40, 50, 60, 50, 40, 30, 20, 10, 0]) seasonality = np.tile(seasonal_pattern, n_years) # Bruit aléatoire noise = np.random.normal(0, 5, n_months) consommation_electrique = pd.Series(trend + seasonality + noise, index=dates) plt.figure(figsize=(12, 6)) plt.plot(consommation_electrique) plt.title('Consommation Électrique Simulé (Tendance + Saisonnalité)') plt.xlabel('Date') plt.ylabel('Consommation (MWh)') plt.grid(True) plt.show() # 2. Diviser la série en entraînement (4 premières années) et test (dernière année) # Nous voulons 4 ans pour l'entraînement et 1 an pour le test. # Les 4 premières années sont de 2018-01-01 à 2021-12-01 inclus. train_end_date = '2021-12-01' train_data = consommation_electrique.loc[:train_end_date] # Le test commence juste après la fin de l'entraînement test_data = consommation_electrique.loc[train_data.index[-1] + pd.DateOffset(months=1):] print(f"Taille de l'ensemble d'entraînement : {len(train_data)} observations") print(f"Taille de l'ensemble de test : {len(test_data)} observations") # 3. Appliquer les modèles de lissage exponentiel # Modèle 1: Holt's Linear Trend (tendance seulement) # Utilise un lissage exponentiel pour le niveau et la tendance # initialization_method="estimated" permet à statsmodels d'estimer les valeurs initiales fit_holt = Holt(train_data, initialization_method="estimated").fit() forecast_holt = fit_holt.forecast(len(test_data)) # Modèle 2: Holt-Winters Seasonal Smoothing (tendance et saisonnalité additive) # Utilise un lissage exponentiel pour le niveau, la tendance et la saisonnalité # period=12 pour une saisonnalité annuelle (données mensuelles) # seasonal='add' car nous avons simulé une saisonnalité additive fit_hw = ExponentialSmoothing(train_data, trend='add', seasonal='add', seasonal_periods=12, initialization_method="estimated").fit() forecast_hw = fit_hw.forecast(len(test_data)) # 4. Visualisation des prédictions plt.figure(figsize=(14, 7)) plt.plot(train_data.index, train_data, label='Entraînement') plt.plot(test_data.index, test_data, label='Réel (Test)', color='orange') plt.plot(forecast_holt.index, forecast_holt, label="Prédiction Holt (Tendance)", linestyle='--', color='green') plt.plot(forecast_hw.index, forecast_hw, label="Prédiction Holt-Winters (Tendance + Saisonnalité)", linestyle=':', color='red') plt.title('Comparaison des Modèles de Lissage Exponentiel') plt.xlabel('Date') plt.ylabel('Consommation (MWh)') plt.legend() plt.grid(True) plt.show() # 5. Évaluer les performances de chaque modèle (RMSE et MAPE) # Fonction pour calculer le MAPE (scikit-learn n'a pas de MAPE direct pour Series) def calculate_mape(y_true, y_pred): # Éviter la division par zéro si y_true contient des zéros # et gérer les inf pour des cas extrêmes y_true, y_pred = np.array(y_true), np.array(y_pred) non_zero_mask = y_true != 0 return np.mean(np.abs((y_true[non_zero_mask] - y_pred[non_zero_mask]) / y_true[non_zero_mask])) * 100 # Holt rmse_holt = sqrt(mean_squared_error(test_data, forecast_holt)) mape_holt = calculate_mape(test_data, forecast_holt) # Holt-Winters rmse_hw = sqrt(mean_squared_error(test_data, forecast_hw)) mape_hw = calculate_mape(test_data, forecast_hw) print("\n--- Évaluation des Modèles ---") print(f"Holt's Linear Trend :") print(f" RMSE: {rmse_holt:.2f}") print(f" MAPE: {mape_holt:.2f}%") print(f"\nHolt-Winters Seasonal Smoothing :") print(f" RMSE: {rmse_hw:.2f}") print(f" MAPE: {mape_hw:.2f}%") # 6. Discussion des résultats ``` > [!note] Discussion des résultats > En observant les graphiques et les métriques, nous constatons que le modèle **Holt-Winters Seasonal Smoothing** a des performances nettement supérieures au modèle **Holt's Linear Trend**. > - **Holt's Linear Trend** ne capture que la tendance. Ses prédictions sont une ligne droite projetant la tendance future, ignorant complètement la saisonnalité. Par conséquent, il montre des erreurs importantes lorsque la saisonnalité est forte. > - **Holt-Winters Seasonal Smoothing** intègre à la fois la tendance et la saisonnalité. Ses prédictions suivent de près les cycles saisonniers observés dans les données d'entraînement et la tendance générale. Cela se traduit par un RMSE et un MAPE beaucoup plus faibles. > > > [!tip] Choix du modèle > > Le choix d'un modèle de lissage exponentiel dépend des composants présents dans votre série temporelle : > > - **Simple Exponential Smoothing (SES)** : Pour les séries sans tendance ni saisonnalité. > > - **Holt's Linear Trend** : Pour les séries avec tendance, mais sans saisonnalité. > > - **Holt-Winters Seasonal Smoothing** : Pour les séries avec tendance et saisonnalité. C'est le plus complet et souvent le plus performant pour ce type de séries. > > Il est crucial d'analyser visuellement votre série (ou d'utiliser une décomposition) pour identifier la présence de ces composants avant de choisir le bon modèle. ### Corrigé Exercice 8 : Modèle de Régression Linéaire pour le Forecasting avec Caractéristiques Temporelles et Lagged Features **Objectifs :** 1. Simuler un `DataFrame` `df_demande` avec une `Demande` journalière sur 2 ans, incluant une saisonnalité hebdomadaire et une tendance. 2. Créer des caractéristiques pertinentes pour la prévision : `Demande_lag_1`, `Demande_lag_7`, `jour_semaine` (one-hot), `jour_annee_sin`, `jour_annee_cos`. 3. Diviser les données en entraînement (les 18 premiers mois) et test (les 6 derniers mois). 4. Entraîner un modèle de régression linéaire. 5. Évaluer le modèle sur l'ensemble de test. 6. Interpréter les coefficients du modèle. ```python import pandas as pd import numpy as np import matplotlib.pyplot as plt from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error, mean_squared_error from sklearn.preprocessing import OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from math import sqrt # 1. Simuler df_demande (2 ans de données journalières) np.random.seed(42) # Pour la reproductibilité n_days = 2 * 365 dates = pd.date_range(start='2022-01-01', periods=n_days, freq='D') # Tendance linéaire trend = np.linspace(50, 100, n_days) # Saisonnalité hebdomadaire (jours de semaine vs week-end) # Demande plus faible le week-end weekly_seasonality = np.array([0, 0, 0, 0, 0, -10, -10]) # Lundi-Vendredi stable, Samedi-Dimanche baisse weekly_seasonality_full = np.tile(weekly_seasonality, n_days // 7) if n_days % 7 != 0: # Gérer les jours restants si n_days n'est pas un multiple de 7 weekly_seasonality_full = np.concatenate((weekly_seasonality_full, weekly_seasonality_full[:n_days % 7])) # Saisonnalité annuelle (légère) annual_seasonality = 10 * np.sin(2 * np.pi * np.arange(n_days) / 365) # Bruit aléatoire noise = np.random.normal(0, 5, n_days) demande = trend + weekly_seasonality_full + annual_seasonality + noise df_demande = pd.DataFrame({'Demande': demande}, index=dates) plt.figure(figsize=(12, 6)) plt.plot(df_demande['Demande']) plt.title('Demande Journalière Simulé (Tendance + Saisonnalité Hebdo/Annuelle)') plt.xlabel('Date') plt.ylabel('Demande') plt.grid(True) plt.show() # 2. Créer des caractéristiques pertinentes df = df_demande.copy() # Lagged features df['Demande_lag_1'] = df['Demande'].shift(1) df['Demande_lag_7'] = df['Demande'].shift(7) # Caractéristiques temporelles df['jour_semaine'] = df.index.dayofweek # Lundi=0, Dimanche=6 df['jour_annee'] = df.index.dayofyear df['jour_annee_sin'] = np.sin(2 * np.pi * df['jour_annee'] / 365.25) # 365.25 pour année bissextile df['jour_annee_cos'] = np.cos(2 * np.pi * df['jour_annee'] / 365.25) # Supprimer les lignes avec NaN (introduites par les lags) df.dropna(inplace=True) print("DataFrame avec caractéristiques (premières lignes après dropna) :") print(df.head(10)) print(f"Taille du DataFrame après dropna : {len(df)} observations") # 3. Diviser les données en entraînement (18 premiers mois) et test (6 derniers mois) # Pour les séries temporelles, on ne fait PAS de train_test_split aléatoire. # On coupe la série à un certain point dans le temps. # Calculer la date de fin d'entraînement (18 mois après le début des données sans NaN) train_end_date = df.index[0] + pd.DateOffset(months=18) train_df = df.loc[df.index < train_end_date] test_df = df.loc[df.index >= train_end_date] # Définir les variables explicatives (X) et la variable cible (y) features_to_use = ['Demande_lag_1', 'Demande_lag_7', 'jour_semaine', 'jour_annee_sin', 'jour_annee_cos'] X_train = train_df[features_to_use] y_train = train_df['Demande'] X_test = test_df[features_to_use] y_test = test_df['Demande'] print(f"\nTaille de l'ensemble d'entraînement (X_train) : {len(X_train)} observations") print(f"Taille de l'ensemble de test (X_test) : {len(X_test)} observations") # 4. Entraîner un modèle de régression linéaire # Utilisation d'un ColumnTransformer pour l'encodage one-hot du jour_semaine preprocessor = ColumnTransformer( transformers=[ ('onehot', OneHotEncoder(handle_unknown='ignore'), ['jour_semaine']) ], remainder='passthrough' # Garde les autres colonnes telles quelles ) model = Pipeline(steps=[ ('preprocessor', preprocessor), ('regressor', LinearRegression()) ]) model.fit(X_train, y_train) # 5. Évaluer le modèle sur l'ensemble de test predictions = model.predict(X_test) mae = mean_absolute_error(y_test, predictions) rmse = sqrt(mean_squared_error(y_test, predictions)) print(f"\n--- Évaluation du Modèle de Régression Linéaire ---") print(f" MAE: {mae:.2f}") print(f" RMSE: {rmse:.2f}") # Visualisation des prédictions plt.figure(figsize=(14, 7)) plt.plot(y_train.index, y_train, label='Données d\'entraînement') plt.plot(y_test.index, y_test, label='Données réelles (Test)', color='orange') plt.plot(y_test.index, predictions, label='Prédictions du modèle', linestyle='--', color='red') plt.title('Prédictions du Modèle de Régression Linéaire') plt.xlabel('Date') plt.ylabel('Demande') plt.legend() plt.grid(True) plt.show() # 6. Interpréter les coefficients du modèle de régression # Récupérer les noms des features après le préprocessing # Le OneHotEncoder va créer des colonnes comme 'jour_semaine_0', 'jour_semaine_1', etc. ohe_feature_names = model.named_steps['preprocessor'].named_transformers_['onehot'].get_feature_names_out(['jour_semaine']) remaining_features = [col for col in features_to_use if col not in ['jour_semaine']] feature_names_final = list(ohe_feature_names) + remaining_features coefficients = model.named_steps['regressor'].coef_ intercept = model.named_steps['regressor'].intercept_ print("\n--- Interprétation des Coefficients du Modèle ---") print(f"Intercept (ordonnée à l'origine) : {intercept:.2f}") for feature, coef in zip(feature_names_final, coefficients): print(f" Coefficient pour '{feature}': {coef:.2f}") ``` > [!note] Interprétation des coefficients > - **`Demande_lag_1` et `Demande_lag_7` :** Ces coefficients sont généralement positifs et significatifs. Un coefficient positif élevé pour `Demande_lag_1` indique que la demande d'aujourd'hui est fortement influencée par celle d'hier. `Demande_lag_7` capture la saisonnalité hebdomadaire : si la demande de la semaine dernière à la même période était élevée, celle d'aujourd'hui le sera probablement aussi. > - **`jour_semaine_x` :** Les coefficients pour les jours de la semaine (après encodage one-hot) indiquent l'impact de chaque jour sur la demande par rapport au jour de référence (celui qui a été "dropé" par `OneHotEncoder` pour éviter la multicolinéarité). Par exemple, si `jour_semaine_6` (Dimanche) a un coefficient négatif, cela signifie que la demande du dimanche est inférieure à celle du jour de référence, toutes choses égales par ailleurs. > - **`jour_annee_sin` et `jour_annee_cos` :** Ces coefficients, pris ensemble, modélisent la saisonnalité annuelle. Leurs valeurs individuelles sont moins interprétables que leur effet combiné. Un modèle de régression linéaire peut les utiliser pour capturer les variations cycliques sur l'année. > > > [!warning] Limites de la régression linéaire pour le forecasting > > - **Pas de gestion explicite de la dépendance temporelle :** Bien que les `lagged features` aident, la régression linéaire ne modélise pas intrinsèquement les corrélations résiduelles dans le temps, contrairement aux modèles ARIMA. > > - **Extrapolation :** Pour la prévision à long terme, la régression linéaire peut être moins robuste si la tendance ou la saisonnalité changent de manière non linéaire. > > - **Stationnarité :** Si la série cible est fortement non stationnaire, les coefficients peuvent être instables ou trompeurs. Des transformations ou l'utilisation de modèles ARIMA peuvent être nécessaires. > > Malgré ces limites, les modèles de régression avec des caractéristiques temporelles bien conçues sont très puissants et flexibles, notamment avec des modèles plus avancés comme les forêts aléatoires ou les XGBoost. ### Corrigé Exercice 9 : Analyse de Stationnarité et Différenciation pour ARIMA **Objectifs :** 1. Simuler une série temporelle `prix_action` sur 5 ans (journalière) avec une tendance forte et une volatilité croissante. 2. Visualiser la série et discuter visuellement de sa stationnarité. 3. Appliquer le test de Dickey-Fuller augmenté (ADF) pour tester la stationnarité. Interpréter les résultats. 4. Si la série est non stationnaire, appliquer une différenciation d'ordre 1 et réappliquer le test ADF. 5. Discuter de l'importance de la stationnarité pour les modèles ARIMA. ```python import pandas as pd import numpy as np import matplotlib.pyplot as plt from statsmodels.tsa.stattools import adfuller # 1. Simuler une série temporelle prix_action (5 ans, journalière) np.random.seed(42) # Pour la reproductibilité n_days = 5 * 365 dates = pd.date_range(start='2019-01-01', periods=n_days, freq='D') # Tendance forte trend = np.linspace(100, 300, n_days) # Volatilité croissante (bruit qui augmente avec le temps) noise = np.random.normal(0, np.linspace(1, 10, n_days), n_days) prix_action = pd.Series(trend + noise + 10 * np.sin(np.arange(n_days) / 30), index=dates) # 2. Visualiser la série et discuter visuellement de sa stationnarité plt.figure(figsize=(14, 7)) plt.plot(prix_action) plt.title('Prix d\'Action Simulé (Tendance et Volatilité Croissante)') plt.xlabel('Date') plt.ylabel('Prix') plt.grid(True) plt.show() print("--- Analyse Visuelle ---") print("La série présente une tendance clairement croissante et une volatilité (amplitude des fluctuations) qui semble augmenter avec le temps. Ces deux caractéristiques suggèrent fortement que la série n'est PAS stationnaire.") # 3. Appliquer le test de Dickey-Fuller augmenté (ADF) def adf_test(series, title=''): print(f"\n--- Test de Dickey-Fuller Augmenté pour '{title}' ---") result = adfuller(series.dropna()) # dropna() pour gérer les NaN si différenciation appliquée labels = ['Statistique ADF', 'p-value', '# Lags utilisés', 'Nombre d\'observations'] for value, label in zip(result[0:4], labels): print(f"{label}: {value:.4f}") print("Valeurs critiques :") for key, value in result[4].items(): print(f" {key}: {value:.4f}") if result[1] <= 0.05: print("Conclusion : La p-value est <= 0.05. Nous rejetons l'hypothèse nulle ($H_0$). La série est probablement stationnaire.") else: print("Conclusion : La p-value est > 0.05. Nous ne pouvons pas rejeter l'hypothèse nulle ($H_0$). La série est probablement non stationnaire.") adf_test(prix_action, title='Série Originale') # 4. Appliquer une différenciation d'ordre 1 et réappliquer le test ADF prix_action_diff1 = prix_action.diff().dropna() # .diff(1) par défaut plt.figure(figsize=(14, 7)) plt.plot(prix_action_diff1) plt.title('Prix d\'Action Différencié d\'Ordre 1') plt.xlabel('Date') plt.ylabel('Différence de Prix') plt.grid(True) plt.show() adf_test(prix_action_diff1, title='Série Différenciée d\'Ordre 1') ``` > [!definition] Stationnarité > Une série temporelle est dite **stationnaire** si ses propriétés statistiques (moyenne, variance, autocovariance) ne changent pas au cours du temps. En d'autres termes, elle ne présente pas de tendance, la saisonnalité est supprimée, et sa volatilité est constante. > > > [!note] Importance pour les modèles ARIMA > > La stationnarité est une hypothèse fondamentale pour de nombreux modèles de séries temporelles, notamment les modèles **ARIMA (AutoRegressive Integrated Moving Average)**. > > - Les modèles AR et MA supposent que la série est stationnaire. > > - Si une série n'est pas stationnaire, ses propriétés statistiques varient avec le temps, ce qui rend difficile l'estimation de paramètres constants pour le modèle. Les prévisions basées sur une série non stationnaire peuvent être peu fiables et extrapolent souvent la tendance ou la saisonnalité de manière irréaliste. > > - Le composant "I" (Integrated) dans ARIMA fait référence à la **différenciation**, qui est le processus utilisé pour rendre une série non stationnaire en une série stationnaire. > > > [!tip] Différenciation > > La **différenciation** consiste à calculer la différence entre une observation courante et une observation précédente. > > - **Différenciation d'ordre 1 :** $ \Delta Y_t = Y_t - Y_{t-1} $. Elle aide à éliminer les tendances linéaires. > > - **Différenciation saisonnière :** $ \Delta_s Y_t = Y_t - Y_{t-s} $ (où $s$ est la période saisonnière, ex: 12 pour des données mensuelles annuelles). Elle aide à éliminer la saisonnalité. > > Souvent, une ou deux différenciations suffisent pour rendre une série stationnaire. ### Corrigé Exercice 10 : Chaîne de Forecasting Complète avec SARIMAX **Objectifs :** 1. Simuler un `DataFrame` `df_ventes` sur 3 ans de données mensuelles avec une tendance, une saisonnalité annuelle et une influence positive de la variable `publicite`. 2. Effectuer un nettoyage de données rudimentaire. 3. Créer des caractéristiques temporelles pertinentes. 4. Diviser les données en entraînement et test. 5. Identifier les ordres $p, d, q, P, D, Q, s$ pour un modèle SARIMAX (utilisez des valeurs données). 6. Entraîner un modèle `SARIMAX` de `statsmodels` en utilisant les variables exogènes. 7. Effectuer des prédictions pour la période de test et pour les 6 mois futurs (hors données). 8. Évaluer les performances du modèle sur l'ensemble de test. 9. Visualiser les prédictions et discuter des limites. ```python import pandas as pd import numpy as np import matplotlib.pyplot as plt from statsmodels.tsa.statespace.sarimax import SARIMAX from sklearn.metrics import mean_absolute_error, mean_squared_error from math import sqrt # 1. Simuler un DataFrame df_ventes (3 ans de données mensuelles) np.random.seed(42) # Pour la reproductibilité n_months = 3 * 12 # 3 ans dates = pd.date_range(start='2020-01-01', periods=n_months, freq='MS') # Tendance linéaire trend = np.linspace(1000, 1500, n_months) # Saisonnalité annuelle seasonal_pattern = np.array([50, 80, 120, 150, 100, 70, 60, 80, 100, 130, 160, 180]) seasonality = np.tile(seasonal_pattern, n_months // 12) # Variable exogène 'publicite' (budget marketing du mois précédent) # Simuler une publicité avec un pic avant les mois de forte saisonnalité publicite = np.random.randint(50, 200, n_months) publicite[np.where(dates.month.isin([3, 4, 9, 10]))] += 100 # Augmenter la pub certains mois publicite = publicite.astype(float) # s'assurer que c'est float pour SARIMAX # Influence de la publicité sur les ventes (avec un léger décalage) # Pour simplifier, on va dire que les ventes sont influencées par la pub du mois courant publicite_effect = 0.5 * publicite # Bruit aléatoire noise = np.random.normal(0, 30, n_months) ventes_mensuelles = pd.Series(trend + seasonality + publicite_effect + noise, index=dates) df_ventes = pd.DataFrame({'Ventes': ventes_mensuelles, 'Publicite': publicite}) # Introduire quelques NaN pour le nettoyage df_ventes.loc[df_ventes.sample(frac=0.02, random_state=1).index, 'Ventes'] = np.nan df_ventes.loc[df_ventes.sample(frac=0.01, random_state=2).index, 'Publicite'] = np.nan print("DataFrame initial simulé (premières lignes) :") print(df_ventes.head()) print("\nValeurs manquantes avant nettoyage :") print(df_ventes.isnull().sum()) # 2. Nettoyage de données rudimentaire # Remplir les NaN par interpolation linéaire df_ventes['Ventes'] = df_ventes['Ventes'].interpolate(method='linear') df_ventes['Publicite'] = df_ventes['Publicite'].interpolate(method='linear') print("\nValeurs manquantes après nettoyage :") print(df_ventes.isnull().sum()) # 3. Créer des caractéristiques temporelles pertinentes df_ventes['month_sin'] = np.sin(2 * np.pi * df_ventes.index.month / 12) df_ventes['month_cos'] = np.cos(2 * np.pi * df_ventes.index.month / 12) # On peut aussi ajouter un lag de la publicité si l'effet est différé df_ventes['Publicite_lag1'] = df_ventes['Publicite'].shift(1) df_ventes.dropna(inplace=True) # Supprimer le premier NaN du lag (le premier mois) print("\nDataFrame après ingénierie de caractéristiques (premières lignes) :") print(df_ventes.head()) # 4. Diviser les données en entraînement et test (les 6 derniers mois pour le test) # L'ensemble d'entraînement sera toutes les données sauf les 6 derniers mois train_data = df_ventes.iloc[:-6] test_data = df_ventes.iloc[-6:] # Séparer la variable cible (y) et les exogènes (X) y_train = train_data['Ventes'] X_train = train_data[['Publicite', 'Publicite_lag1', 'month_sin', 'month_cos']] y_test = test_data['Ventes'] X_test = test_data[['Publicite', 'Publicite_lag1', 'month_sin', 'month_cos']] print(f"\nTaille de l'ensemble d'entraînement : {len(y_train)} observations") print(f"Taille de l'ensemble de test : {len(y_test)} observations") # 5. Identifier les ordres p, d, q, P, D, Q, s pour SARIMAX # Pour cet exercice, nous utiliserons des valeurs suggérées. # p, d, q : ordres non saisonniers (AR, I, MA) # P, D, Q, s : ordres saisonniers (S-AR, S-I, S-MA, période saisonnière) order = (1, 1, 1) # (AR, I, MA) seasonal_order = (1, 1, 1, 12) # (S-AR, S-I, S-MA, période=12 mois) # 6. Entraîner un modèle SARIMAX # exog est utilisé pour les variables exogènes # enforce_stationarity et enforce_invertibility peuvent être mis à False pour éviter des erreurs # d'optimisation sur des séries complexes, mais il faut être conscient des implications. model = SARIMAX(y_train, exog=X_train, order=order, seasonal_order=seasonal_order, enforce_stationarity=False, enforce_invertibility=False) model_fit = model.fit(disp=False) # disp=False pour ne pas afficher les étapes d'optimisation print(model_fit.summary()) # 7. Effectuer des prédictions # Prédiction sur l'ensemble de test # start et end sont des indices numériques par rapport au début de la série d'entraînement forecast_test = model_fit.predict(start=len(y_train), end=len(y_train) + len(y_test) - 1, exog=X_test) forecast_test.index = y_test.index # Assurez-vous que l'index correspond # Prédiction pour les 6 mois futurs (hors données) # Il faut générer les exogènes pour la période future future_dates = pd.date_range(start=df_ventes.index[-1] + pd.DateOffset(months=1), periods=6, freq='MS') future_exog = pd.DataFrame(index=future_dates) # Simuler les valeurs futures pour Publicite. C'est une étape cruciale et souvent difficile. # Ici, nous allons simplement prendre la dernière valeur connue et ajouter un peu de bruit. last_publicite_known = df_ventes['Publicite'].iloc[-1] future_exog['Publicite'] = np.random.uniform(last_publicite_known * 0.9, last_publicite_known * 1.1, 6) # simulation simple # Le Publicite_lag1 pour le premier mois futur sera la dernière Publicite connue # Les Publicite_lag1 suivants seront les Publicite du mois précédent dans future_exog future_exog['Publicite_lag1'] = future_exog['Publicite'].shift(1).fillna(last_publicite_known) # Créer les caractéristiques temporelles pour le futur future_exog['month_sin'] = np.sin(2 * np.pi * future_exog.index.month / 12) future_exog['month_cos'] = np.cos(2 * np.pi * future_exog.index.month / 12) # S'assurer que les colonnes sont dans le même ordre que X_train future_exog = future_exog[X_train.columns] forecast_future = model_fit.predict(start=len(df_ventes), end=len(df_ventes) + len(future_dates) - 1, exog=future_exog) forecast_future.index = future_dates # 8. Évaluer les performances du modèle sur l'ensemble de test rmse = sqrt(mean_squared_error(y_test, forecast_test)) mae = mean_absolute_error(y_test, forecast_test) print(f"\n--- Évaluation du Modèle SARIMAX sur l'ensemble de test ---") print(f" RMSE: {rmse:.2f}") print(f" MAE: {mae:.2f}") # 9. Visualisation des prédictions plt.figure(figsize=(16, 8)) plt.plot(y_train.index, y_train, label='Données d\'entraînement (Ventes réelles)') plt.plot(y_test.index, y_test, label='Données réelles (Test)', color='orange') plt.plot(forecast_test.index, forecast_test, label='Prédictions SARIMAX (Test)', linestyle='--', color='green') plt.plot(forecast_future.index, forecast_future, label='Prédictions SARIMAX (Futur)', linestyle=':', color='red') plt.title('Prévisions des Ventes avec SARIMAX et Exogènes') plt.xlabel('Date') plt.ylabel('Ventes') plt.legend() plt.grid(True) plt.show() # Discussion des limites et améliorations ``` > [!note] Discussion des limites et améliorations > - **Qualité des variables exogènes :** La performance du modèle SARIMAX avec exogènes dépend fortement de la qualité et de la prévisibilité des variables exogènes. Si `Publicite` est difficile à prévoir avec précision pour le futur, les prédictions futures du modèle seront moins fiables. Il est crucial d'avoir des prévisions fiables pour les variables exogènes elles-mêmes pour la période de prévision. > - **Ordres SARIMAX :** Le choix des ordres $(p, d, q)(P, D, Q, s)$ est crucial. Dans cet exercice, nous avons utilisé des valeurs suggérées. En pratique, il faut utiliser des outils comme l'analyse des fonctions d'autocorrélation (ACF) et d'autocorrélation partielle (PACF) des résidus, ou des méthodes de recherche de grille (grid search) pour trouver les meilleurs ordres. > - **Hypothèses :** SARIMAX, comme ARIMA, suppose que les résidus du modèle sont du bruit blanc (non corrélés, moyenne nulle, variance constante). Une analyse des résidus après l'ajustement du modèle est essentielle pour valider ces hypothèses. > - **Données manquantes :** L'interpolation linéaire est une solution simple. Pour des données plus complexes, des méthodes plus sophistiquées (ex: MICE, ou modèles qui gèrent les NaN directement) peuvent être nécessaires. > - **Modèles alternatifs :** Pour des séries avec des relations non linéaires ou de très nombreuses variables exogènes, des modèles basés sur l'apprentissage automatique (Random Forest, Gradient Boosting, réseaux de neurones récurrents comme LSTM) peuvent offrir de meilleures performances, souvent en complément des modèles statistiques comme SARIMAX. > - **Robustesse :** Les modèles SARIMAX peuvent être sensibles aux valeurs aberrantes. Une étape de détection et de gestion des outliers peut améliorer la robustesse.