DEV Community

Cover image for Comment FORECAST.ETS prévoit-il le pic de ventes de décembre ?

Comment FORECAST.ETS prévoit-il le pic de ventes de décembre ?

=FORECAST.ETS(...) ressemble à de la magie : on lui donne trois ans de ventes mensuelles et il renvoie un décembre plus haut que tous les autres mois, un janvier qui retombe et un intervalle de confiance autour des deux. En dessous, il y a un lissage exponentiel avec une tendance et une saison qui se répète, une méthode de la fin des années 1950 qui tient en une quarantaine de lignes. La Forecast Sheet de l'éditeur de tableur de l'application de bureau osFoundry écrit cette fonction dans une nouvelle feuille. Cet article reconstruit la même idée en Python, la fait tourner sur les mêmes 36 mois, et regarde où elle concorde avec la feuille et où une méthode plus simple échoue. Les données sont un exemple : les ventes d'une petite boutique avec une hausse régulière, une forme annuelle fixe et environ 4% de bruit, générées pour cette démonstration.

Ce que produit la feuille

Sur 36 mois (de janvier 2023 à décembre 2025), Forecast Sheet avec la saisonnalité sur auto et un niveau de confiance de 95% écrit trois colonnes, Forecast, Lower et Upper Confidence Bound, et un graphique. La cellule C49, décembre 2026, contient =FORECAST.ETS(A49,$B$2:$B$37,$A$2:$A$37,1,1) et affiche $20,654. Les deux bornes à côté sont $19,008 et $22,300.

Le graphique de la feuille de prévision : historique en bleu, prévision et plage pour 2026

Ce sont les chiffres à reproduire. Tout ce qui suit s'exécute avec uvx --with numpy --with scipy python -I forecast.py sous Python 3.12, numpy 2.5.4 et scipy 1.18.1.

D'abord, trouver la saison

La saisonnalité auto doit deviner la longueur d'un cycle. Une version simple de cette devinette est l'autocorrélation de la variation d'un mois à l'autre. Une tendance ajoute la même quantité à chaque mois et disparaît dans les différences ; une forme annuelle, non.

d = np.diff(y); d = d - d.mean()
ac = [np.sum(d[:-k] * d[k:]) / np.sum(d * d) for k in range(1, 19)]
Enter fullscreen mode Exit fullscreen mode

Sur ces 36 mois, le décalage 12 donne 0.53, le 6 donne 0.06 et le 13 donne -0.01 : la longueur de saison est donc 12. C'est ma propre règle empirique, pas une description de la façon dont l'éditeur choisit sa longueur de saison ; ce qui peut se comparer, c'est le résultat, plus bas.

Holt-Winters, additif

À chaque nouveau mois, trois choses sont mises à jour : un niveau (où en sont les ventes maintenant, saison retirée), une tendance (de combien elles croissent par mois) et douze valeurs de saison (de combien chaque mois du calendrier se situe au-dessus ou en dessous du niveau). Chacune est un mélange pondéré de la nouvelle observation et de l'ancienne estimation, avec des poids alpha, beta et gamma.

def run(y, a, b, g):
    l, t, s = init(y); s = list(s); err = []
    for i, v in enumerate(y):
        err.append(v - (l + t + s[i % M]))              # one-step-ahead error
        ln = a * (v - s[i % M]) + (1 - a) * (l + t)     # level
        t = b * (ln - l) + (1 - b) * t                  # trend
        s[i % M] = g * (v - ln) + (1 - g) * s[i % M]    # season
        l = ln
    return np.array(err), l, t, s
Enter fullscreen mode Exit fullscreen mode

Les valeurs de départ viennent des deux premières années : la moyenne de la première année comme niveau, la différence entre les moyennes des deux premières années divisée par douze comme tendance, et l'écart de chaque mois à la moyenne de la première année comme saison. Les trois poids sont trouvés en minimisant les erreurs quadratiques à un pas avec scipy.optimize.minimize, depuis trois points de départ pour qu'un mauvais départ ne décide pas de la réponse. Prévoir h mois à l'avance, c'est niveau + k * tendance + saison[mois].

Pour l'intervalle, j'ai utilisé une règle empirique : 1.96 fois l'écart-type des résidus fois la racine carrée de l'horizon. La formule d'intervalle de la feuille n'a pas été examinée, seuls ses chiffres sont donc comparés.

Face à la feuille

Sur les 36 mois complets, l'ajustement donne alpha 0.37, beta 0.01 et gamma 0.42, et un écart-type des résidus de $567. Face à la colonne Forecast de la feuille :

Mois Ce script Forecast Sheet
Janv. 2026 $12,136 $12,260
Déc. 2026 $20,698 $20,654
Différence absolue moyenne, 12 mois $297 (2.0%)
Plus grande différence (juin 2026) $587

Les douze mois concordent en moyenne à moins de 2%, ce qu'on attend de deux implémentations de la même famille de méthodes avec des valeurs de départ et un ajustement différents. Cela ne prouve pas que l'éditeur fasse exactement cela, et c'est un seul jeu de données à la forme propre. Avec des données réelles, un mois manquant ou un changement de niveau, les deux s'écarteront davantage.

Les intervalles ne concordent pas : décembre sort ici de $16,849 à $24,548, contre $19,008 à $22,300 dans la feuille. Le mien est plus large et s'élargit plus vite avec l'horizon : $2,223 en janvier à $7,700 en décembre, contre $1,621 et $3,292 dans la feuille. La règle de la racine carrée est grossière, et rien ici ne dit quelle bande est la mieux calibrée.

Ce que la saison apporte

Faire tourner les mêmes données sans saison, avec seulement un niveau et une tendance (la méthode linéaire de Holt), montre ce que vaut le motif :

no season: Jan 17872   Dec 23159   residual sd 2185
Enter fullscreen mode Exit fullscreen mode

Sans la saison, la prévision de janvier est $17,872, là où la feuille donne $12,260 et où les deux janviers précédents ont chuté d'environ 40% par rapport à décembre. L'écart-type des résidus est 2,185 contre 567 avec la saison. La prévision de décembre se trompe dans l'autre sens, $23,159 contre $20,654, parce qu'une droite de tendance n'a aucun moyen de savoir que décembre est un pic et janvier un creux.

Un test sur un an

On ajuste seulement sur janvier 2023 à décembre 2024, on prévoit 2025 et on compare avec ce qui s'est passé. Erreur absolue moyenne en pourcentage (MAPE) sur les douze mois :

Méthode MAPE Prévision déc. 2025
Moyenne globale 19.4% $10,586
Dernier mois répété 37.3% $17,000
Même mois l'an dernier 12.0% $17,000
Même mois l'an dernier fois la croissance 4.1% $20,025
Holt-Winters (ce script) 3.4% $18,511

En décembre 2025, il s'est vendu en réalité $19,480. Deux choses en ressortent. Une simple copie saisonnière de l'an dernier (12.0%) rate la croissance, et en ajoutant la croissance annuelle observée de 17.8% (4.1%), on est presque au niveau de Holt-Winters (3.4%) ; avec deux ans de données, le lissage exponentiel n'est donc pas un grand pas au-delà d'une formule de tableur soignée. Et la bande à 95% a couvert les douze valeurs réelles, ce qui, avec douze points, dit seulement qu'elle n'est pas trop étroite.

C'est un seul découpage d'une seule série d'exemple. Cela ne dit rien du classement des méthodes sur d'autres données.

Un piège : demander à une bibliothèque de statistiques de tout estimer

Le raccourci évident est statsmodels. Avec l'ajustement par défaut sur les mêmes 36 mois :

ETSModel(series, error='add', trend='add', seasonal='add', seasonal_periods=12).fit()
Enter fullscreen mode Exit fullscreen mode

statsmodels 0.15.0 donne décembre 2026 à $22,738 avec une plage de $17,688 à $27,787, soit 10% au-dessus de la feuille. Dans le test sur un an, le même modèle a eu un MAPE de 33.2%, pire que la moyenne globale. Probablement, un modèle qui estime douze valeurs initiales de saison plus les poids de lissage à partir de 24 ou 36 points a trop de nombres libres, et l'ajustement finit sur quelque chose d'étrange. Le script ci-dessus fixe plutôt les valeurs de départ à partir des premières années, une des différences entre les deux ; cela n'a pas été testé isolément. Mieux vaut comparer toute prévision de bibliothèque à « même mois l'an dernier fois la croissance » avant de lui faire confiance.

À l'usage

Le script (forecast.py) est toute l'expérience. Pour de vrais chiffres de ventes, le plus rapide est la Forecast Sheet de l'application, gratuite sur la page de téléchargement d'osFoundry et qui travaille sur le fichier de l'ordinateur. Écrire la méthode à la main sert à comprendre ce que signifie la plage, et à remarquer quand une prévision doit être jetée.

Top comments (0)