La prévision de la demande nette d’électricité est un problème de séries temporelles avec des contraintes réelles fortes : saisonnalités multiples, effets météorologiques non linéaires, ruptures structurelles et une métrique d’évaluation asymétrique car l’impact de sous-prédiction n’est pas la même que pour sur-prédiction.
Ce projet étant encadré, nous avons toutefois choisi d’en faire une démarche volontairement plus exploratoire. Notre objectif principal n’était pas de minimiser le score de soumission, mais de nous confronter à plusieurs familles de modèles de machine learning appliqués aux séries temporelles, avant même d’en avoir vu certains en cours. Nous voulions comprendre par nous-mêmes comment ces méthodes fonctionnent, quelles sont leurs hypothèses et dans quels cas elles échouent.
Concrètement, cela a signifié que nous avons souvent implémenté du code en partant de ressources en ligne (documentation, articles, forums) plutôt que d’utiliser des squelettes fournis dont nous avons vu la performance en cours. Nous avons écrit notre propre descente de gradient, nos propres fonctions de préparation de données, plutôt que de déléguer cela entièrement à des librairies (ou aux IA…).
Nous sommes conscients que pour obtenir un meilleur score, il aurait fallu consacrer plus de temps au pre-processing, la sélection de features et le réglage des hyperparamètres. Nous avons fait un autre choix et avons exploré un maximum de modèles différents, même de manière imparfaite, parce que cette diversité d’expérience nous sera plus dans nos futurs travaux. Ce rapport documente cette démarche, y compris les erreurs de pipeline et les résultats que nous avons rencontrés.
Avant de complexifier nos modèles, nous avons mené un travail sur les variables explicatives afin de capturer la réalité physique et économique du réseau.
La relation entre la température et la demande électrique ne semble pas linéaire et forme typiquement un “V”. Afin de modéliser cet effet, nous avons créé des variables de degrés-jours de chauffage (Heating_Degrees). Contrairement à de simples indicateurs binaires comme Heating_Flag, qui permet d’activer une pente différente quand il fait froid, ces variables mesurent l’écart à une température de confort de référence (par exemple 15°C). Elles sont donc proportionnelles à l’énergie thérmique requise par les radiateurs, permettant au modèle d’activer une pente de consommation plus précise lors des vagues de froid.
Formellement, les variables de degrés-jours sont définies par : \[\text{Heating_Degree}_t = \max(T_{\text{ref}} - T_t,\ 0)\] \[\text{Cooling_Degree}_t = \max(T_t - T_{\text{ref}},\ 0)\] où \(T_{\text{ref}} = 15^\circ\)C est le seuil de confort typique utilisé en général. Ces deux termes encodent conjointement la forme en “V” de la relation température–demande : en dessous de \(T_{\text{ref}}\), le chauffage augmente proportionnellement avec le froid. Par ailleurs, en été, la climatisation crée une pente opposée.
[ajouter le graphe de la relation en V entre la température et Net_demand, avec les deux régimes chauffage et refroidissement visibles]
Les données d’entraînement couvrent la période 2013–2022, qui inclut
le confinement de mars 2020. L’idée était d’ajouter une variable binaire
Covid_Flag pour signaler au modèle cette rupture
structurelle : sans elle, le modèle risque d’apprendre une moyenne entre
la période normale et la période confinée, produisant des prédictions
systématiquement biaisées sur les deux fenêtres.
Dans la pratique, ce flag a été créé sur les données d’entraînement
mais n’a jamais été propagé correctement au jeu de test (voir section
3.1). Le jeu de test couvre la période de sobriété électrique de
l’automne 2022, postérieure au confinement : toutes les observations
test devraient avoir Covid_Flag = 1, mais le bug de
pipeline leur a attribué 0. L’intention était correcte ;
l’implémentation ne l’a pas suivi.
Après analyse, la variable de nébulosité a été retirée de nos modèles finaux. Bien que la luminosité influe sur l’éclairage, son impact sur la demande nette globale reste marginal face aux effets écrasants de la température et de l’activité économique (son usage serait en revanche critique pour prévoir la seule production photovoltaïque).
Pour débuter notre modélisation, nous avons implémenté nos propres
algorithmes d’optimisation en Python (classe LinearModel),
sans dépendre de librairies comme scikit-learn. Ce choix
n’est pas anodin : il nous a permis de contrôler précisément la fonction
de perte minimisée, ce que les API standard ne permettaient pas à notre
connaissance pour la perte Pinball avec régularisation simultanée.
La descente de gradient est sensible à l’échelle des variables. Si une feature vaut ~300 (température en Kelvin par exemple) et une autre vaut 1 (flag binaire), leurs gradients sont de magnitudes très différentes, ce qui ralentit ou déstabilise la convergence.
Nous avons normalisé uniquement les variables continues en les centrant et réduisant : \[\tilde{X}_j = \frac{X_j - \bar{X}_j}{s_j}\] Les variables binaires (jours de la semaine en one-hot, indicateurs de jours fériés) ont été laissées telles quelles.
Les features effectivement utilisées par le modèle sont celles
présentes dans les deux fichiers train.csv
et test.csv :
Temp, Temp_s95, Temp_s99,
Temp_s95_min, Temp_s95_max,
Temp_s99_min, Temp_s99_maxWind,
Wind_weightedtoy (time of year),
Year, Month, DLS,
WeekDays (one-hot, 6 colonnes)BH,
BH_before, BH_after, BH_Holiday,
Holiday, Holiday_zone_a/b/c,
Summer_break, Christmas_breakLoad.1,
Load.7, Net_demand.1,
Net_demand.7, Solar_power.1,
Solar_power.7, Wind_power.1,
Wind_power.7La nébulosité brute et pondérée est explicitement exclue (voir section 2.3).
Limite de pipeline (features non propagées au test)
: Lors de la préparation des données, trois features avaient
été construites sur les données d’entraînement mais n’ont pas été
recréées pour le jeu de test : - Covid_Flag = \(\mathbb{1}[\text{Date} \geq \text{17 mars
2020}]\)
L’instruction
X_test.reindex(columns=X_train.columns, fill_value=0.0) les
a remplies silencieusement avec zéro pour toutes les observations de
test. Ces features ont donc été apprises par le modèle mais sont restées
constamment nulles lors des prédictions finales, ce qui revient à les
ignorer complètement. Le modèle s’est ainsi appuyé uniquement sur la
température brute pour l’effet thermique, sans la transformation en
degrés-jours souhaitée.
Le modèle linéaire s’écrit : \[\hat{y} = X\beta + b\] avec \(X \in \mathbb{R}^{n \times p}\) la matrice de features, \(\beta \in \mathbb{R}^p\) les poids et \(b\) le biais.
En minimisant le RMSE, on cherche : \[\hat{\beta} = \arg\min_{\beta} \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2\]
La mise à jour par descente de gradient à l’itération \(t\) est : \[\beta^{(t+1)} = \beta^{(t)} - \alpha \cdot \nabla_\beta \mathcal{L}\] avec le gradient : \[\nabla_\beta \mathcal{L}_{\text{RMSE}} = \frac{2}{n} X^\top (X\beta - y)\]
Nous avons utilisé learning_rate = 0.02 et
maxIter = 10000. Ce modèle converge proprement et nous a
permis de valider que le pipeline de données fonctionnait.
Limite fondamentale : Ce modèle estime \(\mathbb{E}[Y \mid X]\), c’est-à-dire la demande moyenne conditionnelle. Or notre métrique d’évaluation est la Pinball Loss au quantile 80%, et non la moyenne. Utiliser ce modèle directement pour la soumission constitue donc un biais méthodologique.
Pour corriger ce biais, nous avons adapté la descente de gradient pour minimiser directement la Pinball Loss (aussi appelée Quantile Loss) au quantile \(\tau = 0.80\) :
\[\mathcal{L}_{\tau}(y, \hat{y}) = \frac{1}{n}\sum_{i=1}^{n} \rho_\tau(y_i - \hat{y}_i)\]
où la fonction “check” \(\rho_\tau\) est définie par :
\[\rho_\tau(r) = \begin{cases} \tau \cdot r & \text{si } r \geq 0 \\ (\tau - 1) \cdot r & \text{si } r < 0 \end{cases}\]
Cette fonction est asymétrique : elle pénalise 4 fois plus une sous-estimation qu’une surestimation (\(\tau = 0.8\) implique un ratio \(0.8 / 0.2 = 4\)). Le modèle est donc naturellement poussé à prédire des valeurs hautes, ciblant les pics de consommation.
La Pinball Loss n’est pas différentiable en zéro. Le gradient par rapport aux poids s’écrit néanmoins presque partout :
\[\nabla_\beta \mathcal{L}_{\tau} = \frac{1}{n} X^\top \cdot \mathbf{g}\]
où \(\mathbf{g}_i = \mathbb{1}[\hat{y}_i > y_i] - \tau\), ce qui donne : - \(g_i = 1 - \tau = 0.2\) si \(\hat{y}_i > y_i\) (surestimation, pénalité faible) - \(g_i = -\tau = -0.8\) si \(\hat{y}_i \leq y_i\) (sous-estimation, pénalité forte)
Nous avons utilisé learning_rate = 0.02 et
maxIter = 17000 (plus d’itérations car la Pinball Loss est
plus difficile à descendre que le RMSE). Un critère de convergence
précoce s’arrête si l’objectif passe sous \(10^{-6}\).
Vérification empirique de la calibration : À la fin de l’entraînement, on vérifie que \(P(\hat{y} \geq y) \approx \tau = 0.80\). Si cette couverture est trop éloignée de 80%, c’est un signal que la descente n’a pas convergé ou que le learning rate est inadapté.
Résultat obtenu et bug constaté : En pratique, les prédictions finales de ce modèle sont de l’ordre de 470 MW, là où la demande réelle est autour de 39 000 MW. Ce résultat est clairement absurde et ne peut pas s’expliquer uniquement par le bug de pipeline décrit en 3.1.
La cause probable est un problème de normalisation asymétrique : les
features de X_test ont été normalisées avec les
statistiques calculées sur X_train (moyennes et
écarts-types de l’entraînement), mais certaines colonnes présentes en
train et absentes en test (comme les degrés-jours) sont remplies avec
fill_value=0.0 avant la normalisation, ce
qui correspond à une valeur hors distribution. Le modèle, dont les poids
ont été appris sur des features normalisées dans un certain intervalle,
reçoit des entrées dans un espace différent et produit des prédictions
hors échelle.
Nous comprenons bien ce que ce modèle est censé faire : si le pipeline était correct, la descente de gradient devrait converger vers des poids \(\beta\) tels que, pour chaque observation, \(\hat{y}_i\) dépasse la vraie valeur \(y_i\) dans environ 80% des cas. Le modèle apprendrait à se décaler vers le haut par rapport à la moyenne, ciblant les pics de consommation plutôt que le niveau moyen. Ce principe est valide, c’est l’implémentation du pipeline qui n’a pas fonctionné comme prévu.
Pour éviter le surapprentissage et réduire l’influence des features peu informatives, nous avons ajouté un terme de pénalité sur les poids.
Ridge (L2) : on pénalise la norme euclidienne des poids : \[\mathcal{L}_{\text{Ridge}} = \mathcal{L}_{\tau} + \lambda \sum_{j=1}^{p} \beta_j^2\] Le gradient de la pénalité est \(2\lambda\beta\), ce qui pousse progressivement tous les poids vers zéro mais ne les annule jamais complètement.
Lasso (L1) : on pénalise la norme \(L_1\) des poids : \[\mathcal{L}_{\text{Lasso}} = \mathcal{L}_{\tau} + \lambda \sum_{j=1}^{p} |\beta_j|\] Le gradient de la pénalité est \(\lambda \cdot \text{sign}(\beta)\). Cette pénalité peut annuler complètement des coefficients, produisant une sélection automatique de variables. Elle est statistiquement équivalente à poser une prior de Laplace sur \(\beta\).
Le biais \(b\) n’est pas régularisé (ni en L1 ni en L2), conformément à la pratique standard.
Nous avons conduit un sweep de valeurs de \(\lambda\) allant de \(0\) à \(10\) sur un split temporel train/validation
(80/20), en mesurant la Pinball Loss de validation, la couverture \(P(\hat{y} \geq y)\) et la norme \(\|\beta\|_2\). Les résultats sont
sauvegardés dans
regularization_experiments_summary.csv.
[ajouter la courbe de la Pinball Loss de validation en fonction de \(\lambda\) pour les régularisations L1 et L2, montrant le plateau optimal dans la zone \([10^{-4},\ 10^{-2}]\)]
Ce que nous avons observé : Les \(\lambda\) très élevés (\(\lambda \geq 1\)) dégradent la performance, car ils forcent tous les poids vers zéro, perdant ainsi l’information des features. Les \(\lambda\) très faibles (\(\lambda \leq 10^{-6}\)) sont pratiquement équivalents à aucune régularisation. La zone utile se situe entre \(10^{-4}\) et \(10^{-2}\), avec des gains modestes sur le jeu de validation. Ce résultat indique que la régularisation n’est pas le levier principal ici : le vrai problème est la non-linéarité de la relation température–demande, que le modèle linéaire ne peut pas capturer seul.
Une propriété utile du modèle linéaire est que les coefficients \(\beta_j\) sont directement interprétables après normalisation : leur valeur absolue mesure l’impact marginal de chaque variable sur la prévision.
Nous avons bien implémenté ce classement dans le notebook (tri par \(|\beta_j|\) décroissant). Cependant, étant donné que le modèle Pinball produit des prédictions de l’ordre de 470 MW (section 3.3), les poids appris sont le résultat d’une descente de gradient sur un espace de features incohérent. Il n’est donc pas raisonnable d’interpréter ces coefficients comme des mesures d’importance réelle des variables.
Le modèle linéaire remplit son rôle de baseline interprétable :
Ses limites sont claires. D’abord, le bug de pipeline identifié en 3.1 : les degrés-jours et le flag COVID n’étaient pas propagés au test, privant le modèle de transformations qu’il avait apprises. Ensuite, même sans ce bug, un modèle linéaire ne peut pas représenter la relation en “V” entre température et demande sans aide explicite. La saisonnalité hebdomadaire est capturée via les dummies, mais les interactions (par exemple : jour froid un weekend) ne sont pas modélisables sans termes d’interaction explicites. Ces limitations ont motivé notre passage aux modèles non-linéaires (GAM).
Un modèle ARIMA(p, d, q) décompose une série temporelle en trois composantes :
AR(p) (AutoRégressif) : la valeur actuelle dépend linéairement des \(p\) valeurs passées : \[Y_t = \phi_1 Y_{t-1} + \phi_2 Y_{t-2} + \dots + \phi_p Y_{t-p} + \epsilon_t\]
I(d) (Intégré) : la série est différenciée \(d\) fois pour la rendre stationnaire. Une série est stationnaire si sa moyenne et sa variance ne dépendent pas du temps.
MA(q) (Moyenne Mobile) : la valeur actuelle dépend des \(q\) erreurs passées : \[Y_t = \epsilon_t + \theta_1 \epsilon_{t-1} + \dots + \theta_q \epsilon_{t-q}\]
Le modèle ARIMA combine ces trois termes. Pour identifier les bons ordres \(p\), \(d\), \(q\), on utilise classiquement : - Le test ADF (Augmented Dickey-Fuller) pour tester la stationnarité et choisir \(d\) - Les graphes ACF (AutoCorrelation Function) et PACF (Partial ACF) pour guider \(q\) et \(p\) - Le critère AIC pour comparer les modèles en pénalisant la complexité : \[\text{AIC} = -2\ln(\hat{L}) + 2k\] où \(\hat{L}\) est la vraisemblance maximisée du modèle et \(k\) le nombre de paramètres libres.
La règle de lecture classique des graphes ACF/PACF est la suivante : si le PACF coupe net après le lag \(p\) et que l’ACF décroît exponentiellement, cela suggère un processus AR(\(p\)) ; si l’ACF coupe net après le lag \(q\) et le PACF décroît, cela suggère un MA(\(q\)). Des pics significatifs aux multiples de \(s\) dans les deux graphes indiquent une saisonnalité à la période \(s\).
[ajouter les graphes ACF et PACF de la série Net_demand brute, avec les pics aux lags 7, 14, 21 bien visibles]
ARIMA a une limite fondamentale : il ne modélise pas la saisonnalité. Or la demande électrique a une saisonnalité hebdomadaire très marquée (le lundi ressemble au lundi de la semaine précédente, pas au dimanche).
SARIMA(p, d, q)(P, D, Q, s) étend ARIMA avec des termes saisonniers de période \(s\) :
\[\Phi_P(B^s)\phi_p(B)(1-B^s)^D(1-B)^d Y_t = \Theta_Q(B^s)\theta_q(B)\epsilon_t\]
où \(B\) est l’opérateur retard (\(B Y_t = Y_{t-1}\)) et \(s\) est la période saisonnière. Dans notre contexte, \(s = 7\) (saisonnalité hebdomadaire sur des données journalières). Les paramètres \((P, D, Q)\) jouent le même rôle que \((p, d, q)\) mais sur les lags saisonniers : le terme AR saisonnier relie \(Y_t\) à \(Y_{t-7}\), \(Y_{t-14}\), etc.
SARIMAX ajoute un vecteur de variables exogènes \(X_t\) au modèle SARIMA :
\[Y_t = X_t \beta + \text{Sarima}(\text{résidus})\]
Le modèle estime d’abord la contribution des variables externes
(température, calendrier, lags de demande), puis modélise la structure
temporelle du résidu restant. C’est l’approche hybride que nous avons
explorée : utiliser Net_demand.1 (la vraie valeur du jour
précédent) comme variable exogène donne au modèle une information
fraîche à chaque pas de prédiction.
Avant de choisir les paramètres, nous avons analysé la série
Net_demand :
La saisonnalité hebdomadaire était clairement visible dans les graphes ACF, avec des pics significatifs aux multiples de 7.
[ajouter la décomposition saisonnière additive de Net_demand : tendance, composante saisonnière hebdomadaire et résidu]
Nous avons fait un grid search sur les ordres \((p, q) \in \{0,\dots,4\} \times \{0,\dots,3\}\) avec \(d = 0\) (la série étant appliquée sur la série déjà différenciée manuellement), en sélectionnant le meilleur modèle par AIC.
Le meilleur ARIMA trouvé a ensuite été soumis et a obtenu un score Pinball d’environ 1200, ce qui était décevant. Le diagnostic résiduel expliquait pourquoi : le test de Ljung-Box aux lags 7, 14 et 21 rejetait l’hypothèse d’indépendance des résidus (\(p < 0.05\)), et l’ACF des résidus montrait des pics significatifs aux multiples de 7. ARIMA n’avait pas capturé la saisonnalité hebdomadaire, dont la structure restait entièrement dans les résidus.
La statistique de Ljung-Box est définie par : \[Q(h) = n(n+2)\sum_{k=1}^{h} \frac{\hat{\rho}_k^2}{n-k}\] où \(\hat{\rho}_k\) est l’autocorrélation empirique des résidus au lag \(k\) et \(n\) la taille de l’échantillon. Sous \(H_0\) d’absence d’autocorrélation résiduelle, \(Q(h) \sim \chi^2(h)\). Rejeter \(H_0\) au lag \(h = 7\) indique que la saisonnalité hebdomadaire n’a pas été absorbée par le modèle.
[ajouter le graphe de l’ACF des résidus du meilleur ARIMA, montrant les pics résiduels aux lags 7, 14, 21]
Nous avons ajouté les termes saisonniers avec \(s = 7\), en faisant un nouveau grid search sur \((P, D, Q) \in \{0,1\} \times \{0,1\} \times \{0,1\}\).
La première approche consistait à prédire toute la période de test d’un seul coup (multi-step forecast). Les résultats montraient un phénomène bien connu : les premières prédictions (jours 1 à 3-4) étaient relativement cohérentes avec la réalité, mais après 4-5 jours, les prédictions convergaient progressivement vers la moyenne de la série.
Ce comportement n’est pas un bug — c’est une propriété mathématique fondamentale des processus ARIMA. Pour un horizon \(h\) tendant vers l’infini, la prévision conditionnelle d’un processus ARIMA stationnaire converge vers la moyenne inconditionnelle de la série : \[\lim_{h \to \infty} \mathbb{E}[Y_{t+h} \mid Y_t, Y_{t-1}, \dots] = \mu\]
En pratique, cette convergence est rapide : dès que l’horizon dépasse la portée des termes AR et saisonniers, le modèle “oublie” l’état actuel et revient à sa valeur d’équilibre. Sur une demande électrique avec saisonnalité hebdomadaire, cela se traduit par des prédictions plates après quelques jours.
[ajouter le graphe des prédictions SARIMA naïf (multi-step depuis un seul point de départ) vs valeurs réelles, montrant la convergence progressive vers la moyenne à partir du 4e–5e jour]
Pour contourner ce problème, nous avons adopté une stratégie de prédiction glissante (rolling forecast) : plutôt que de prédire toute la période d’un coup, on prédit un jour à la fois, puis on met à jour l’état interne du modèle avec la vraie valeur observée avant de prédire le jour suivant.
La clé est la variable Load.1 (la vraie charge du jour
précédent), disponible dans test.csv. À chaque pas \(i\) :
Load.1.append(new_obs, refit=False) de statsmodels — ce qui
propage la vraie observation sans refaire un entraînement completTechniquement, .append(refit=False) effectue une mise à
jour du filtre de Kalman sous-jacent au modèle SARIMAX (statsmodels
utilise une représentation espace-état). Le modèle révise sa
distribution a posteriori sur l’état caché à partir de la nouvelle
observation, sans modifier les paramètres \(\phi, \theta, \Phi, \Theta\) déjà estimés
sur les données d’entraînement.
Formellement, pour un modèle en représentation espace-état \(Y_t = \mathbf{Z}\boldsymbol{\alpha}_t + \epsilon_t\), les équations de mise à jour du filtre de Kalman à la réception d’une nouvelle observation \(y_t^*\) sont : \[\boldsymbol{\alpha}_{t|t} = \boldsymbol{\alpha}_{t|t-1} + \mathbf{P}_{t|t-1}\mathbf{Z}^\top F_t^{-1}\, v_t\] \[\mathbf{P}_{t|t} = \mathbf{P}_{t|t-1} - \mathbf{P}_{t|t-1}\mathbf{Z}^\top F_t^{-1}\mathbf{Z}\mathbf{P}_{t|t-1}\] où \(v_t = y_t^* - \mathbf{Z}\boldsymbol{\alpha}_{t|t-1}\) est l’innovation (écart entre la réalité et la prédiction a priori), et \(F_t = \mathbf{Z}\mathbf{P}_{t|t-1}\mathbf{Z}^\top + H\) est la variance de cette innovation. Les paramètres du modèle restent fixes ; seule la distribution de l’état interne \(\boldsymbol{\alpha}_t\) est révisée.
Ce mécanisme donne au modèle un ancrage local à chaque nouvelle journée. Il ne prédit plus à 30 jours d’intervalle depuis un état figé, mais à 1 jour depuis un état mis à jour. Le résultat a été spectaculaire : le score est passé de ~1200 à ~650, uniquement grâce à ce changement de stratégie de prédiction, sans changer le modèle lui-même.
[ajouter le graphe des prédictions SARIMAX rolling vs valeurs réelles de Net_demand sur la période de test (valeurs autour de 40 000–45 000 MW)]
Nous avons également testé plusieurs combinaisons de paramètres saisonniers \((P, D, Q)\) avec cette stratégie glissante pour affiner davantage le score.
SARIMA nous a appris deux choses importantes. D’abord, qu’un modèle de séries temporelles sans saisonnalité explicite est insuffisant pour la demande électrique : les résidus d’ARIMA contenaient encore toute la structure hebdomadaire, ce que le test de Ljung-Box a clairement confirmé. Ensuite, que la stratégie de prédiction (naïve vs glissante) peut avoir autant d’impact sur le score que le choix du modèle lui-même — passer de multi-step à rolling a divisé le score par presque deux.
La limite principale de SARIMAX reste sa nature univariée (ou faiblement multivariée) : il modélise la dynamique temporelle de la série, mais ne capte pas les effets non linéaires de la température ou du calendrier aussi bien qu’un modèle de machine learning dédié. C’est ce qui a motivé notre passage vers les GAM.
La demande nette est définie par : \[\text{Net_demand} = \text{Load} - \text{Solar_power} - \text{Wind_power}\]
Plutôt que de prédire Net_demand directement, on peut
prédire séparément chacun des trois termes, puis les combiner. Cette
idée nous avait été soufflée en observant l’approche d’un autre groupe
(MAP&IA) qui avait utilisé trois GAM distincts pour les trois
composantes.
Nous y avions pensé dès le début, mais nous l’avions mise de côté sur
la régression linéaire pour une raison précise : additionner trois
modèles, c’est additionner trois sources d’erreur. Si chaque modèle a
une erreur résiduelle \(\epsilon_i\),
l’erreur finale sur Net_demand est \(\epsilon_{\text{Load}} + \epsilon_{\text{Solar}} +
\epsilon_{\text{Wind}}\), ce qui peut être pire qu’une seule
erreur directe selon la covariance entre ces erreurs. Nous avions donc
jugé que pour un modèle linéaire simple, la prédiction directe de
Net_demand était plus sûre.
Nous avons repris cette idée dans le cadre SARIMAX, où la
décomposition prend plus de sens : chacune des trois séries a une
dynamique temporelle propre, et un modèle unique sur
Net_demand mélange des processus de natures très
différentes.
Load est la consommation brute d’électricité. Elle suit
des patterns saisonniers très réguliers (journalier, hebdomadaire,
annuel), ce qui la rend bien adaptée à un modèle SARIMAX avec rolling
forecast. Les prédictions obtenues sur le jeu de test
(Load_forecast dans
sarimax_rolling_forecasts.csv) donnent des valeurs autour
de 40 000–45 000 MW, cohérentes avec les ordres de grandeur
attendus.
Solar_power est plus complexe : la production
photovoltaïque varie fortement selon la saison, l’heure de la journée et
la nébulosité. Nous avons testé un SARIMAX légèrement plus élaboré sur
cette série, en tenant compte de la saisonnalité journalière. Les
prédictions (sarimax_solar_rolling_forecasts_q06.csv) sont
dans l’ordre de grandeur attendu (~2 000–3 000 MW), même si la qualité
de l’ajustement est moins bonne que pour Load.
Wind_power est le terme le plus problématique des trois.
Contrairement à Load ou Solar_power, la
production éolienne ne suit pas de pattern temporel facilement
exploitable. Elle dépend de la météo locale à court terme — direction,
vitesse, variabilité du vent — ce qui en fait en grande partie une série
aléatoire à l’échelle de quelques jours.
Nous avons cherché des approches dans la littérature, et il ressort que les modèles dits “boîte noire” — réseaux de neurones récurrents (LSTM), forêts aléatoires, gradient boosting — obtiennent généralement de meilleurs résultats sur la prédiction éolienne que les modèles statistiques classiques. La raison est que ces modèles peuvent capturer des interactions complexes entre variables exogènes (température, vent spatialisé, saison) sans hypothèse de structure.
Nous avons finalement modélisé Wind_power avec
XGBoost, ce qui a produit des prédictions numériquement
raisonnables visuellement raisonnables
(xgboost_wind_forecasts.csv). Cependant, nous avons
remarqué que le processus de choix des hyperparamètres (profondeur des
arbres, taux d’apprentissage, nombre d’estimateurs) était difficile à
justifier : on peut faire varier ces paramètres et observer l’effet sur
la validation, mais il est compliqué de donner une intuition statistique
claire sur pourquoi telle valeur est meilleure qu’une autre,
contrairement à \(\lambda\) dans Ridge
ou à \(s\) dans SARIMA.
Pour capturer les effets fortement non-linéaires de la météo (sans avoir à créer manuellement des dizaines de variables d’interaction), nous avons implémenté un Modèle Additif Généralisé (GAM).
Un GAM s’écrit sous la forme : \[Y = \beta_0 + \sum_{j=1}^{p} f_j(X_j) + \epsilon\] où chaque \(f_j\) est une spline — une fonction lisse estimée à partir des données en minimisant un critère des moindres carrés pénalisé : \[\sum_{i=1}^{n} \left(y_i - \hat{y}_i\right)^2 + \lambda_j \int \left[f_j''(x)\right]^2 dx\] La pénalité \(\int [f_j''(x)]^2 dx\) mesure la courbure (“wiggliness”) de la spline. Le paramètre \(\lambda_j\) contrôle le compromis biais-variance : \(\lambda_j \to 0\) revient à interpoler les données (surapprentissage) ; \(\lambda_j \to \infty\) force la spline à être une droite (sous-apprentissage).
Les \(\lambda_j\) sont estimés automatiquement par REML (Restricted Maximum Likelihood), qui traite les coefficients des splines comme des effets aléatoires. La log-vraisemblance REML à maximiser est proportionnelle à : \[\ell_{\text{REML}}(\boldsymbol{\lambda}) \propto -\frac{1}{2}\log\left|\mathbf{X}^\top\mathbf{X} + \mathbf{S}_{\boldsymbol{\lambda}}\right| - \frac{n - p}{2}\log\hat{\sigma}^2\] où \(\mathbf{S}_{\boldsymbol{\lambda}} = \sum_j \lambda_j \mathbf{S}_j\) est la matrice de pénalité globale (chaque \(\mathbf{S}_j\) code la pénalité de la deuxième dérivée de \(f_j\)). REML est préférée à ML car elle corrige le biais d’estimation des composantes de variance — c’est l’analogue statistique de diviser par \(n-1\) plutôt que \(n\) dans la variance empirique.
Nous avons implémenté quatre modèles GAM en R avec le package
mgcv, tous estimés par REML.
GAM 6 — modèle de base :
gam(Net_demand ~ s(Time, k=3, bs='cr') + s(toy, k=30, bs='cc') +
s(Temp, k=10) + s(Net_demand.1) + s(Net_demand.7) +
s(Temp_s99) + as.factor(WeekDays) + BH +
s(Wind) + te(Time, Nebulosity, k=c(4,10)))
Le terme s(toy, bs='cc') utilise une spline
cyclique pour la saisonnalité annuelle
(toy = time of year, valeur entre 0 et 1) : cela force la
courbe à être continue entre le 31 décembre et le 1er janvier. Les lags
Net_demand.1 et Net_demand.7 sont inclus pour
capturer l’autocorrélation temporelle sans avoir à modéliser
explicitement un processus ARIMA. Le terme
te(Time, Nebulosity) est un produit
tensoriel permettant à l’effet de la nébulosité d’évoluer dans
le temps.
GAM 7 — saisonnalité évolutive :
gam(Net_demand ~ te(Time, toy, k=c(4,20), bs=c('cr','cc')) + ...)
Ici, l’interaction te(Time, toy) permet à la forme de la
courbe saisonnière de changer d’une année à l’autre —
ce qui est pertinent pour capturer la tendance à la baisse de la
consommation liée à la sobriété énergétique.
GAM 8 — interaction température-vent :
gam(Net_demand ~ s(Time, k=3) + s(toy, k=30, bs='cc') +
te(Temp, Wind, k=c(8,8)) + ...)
L’hypothèse ici est que l’effet du vent sur la consommation
dépend de la température : par grand froid, le vent
accentue la sensation de froid (effet wind-chill), augmentant le recours
au chauffage. Le tenseur te(Temp, Wind) capture cette
interaction sans supposer qu’elle est additive.
GAM 9 — sélection automatique de variables :
gam(Net_demand ~ s(Time, bs='cs') + s(toy, bs='cc') + ..., select=TRUE)
Le paramètre select=TRUE et les splines de type
bs='cs' (cubic shrinkage) ajoutent une pénalité
supplémentaire qui peut annuler complètement une spline
si la variable est non-informative. C’est l’équivalent d’un Lasso pour
les GAM : le modèle peut décider lui-même de retirer une variable en
mettant son effet à zéro.
Les quatre modèles ont été comparés par AIC et par la déviance expliquée (\(R^2\) de déviance). Ces métriques mesurent la qualité d’ajustement en pénalisant la complexité (AIC) — un modèle avec un AIC plus faible est préféré.
Un outil de diagnostic complémentaire est la vérification des
degrés de liberté effectifs (edf) de chaque spline. Si
edf ≈ k (la dimension de la base choisie), la spline est
“saturée” : le signal dans les données nécessite plus de flexibilité que
ce que la base actuelle permet, et il faudrait augmenter k.
Si edf << k, la pénalité REML a fortement lissé la
spline et le choix de k n’est pas limitant.
Sur la base de cette comparaison, GAM 8 a été retenu comme meilleur modèle et ses prédictions ont été soumises.
[ajouter les effets partiels estimés de GAM 8 : s(toy), s(Temp), te(Temp, Wind) — pour visualiser les non-linéarités capturées par les splines]
[ajouter le graphe gam.check de GAM 8 : QQ-plot des résidus et résidus vs valeurs ajustées, pour évaluer l’hypothèse gaussienne]
Cependant, l’utilisation de la fonction standard gam()
repose sur des hypothèses statistiques fortes : 1.
Erreurs gaussiennes et homoscédastiques : \(\epsilon \sim \mathcal{N}(0, \sigma^2)\).
Le modèle optimise la log-vraisemblance gaussienne, ce qui revient à
minimiser le MSE — il prédit donc la moyenne, pas le quantile 80. 2.
Indépendance des résidus : en séries temporelles, les
résidus consécutifs sont corrélés. L’inclusion des lags
Net_demand.1 et Net_demand.7 comme variables
explicatives atténue ce problème sans le résoudre complètement. 3.
Additivité : les effets de chaque variable
s’additionnent, sauf là où un tenseur te() a été
explicitement utilisé pour modéliser une interaction.
En parallèle des GAM R, nous avons aussi testé une implémentation
Python avec la librairie pyGAM. Le modèle utilisé est
ExpectileGAM avec expectile=0.8, qui est une
approximation de la régression quantile au sens des expectiles.
Un expectile au niveau \(\tau\) est défini comme la solution de : \[\hat{\mu}_\tau = \arg\min_{\mu} \sum_{i=1}^{n} w_\tau(y_i - \mu)^2\] avec \(w_\tau = \tau\) si \(y_i > \mu\) et \(w_\tau = 1 - \tau\) sinon. Pour \(\tau = 0.5\) on retrouve la moyenne. Pour \(\tau = 0.8\) on obtient une valeur qui se comporte comme un quantile haut, mais l’expectile n’est pas exactement le quantile — la correspondance exacte dépend de la distribution des résidus.
Le modèle Python appliquait une spline s(j) à chaque
feature \(j\), avec la même
normalisation et le même pipeline que pour la régression linéaire. Un
split temporel 80/20 a permis de comparer directement la Pinball Loss de
validation de ExpectileGAM avec celle du modèle
linéaire.
Contre toute attente, le GAM classique R (prédisant la
moyenne conditionnelle) a obtenu un meilleur score Pinball 80
sur le jeu de test que le modèle qgam (ajusté directement
sur la Pinball Loss). Plusieurs raisons peuvent expliquer cela :
mgcv sont très stables. L’outil
qgam est plus récent et potentiellement moins bien calibré
sur ce type de données.Face à ce résultat empirique, nous avons retenu GAM 8 (modèle moyen R) comme notre meilleur modèle GAM.
Afin de visualiser l’apport de chaque itération (de la descente de gradient pénalisée au GAM en passant par le modèle naïf), nous avons comparé leurs prédictions sur notre ensemble de test.
# Illustration pédagogique du principe de la régression quantile.
# Ce graphique montre ce qu'un modèle linéaire RMSE et un modèle GAM
# sont censés produire sur la période de test.
# La régression quantile (Pinball) n'est pas représentée ici car
# notre implémentation a produit des prédictions hors échelle (~470 MW)
# due au bug de pipeline décrit en section 3.1 et 3.3.
set.seed(42)
dates_test <- seq(as.POSIXct("2022-09-01"), as.POSIXct("2022-09-14"), by="hour")
n <- length(dates_test)
y_true <- 39000 + 5000 * sin(seq(0, 14*pi, length.out = n)) + rnorm(n, 0, 1000)
df_results <- data.frame(
Date = dates_test,
Reel = y_true,
RegLin_RMSE = y_true + rnorm(n, 0, 3000) - 1000,
GAM_Moyenne = y_true + rnorm(n, 0, 800)
)
library(tidyr)
df_long <- df_results %>%
pivot_longer(cols = -Date, names_to = "Modele", values_to = "Prediction")
ggplot(df_long, aes(x = Date, y = Prediction, color = Modele, linetype = Modele)) +
geom_line(alpha = 0.8, linewidth = 0.8) +
scale_color_manual(values = c(
"Reel" = "black",
"RegLin_RMSE" = "red",
"GAM_Moyenne" = "blue"
)) +
scale_linetype_manual(values = c(
"Reel" = "solid",
"RegLin_RMSE" = "dashed",
"GAM_Moyenne" = "dotted"
)) +
labs(
title = "Illustration : prédictions Régression Linéaire vs GAM (données simulées)",
subtitle = "Les données ici sont simulées à titre illustratif — les vrais résultats sont dans les fichiers de soumission",
x = "Date",
y = "Demande Nette d'Électricité (MW)",
color = "Modèle",
linetype = "Modèle"
) +
theme_minimal() +
theme(legend.position = "bottom")
Ce projet nous a permis de passer par toute la chaîne d’un problème de prévision de séries temporelles réel : préparation des données, choix et implémentation des modèles, diagnostic des résultats, et identification des erreurs.
Ce que nous avons fait concrètement :
Ce que nous n’avons pas réussi à faire fonctionner :
Covid_Flag et des
degrés-jours au jeu de test : ces features ont été apprises mais sont
restées nulles en prédiction.Ce que nous retenons :
La leçon la plus concrète est que la stratégie de prédiction (glissante vs naïve) a eu plus d’impact sur le score SARIMAX que la recherche d’hyperparamètres. C’est un résultat peu intuitif qui ne ressort pas des présentations théoriques des modèles.
Sur la question des modèles boîte noire (XGBoost, deep learning), nous avons fait le choix délibéré de ne pas les pousser : les utiliser efficacement en deux semaines sans comprendre leur mécanique interne aurait produit du code difficile à défendre. C’est un compromis que nous assumons, et qui correspond à l’objectif que nous nous étions fixé au départ : construire de la connaissance plutôt que du score.