Feature engineering pour le forecasting — 90 % du résultat tient dans ces 19 fichiers
Tout le monde parle d'algorithmes. Personne ne parle de feature engineering. C'est pourtant 90 % de la qualité d'un modèle ML en production. Voici les 19 extracteurs que SaleCast utilise pour transformer une série temporelle brute en signal exploitable — et pourquoi 'utiliser un meilleur algorithme' ne sera jamais aussi rentable.
Cet article décortique le projetSaleCastLe mythe qui pourrit l'industrie ML
Le débat permanent en ML :
"On utilise LightGBM ou XGBoost ?" "TiRex c'est mieux que Chronos ?" "Et si on essayait un transformer ?"
Les juniors passent des semaines à benchmarker des algorithmes. Les seniors les regardent, calmement, et savent que ce n'est pas là que se joue la performance.
90 % de la qualité d'un modèle de forecasting en production ne dépend pas de l'algorithme. Elle dépend des features qu'on lui donne.
Donne du LightGBM les bonnes features → tu gagnes. Donne du transformer state-of-the-art les mauvaises features → tu perds.
Voici les 19 extracteurs que j'utilise sur SaleCast — chacun dans son propre fichier, chacun avec une raison précise d'exister.
La structure du FeaturePipeline
public sealed class FeaturePipeline
{
private readonly IReadOnlyList<IFeatureExtractor> _extractors;
// Pre-computed offsets pour chaque famille de features
private readonly int _temporalOffset; // TemporalFeatures: 10
private readonly int _calendarOffset; // CalendarFeatures: 7
private readonly int _lifecycleOffset; // ProductLifecycleFeatures: 3
private readonly int _cyclicalOffset; // CyclicalTemporalFeatures: 10 (sin/cos)
private readonly int _lagOffset; // LagFeatures: 19 (Lag_1...Lag_365)
private readonly int _rollingOffset; // RollingFeatures: 13
// ... + 13 autres extracteurs spécialisés
}
Chaque extracteur produit un sous-vecteur de features. Le pipeline orchestre le tout et concatène. Total : 110+ features pour un produit donné, à une date donnée.
C'est ce vecteur de 110 floats qui rentre dans LightGBM.
Les 6 familles de features qui changent tout
1. Features temporelles brutes — TemporalFeatures
10 features : jour de la semaine, jour du mois, mois, trimestre, semaine de l'année, weekend, lundi, vendredi, fin de mois, début de mois.
Pourquoi c'est essentiel : le commerce est saisonnier au jour. Le lundi n'est pas le mardi. Les samedis sont différents.
Sans ces features, ton modèle ne sait pas qu'on est lundi. Avec ces features, il l'utilise.
2. Features cycliques sin/cos — CyclicalTemporalFeatures
L'astuce qui change tout. Au lieu d'encoder "jour 365" comme un entier, on encode :
sin(2π × dayOfYear / 365), cos(2π × dayOfYear / 365)
Pourquoi : le 31 décembre et le 1er janvier ne sont pas "365 unités d'écart". Ils sont proches. L'encodage cyclique le capture mathématiquement.
J'ai 10 features cycliques : sin/cos pour DayOfWeek (k=1), Month (k=1), DayOfYear (k=1, 2, 3 pour capturer plusieurs harmoniques).
Gain de précision mesuré : -8 % MAE moyen vs. encodage entier.
3. Features de retard (lags) — LagFeatures
19 features : Lag_1, Lag_2, Lag_3, Lag_4, Lag_5, Lag_6, Lag_7, Lag_14, Lag_21, Lag_28, Lag_30, Lag_60, Lag_90, Lag_180, Lag_364, Lag_365, Lag_7_Diff, Lag_30_Diff, Lag_365_Diff.
Lag_X = la vente d'il y a X jours.
Lag_X_Diff = la différence entre Lag_1 et Lag_X (croissance / décroissance).
Pourquoi 365 ? Parce que la saisonnalité d'un magasin est annuelle. Le 14 juin 2025 ressemble plus au 14 juin 2024 qu'au 14 mars 2025.
Pourquoi 364 et 365 ? Parce qu'un même jour de semaine + une même date sont deux signaux différents. Le 14 août dimanche ≠ 14 août mardi.
C'est ce genre de détail qui fait gagner 2-3 % de précision sur l'ensemble du catalogue. Multiplié par 2 800 SKU sur 12 mois, c'est mesurable en €.
4. Features glissantes (rolling) — RollingFeatures
13 features : moyennes mobiles 7/14/30/90 jours, écarts-types 7/30, min/max sur 30 jours, etc.
Pourquoi : un produit qui s'est vendu 50 unités hier mais 5 il y a un an n'est pas le même qu'un produit qui se vend 27 unités régulièrement depuis 1 an, même si leur moyenne 7 jours est identique.
Le rolling std capture la volatilité. C'est ce qui sépare un produit "calme" d'un produit "explosif".
5. Features de catégorie — CrossProductFeatures
public IReadOnlyList<string> FeatureNames { get; } =
[
"CategoryAvgDemand7", // Demande moyenne 7j de la catégorie
"CategoryPromoActive", // Un produit de la catégorie est-il en promo ?
"DemandShareInCategory", // Part de marché du produit dans sa catégorie
"CategoryTrend7" // Tendance 7j de la catégorie (ratio récent/ancien)
];
Pourquoi : ton produit individuel a peu d'historique → la catégorie en a beaucoup. Le signal de catégorie est plus fort que le signal de produit isolé pour les nouveaux produits.
C'est la base du cold start dont j'ai parlé dans un autre article.
6. Features de cycle de vie — ProductLifecycleFeatures
3 features : DaysOnSale (depuis combien de jours le produit est en catalogue), IsNewArrival (≤ 30 jours), IsMature (≥ 180 jours).
Pourquoi : les nouveautés vendent différemment des classiques. Une feature "DaysOnSale" permet au modèle de comprendre la phase du produit sans qu'on l'explicite.
Les 7 features qui font les vrais gains
HolidayCalendar — 5,6 K de code
public sealed class HolidayCalendar : IFeatureExtractor
{
// Features:
// - IsHoliday : 1 si jour férié
// - IsPreHoliday : 1 si veille de férié
// - IsPostHoliday : 1 si lendemain
// - DaysUntilNextHoliday : distance au prochain férié
// - HolidayType : 1=religieux, 2=civique, 3=commercial
}
Avec support multi-pays (France, Allemagne, Espagne, UK, US, Belgique). Pour un client e-commerce qui vend en plusieurs pays, on calcule séparément.
Gain mesuré : -22 % MAE sur les semaines contenant un férié.
PriceFeatures — 1,5 K
// - CurrentUnitPrice
// - PriceChange7d (prix de la semaine vs cette semaine)
// - PriceChange30d
// - IsDiscounted (prix actuel < prix de référence)
// - DiscountPercent
L'élasticité prix n'est pas magique. Si le prix baisse, les ventes augmentent. Le modèle a besoin de voir le prix pour le comprendre.
J'ai vu des modèles deployés sans cette feature. Ils prédisent comme si le prix n'existait pas. Catastrophe sur les périodes de promo.
PromotionFeatures — 1 K
// - PromoActive : flag
// - PromoDaysRemaining : combien il reste de jours de promo
// - DaysSinceLastPromo : récence de la dernière promo
Pourquoi : les promos ne se comportent pas comme une "réduction de prix
permanente". Elles créent un rush d'achat anticipé puis une accalmie après.
La feature DaysSinceLastPromo capture la dimension psychologique du consommateur.
PurchaseBehaviorFeatures — 2,1 K
// - AvgOrderQuantity : moyenne d'unités par commande
// - OrderFrequency7d
// - RepeatBuyerRatio : % de clients qui rachètent
Ça donne au modèle la forme des achats. Un produit acheté à 5 unités par commande n'est pas le même qu'un produit acheté à 1 unité.
IntermittencyFeatures — 2,3 K
// - DaysWithoutSale_lastN
// - SaleSparsity : % de jours avec vente sur la période
// - AverageInterval : intervalle moyen entre 2 ventes
// - LumpinessIndex : ADI × CV² (cf. classification Syntetos-Boylan)
Pour les produits intermittents (chaussures de ski en juillet, guirlandes en mars), ces features sont vitales. Le modèle doit savoir qu'il prédit sur un produit "rare" et pas "régulier".
GeographicFeatures — 1,6 K
// - StoreCity, StoreRegion
// - WeatherCondition_today (sun/rain/snow/cloudy)
// - Temperature_avg
// - WeatherTrend_7d
Branchage sur API météo. Pour un client e-commerce avec entrepôts régionaux, le temps qu'il fait à Lille ≠ Marseille. Et un coup de chaud sur Marseille fait exploser les ventes de glace.
C'est aussi vrai pour les achats e-commerce non liés à la météo en apparence — la pluie augmente le e-commerce de 15 % en moyenne (gens à la maison).
CostMarginFeatures — 1,3 K
// - UnitCost, GrossMargin
// - MarginRatio : marge / prix
// - CostTrend30d : évolution du coût d'approvisionnement
Pas une feature classique. C'est utile pour les modèles de pricing plus que pour le forecasting pur. Mais ça permet au modèle de capturer la stratégie business du produit (haute marge vs low cost).
Le pattern qui rend tout ça maintenable
19 extracteurs séparés, chacun dans son fichier, chacun avec son interface :
public interface IFeatureExtractor
{
IReadOnlyList<string> FeatureNames { get; }
float[] Extract(ProductSalesHistory history, int dateIndex);
}
C'est tout. Une seule méthode. Un seul contrat.
L'ajout d'un nouvel extracteur — par exemple SocialMediaSentimentFeatures —
prend 30 minutes :
- Créer le fichier
SocialMediaSentimentFeatures.cs - Implémenter
IFeatureExtractor - L'ajouter à la liste des extracteurs dans
FeaturePipeline - Re-entrainer le modèle
Le reste du système (training, inference, explainabilité SHAP, drift detection) fonctionne automatiquement avec la nouvelle feature. Aucune autre modification.
C'est la beauté d'une architecture où l'abstraction est juste.
L'effet sur la précision
J'ai mesuré la précision en retirant chaque famille de features une par une (ablation study). Sur 2 800 SKU testés :
| Features retirées | MAE médiane | Δ vs full |
|---|---|---|
| Tout (full pipeline) | 4.2 | baseline |
| Sans Lag features (-19) | 7.8 | +85 % |
| Sans Calendar features (-7) | 5.1 | +21 % |
| Sans HolidayCalendar (-5) | 6.4 | +52 % |
| Sans CrossProduct (-4) | 4.9 | +17 % |
| Sans Cyclical (-10) | 4.6 | +9 % |
| Sans Price (-5) | 5.8 | +38 % |
| Sans Weather (-4) | 4.4 | +5 % |
Conclusion :
- Les lag features sont le pilier — sans elles, c'est inutilisable.
- Holiday + Price sont vitales pour le e-commerce.
- Weather est marginale sur l'ensemble mais grosse pour ~15 % des SKU (produits météo-sensibles).
Ce que ça change dans ma pratique
Quand je commence un projet ML, je ne commence plus par "quel algorithme ?". Je commence par "quelles features ?".
Les algorithmes sont commodifiés. LightGBM, XGBoost, CatBoost, TiRex, Chronos — ils sont tous à 5 % les uns des autres sur les bonnes données.
Les features sont propres à ton domaine. Personne d'autre que toi ne sait qu'un client B2B regarde le calendrier scolaire avant d'acheter du papier en gros, ou qu'un produit "saison été" se vend dès mars en Espagne mais en mai en Belgique.
C'est ce savoir métier encodé en features qui sépare un modèle qui marche en prod d'un modèle qui marche sur le dataset Kaggle.
La leçon
Le ML production sérieux est 20 % d'algorithme, 80 % de feature engineering.
Si tu débutes en ML, ne passe pas une seconde à benchmarker LightGBM vs XGBoost vs CatBoost. Passe ton temps à comprendre :
- Quelles données ton domaine génère
- Lesquelles ont du signal
- Comment les transformer en floats pour le modèle
Le reste — le choix de l'algo, l'hyperparameter tuning, le déploiement — c'est du commodity.
Le feature engineering, c'est ton avantage compétitif structurel.
Stack & code
- 19 IFeatureExtractor dans
SaleCast.Forecasting/Features/ - 110+ features au total dans le vecteur d'entrée
- FeaturePipeline.cs = 16 K — l'orchestrateur central
- Pre-computed offsets pour accès O(1) à chaque famille de features
- Holiday providers : FR, DE, ES, UK, US, BE (extensible)
- Tests d'ablation intégrés dans
SaleCast.Tests/— rejoue chaque retrait de famille pour mesurer son impact
Le code des extracteurs fait 18 K lignes au total. C'est plus que la totalité du code des algorithmes (Holt-Winters, MSTL, Croston, etc. cumulés).
Et c'est normal. Le feature engineering est le plus gros bloc d'un projet ML sérieux. Si chez toi il est plus petit que ton bloc d'algorithmes, tu as inversé les priorités.
Florian Sola
Lead Technique · Haute performance temps réel · 9 ans d'expérience