#r "nuget: Microsoft.ML, 5.0.0"
#r "nuget: Microsoft.ML.TimeSeries, 5.0.0"- Microsoft.ML, 5.0.0
- Microsoft.ML.TimeSeries, 5.0.0
Navigation : Index | << ML-4-Evaluation | Suivant >> ML-6-ONNX
A la fin de ce notebook, vous saurez : 1. Comprendre les principes du forecasting de séries temporelles 2. Utiliser ForecastBySsa (Singular Spectrum Analysis) pour la prévision 3. Détecter la saisonnalité dans les données temporelles 4. Interpréter les intervalles de confiance des prévisions 5. Comparer différentes configurations de modèles de forecasting
Le forecasting (prévision) de séries temporelles consiste à prédire les valeurs futures d’une variable basée sur ses valeurs passées.
| Concept | Description |
|---|---|
| Trend | Tendance à long terme (croissance, décroissance) |
| Saisonnalité | Pattern périodique (hebdomadaire, mensuel, annuel) |
| Bruit | Variations aléatoires non prévisibles |
| Horizon | Nombre de pas de temps à prédire |
| Intervalle de confiance | Plage de valeurs probables (95%) |
ForecastBySsa utilise l’algorithme SSA (Broomhead & King, 1986 ; Golyandina & Zhigljavsky, 2013) qui décompose la série temporelle en composantes :
Série Temporelle = Trend + Saisonnalité + Bruit
Avantages de SSA : - Fonctionne avec des données non stationnaires - Capture automatiquement la saisonnalité - Fournit des intervalles de confiance - Peu de paramètres à régler
Packages et using charges avec succes !
Les packages nécessaires sont : - Microsoft.ML : framework principal de machine learning - Microsoft.ML.TimeSeries : algorithmes de séries temporelles (ForecastBySsa) - SvgChartHelper.Bar (deja charge via #load).Interactive : visualisations interactives
Nous utilisons un dataset de ventes quotidiennes sur 2 ans (2023-2024) avec une tendance et une saisonnalité hebdomadaire.
daily-sales.csv)| Colonne | Type | Description |
|---|---|---|
| Date | DateTime | Date de la vente |
| Year | int | Année (2023 ou 2024) |
| Month | int | Mois (1-12) |
| Day | int | Jour du mois (1-31) |
| DayOfWeek | int | Jour de la semaine (0=Lundi, 6=Dimanche) |
| Sales | int | Nombre de ventes |
Note technique : les cellules de code de cette section (Partie 1 à Partie 7 et les exercices) s’exécutent dans le kernel .NET Interactive avec les assemblies ML.NET entièrement résolues. Les sorties committées (par exemple
Modèle entraîné avec succèset les métriques MAE/RMSE en Parties 3-4) attestent d’une exécution complète du pipeline de prévisionForecastBySsa.
// Définition du schéma de données
public class DailySalesData
{
[LoadColumn(0)]
public DateTime Date { get; set; }
[LoadColumn(1)]
public float Year { get; set; }
[LoadColumn(2)]
public float Month { get; set; }
[LoadColumn(3)]
public float Day { get; set; }
[LoadColumn(4)]
public float DayOfWeek { get; set; }
[LoadColumn(5)]
public float Sales { get; set; }
}
public class SalesForecast
{
public float[] ForecastedSales { get; set; }
public float[] LowerBoundSales { get; set; }
public float[] UpperBoundSales { get; set; }
}
// Initialiser MLContext
var mlContext = new MLContext(seed: 42);
// Charger les données
var dataPath = Path.Combine(Environment.CurrentDirectory, "daily-sales.csv");
Console.WriteLine($"Chargement des données depuis {Path.GetFileName(dataPath)}");
var fullData = mlContext.Data.LoadFromTextFile<DailySalesData>(
path: dataPath,
hasHeader: true,
separatorChar: ',');
// Afficher les statistiques
var dataStats = mlContext.Data.CreateEnumerable<DailySalesData>(fullData, reuseRowObject: false);
var totalDays = dataStats.Count();
var totalSales = dataStats.Sum(x => x.Sales);
var avgSales = dataStats.Average(x => x.Sales);
var minDate = dataStats.Min(x => x.Date);
var maxDate = dataStats.Max(x => x.Date);
Console.WriteLine($"\n=== Statistiques du dataset ===");
Console.WriteLine($"Période : {minDate:yyyy-MM-dd} à {maxDate:yyyy-MM-dd}");
Console.WriteLine($"Nombre total de jours : {totalDays}");
Console.WriteLine($"Ventes totales : {totalSales:N0}");
Console.WriteLine($"Ventes moyennes par jour : {avgSales:F1}");
// Afficher les premières lignes
Console.WriteLine("\n=== 10 premières lignes ===");
var firstRows = mlContext.Data.CreateEnumerable<DailySalesData>(fullData, reuseRowObject: false)
.Take(10)
.Select(x => $"{x.Date:yyyy-MM-dd} | Year: {x.Year} | DayOfWeek: {x.DayOfWeek} | Sales: {x.Sales}");
foreach (var row in firstRows)
{
Console.WriteLine(row);
}Chargement des données depuis daily-sales.csv
=== Statistiques du dataset ===
Période : 2023-01-01 à 2024-12-31
Nombre total de jours : 731
Ventes totales : 109 226
Ventes moyennes par jour : 149,4
=== 10 premières lignes ===
2023-01-01 | Year: 2023 | DayOfWeek: 6 | Sales: 104
2023-01-02 | Year: 2023 | DayOfWeek: 0 | Sales: 122
2023-01-03 | Year: 2023 | DayOfWeek: 1 | Sales: 135
2023-01-04 | Year: 2023 | DayOfWeek: 2 | Sales: 128
2023-01-05 | Year: 2023 | DayOfWeek: 3 | Sales: 85
2023-01-06 | Year: 2023 | DayOfWeek: 4 | Sales: 69
2023-01-07 | Year: 2023 | DayOfWeek: 5 | Sales: 93
2023-01-08 | Year: 2023 | DayOfWeek: 6 | Sales: 108
2023-01-09 | Year: 2023 | DayOfWeek: 0 | Sales: 119
2023-01-10 | Year: 2023 | DayOfWeek: 1 | Sales: 135
| Métrique | Ce que cela signifie |
|---|---|
| Période 2023-2024 | 2 années de données pour capturer la saisonnalité |
| ~730 jours | Données quotidiennes pour une analyse fine |
| Ventes moyennes | Niveau de base autour duquel les ventes oscillent |
Analyse visuelle : Nous allons visualiser les données pour identifier la tendance et la saisonnalité.
Visualisons les ventes sur la période complète pour identifier : - La tendance à long terme - La saisonnalité hebdomadaire - Les anomalies éventuelles
#load "../../Probas/Infer/SvgChartHelper.cs"
// Visualisation SVG statique (remplace Plotly-CDN qui rend BLANC en consultation statique --
// GitHub sandbox les <script>; EPIC #3801 Prong A + EPIC #6927). Technique #6942 : <svg> inline
// via SvgChartHelper (zero-dependance, rend sur GitHub/nbviewer/offline, aucun CDN ni <script>).
var allData = mlContext.Data.CreateEnumerable<DailySalesData>(fullData, reuseRowObject: false)
.OrderBy(x => x.Date)
.ToArray();
var dates = allData.Select(x => x.Date.ToString("yyyy-MM-dd")).ToArray();
var sales = allData.Select(x => (double)x.Sales).ToArray();
display(SvgChartHelper.Line("Ventes quotidiennes (2023-2024)", dates, sales, width: 1200, height: 400));Observations attendues :
| Pattern | Description |
|---|---|
| Tendance croissante | Les ventes augmentent progressivement de 2023 à 2024 |
| Oscillations | Pattern hebdomadaire visible (pics et creux) |
| Variabilité | Fluctuations aléatoires autour de la tendance |
Question : Identifiez visuellement la période des pics de ventes. Est-ce hebdomadaire ?
Calculez une moyenne mobile sur 7 jours des ventes et identifiez les jours ou les ventes reelles s’ecartent de plus de 2 ecarts-types de la moyenne mobile. Affichez les dates et les valeurs de ces anomalies.
Indice : Pour chaque jour, calculez la moyenne des 7 jours précédents (fenêtre glissante). L’ecart-type de la fenêtre mesure la variabilite locale. Un point est une anomalie si |valeur - moyenne_mobile| > 2 * ecart_type. Utilisez une boucle ou LINQ avec Skip/Take pour la fenêtre glissante.
// Exercice 1 : Detection d'anomalies par ecart a la moyenne mobile
// TODO etudiant : Identifiez les jours anormaux dans la serie temporelle
// Indice : moyenne mobile sur 7 jours, seuil = 2 ecarts-types
// Etape 1 : charger les donnees dans un tableau trie par date
// Etape 2 : pour chaque jour (a partir du 8e), calculer la moyenne et l'ecart-type des 7 jours precedents
// Etape 3 : verifier si la valeur du jour s'ecarte de plus de 2 sigma de la moyenne mobile
// Etape 4 : afficher les dates anormales avec leur valeur, la moyenne mobile et l'ecart
// var anomalies = ... // TODO etudiant : remplacer par la liste des anomalies detectees
Console.WriteLine("Exercice a completer : detection d'anomalies par moyenne mobile");Exercice a completer : detection d'anomalies par moyenne mobile
Nous allons utiliser ForecastBySsa pour prédire les ventes futures.
| Paramètre | Valeur | Description |
|---|---|---|
windowSize |
7 | Taille de la fenêtre pour l’analyse SSA (7 = saisonnalité hebdomadaire) |
seriesLength |
30 | Longueur de la série utilisée pour chaque prévision |
trainSize |
365 | Nombre de points d’entraînement (1 année) |
horizon |
7 | Nombre de jours à prédire (1 semaine) |
confidenceLevel |
0.95 | Niveau de confiance pour les intervalles (95%) |
using Microsoft.ML.Transforms.TimeSeries;
// Séparer les données : entraînement sur 2023, test sur 2024
var trainData = mlContext.Data.FilterRowsByColumn(fullData, "Year", upperBound: 2024);
var testData = mlContext.Data.FilterRowsByColumn(fullData, "Year", lowerBound: 2024);
Console.WriteLine("=== Division des données ===");
Console.WriteLine($"Entraînement (2023) : {mlContext.Data.CreateEnumerable<DailySalesData>(trainData, reuseRowObject: false).Count()} jours");
Console.WriteLine($"Test (2024) : {mlContext.Data.CreateEnumerable<DailySalesData>(testData, reuseRowObject: false).Count()} jours");
// Définir le pipeline de forecasting
var forecastingPipeline = mlContext.Forecasting.ForecastBySsa(
outputColumnName: "ForecastedSales",
inputColumnName: "Sales",
windowSize: 7, // Fenêtre de 7 jours (saisonnalité hebdomadaire)
seriesLength: 30, // Utiliser 30 jours pour la prévision
trainSize: 365, // Entraîner sur 1 année
horizon: 7, // Prédire 7 jours à l'avance
confidenceLevel: 0.95f, // Intervalle de confiance à 95%
confidenceLowerBoundColumn: "LowerBoundSales",
confidenceUpperBoundColumn: "UpperBoundSales");
Console.WriteLine("\n=== Entraînement du modèle ForecastBySsa ===");
var forecaster = forecastingPipeline.Fit(trainData);
Console.WriteLine("Modèle entraîné avec succès");=== Division des données ===
Entraînement (2023) : 365 jours
Test (2024) : 366 jours
=== Entraînement du modèle ForecastBySsa ===
Modèle entraîné avec succès
| Paramètre | Impact |
|---|---|
windowSize: 7 |
Capture la saisonnalité hebdomadaire |
seriesLength: 30 |
Utilise le dernier mois pour prédire la semaine suivante |
trainSize: 365 |
Apprentissage sur 1 année complète |
horizon: 7 |
Prévisions à 7 jours (court terme) |
Note : Ces paramètres peuvent être ajustés selon vos données et vos besoins de prévision.
Évaluons la qualité des prévisions en comparant les valeurs prédites aux valeurs réelles sur l’année 2024.
ForecastBySsa (horizon=7) produit, pour chaque date d’origine, un vecteur de prévision de longueur 7 — la première valeur étant la prévision du lendemain, la seconde celle du surlendemain, etc. Aplatir ce vecteur (SelectMany) puis le zipper aux valeurs réelles fausse l’alignement : on apparierait la prévision du jour J+k avec la vente du jour J.
Le protocole retenu est l’évaluation par origine et horizon explicites : chaque prédiction est indexée par (date d'origine, horizon h, date cible) où date cible = origine + h jours. La table (origine, horizon, date cible, réel, prédit, naïf) est reconstruite ci-dessous et alimente toutes les métriques, la figure et les intervalles de confiance.
// Protocole d'évaluation explicite : chaque prédiction est indexée par (origine, horizon, date cible).
// La classe statique ForecastEval reconstruit la table alignée et fournit les métriques.
public static class ForecastEval
{
// Table alignée : pour chaque origine et chaque horizon h (1..7), prédit + réel + naïf de la cible.
// Le prédit naïf (persistance) pointe la valeur d'origine : Y(t+h) = Y(t).
public static (DateTime Origin, int H, DateTime Target, double Actual, double Pred, double Naive, double Lower, double Upper)[] BuildAlignedRows(
SalesForecast[] predRows, DailySalesData[] testRows, System.Collections.Generic.Dictionary<DateTime, double> salesByDate, DateTime maxTest)
{
var rows = new System.Collections.Generic.List<(DateTime, int, DateTime, double, double, double, double, double)>();
for (int i = 0; i < testRows.Length; i++)
{
var origin = testRows[i].Date;
for (int h = 1; h <= 7; h++)
{
var target = origin.AddDays(h);
if (target > maxTest) break;
if (!salesByDate.TryGetValue(target, out var actual)) continue;
var row = predRows[i];
rows.Add((origin, h, target, actual, (double)row.ForecastedSales[h - 1], (double)testRows[i].Sales,
(double)row.LowerBoundSales[h - 1], (double)row.UpperBoundSales[h - 1]));
}
}
return rows.ToArray();
}
public static (double mae, double rmse) Metrics(System.Collections.Generic.IEnumerable<double> actuals, System.Collections.Generic.IEnumerable<double> preds)
{
var err = actuals.Zip(preds, (a, p) => a - p).ToArray();
return (err.Average(e => Math.Abs(e)), Math.Sqrt(err.Average(e => e * e)));
}
}// Faire des prédictions sur les données de test
var predictions = forecaster.Transform(testData);
// Reconstruire la table alignée (origine, horizon, date cible, réel, prédit, naïf)
var testRows = mlContext.Data.CreateEnumerable<DailySalesData>(testData, reuseRowObject: false)
.OrderBy(x => x.Date).ToArray();
var predRows = mlContext.Data.CreateEnumerable<SalesForecast>(predictions, reuseRowObject: false).ToArray();
var salesByDate = testRows.ToDictionary(x => x.Date, x => (double)x.Sales);
var maxTest = testRows.Max(x => x.Date);
var aligned = ForecastEval.BuildAlignedRows(predRows, testRows, salesByDate, maxTest);
Console.WriteLine($"=== Table alignée ({aligned.Length} paires origine x horizon) ===");
Console.WriteLine("Date origine | Hor | Date cible | Réel | Prédit | Naïf");
for (int k = 0; k < 8; k++)
{
var r = aligned[k];
Console.WriteLine($"{r.Origin:yyyy-MM-dd} | h{r.H} | {r.Target:yyyy-MM-dd} | {r.Actual,4:F0} | {r.Pred,6:F1} | {r.Naive,4:F0}");
}
// Horizon 1 : évaluation un pas en avant sur toute l'année hors échantillon
var h1 = aligned.Where(r => r.H == 1).ToArray();
var ssaH1 = ForecastEval.Metrics(h1.Select(r => r.Actual), h1.Select(r => r.Pred));
var naiveH1 = ForecastEval.Metrics(h1.Select(r => r.Actual), h1.Select(r => r.Naive));
Console.WriteLine($"\n=== Horizon 1 (pas en avant, {h1.Length} paires) ===");
Console.WriteLine($"SSA MAE {ssaH1.mae:F2} | RMSE {ssaH1.rmse:F2}");
Console.WriteLine($"Naïve MAE {naiveH1.mae:F2} | RMSE {naiveH1.rmse:F2}");
Console.WriteLine($"Gain MAE {(naiveH1.mae - ssaH1.mae) / naiveH1.mae * 100:F1}% | RMSE {(naiveH1.rmse - ssaH1.rmse) / naiveH1.rmse * 100:F1}%");
Console.WriteLine($"\nLe SSA bat-il la persistance ? {(ssaH1.mae < naiveH1.mae ? "OUI" : "NON")}");
// Métriques par horizon (évaluation multi-horizon par origine)
Console.WriteLine("\n=== Métriques par horizon : SSA vs Naïve ===");
Console.WriteLine("Horizon | SSA MAE | SSA RMSE | Naïve MAE | Naïve RMSE | Gain MAE | Gain RMSE | n");
for (int h = 1; h <= 7; h++)
{
var hr = aligned.Where(r => r.H == h).ToArray();
if (hr.Length == 0) continue;
var sm = ForecastEval.Metrics(hr.Select(r => r.Actual), hr.Select(r => r.Pred));
var nm = ForecastEval.Metrics(hr.Select(r => r.Actual), hr.Select(r => r.Naive));
Console.WriteLine($"{h,7} | {sm.mae,7:F2} | {sm.rmse,7:F2} | {nm.mae,8:F2} | {nm.rmse,8:F2} | {(nm.mae - sm.mae) / nm.mae * 100,8:F1}% | {(nm.rmse - sm.rmse) / nm.rmse * 100,8:F1}% | {hr.Length}");
}
// Agrégat sur tous les horizons
var allMae = ForecastEval.Metrics(aligned.Select(r => r.Actual), aligned.Select(r => r.Pred));
var allNaive = ForecastEval.Metrics(aligned.Select(r => r.Actual), aligned.Select(r => r.Naive));
Console.WriteLine($"\nAgrégat tous horizons ({aligned.Length}) : SSA MAE {allMae.mae:F2} RMSE {allMae.rmse:F2} | Naïve MAE {allNaive.mae:F2} RMSE {allNaive.rmse:F2} | gain {(allNaive.mae - allMae.mae) / allNaive.mae * 100:F1}%");=== Table alignée (2534 paires origine x horizon) ===
Date origine | Hor | Date cible | Réel | Prédit | Naïf
2024-01-01 | h1 | 2024-01-02 | 181 | 186,7 | 169
2024-01-01 | h2 | 2024-01-03 | 163 | 166,1 | 169
2024-01-01 | h3 | 2024-01-04 | 138 | 141,6 | 169
2024-01-01 | h4 | 2024-01-05 | 113 | 134,6 | 169
2024-01-01 | h5 | 2024-01-06 | 127 | 125,3 | 169
2024-01-01 | h6 | 2024-01-07 | 155 | 152,5 | 169
2024-01-01 | h7 | 2024-01-08 | 188 | 177,5 | 169
2024-01-02 | h1 | 2024-01-03 | 163 | 165,3 | 181
=== Horizon 1 (pas en avant, 365 paires) ===
SSA MAE 10,51 | RMSE 13,41
Naïve MAE 19,19 | RMSE 23,11
Gain MAE 45,2% | RMSE 42,0%
Le SSA bat-il la persistance ? OUI
=== Métriques par horizon : SSA vs Naïve ===
Horizon | SSA MAE | SSA RMSE | Naïve MAE | Naïve RMSE | Gain MAE | Gain RMSE | n
1 | 10,51 | 13,41 | 19,19 | 23,11 | 45,2% | 42,0% | 365
2 | 10,45 | 13,36 | 30,71 | 35,65 | 66,0% | 62,5% | 364
3 | 10,32 | 13,22 | 37,96 | 43,20 | 72,8% | 69,4% | 363
4 | 10,30 | 13,16 | 37,99 | 43,40 | 72,9% | 69,7% | 362
5 | 10,45 | 13,36 | 30,57 | 35,72 | 65,8% | 62,6% | 361
6 | 10,47 | 13,26 | 19,41 | 23,38 | 46,1% | 43,3% | 360
7 | 10,62 | 13,28 | 12,03 | 14,87 | 11,7% | 10,7% | 359
Agrégat tous horizons (2534) : SSA MAE 10,44 RMSE 13,29 | Naïve MAE 26,86 RMSE 32,97 | gain 61,1%
| Métrique | Bonne performance | Mauvaise performance |
|---|---|---|
| MAE faible | < 10% des ventes moyennes | > 30% des ventes moyennes |
| RMSE proche de MAE | Erreurs uniformes | Grosses erreurs ponctuelles |
Exemple : Si les ventes moyennes sont de 150, un MAE de 15 (10%) est considéré comme bon.
Un MAE ne dit pas si le modèle SSA est utile : il dit seulement de combien les prévisions s’écartent en moyenne de la vérité. La question pédagogique déterminante est : le SSA bat-il une prédiction triviale ?
Baseline naïve (persistence, Hyndman & Koehler 2006) : pour une série journalière, le forecast le plus simple est Y(t+1) = Y(t) (la valeur de demain = la valeur d’aujourd’hui). Cette baseline est surprenamment efficace sur des séries à forte autocorrélation court-terme — c’est précisément le cas des ventes quotidiennes.
Test discriminant : si SSA ne fait pas significativement mieux que Y(t+1) = Y(t), alors SSA n’ajoute aucune valeur prédictive au-delà de la dernière observation.
Le protocole d’origine retenu évalue SSA et la baseline naïve sur exactement les mêmes paires (origine, horizon 1) de l’année hors échantillon. La table ci-dessous (partie 4) donne :
| Modèle | MAE | RMSE | Verdict |
|---|---|---|---|
| SSA (w=7, s=30), horizon 1 | 10.5 | 13.4 | calculé |
Baseline naïve Y(t+1)=Y(t) |
19.2 | 23.1 | calculé sur les mêmes dates |
Gain relatif : 45.2% en MAE, 42.0% en RMSE — le SSA bat nettement la persistance. La série porte donc une structure (tendance + saisonnalité hebdomadaire) que la dernière valeur seule ne porte pas : l’investissement ML.NET est ici justifié pour la prédiction ponctuelle.
Leçon : corriger le protocole d’évaluation a fait passer le MAE affiché de 33.8 (valeur buggée par l’appariement
SelectMany.Zip) à 10.5. C’est exactement pourquoi toujours comparer à une baseline triviale (Hyndman & Koehler, 2006) et aligner chaque prédiction sur sa date cible sont deux réflexes indissociables de l’évaluation forecasting.
Calculez le MAPE (Mean Absolute Percentage Error) sur les predictions de la Partie 4, puis decomposez l’erreur par jour de la semaine pour identifier les jours ou le modèle est le moins précis.
Indice : Le MAPE = moyenne(|reel - predit| / |reel|) * 100. Utilisez LINQ pour joindre les predictions avec les données originales (incluant DayOfWeek), puis groupez par DayOfWeek et calculez le MAPE par jour. Visualisez avec un graphique en barres.
// Exercice 2 : Calcul du MAPE et analyse d'erreur par jour de la semaine
// TODO etudiant : Calculez le MAPE global et par jour de la semaine
// Indice : MAPE = moyenne(|reel - predit| / |reel|) * 100, utilisez GroupBy sur DayOfWeek
// Etape 1 : extraire les valeurs reelles et predites depuis les predictions de la Partie 4
// Etape 2 : calculer le MAPE global sur toutes les predictions
// Etape 3 : joindre les predictions avec le jour de la semaine (DayOfWeek)
// Etape 4 : grouper par jour et calculer le MAPE par jour, afficher les resultats
// var mapeByDay = ... // TODO etudiant : remplacer par le MAPE par jour de la semaine
Console.WriteLine("Exercice a completer : calcul du MAPE et analyse d'erreur par jour");Exercice a completer : calcul du MAPE et analyse d'erreur par jour
Comparons les prévisions aux valeurs réelles pour les 30 premiers jours de 2024.
// Reconstruire la figure des 30 premiers jours depuis la table alignée (horizon 1)
var h1Series = aligned.Where(r => r.H == 1).OrderBy(r => r.Target).Take(30).ToArray();
var comparisonDates = h1Series.Select(r => r.Target.ToString("MM-dd")).ToArray();
var comparisonActual = h1Series.Select(r => r.Actual).ToArray();
var comparisonForecast = h1Series.Select(r => r.Pred).ToArray();
// Créer le graphique de comparaison (SVG inline via SvgChartHelper - EPIC #6927)
var xs = Enumerable.Range(1, comparisonActual.Length).Select(i => (double)i).ToArray();
display(SvgChartHelper.Overlay(
"Comparaison : Réel vs Prévisions (horizon 1, 30 premiers jours de 2024)",
"Date (Janvier 2024)",
"Nombre de ventes",
new[]
{
new SvgSeries("Ventes réelles", xs, comparisonActual, TraceStyle.LineMarkers, "#4C72B0"),
new SvgSeries("Prévisions (horizon 1)", xs, comparisonForecast, TraceStyle.Line, "#DD8452"),
},
width: 1000,
height: 400));Analyse visuelle :
| Observation | Signification |
|---|---|
| Lignes parallèles | Bonne capture de la tendance |
| Décalage constant | Le modèle suit bien les variations |
| Croisements fréquents | Le modèle n’anticipe pas bien les changements |
Question : Les prévisions sont-elles systématiquement au-dessus ou en-dessous des valeurs réelles ?
ForecastBySsa fournit des intervalles de confiance qui quantifient l’incertitude des prévisions.
// Intervalles de confiance du premier point d'origine, depuis la table alignée
var firstOrigin = aligned[0].Origin;
var originRows = aligned.Where(r => r.Origin == firstOrigin).OrderBy(r => r.H).ToArray();
Console.WriteLine($"=== Prévisions à 7 jours avec intervalles de confiance (95%) — origine {firstOrigin:yyyy-MM-dd} ===");
Console.WriteLine("\nDate cible | Réel | Prévision | Borne inf | Borne sup | Largeur | Dans IC");
Console.WriteLine(new string('-', 88));
foreach (var r in originRows)
{
var lower = Math.Max(0, r.Lower);
var upper = r.Upper;
var width = upper - lower;
var inBounds = r.Actual >= lower && r.Actual <= upper ? "OUI" : "NON";
Console.WriteLine($"{r.Target:yyyy-MM-dd} | {r.Actual,4} | {r.Pred,9:F1} | {lower,9:F1} | {upper,9:F1} | {width,6:F1} | {inBounds}");
}
// Mesurer la couverture RÉELLE des intervalles sur les dates effectivement évaluées (pas une simulation)
var h1Cov = aligned.Where(r => r.H == 1).ToArray();
var insideH1 = h1Cov.Count(r => r.Actual >= r.Lower && r.Actual <= r.Upper);
var insideAll = aligned.Count(r => r.Actual >= r.Lower && r.Actual <= r.Upper);
Console.WriteLine($"\n=== Couverture des intervalles mesurée (niveau nominal 95%) ===");
Console.WriteLine($"Horizon 1 : {insideH1}/{h1Cov.Length} dans l'intervalle -> {(double)insideH1 / h1Cov.Length * 100:F1}%");
Console.WriteLine($"Tous horizons : {insideAll}/{aligned.Length} dans l'intervalle -> {(double)insideAll / aligned.Length * 100:F1}%");=== Prévisions à 7 jours avec intervalles de confiance (95%) — origine 2024-01-01 ===
Date cible | Réel | Prévision | Borne inf | Borne sup | Largeur | Dans IC
----------------------------------------------------------------------------------------
2024-01-02 | 181 | 186,7 | 169,9 | 203,5 | 33,7 | OUI
2024-01-03 | 163 | 166,1 | 148,0 | 184,1 | 36,1 | OUI
2024-01-04 | 138 | 141,6 | 123,0 | 160,2 | 37,3 | OUI
2024-01-05 | 113 | 134,6 | 114,4 | 154,8 | 40,4 | NON
2024-01-06 | 127 | 125,3 | 104,8 | 145,8 | 41,0 | OUI
2024-01-07 | 155 | 152,5 | 131,4 | 173,5 | 42,1 | OUI
2024-01-08 | 188 | 177,5 | 155,9 | 199,1 | 43,2 | OUI
=== Couverture des intervalles mesurée (niveau nominal 95%) ===
Horizon 1 : 285/365 dans l'intervalle -> 78,1%
Tous horizons : 2164/2534 dans l'intervalle -> 85,4%
| Concept | Description |
|---|---|
| Borne inférieure | Pire cas réaliste (2.5ème percentile) |
| Borne supérieure | Meilleur cas réaliste (97.5ème percentile) |
| Largeur | Incertitude de la prévision (plus large = plus incertain) |
| OUI | La valeur réelle est dans l’intervalle (bon signe) |
Règle empirique : si ~95% des valeurs réelles sont dans l’intervalle, le modèle est bien calibré.
Mesure réelle : sur les dates évaluées en horizon 1, la couverture mesurée est 78.1% (285/365) ; tous horizons confondus 85.4% (2164/2534). La couverture est inférieure au niveau nominal de 95% : l’intervalle de confiance SSA est optimiste — il contient la valeur réelle moins souvent que le niveau déclaré ne le promet. C’est un résultat standard de la v1 de ForecastBySsa (les intervalles ne tiennent pas compte de l’incertitude d’estimation du modèle) ; à retenir avant d’utiliser ces bornes comme promesse de couverture.
Reentrainez le modèle SSA en modifiant le paramètre confidenceLevel (0.80, 0.90, 0.95, 0.99) et comparez la largeur des intervalles et le pourcentage de valeurs reelles contenues dans chaque intervalle.
Indice : Pour chaque niveau de confiance, créez un nouveau pipeline ForecastBySsa avec la valeur correspondante. Parcourez les predictions et comptez combien de valeurs reelles tombent dans l’intervalle [LowerBound, UpperBound]. Un niveau de confiance plus eleve devrait produire des intervalles plus larges mais un meilleur taux de couverture.
// Exercice 3 : Impact du niveau de confiance sur les intervalles
// TODO etudiant : Comparez les intervalles pour differents niveaux de confiance
// Indice : bouclez sur les niveaux 0.80, 0.90, 0.95, 0.99 et creez un pipeline pour chacun
// Etape 1 : definir la liste des niveaux de confiance a tester
// Etape 2 : pour chaque niveau, creer un pipeline ForecastBySsa avec ce confidenceLevel
// Etape 3 : entrainer le modele et extraire les intervalles (LowerBound, UpperBound)
// Etape 4 : calculer le pourcentage de valeurs reelles dans l'intervalle et la largeur moyenne
// var confidenceResults = ... // TODO etudiant : remplacer par les resultats de comparaison
Console.WriteLine("Exercice a completer : impact du niveau de confiance sur les intervalles");Exercice a completer : impact du niveau de confiance sur les intervalles
Comparons différentes configurations de ForecastBySsa pour trouver la meilleure.
// Tester différentes configurations avec le même protocole d'évaluation aligné (horizon 1)
var configs = new[]
{
new { windowSize = 7, seriesLength = 30, Name = "Config 1: w=7, s=30 (base)" },
new { windowSize = 14, seriesLength = 30, Name = "Config 2: w=14, s=30 (saisonnalité 2 sem)" },
new { windowSize = 7, seriesLength = 60, Name = "Config 3: w=7, s=60 (plus d'historique)" }
};
Console.WriteLine("=== Comparaison des configurations (horizon 1, protocole aligné) ===");
Console.WriteLine("\nConfiguration | MAE | RMSE | Gain vs naïve | n");
Console.WriteLine(new string('-', 78));
foreach (var config in configs)
{
var pipeline = mlContext.Forecasting.ForecastBySsa(
outputColumnName: "ForecastedSales",
inputColumnName: "Sales",
windowSize: config.windowSize,
seriesLength: config.seriesLength,
trainSize: 365,
horizon: 7,
confidenceLevel: 0.95f,
confidenceLowerBoundColumn: "LowerBoundSales",
confidenceUpperBoundColumn: "UpperBoundSales");
var model = pipeline.Fit(trainData);
var preds = model.Transform(testData);
var pr = mlContext.Data.CreateEnumerable<SalesForecast>(preds, reuseRowObject: false).ToArray();
var rows = ForecastEval.BuildAlignedRows(pr, testRows, salesByDate, maxTest).Where(r => r.H == 1).ToArray();
var sm = ForecastEval.Metrics(rows.Select(r => r.Actual), rows.Select(r => r.Pred));
var nm = ForecastEval.Metrics(rows.Select(r => r.Actual), rows.Select(r => r.Naive));
Console.WriteLine($"{config.Name,-45} | {sm.mae,5:F1} | {sm.rmse,5:F1} | {(nm.mae - sm.mae) / nm.mae * 100,5:F1}% | {rows.Length}");
}=== Comparaison des configurations (horizon 1, protocole aligné) ===
Configuration | MAE | RMSE | Gain vs naïve | n
------------------------------------------------------------------------------
Config 1: w=7, s=30 (base) | 10,5 | 13,4 | 45,2% | 365
Config 2: w=14, s=30 (saisonnalité 2 sem) | 9,0 | 11,3 | 53,1% | 365
Config 3: w=7, s=60 (plus d'historique) | 10,5 | 13,4 | 45,2% | 365
| Configuration | MAE (horizon 1) | RMSE | Gain vs naïve | Lecture |
|---|---|---|---|---|
| windowSize=7, seriesLength=30 (base) | 10.5 | 13.4 | 45.2% | Saisonnalité hebdomadaire |
| windowSize=14, seriesLength=30 | 9.0 | 11.3 | 53.1% | Meilleure : cycle bi-hebdomadaire |
| windowSize=7, seriesLength=60 | 10.5 | 13.4 | 45.2% | Identique à la base (l’historique allongé ne change rien ici) |
Recommandation : la configuration windowSize=14 (saisonnalité bi-hebdomadaire) donne le MAE le plus faible sur les mêmes dates que la baseline naïve. Leçon : le
seriesLength(30 vs 60) n’a ici aucun effet — c’est lewindowSizequi porte la saisonnalité discriminante. Comparez toujours les configurations avec le même protocole d’évaluation aligné, sinon le classement peut être un artefact d’appariement.
Ce notebook a couvert les principes du forecasting avec ML.NET :
| Concept | Point clé |
|---|---|
| ForecastBySsa (Broomhead & King, 1986 ; Golyandina & Zhigljavsky, 2013) | Algorithme SSA pour décomposition trend/saisonnalité/bruit |
| Paramètres | windowSize capture la saisonnalité, horizon = horizon de prévision |
| Intervalles de confiance | Quantifient l’incertitude des prévisions |
| Évaluation | MAE et RMSE pour mesurer la précision |
Prochaines étapes : - Essayer différents paramètres selon vos données - Tester sur d’autres types de séries temporelles - Explorer d’autres algorithmes (AutoML pour TimeSeries)
Navigation : Index | << ML-4-Evaluation | Suivant >> ML-6-ONNX
Analysez les données temporelles pour identifier les patterns saisonniers mensuels. Groupez les ventes par mois et calculez la moyenne de chaque mois pour mettre en evidence la saisonnalite annuelle.
Indice : Utilisez LINQ (GroupBy) pour grouper les données par mois et calculer la moyenne de chaque groupe. Visualisez avec un graphique en barres via XPlot.Plotly pour comparer les mois entre eux.
// Exercice 4 : Detection de saisonnalite
// TODO etudiant : Analysez la saisonnalite mensuelle dans les donnees de ventes
// Indice : utilisez GroupBy pour grouper par mois et calculer la moyenne
// Etape 1 : grouper les donnees par mois (champ Month)
// Etape 2 : calculer la moyenne des ventes pour chaque mois
// Etape 3 : afficher les resultats dans un tableau
// Etape 4 : creer un graphique en barres avec SvgChartHelper.Bar (deja charge via \#load) pour visualiser les differences
// var seasonalData = ... // TODO etudiant : remplacer par le resultat du groupement
Console.WriteLine("Exercice a completer : detection de saisonnalite mensuelle");Exercice a completer : detection de saisonnalite mensuelle
Entrainez plusieurs modèles SSA avec des horizons différents (3, 6 et 12 mois) et comparez leur precision a l’aide des metriques L1 (MAE) et L2 (RMSE).
Indice : Pour chaque horizon, créez un nouveau pipeline ForecastBySsa avec la valeur d’horizon correspondante. Utilisez les mêmes données d’entrainement et de test que dans les parties précédentes pour avoir une comparaison equitable.
// Exercice 5 : Previsions multi-horizon
// TODO etudiant : Comparez les previsions avec differents horizons (3, 6, 12 mois)
// Indice : utilisez une boucle sur les horizons et creez un pipeline ForecastBySsa pour chacun
// Etape 1 : definir les horizons a tester (3, 6, 12)
// Etape 2 : pour chaque horizon, creer et entrainer un pipeline ForecastBySsa
// Etape 3 : calculer MAE et RMSE pour chaque configuration
// Etape 4 : afficher les resultats sous forme de tableau comparatif
// var horizonResults = ... // TODO etudiant : remplacer par les resultats de comparaison
Console.WriteLine("Exercice a completer : previsions multi-horizon (3, 6, 12 mois)");Exercice a completer : previsions multi-horizon (3, 6, 12 mois)
Créez un modèle de forecasting pour prédire la température quotidienne sur 7 jours, en utilisant un dataset synthétique avec saisonnalité annuelle.
windowSize pour capturer la saisonnalitéIndices - Température = sin(day_of_year / 365 * 2π) * 15 + 15 + bruit (saisonnalité annuelle) - Pour une saisonnalité annuelle, windowSize = 365 est trop grand, essayez 30-60 - Utilisez seriesLength = 60-90 pour capturer suffisamment d’historique - L’horizon reste à 7 jours (1 semaine de prévision)
// Exercice 6 : Prévision de température avec saisonnalité annuelle
// TODO: Générer les données de température (365 jours)
var rand = new Random(42);
var temperatures = Enumerable.Range(0, 365)
.Select(day =>
{
// TODO: Appliquez la formule de saisonnalité annuelle
// Temperature = sin(day / 365 * 2π) * amplitude + moyenne + bruit
double temp = 0; // Remplacez par votre formule
return (float)temp;
})
.ToArray();
// TODO: Créer une classe TemperatureData
public class TemperatureData
{
// Ajoutez les propriétés nécessaires
}
// TODO: Créer le DataFrame ou IDataView avec les données
// MLContext tempContext = ... // TODO etudiant
// IDataView tempData = ... // TODO etudiant
// TODO: Diviser en train (300 jours) et test (65 jours)
// Indice: utilisez FilterRowsByColumn ou Take/Skip
// TODO: Configurer ForecastBySsa avec les bons paramètres
// IEstimator<ITransformer> tempPipeline = ... // TODO etudiant : utilisez tempContext.Forecasting.ForecastBySsa
// TODO: Entraîner le modèle
// ITransformer tempModel = ... // TODO etudiant
// TODO: Faire des prédictions et calculer MAE/RMSE
// IDataView tempPredictions = ... // TODO etudiant
var tempMAE = 0.0;
var tempRMSE = 0.0;
Console.WriteLine("=== Résultats Prévision Température ===");
Console.WriteLine($"MAE: {tempMAE:F2}°C");
Console.WriteLine($"RMSE: {tempRMSE:F2}°C");
// TODO: Afficher les 7 premières prévisions vs valeurs réelles
Console.WriteLine("\n=== Prévisions à 7 jours ===");
Console.WriteLine("Jour | Réel | Prédit | Erreur");
// Affichez les résultats
// TODO: Analyse - Quelle configuration de windowSize fonctionne le mieux ?=== Résultats Prévision Température ===
MAE: 0,00°C
RMSE: 0,00°C
=== Prévisions à 7 jours ===
Jour | Réel | Prédit | Erreur
Singular Spectrum Analysis (SSA) - Broomhead, D. S., & King, G. P. (1986). Extracting Qualitative Dynamics from Experimental Data. Physica D: Nonlinear Phenomena, 20(2-3), 217-236. — origine de la méthode SSA (décomposition en fonctions propres, « singular spectrum analysis »). - Golyandina, N., & Zhigljavsky, A. (2013). Singular Spectrum Analysis for Time Series. Springer. — référence canonique moderne de la SSA, base de l’algorithme ForecastBySsa de ML.NET.
Évaluation des prévisions - Hyndman, R. J., & Koehler, A. B. (2006). Another Look at Measures of Forecast Accuracy. International Journal of Forecasting, 22(4), 679-688. — métriques MAE et RMSE utilisées pour l’évaluation des prévisions.
Framework (ML.NET) - Ahmed, Z., Amizadeh, S., Bilenko, M., et al. (2019). DOI: 10.1145/3292500.3330667 — Machine Learning at Microsoft with ML.NET. ACM SIGKDD (KDD).