Approximate Exogenous Variables for Forecasting

Compare SARIMA, SARIMAX, and XGBoost models with approximated future variables.

Introduction

30 Second Summary

Quand tu entraines un modele de prevision avec des variables externes (meteo, age d'equipement), les resultats sont souvent excellents sur les donnees historiques. Mais en production, personne ne connait la meteo dans 3 ans.

Dans ce projet, tu vas entrainer 4 modeles de prevision, construire des strategies d'approximation pour les variables futures inconnues, et mesurer si le gain apporte par les exogenes survit quand on doit les deviner.

What You'll Build

Un notebook d'analyse comparative ou tu lances une prevision, tu compares les courbes avec et sans variables exogenes, et tu demontres chiffres a l'appui quelle strategie adopter en production.

By the end of this project, you'll have:

  • Un tableau comparatif de 4 modeles (SARIMA, SARIMAX, XGBoost sans et avec exogenes) avec les metriques MAE et RMSE qui prouvent le gain reel des variables externes.
  • Un pipeline d'approximation fonctionnel qui projette la meteo future par climatologie saisonniere et l'age du parc par extrapolation lineaire, avec des visualisations superposees aux vraies valeurs.
  • Une analyse chiffree de la degradation qui repond a la question : quel pourcentage du gain initial est conserve malgre l'approximation, et une recommandation argumentee pour la strategie de production.
  • Secret Mission: une simulation Monte Carlo qui genere des intervalles de confiance realistes sur les previsions au lieu d'un point unique.

Y a-t-il des prerequis ?

Ce projet suppose un environnement Python fonctionnel avec statsmodels, xgboost, pandas, numpy, matplotlib et scikit-learn deja installes. Tu auras aussi besoin du dataset de maintenance hebdomadaire (235 observations avec les colonnes WEEK, Maintenance, temp_max_mean, alertes meteo, Age du K moy, vacances).

Before We Start

Avant de plonger dans le code, prenons un moment pour cadrer ce que tu vas construire et pourquoi. Tu vas entrainer 4 modeles de prevision, construire des strategies d'approximation pour les variables exogenes inconnues a horizon long terme, et mesurer si le gain des exogenes survit quand on doit les approximer.

Load and Prepare the Data

Pour comparer la performance de modeles avec et sans variables exogenes, tu as besoin d'une base solide. Cela commence par une separation propre train/test et une identification claire de ce que tu connais a l'avance versus ce que tu devras approximer.

Sans cette preparation, les comparaisons des prochaines etapes seront faussees. Un test set qui "fuit" dans le train set ou des variables mal classifiees invalident toute conclusion sur la degradation de performance.

In this step, get ready to:
  • Charger le dataset et configurer l'index temporel hebdomadaire.
  • Separer les donnees en train/test (80/20) avec les dernieres observations comme test.
  • Classifier les variables en trois categories : target, exogenes connues, et exogenes a approximer.
Charger le dataset et configurer l'index temporel

Les modeles de series temporelles dans statsmodels attendent un DataFrame avec un index de type DatetimeIndex a frequence reguliere. Sans frequence explicite, les fonctions de forecast ne savent pas combien de temps separe chaque observation.

  • Dans une nouvelle cellule de ton notebook, charge le dataset et configure l'index en executant ce code :
# Charger le dataset
df = pd.read_csv('maintenance_dataset.csv', parse_dates=['WEEK'], index_col='WEEK')
df = df.asfreq('W')

What does this code do?

La fonction pd.read_csv() charge le fichier CSV. Le parametre parse_dates=['WEEK'] convertit automatiquement la colonne WEEK en objets datetime.

L'appel df.asfreq('W') declare explicitement que les observations sont espacees d'une semaine. C'est ce qui permet a SARIMAX de savoir combien de pas correspondent a un horizon de prevision donne.

  • Execute la cellule. Si aucune erreur n'apparait, ton DataFrame est charge correctement.

FileNotFoundError?

Verifie que le fichier maintenance_dataset.csv se trouve dans le meme repertoire que ton notebook. Tu peux verifier avec !ls dans une cellule.

Si la colonne de dates ne s'appelle pas exactement WEEK, tu obtiendras une KeyError. Verifie les noms de colonnes avec pd.read_csv('maintenance_dataset.csv').columns d'abord.

Help me debug a FileNotFoundError or KeyError when loading my maintenance dataset in Jupyter

Separer les donnees en train/test (80/20)

Le split temporel conserve les dernieres observations comme test set. Contrairement a un split aleatoire, cela simule le vrai scenario de prevision : tu entraines sur le passe et tu predis le futur.

  • Dans une nouvelle cellule, cree le split et affiche les dimensions en executant ce code :
# Train/test split 80/20
split_idx = int(len(df) * 0.8)
df_train = df.iloc[:split_idx]
df_test = df.iloc[split_idx:]

print('Train shape:', df_train.shape)
print('Test shape:', df_test.shape)
print('Test period:', df_test.index[0], 'to', df_test.index[-1])

What does this code do?

Le calcul int(len(df) * 0.8) trouve l'index de la ligne a 80% du dataset. Tout ce qui precede va dans le train, tout ce qui suit dans le test.

Avec 235 observations, le split donne environ 188 semaines d'entrainement et 47 semaines de test. Ces 47 semaines representent environ 1 an de donnees reelles pour mesurer la performance.

Avant d'executer la cellule, a ton avis combien d'observations contiendra le test set avec un dataset de 235 lignes et un split a 80% ?

  • Execute la cellule et compare avec ta prediction.

Tu devrais voir le train set avec environ 188 lignes et le test set avec environ 47 lignes. La periode du test set couvre au moins 6 mois de donnees reelles.

Test set vide ou trop petit?

Si ton test set affiche 0 lignes, verifie que le DataFrame df n'est pas vide. Execute len(df) pour confirmer qu'il contient bien des donnees.

Si tu vois un nombre tres different de 47, verifie que tu n'as pas filtre ou supprime des lignes lors du chargement.

Help me debug my train/test split producing unexpected sizes

Classifier les variables en trois categories

La distinction entre variables connues et variables a approximer est cruciale pour la suite. Les vacances scolaires suivent un calendrier officiel previsible a l'infini. La meteo et l'age du parc, eux, sont inconnus a horizon 3 ans.

  • Dans une nouvelle cellule, definis les trois categories de variables en executant ce code :
# Classification des variables
target = 'Maintenance'
exog_known = ['vacances']
exog_to_approx = ['temp_max_mean', 'alertes_meteo', 'Age du K moy']

Why classify variables this way?

La variable target est ce que tu cherches a predire : le nombre d'interventions de maintenance.

Les exog_known sont les variables connues a n'importe quel horizon futur (calendrier officiel des vacances). Tu peux les utiliser sans approximation.

Les exog_to_approx sont les variables dont tu n'auras pas les vraies valeurs dans 3 ans. C'est pour celles-ci que tu construiras des strategies d'approximation au Step 4.

  • Execute la cellule. Pas de sortie visible ici. Ces variables seront utilisees dans toutes les etapes suivantes pour selectionner les bonnes colonnes.

✔️ Awesome, I've got everything!

Verifie bien que tu as sauvegarde ton notebook.

ⓧ I'd like to double check the full code

Voici le code complet de ce step, imports inclus :

# Step 1: Setup et preparation des donnees
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from statsmodels.tsa.statespace.sarimax import SARIMAX
from xgboost import XGBRegressor
from sklearn.metrics import mean_absolute_error, mean_squared_error

# Charger le dataset
df = pd.read_csv('maintenance_dataset.csv', parse_dates=['WEEK'], index_col='WEEK')
df = df.asfreq('W')

# Train/test split 80/20
split_idx = int(len(df) * 0.8)
df_train = df.iloc[:split_idx]
df_test = df.iloc[split_idx:]

print('Train shape:', df_train.shape)
print('Test shape:', df_test.shape)
print('Test period:', df_test.index[0], 'to', df_test.index[-1])

# Classification des variables
target = 'Maintenance'
exog_known = ['vacances']
exog_to_approx = ['temp_max_mean', 'alertes_meteo', 'Age du K moy']

Tes donnees sont pretes. Tu as un train set solide, un test set d'environ un an pour mesurer la performance, et une classification claire de tes variables. Prochaine etape : entrainer les modeles baseline sans exogenes pour etablir la reference de performance.

Train Baseline Models Without Exogenous Variables

Ton dataset est pret, proprement separe en train et test avec les variables classifiees. Maintenant, il te faut une ligne de reference.

Sans modeles purement endogenes pour comparer, tu ne pourras jamais quantifier le gain reel des variables exogenes. Dans ce step, tu entraines deux modeles qui n'utilisent aucune information externe : SARIMA pour la structure temporelle, et XGBoost pour les patterns calendaires.

In this step, get ready to:
  • Entrainer un modele SARIMA avec selection automatique d'ordre via AIC.
  • Entrainer un XGBoost baseline avec uniquement des features calendaires (sin/cos encoding).
  • Calculer MAE et RMSE pour les deux modeles et visualiser les predictions.
Entrainer SARIMA avec selection d'ordre via AIC

Le modele SARIMA capture la saisonnalite et les tendances d'une serie temporelle sans aucune variable externe. Pour trouver les meilleurs hyperparametres (p, q), tu vas iterer sur plusieurs combinaisons et garder celle qui minimise le critere AIC.

  • Dans ton notebook, cree une nouvelle cellule sous le code existant et colle le code suivant :
# Step 2: Modeles baseline sans variables exogenes

# SARIMA - selection d'ordre via AIC
best_aic = np.inf
best_order = (1, 1, 1)
best_seasonal_order = (1, 1, 1, 52)
for p in range(0, 3):
    for q in range(0, 3):
        try:
            model = SARIMAX(df_train[target], order=(p, 1, q),
                            seasonal_order=(1, 1, 1, 52),
                            enforce_stationarity=False,
                            enforce_invertibility=False)
            result = model.fit(disp=False)
            if result.aic < best_aic:
                best_aic = result.aic
                best_order = (p, 1, q)
        except:
            continue

sarima_model = SARIMAX(df_train[target], order=best_order,
                       seasonal_order=best_seasonal_order,
                       enforce_stationarity=False,
                       enforce_invertibility=False).fit(disp=False)
sarima_pred = sarima_model.forecast(steps=len(df_test))

What does this code do?

La boucle teste toutes les combinaisons (p, q) de 0 a 2 avec d=1 fixe, et une composante saisonniere hebdomadaire (s=52). Chaque modele est ajuste et son AIC est compare au meilleur score trouve.

Une fois le meilleur ordre identifie, le modele final est re-entraine avec ces parametres et genere les previsions sur toute la longueur du test set via forecast().

  • Execute la cellule.
  • La recherche par grille avec seasonal_order=(1, 1, 1, 52) est intensive en calcul. Attends que la cellule termine son execution (cela peut prendre 1 a 3 minutes selon ta machine).

La cellule se termine sans erreur et sans sortie visible. C'est normal : disp=False supprime les messages d'optimisation.

La cellule tourne depuis plus de 5 minutes ?

Verifie que tu n'as pas elargi les ranges au-dela de range(0, 3). Plus de combinaisons = temps exponentiel.

Si ton hardware est limite, reduis temporairement a range(0, 2) pour tester que le code fonctionne, puis relance avec range(0, 3) ensuite.

Besoin d'aide ? Mon SARIMA prend trop de temps a entrainer, comment optimiser ?

Entrainer XGBoost avec features calendaires uniquement

XGBoost traite la prevision temporelle comme un probleme tabulaire. Sans variables exogenes, tu n'utilises que des features derivees du calendrier : semaine de l'annee, mois, et un encodage sin/cos pour capturer la cyclicite.

  • Cree une nouvelle cellule et colle ce code :
# XGBoost baseline - features calendaires uniquement
def make_calendar_features(index):
    df_feat = pd.DataFrame(index=index)
    df_feat['week_of_year'] = index.isocalendar().week.astype(int)
    df_feat['month'] = index.month
    df_feat['sin_week'] = np.sin(2 * np.pi * df_feat['week_of_year'] / 52)
    df_feat['cos_week'] = np.cos(2 * np.pi * df_feat['week_of_year'] / 52)
    return df_feat

X_train_cal = make_calendar_features(df_train.index)
X_test_cal = make_calendar_features(df_test.index)
y_train = df_train[target]

xgb_baseline = XGBRegressor(n_estimators=200, learning_rate=0.05, random_state=42)
xgb_baseline.fit(X_train_cal, y_train)
xgb_baseline_pred = xgb_baseline.predict(X_test_cal)

What does this code do?

La fonction make_calendar_features() extrait 4 colonnes a partir de l'index temporel : la semaine de l'annee, le mois, et les transformations sin/cos de la semaine. L'encodage sin/cos permet au modele de comprendre que la semaine 52 est proche de la semaine 1 (cyclicite).

Le XGBRegressor est entraine sur ces seules features calendaires, sans aucune variable meteo ni age du parc. C'est ton deuxieme baseline purement endogene.

  • Execute la cellule.

XGBoost s'entraine en quelques secondes. La cellule se termine sans erreur.

Erreur sur isocalendar() ?

Verifie que ton index df_train.index est bien un DatetimeIndex. Si tu vois une erreur d'attribut, retourne a ta cellule du Step 1 et confirme que parse_dates=['WEEK'] et index_col='WEEK' sont bien presents dans pd.read_csv().

Aide supplementaire : Mon isocalendar() ne fonctionne pas sur l'index

Calculer les metriques et visualiser les predictions

Les deux baselines sont entraines. Il est temps de quantifier leurs performances avec MAE (erreur absolue moyenne) et RMSE (racine de l'erreur quadratique moyenne), puis de visualiser les courbes.

Avant d'executer, reflechis : selon toi, quel modele performe mieux sur des donnees hebdomadaires avec forte saisonnalite ? SARIMA avec sa composante saisonniere explicite, ou XGBoost avec son encodage sin/cos ?

  • Cree une nouvelle cellule et colle ce code :
# Metriques
mae_sarima = mean_absolute_error(df_test[target], sarima_pred)
rmse_sarima = np.sqrt(mean_squared_error(df_test[target], sarima_pred))
mae_xgb_baseline = mean_absolute_error(df_test[target], xgb_baseline_pred)
rmse_xgb_baseline = np.sqrt(mean_squared_error(df_test[target], xgb_baseline_pred))

print(f'SARIMA - MAE: {mae_sarima:.3f}, RMSE: {rmse_sarima:.3f}')
print(f'XGBoost baseline - MAE: {mae_xgb_baseline:.3f}, RMSE: {rmse_xgb_baseline:.3f}')

# Visualisation
plt.figure(figsize=(14, 5))
plt.plot(df_test.index, df_test[target], label='Vraies valeurs', color='black')
plt.plot(df_test.index, sarima_pred, label='SARIMA', linestyle='--')
plt.plot(df_test.index, xgb_baseline_pred, label='XGBoost baseline', linestyle=':')
plt.legend()
plt.title('Modeles baseline vs vraies valeurs')
plt.show()

What does this code do?

Les 4 premieres lignes calculent la MAE et RMSE de chaque modele en comparant les predictions aux vraies valeurs du test set. Les print() affichent les scores pour comparaison directe.

Le graphique superpose les courbes de predictions des deux modeles a la courbe reelle (en noir). Cela te permet de voir ou chaque modele echoue ou reussit visuellement.

  • Execute la cellule.

Tu vois les scores MAE et RMSE affiches pour les deux modeles, suivis d'un graphique avec trois courbes. La courbe noire (vraies valeurs) sert de reference, et les deux autres montrent comment SARIMA et XGBoost approximent la realite.

Que regarder dans ce graphique ?

Compare les deux courbes de prediction a la courbe noire. Un modele qui suit bien les pics et creux saisonniers a mieux capture la structure temporelle.

Note les valeurs MAE et RMSE. Ces chiffres sont ta reference absolue. Dans les prochains steps, tu mesureras si les variables exogenes ameliorent ces scores.

Le graphique ne s'affiche pas ?

Verifie que matplotlib est bien importe (import matplotlib.pyplot as plt dans ta premiere cellule). Si tu utilises Jupyter classique, ajoute %matplotlib inline en haut de ton notebook.

Besoin d'aide ? Mon graphique matplotlib ne s'affiche pas dans Jupyter

✔️ Awesome, I've got everything!

Parfait. Verifie que tu as bien sauvegarde ton notebook avant de passer a la suite.

ⓧ I'd like to double check the full code

Ton notebook doit contenir les cellules suivantes dans cet ordre. Remonte et confirme que chacune est presente :

  • Cellule 1 : imports, chargement du dataset, train/test split, classification des variables (Step 1).
  • Cellule 2 : grid search SARIMA via AIC, entrainement du modele final, sarima_pred.
  • Cellule 3 : fonction make_calendar_features(), entrainement XGBoost, xgb_baseline_pred.
  • Cellule 4 : calcul MAE/RMSE, print des scores, graphique matplotlib.

Tes deux baselines sont en place avec des scores de reference solides. Dans le prochain step, tu ajoutes les variables exogenes reelles pour mesurer le gain maximum possible.

Train Models With Real Exogenous Variables

Your baseline models now provide a performance floor. SARIMA captures temporal patterns and XGBoost captures calendar seasonality, but neither knows anything about external factors like weather, fleet age, or school holidays.

In this step, you will feed real exogenous variables to both model families and measure how much performance improves. This establishes the performance ceiling: the best you can do when you have perfect knowledge of the future.

In this step, get ready to:
  • Train a SARIMAX model with real exogenous variables and generate forecasts.
  • Train an XGBoost model combining calendar features with real exogenous variables.
  • Compare all 4 models and quantify the gain from exogenous information.
Train SARIMAX with real exogenous variables

SARIMAX extends SARIMA by treating exogenous variables as OLS regressors in a "regression with SARIMA errors" framework. The key constraint: to forecast into the future, you must supply the exogenous values for that future period via the exog parameter. For now, you will use the real test values as a best-case scenario.

  • In your notebook, create a new cell below your Step 2 code. Define the combined exogenous variable list and train the SARIMAX model by adding this code:
# Step 3: Modeles avec variables exogenes reelles
exog_all = exog_to_approx + exog_known

# SARIMAX avec exogenes reelles
sarimax_model = SARIMAX(df_train[target],
                        exog=df_train[exog_all],
                        order=best_order,
                        seasonal_order=best_seasonal_order,
                        enforce_stationarity=False,
                        enforce_invertibility=False).fit(disp=False)
sarimax_pred = sarimax_model.forecast(steps=len(df_test), exog=df_test[exog_all])

What does this code do?

exog_all combines the variables that need approximation (temp_max_mean, alertes_meteo, Age du K moy) with the known variable (vacances) into a single list for the model.

The SARIMAX model reuses best_order and best_seasonal_order from your SARIMA grid search so the only change is the addition of exogenous regressors. The .forecast() call receives the real test exogenous values via exog=df_test[exog_all].

  • Run the cell.
  • The cell should execute without errors. If it takes a few seconds, that is normal for SARIMAX with seasonal order 52.

Seeing a shape mismatch error?

Check that exog_all contains exactly 4 column names and all of them exist in both df_train and df_test. A typo in a column name will cause a KeyError.

Help me debug my SARIMAX exogenous variable error

Train XGBoost with exogenous features

XGBoost handles exogenous variables differently from SARIMAX. Instead of a "regression with ARIMA errors" framework, exogenous variables are simply additional columns in the feature matrix alongside the calendar features you already built.

  • In a new cell, train the XGBoost model with combined calendar and exogenous features by adding this code:
# XGBoost avec exogenes reelles
X_train_exog = pd.concat([X_train_cal, df_train[exog_all]], axis=1)
X_test_exog = pd.concat([X_test_cal, df_test[exog_all]], axis=1)

xgb_exog = XGBRegressor(n_estimators=200, learning_rate=0.05, random_state=42)
xgb_exog.fit(X_train_exog, y_train)
xgb_exog_pred = xgb_exog.predict(X_test_exog)

What does this code do?

pd.concat([X_train_cal, df_train[exog_all]], axis=1) merges your calendar features (week, month, sin/cos encoding) with the real exogenous columns into a single feature matrix. The model now sees both temporal patterns and external signals.

The hyperparameters stay identical to the baseline (n_estimators=200, learning_rate=0.05) so any performance difference is attributable purely to the added exogenous information.

  • Run the cell. It should complete in under a second.

Getting a column name conflict?

If pd.concat produces duplicate column names, it means one of the exogenous columns shares a name with a calendar feature. Verify that X_train_cal.columns and exog_all have no overlap.

Help me fix my XGBoost feature concatenation issue

Compare all 4 models and quantify the exogenous gain

You now have predictions from 4 models: two without exogenous information (SARIMA, XGBoost baseline) and two with real exogenous values (SARIMAX, XGBoost + exog). Time to measure the difference.

  • In a new cell, compute the metrics and build a comparison table by adding this code:
mae_sarimax = mean_absolute_error(df_test[target], sarimax_pred)
rmse_sarimax = np.sqrt(mean_squared_error(df_test[target], sarimax_pred))
mae_xgb_exog = mean_absolute_error(df_test[target], xgb_exog_pred)
rmse_xgb_exog = np.sqrt(mean_squared_error(df_test[target], xgb_exog_pred))

# Tableau comparatif
results = pd.DataFrame({
    'Modele': ['SARIMA', 'SARIMAX (reel)', 'XGBoost baseline', 'XGBoost + exog (reel)'],
    'MAE': [mae_sarima, mae_sarimax, mae_xgb_baseline, mae_xgb_exog],
    'RMSE': [rmse_sarima, rmse_sarimax, rmse_xgb_baseline, rmse_xgb_exog]
})
print(results)

What does this table show?

The table puts all 4 models side by side on the same metrics (MAE, RMSE). Lower values mean better predictions. The difference between SARIMA and SARIMAX shows the gain from exogenous variables in the statistical model. The difference between XGBoost baseline and XGBoost + exog shows the same gain in the machine learning model.

  • Run the cell. You should see a DataFrame with 4 rows and 2 metric columns printed below.
  • Verify that the SARIMAX and XGBoost + exog rows have lower MAE and RMSE than their baseline counterparts.

Now visualize the predictions from all 4 models against the real values to see how they track the actual maintenance patterns.

  • In a new cell, create the comparative plot by adding this code:
# Visualisation comparative
plt.figure(figsize=(14, 5))
plt.plot(df_test.index, df_test[target], label='Vraies valeurs', color='black', linewidth=2)
plt.plot(df_test.index, sarima_pred, label='SARIMA', linestyle='--')
plt.plot(df_test.index, sarimax_pred, label='SARIMAX (reel)', linestyle='-.')
plt.plot(df_test.index, xgb_baseline_pred, label='XGBoost baseline', linestyle=':')
plt.plot(df_test.index, xgb_exog_pred, label='XGBoost + exog (reel)', linestyle='-')
plt.legend()
plt.title('Comparaison des 4 modeles sur le test set')
plt.show()

What should I look for in the chart?

The black line is ground truth. Models with exogenous variables (dash-dot and solid lines) should track the peaks and valleys more closely than the baselines. Pay attention to seasonal spikes. The exogenous-informed models should follow sharp weather-driven or age-driven changes that the baselines miss.

Before you run this, what do you expect? Will the exogenous models track the real values more closely at the peaks, or will the improvement be uniform across all time points?

  • Run the cell. You should see a line chart with 5 curves: the thick black ground-truth line and 4 coloured model predictions. The models with exogenous data should visibly hug the black line more tightly, especially at seasonal peaks.

All curves look identical?

If your SARIMAX and SARIMA lines overlap perfectly, it likely means exog_all is empty or contains NaN values. Run print(exog_all) and df_train[exog_all].isnull().sum() to verify you have valid data in all 4 columns.

Help me understand why my exogenous models show no improvement over baseline

✔️ Awesome, I've got everything!

Great. Double check you have saved your notebook.

ⓧ I'd like to double check the full code

Here is the full Step 3 code. Scroll up and confirm each chunk is present in order:

  • exog_all definition combining exog_to_approx + exog_known.
  • SARIMAX model training with exog=df_train[exog_all] and forecast with exog=df_test[exog_all].
  • XGBoost with exogenous: pd.concat of calendar features with exogenous columns, then fit and predict.
  • Metrics computation (mae_sarimax, rmse_sarimax, mae_xgb_exog, rmse_xgb_exog) and comparison DataFrame printed.
  • Comparative plot with all 4 model predictions against true values.

You have now quantified the performance ceiling. The exogenous variables bring a measurable gain when you know their true values. Next up, you will build approximation strategies for the variables you cannot know in advance, and discover whether that gain survives the uncertainty.

Build Approximation Strategies for Future Variables

In the previous step, you measured the maximum gain that real exogenous variables bring to your forecasts. That gain is only useful if you can reproduce it in production.

The problem: in production, you don't have next year's weather or the fleet age 3 years from now. If you can't approximate these variables, the entire gain you measured is useless. This step builds the approximation profiles that make long-horizon forecasting possible.

In this step, get ready to:
  • Build a seasonal climatological profile for the weather variables.
  • Fit a linear trend on fleet age and extrapolate into the future.
  • Generate the approximated exogenous DataFrame and compare it visually to the real values.
Build the climatological profile for weather variables

Seasonal climatology is the standard approach in meteorology for approximating future weather. You compute the historical mean and standard deviation for each week of the year, then use that weekly profile as your best deterministic guess for the future.

Why climatology for weather?

Weather forecasts beyond 2 weeks lose skill rapidly. Over a 3-year horizon, predicting specific temperatures is impossible. The climatological mean captures the seasonal pattern (hot summers, cold winters) without pretending to know the exact value.

The standard deviation you also compute here will be useful later if you want to run Monte Carlo simulations.

  • In your notebook, create a new cell below the Step 3 visualization and compute the climatological statistics by pasting this code:
# Step 4: Strategies d'approximation des variables futures

# Climatologie pour temp_max_mean et alertes_meteo
df_train_clim = df_train[exog_to_approx].copy()
df_train_clim['week_of_year'] = df_train.index.isocalendar().week.astype(int)

clim_stats = df_train_clim.groupby('week_of_year')[['temp_max_mean', 'alertes_meteo']].agg(['mean', 'std'])
clim_stats.columns = ['_'.join(col) for col in clim_stats.columns]

What does this code do?

  • df_train_clim copies the exogenous-to-approximate columns and adds a week_of_year column to group by.
  • The groupby computes the mean and standard deviation of each weather variable for each week of the year (52 weeks).
  • clim_stats is your seasonal profile. Each row is a week number (1-52), each column gives the average and variability of that weather variable for that week.
  • Run the cell.
  • In the next cell, type clim_stats.head(10) and run it. You should see a table with columns like temp_max_mean_mean, temp_max_mean_std, alertes_meteo_mean, and alertes_meteo_std indexed by week number.

Seeing an empty DataFrame or NaN values?

Check that exog_to_approx contains the correct column names matching your dataset. If the train set has fewer than 52 unique weeks, some week numbers may be missing. That is normal and handled in the next chunk.

Help me debug my clim_stats DataFrame. I'm seeing NaN or empty results when computing climatological statistics.

Fit linear extrapolation and generate the approximated DataFrame

Fleet age (Age du K moy) increases steadily over time as vehicles get older. A linear trend captures this progression and lets you project it forward.

With both approximation strategies ready, you can now assemble a complete DataFrame of approximated exogenous variables covering the test set period.

  • Create a new cell and paste this code to fit the linear trend and build the approximated exogenous DataFrame:
# Extrapolation lineaire pour Age du K moy
train_time_idx = np.arange(len(df_train))
age_slope, age_intercept = np.polyfit(train_time_idx, df_train['Age du K moy'], 1)

# Generer le DataFrame d'exogenes approximees sur le test set
test_week_of_year = df_test.index.isocalendar().week.astype(int).values
test_time_idx = np.arange(len(df_train), len(df_train) + len(df_test))

df_exog_approx = pd.DataFrame(index=df_test.index)
df_exog_approx['temp_max_mean'] = [clim_stats.loc[w, 'temp_max_mean_mean'] if w in clim_stats.index else clim_stats['temp_max_mean_mean'].mean() for w in test_week_of_year]
df_exog_approx['alertes_meteo'] = [clim_stats.loc[w, 'alertes_meteo_mean'] if w in clim_stats.index else clim_stats['alertes_meteo_mean'].mean() for w in test_week_of_year]
df_exog_approx['Age du K moy'] = age_slope * test_time_idx + age_intercept
df_exog_approx['vacances'] = df_test['vacances']  # variable connue

What does this code do?

  • np.polyfit fits a degree-1 polynomial (straight line) to the training age data. age_slope is how much the fleet ages per week. age_intercept is the starting value.
  • test_time_idx continues the time index beyond the training set so the linear projection extends into the future.
  • For weather columns, each test week is filled with the climatological mean for that week number. The fallback (else clim_stats[...].mean()) handles any week number missing from the training data.
  • vacances is copied directly from the test set because school holidays are known in advance from the official calendar.
  • Run the cell.
  • In a new cell, type df_exog_approx.head() and run it. You should see a DataFrame with 4 columns (temp_max_mean, alertes_meteo, Age du K moy, vacances) indexed by the test set dates, with smooth approximated values instead of noisy real observations.

Getting a KeyError on clim_stats?

If you see a KeyError mentioning a week number, it means that particular week exists in the test set but not in the training data. The if w in clim_stats.index guard handles this. Make sure you copied the list comprehension exactly, including the fallback clause.

Help me debug my approximated exogenous DataFrame. I'm getting a KeyError when looking up week numbers in clim_stats.

Compare approximations with real values visually

The key question is: do your approximations capture the general patterns of the real data? The climatological profile should follow the seasonal shape. The linear extrapolation should track the overall trend.

  • Before you run the next cell, predict: will the approximated weather curve be smoother or noisier than the real values?
  • Create a new cell and paste this visualization code to overlay the approximations against reality:
# Visualisation approximations vs vraies valeurs
fig, axes = plt.subplots(3, 1, figsize=(14, 10))
for i, var in enumerate(exog_to_approx):
    axes[i].plot(df_test.index, df_test[var], label='Vraies valeurs', color='black')
    axes[i].plot(df_test.index, df_exog_approx[var], label='Approximation', linestyle='--', color='orange')
    axes[i].set_title(f'Approximation vs realite : {var}')
    axes[i].legend()
plt.tight_layout()
plt.show()

What does this code do?

  • The loop creates 3 subplots, one per variable in exog_to_approx.
  • Black lines show the real test values. Orange dashed lines show your approximation.
  • For weather variables, you expect the orange curve to be a smooth seasonal average while the black line jumps around. For Age du K moy, both lines should trend upward, with the orange line being perfectly straight.
  • Run the cell.

You should see 3 plots. The weather approximations (top two plots) follow the seasonal shape but are much smoother than the noisy real values. The Age du K moy approximation (bottom plot) shows a straight line that tracks the general upward trend of the real data.

What do these plots tell you?

The climatology captures the seasonal pattern (when it's hot vs cold) but misses the week-to-week volatility. The linear extrapolation captures the trend direction but assumes constant growth. These gaps are the "approximation cost" you'll measure in the next step.

Plots look flat or completely off?

If the orange approximation line is flat, check that test_week_of_year contains varying week numbers (not all the same value). Run test_week_of_year[:10] to verify.

If the Age extrapolation line shoots far above or below the real data, inspect age_slope and age_intercept. The slope should be a small positive number reflecting gradual aging.

My approximation plots look wrong. Help me debug the climatological profile or linear extrapolation.

✔️ Awesome, I've got everything!

Great. Make sure you have saved your notebook before moving on.

ⓧ I'd like to double check the full code

Scroll up through the cells you added in this step and confirm these 3 blocks are present in order:

  • Climatology cell: creates df_train_clim and clim_stats with the Step 4 header comment.
  • Extrapolation + DataFrame cell: fits age_slope / age_intercept and builds df_exog_approx with all 4 columns.
  • Visualization cell: creates a 3-subplot figure comparing approximated vs real values for each variable.

Your approximation strategies are built and visually validated. Next up, you'll feed these approximated exogenous variables into your SARIMAX and XGBoost models to measure exactly how much performance you lose compared to using the real values.

Forecast With Approximated Variables and Measure Degradation

You've established the performance ceiling with real exogenous variables and built approximation strategies for the unknowns. Now comes the critical question: how much performance do you actually lose when you feed approximated values instead of real ones?

This is the moment of truth. You'll replace real exogenous variables with your climatological profiles and linear extrapolations, then measure whether the gain survives or collapses.

In this step, get ready to:
  • Generate SARIMAX and XGBoost forecasts using approximated exogenous variables.
  • Compare all 6 model configurations in a final table with MAE and RMSE.
  • Calculate the percentage of gain preserved and formulate a production recommendation.
Generate forecasts using approximated exogenous variables

In the previous steps, you fed the models real test-set values for temperature, weather alerts, and equipment age. In production at a 3-year horizon, those values don't exist yet. You only have the approximations you just built.

  • In your notebook, create a new cell and generate forecasts from both trained models using df_exog_approx instead of the real test exogenous values:
# Step 5: Previsions avec exogenes approximees et mesure de la degradation

# SARIMAX avec exogenes approximees
sarimax_approx_pred = sarimax_model.forecast(steps=len(df_test), exog=df_exog_approx)

# XGBoost avec exogenes approximees
X_test_exog_approx = pd.concat([X_test_cal, df_exog_approx], axis=1)
xgb_exog_approx_pred = xgb_exog.predict(X_test_exog_approx)

What does this code do?

For SARIMAX, you call .forecast() with the approximated DataFrame instead of the real test values. The model doesn't know the difference. It treats these approximated values as if they were ground truth.

For XGBoost, you concatenate the calendar features with the approximated exogenous variables, just like you did with the real values. The model uses these columns as input features for prediction.

  • Run the cell.

The cell executes without error. Both sarimax_approx_pred and xgb_exog_approx_pred now hold forecast arrays generated from your approximation strategies.

Seeing a shape mismatch error?

If SARIMAX raises a dimension error, check that df_exog_approx has the same columns in the same order as the exogenous data you used during training (exog_all). The model expects the exact same feature structure at forecast time.

help me debug a shape mismatch in SARIMAX forecast with approximated exogenous variables

Calculate degraded metrics and build the comparison table

Now you need to quantify the damage. You'll compute MAE and RMSE for these "degraded" forecasts and assemble all 6 configurations into a single comparison table.

  • In a new cell, calculate the error metrics and display the full comparison table by running this code:
mae_sarimax_approx = mean_absolute_error(df_test[target], sarimax_approx_pred)
rmse_sarimax_approx = np.sqrt(mean_squared_error(df_test[target], sarimax_approx_pred))
mae_xgb_exog_approx = mean_absolute_error(df_test[target], xgb_exog_approx_pred)
rmse_xgb_exog_approx = np.sqrt(mean_squared_error(df_test[target], xgb_exog_approx_pred))

# Tableau final des 6 configurations
final_results = pd.DataFrame({
    'Modele': ['SARIMA', 'SARIMAX (reel)', 'SARIMAX (approx)',
               'XGBoost baseline', 'XGBoost + exog (reel)', 'XGBoost + exog (approx)'],
    'MAE': [mae_sarima, mae_sarimax, mae_sarimax_approx,
            mae_xgb_baseline, mae_xgb_exog, mae_xgb_exog_approx],
    'RMSE': [rmse_sarima, rmse_sarimax, rmse_sarimax_approx,
             rmse_xgb_baseline, rmse_xgb_exog, rmse_xgb_exog_approx]
})
print(final_results)

What does this table tell you?

The table shows three tiers for each model family. First, the baseline without exogenous variables. Second, the best-case performance with real exogenous values. Third, the realistic production scenario with approximated exogenous values.

If the "approx" row is closer to the "reel" row than to the baseline, the approximation strategy preserves most of the gain. If it's closer to the baseline, the approximations are too noisy to help.

  • Run the cell.

You should see a DataFrame printed with all 6 rows, each showing its MAE and RMSE values. Compare the "approx" rows to both the baseline and the "reel" rows to spot where the approximation lands.

Seeing a NameError for one of the metrics?

Variables like mae_sarima or mae_xgb_baseline were computed in earlier steps. Make sure you ran all previous cells in order before executing this one. In Jupyter, use Kernel > Restart & Run All if you're unsure.

help me fix a NameError when building the final comparison table

Visualize the degradation and formulate your recommendation

A table of numbers is useful, but a chart makes the story obvious at a glance. You'll calculate what percentage of the original gain survives the approximation, then plot all configurations side by side.

Before you run this next cell, make a prediction: do you think the approximated exogenous variables will retain more than 50% of the gain, or will degradation push performance back toward the baseline?

  • In a new cell, compute the degradation percentages, plot the bar chart, and print the production recommendation by running this code:
# Degradation: % du gain conserve
gain_sarimax = mae_sarima - mae_sarimax
gain_sarimax_approx = mae_sarima - mae_sarimax_approx
pct_gain_sarimax = (gain_sarimax_approx / gain_sarimax * 100) if gain_sarimax != 0 else 0

gain_xgb = mae_xgb_baseline - mae_xgb_exog
gain_xgb_approx = mae_xgb_baseline - mae_xgb_exog_approx
pct_gain_xgb = (gain_xgb_approx / gain_xgb * 100) if gain_xgb != 0 else 0

print(f'SARIMAX: {pct_gain_sarimax:.1f}% du gain initial conserve avec approximation')
print(f'XGBoost: {pct_gain_xgb:.1f}% du gain initial conserve avec approximation')

# Graphique de degradation
plt.figure(figsize=(10, 5))
modeles = ['SARIMA\n(baseline)', 'SARIMAX\n(reel)', 'SARIMAX\n(approx)',
           'XGBoost\n(baseline)', 'XGBoost\n+exog reel', 'XGBoost\n+exog approx']
maes = [mae_sarima, mae_sarimax, mae_sarimax_approx,
        mae_xgb_baseline, mae_xgb_exog, mae_xgb_exog_approx]
colors = ['gray', 'green', 'orange', 'gray', 'green', 'orange']
plt.bar(modeles, maes, color=colors)
plt.ylabel('MAE')
plt.title('Comparaison MAE : baseline vs exog reelles vs exog approximees')
plt.show()

# Recommandation
print('\n=== RECOMMANDATION ===')
print(f'Si le gain conserve > 50%, utiliser les exogenes approximees en production.')
print(f'Sinon, privilegier le modele sans exogenes pour sa robustesse.')

How is the degradation percentage calculated?

The logic is straightforward. gain_sarimax measures how much MAE improved when you used real exogenous variables vs the pure SARIMA baseline. gain_sarimax_approx measures how much improvement remains when you substitute approximations.

Dividing the two gives the retention ratio. 100% means the approximation preserved all the benefit. 0% means the approximation erased it entirely, making the model no better than the baseline.

  • Run the cell.

You should see two printed percentage lines showing how much of the original gain each model retains. A bar chart appears below, with gray bars for baselines, green for real exogenous, and orange for approximated exogenous. The recommendation logic prints at the bottom.

How to read the bar chart?

Lower bars mean better performance (lower MAE). The gap between gray and green shows the value of real exogenous variables. The gap between green and orange shows the cost of approximation. If the orange bar stays much closer to green than to gray, your approximation strategy works well enough for production use.

Chart not displaying?

If you see a blank output area, make sure you have %matplotlib inline at the top of your notebook, or that your Jupyter environment is set to render plots inline by default.

help me fix my matplotlib chart not showing in Jupyter Notebook

✔️ Awesome, I've got everything!

Great. Make sure you've saved your notebook with all cells executed in order.

ⓧ I'd like to double check the full code

Your notebook has grown across 5 steps. Scroll through and confirm these sections appear in order:

  • Step 1: Imports, data loading, train/test split, variable classification.
  • Step 2: SARIMA order selection, SARIMA forecast, XGBoost baseline with calendar features, MAE/RMSE for both baselines.
  • Step 3: SARIMAX with real exogenous, XGBoost with real exogenous, MAE/RMSE for both.
  • Step 4: Climatology stats, linear trend for age, df_exog_approx generation, visualization.
  • Step 5: Forecasts with approximated exogenous, metrics, comparison table, degradation chart, and recommendation.

Secret mission

Simulation Monte Carlo pour les variables meteo

La climatologie saisonniere te donne un seul scenario moyen pour tes variables meteo futures. Mais la meteo est aleatoire. Dans cette mission, tu vas generer 500 trajectoires meteo possibles par simulation Monte Carlo et obtenir un intervalle de confiance realiste sur tes previsions.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • Jupyter Notebook file (notebook.ipynb).
  • Dataset local (maintenance_dataset.csv).

Keep everything running

No action needed. Choose this if you plan to continue experimenting with different approximation strategies or extending the analysis.

  • Your notebook.ipynb file stays on your machine with all outputs saved.
  • Your Python packages (statsmodels, xgboost, pandas, numpy, matplotlib, scikit-learn) remain installed and available for other projects.
  • The dataset maintenance_dataset.csv stays in place for future analysis.

Pause - I'll come back to this later

Shut down any running Jupyter processes to free up memory. Your files remain on disk for when you return.

  • In your terminal where Jupyter is running, press Ctrl+C to stop the Jupyter server.
  • Confirm the shutdown when prompted.
  • Your notebook and dataset remain saved on your machine. Restart Jupyter later to pick up where you left off.

Delete - I don't want to use this again

Remove all project files from your machine. Your Python packages will remain installed since other projects may use them.

  • Stop the Jupyter server by pressing Ctrl+C in the terminal where it is running.
  • Delete the notebook and dataset. On macOS/Linux, run:
rm notebook.ipynb maintenance_dataset.csv

On Windows, run:

Remove-Item notebook.ipynb, maintenance_dataset.csv
  • Confirm the files are gone by running ls (macOS/Linux) or dir (Windows) in the same directory. Neither file should appear.

Nice Work!

Nice Work!

Bravo. Tu viens de construire un notebook d'analyse comparative complet qui repond a une question cruciale en maintenance predictive : est-ce que le gain des variables exogenes survit quand on doit les approximer a horizon long terme ?

Tu as maintenant les outils et la methodologie pour prendre des decisions argumentees sur la strategie de prevision en production.

Voici ce que tu as appris :

  • Entrainer et comparer 4 modeles de prevision (SARIMA, SARIMAX, XGBoost sans et avec exogenes) pour quantifier le gain reel apporte par les variables externes.
  • Construire des strategies d'approximation (climatologie saisonniere, extrapolation lineaire) pour projeter des variables inconnues a horizon long terme et les injecter dans les modeles.
  • Mesurer la degradation de performance causee par les approximations et formuler une recommandation argumentee pour la strategie de prevision en production.
  • Secret Mission : implementer une simulation Monte Carlo generant N=500 trajectoires meteo et produisant des intervalles de confiance realistes sur les previsions a horizon long terme.

Ready to quiz yourself?