{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# ⏱️ TP-4 : Prédiction de Séries Temporelles avec LSTM\n", "\n", "**Objectif** : Prédire la consommation électrique avec des réseaux LSTM.\n", "\n", "**Compétences** :\n", "- Préparation de données temporelles\n", "- Fenêtres glissantes (windowing)\n", "- Architecture LSTM avec Keras\n", "- Early stopping et régularisation" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.preprocessing import MinMaxScaler\n", "from sklearn.metrics import mean_squared_error, mean_absolute_error\n", "\n", "import tensorflow as tf\n", "from tensorflow.keras.models import Sequential\n", "from tensorflow.keras.layers import LSTM, Dense, Dropout\n", "from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau\n", "\n", "# Reproductibilité\n", "np.random.seed(42)\n", "tf.random.set_seed(42)\n", "\n", "print(f\"✅ TensorFlow version : {tf.__version__}\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Génération de données synthétiques (consommation électrique)\n", "# En pratique, remplacez par vos données réelles\n", "\n", "def generate_energy_data(n_days=365*2):\n", " \"\"\"Génère des données de consommation électrique simulées\"\"\"\n", " hours = np.arange(n_days * 24)\n", " \n", " # Tendance\n", " trend = 0.001 * hours\n", " \n", " # Saisonnalité journalière\n", " daily = 10 * np.sin(2 * np.pi * hours / 24)\n", " \n", " # Saisonnalité hebdomadaire\n", " weekly = 5 * np.sin(2 * np.pi * hours / (24 * 7))\n", " \n", " # Saisonnalité annuelle\n", " yearly = 15 * np.sin(2 * np.pi * hours / (24 * 365))\n", " \n", " # Bruit\n", " noise = np.random.normal(0, 3, len(hours))\n", " \n", " # Consommation totale\n", " consumption = 50 + trend + daily + weekly + yearly + noise\n", " consumption = np.maximum(consumption, 0) # Pas de valeurs négatives\n", " \n", " return consumption\n", "\n", "# Génération des données\n", "data = generate_energy_data(n_days=730) # 2 ans de données\n", "\n", "# Création du DataFrame\n", "dates = pd.date_range(start='2022-01-01', periods=len(data), freq='H')\n", "df = pd.DataFrame({'consumption': data}, index=dates)\n", "\n", "print(f\"📊 Période : {df.index[0]} à {df.index[-1]}\")\n", "print(f\"📊 Total : {len(df)} heures de données\")\n", "df.head()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Visualisation des données\n", "fig, axes = plt.subplots(3, 1, figsize=(15, 10))\n", "\n", "# Vue complète\n", "axes[0].plot(df.index, df['consumption'], alpha=0.7)\n", "axes[0].set_title('Consommation électrique - Vue complète (2 ans)')\n", "axes[0].set_ylabel('kWh')\n", "\n", "# Vue d'une semaine\n", "one_week = df.iloc[:24*7]\n", "axes[1].plot(one_week.index, one_week['consumption'], marker='o')\n", "axes[1].set_title('Consommation - Vue hebdomadaire')\n", "axes[1].set_ylabel('kWh')\n", "\n", "# Vue d'une journée\n", "one_day = df.iloc[:24]\n", "axes[2].plot(one_day.index.hour, one_day['consumption'], marker='o')\n", "axes[2].set_title('Consommation - Vue journalière')\n", "axes[2].set_xlabel('Heure')\n", "axes[2].set_ylabel('kWh')\n", "\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Feature Engineering temporel\n", "df['hour'] = df.index.hour\n", "df['day_of_week'] = df.index.dayofweek\n", "df['month'] = df.index.month\n", "df['is_weekend'] = (df['day_of_week'] >= 5).astype(int)\n", "\n", "# Lags (valeurs précédentes)\n", "for lag in [1, 2, 3, 24, 48]:\n", " df[f'lag_{lag}'] = df['consumption'].shift(lag)\n", "\n", "# Rolling statistics\n", "df['rolling_mean_24'] = df['consumption'].rolling(window=24).mean()\n", "df['rolling_std_24'] = df['consumption'].rolling(window=24).std()\n", "\n", "# Suppression des NaN\n", "df = df.dropna()\n", "\n", "print(f\"✅ Features créées : {df.shape[1]} colonnes\")\n", "df.head()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Préparation des séquences pour LSTM\n", "def create_sequences(data, target_col, sequence_length=24):\n", " \"\"\"\n", " Crée des séquences pour LSTM\n", " data : DataFrame avec features\n", " target_col : nom de la colonne cible\n", " sequence_length : longueur de la séquence (ex: 24 heures)\n", " \"\"\"\n", " X, y = [], []\n", " values = data.values\n", " target_idx = data.columns.get_loc(target_col)\n", " \n", " for i in range(sequence_length, len(values)):\n", " X.append(values[i-sequence_length:i])\n", " y.append(values[i, target_idx])\n", " \n", " return np.array(X), np.array(y)\n", "\n", "# Séparation train/test\n", "train_size = int(len(df) * 0.8)\n", "train_df = df.iloc[:train_size]\n", "test_df = df.iloc[train_size:]\n", "\n", "# Normalisation\n", "scaler = MinMaxScaler()\n", "train_scaled = scaler.fit_transform(train_df)\n", "test_scaled = scaler.transform(test_df)\n", "\n", "# Conversion en DataFrame pour garder les noms de colonnes\n", "train_scaled = pd.DataFrame(train_scaled, columns=df.columns, index=train_df.index)\n", "test_scaled = pd.DataFrame(test_scaled, columns=df.columns, index=test_df.index)\n", "\n", "# Création des séquences\n", "SEQUENCE_LENGTH = 24 # 24 heures d'historique\n", "\n", "X_train, y_train = create_sequences(train_scaled, 'consumption', SEQUENCE_LENGTH)\n", "X_test, y_test = create_sequences(test_scaled, 'consumption', SEQUENCE_LENGTH)\n", "\n", "print(f\"📊 X_train shape : {X_train.shape}\")\n", "print(f\"📊 y_train shape : {y_train.shape}\")\n", "print(f\"📊 X_test shape : {X_test.shape}\")\n", "print(f\"📊 y_test shape : {y_test.shape}\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Construction du modèle LSTM\n", "model = Sequential([\n", " LSTM(64, return_sequences=True, input_shape=(SEQUENCE_LENGTH, X_train.shape[2])),\n", " Dropout(0.2),\n", " LSTM(32, return_sequences=False),\n", " Dropout(0.2),\n", " Dense(16, activation='relu'),\n", " Dense(1)\n", "])\n", "\n", "model.compile(\n", " optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),\n", " loss='mse',\n", " metrics=['mae']\n", ")\n", "\n", "model.summary()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Callbacks\n", "callbacks = [\n", " EarlyStopping(\n", " monitor='val_loss',\n", " patience=10,\n", " restore_best_weights=True\n", " ),\n", " ReduceLROnPlateau(\n", " monitor='val_loss',\n", " factor=0.5,\n", " patience=5,\n", " min_lr=1e-6\n", " )\n", "]\n", "\n", "# Entraînement\n", "history = model.fit(\n", " X_train, y_train,\n", " epochs=100,\n", " batch_size=32,\n", " validation_split=0.2,\n", " callbacks=callbacks,\n", " verbose=1\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Visualisation de l'entraînement\n", "fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n", "\n", "# Loss\n", "axes[0].plot(history.history['loss'], label='Train')\n", "axes[0].plot(history.history['val_loss'], label='Validation')\n", "axes[0].set_title('Loss (MSE)')\n", "axes[0].set_xlabel('Epoch')\n", "axes[0].set_ylabel('Loss')\n", "axes[0].legend()\n", "\n", "# MAE\n", "axes[1].plot(history.history['mae'], label='Train')\n", "axes[1].plot(history.history['val_mae'], label='Validation')\n", "axes[1].set_title('MAE')\n", "axes[1].set_xlabel('Epoch')\n", "axes[1].set_ylabel('MAE')\n", "axes[1].legend()\n", "\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Prédictions\n", "y_pred = model.predict(X_test)\n", "\n", "# Métriques (sur données normalisées)\n", "mse = mean_squared_error(y_test, y_pred)\n", "mae = mean_absolute_error(y_test, y_pred)\n", "rmse = np.sqrt(mse)\n", "\n", "print(f\"📊 MSE : {mse:.6f}\")\n", "print(f\"📊 MAE : {mae:.6f}\")\n", "print(f\"📊 RMSE : {rmse:.6f}\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Visualisation des prédictions\n", "plt.figure(figsize=(15, 6))\n", "\n", "# Plot des 500 premières prédictions\n", "n_plot = 500\n", "plt.plot(y_test[:n_plot], label='Réel', alpha=0.8)\n", "plt.plot(y_pred[:n_plot], label='Prédit', alpha=0.8)\n", "plt.title(f'Prédictions LSTM - {n_plot} premiers points de test')\n", "plt.xlabel('Temps')\n", "plt.ylabel('Consommation (normalisée)')\n", "plt.legend()\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Scatter plot : Réel vs Prédit\n", "plt.figure(figsize=(8, 8))\n", "plt.scatter(y_test, y_pred, alpha=0.5)\n", "plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'r--', lw=2)\n", "plt.xlabel('Valeurs réelles')\n", "plt.ylabel('Valeurs prédites')\n", "plt.title('Réel vs Prédit')\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 🎓 Conclusion\n", "\n", "Dans ce TP, nous avons :\n", "\n", "1. ✅ **Généré** des données de consommation électrique avec patterns temporels\n", "2. ✅ **Créé** des features temporelles (heure, jour, mois, lags, rolling stats)\n", "3. ✅ **Préparé** les séquences pour LSTM avec windowing\n", "4. ✅ **Construit** un modèle LSTM avec Dropout et Early Stopping\n", "5. ✅ **Évalué** les performances sur l'ensemble de test\n", "\n", "**Améliorations possibles** :\n", "- Utiliser des données météo comme features externes\n", "- Tester des architectures plus complexes (Bidirectional LSTM, GRU)\n", "- Faire du multi-step forecasting (prédire plusieurs heures en avance)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.8.0" } }, "nbformat": 4, "nbformat_minor": 4 }