Contrairement aux lissages exponentiels simple et double, le lissage de Holt Winters permet de prendre en compte à la fois la tendance, mais également la notion de cycles/saisonnalité.

Pour prendre en compte l’ensemble de ces facteurs, nous allons devoir désormais utiliser 3 paramètres : alpha, gamma et delta.

Cet article a pour objectif de présenter une implémentation possible du lissage de Holt Winters en Javascript. Il fait suite au post sur le lissage exponentiel double, et portera les mêmes points :

  • Implémentation du lissage du Holt-Winters en Javascript;
  • Calcul de la valeur optimale d'alpha, gamma et delta;
  • Utilisation de la librairie HighchartJs pour générer un graphique.

Nos données

Nous utiliserons les données suivantes :

var data = [61.5, 63.2, 55.8, 71.4, 70, 71.4, 63.9, 78.9, 78.3, 78.6, 71.9, 87, 86.2, 87.5, 80.1, 92.5];

Mois CA(k€)
Janvier 61.5
Février 63.2
Mars 55.8
Avril 71.4
Mai 70
Juin 71.4
Juillet 63.9
Août 78.9
Septembre 78.3
Octobre 78.6
Novembre 71.9
Décembre 87
Janvier 86.2
Février 87.5
Mars 80.1
Avril 92.5
Mai ?
Juin ?
Juillet ?
Août ?

Ici, nous sommes en présence de cycles durant 4 mois et dont la magnitude ne croît pas au cours du temps. Nous utiliserons donc le lissage de Holt-Winters additif (par opposition au multiplicatif). Notre objectif sera de déterminer les valeurs du CA pour le prochain cycle.

Lissage de Holt Winters

Le code est extrait et adapté de la librairie zodiac-ts que j’ai mise en ligne sur Github.

//Création de la classe HoltWintersSmoothing
//On doit lui passer en paramètre les données, les 3 paramètres alpha, gamma et delta nous avons parlé précédemment, la longueur de la saison, ainsi qu'un booléen indiquant si on souhaite appliquer le lissage multiplicatif ou addifif

HoltWintersSmoothing = function(data, alpha, gamma, delta, seasonLength, mult)
{
	this.data = data;
	this.alpha = alpha;
	this.gamma = gamma;
	this.delta = delta;
	this.seasonLength = seasonLength;
	this.mult = mult;
	this.forecast = null;
};

//Nous définissons la méthode prédict qui aura pour but d'appeller la méthode de prédiction multicative ou additive selon le paramètre passé dans le constructeur
HoltWintersSmoothing.prototype.predict =function ()
{
	if(this.mult)
	{
		return this.predictMult();
	}
	else
	{
		return this.predictAdd();
	}
}

//Méthode de prédiction pour le cas additif
HoltWintersSmoothing.prototype.predictAdd = function()
{
	A = Array();
	B = Array();
	S = Array();

	A[this.seasonLength-1] = 0;
	var averageFirstSeason = 0;
	for(var i = 0; i < this.seasonLength; ++i)
	{
		averageFirstSeason += this.data[i];
	} 
	B[this.seasonLength-1] = averageFirstSeason/this.seasonLength;

	for(var i = 0; i < this.seasonLength; ++i)
	{
		S[i] = this.data[i] - averageFirstSeason/this.seasonLength;
	}

	for(var i = this.seasonLength; i < this.data.length; ++i)
	{
		B[i] = this.alpha*(this.data[i]- S[i - this.seasonLength])+(1-this.alpha)*(B[i-1]+A[i-1]);
		A[i] = this.gamma*(B[i]-B[i-1])+(1-this.gamma)*A[i-1];
		S[i] = this.delta*(this.data[i]-B[i])+(1-this.delta)*S[i-this.seasonLength];
	}

	var forecast = Array();
	for(var i = 0; i < this.seasonLength; ++i)
	{
		forecast[i]= null;
	}

	for(var i = this.seasonLength; i < this.data.length; ++i)
	{
		forecast[i] = A[i-1] + B[i-1] + S[i - this.seasonLength];
	}

	for(var i = this.data.length; i < this.data.length + this.seasonLength; ++i)
	{
		forecast[i] = B[this.data.length-1] + (i - this.data.length + 1)*A[this.data.length-1] + S[i - this.seasonLength];
	}

	this.forecast = forecast;
	return forecast;
}

//Méthode de prédiction pour le cas multiplicatif
HoltWintersSmoothing.prototype.predictMult = function()
{
	A = Array();
	B = Array();
	S = Array();

	A[this.seasonLength-1] = 0;
	var averageFirstSeason = 0;
	for(var i = 0; i < this.seasonLength; ++i)
	{
		averageFirstSeason += this.data[i];
	} 
	B[this.seasonLength-1] = averageFirstSeason/this.seasonLength;

	for(var i = 0; i < this.seasonLength; ++i)
	{
		S[i] = (this.data[i])/(averageFirstSeason/this.seasonLength);
	}

	for(var i = this.seasonLength; i < this.data.length; ++i)
	{
		B[i] = this.alpha*(this.data[i]/S[i - this.seasonLength])+(1-this.alpha)*(B[i-1]+A[i-1]);
		A[i] = this.gamma*(B[i]-B[i-1])+(1-this.gamma)*A[i-1];
		S[i] = this.delta*(this.data[i]/B[i])+(1-this.delta)*S[i-this.seasonLength];
	}

	var forecast = Array();
	for(var i = 0; i < this.seasonLength; ++i)
	{
		forecast[i]= null;
	}

	for(var i = this.seasonLength; i < this.data.length; ++i)
	{
		forecast[i] = (A[i-1] + B[i-1])*S[i - this.seasonLength];
	}

	for(var i = this.data.length; i < this.data.length + this.seasonLength; ++i)
	{
		forecast[i] = (B[this.data.length-1] + (i - this.data.length + 1)*A[this.data.length-1])*S[i -this.seasonLength];
	}

	this.forecast = forecast;
	return forecast;
}

Trouver les valeurs optimales d'alpha, gamma et delta

Pour trouver les meilleures valeurs des paramètres alpha, gamma et delta nous cherchons de nouveau à minimiser l’erreur quadratique. Celle-ci est égale à 1/(n-1)*(valeur[1]-prediction[1] + valeur[2]-prediction[2] + … + valeur[n]-prediction[n]).

En Javascript cela donne :

HoltWintersSmoothing.prototype.computeMeanSquaredError = function()
{ 
	var SSE = 0.0;
	var n = 0;
	for(var i = 0; i < this.data.length; ++i)
	{
		if(this.data[i] != null && this.forecast[i] != null)
		{
			SSE += Math.pow(this.data[i] - this.forecast[i], 2);	
			n++;
		} 
		
	}
	return 1/(n-1)*SSE;
};

Pour trouver les meilleures valeurs, on itère entre 0 et 1 pour chaque paramètre, avec un pas passé en paramètre à notre méthode. L’objectif est de garder les valeurs d’alpha, gamma et delta permettant de minimiser l’erreur quadratique.

HoltWintersSmoothing.prototype.optimizeParameters = function(iter)
{
	var incr = 1/iter;
	var bestAlpha = 0.0;
	var bestError = -1;
	this.alpha = bestAlpha;
	var bestGamma = 0.0;
	this.gamma = bestGamma;
	var bestDelta = 0.0;
	this.delta = bestDelta;

	while(this.alpha < 1)
	{
		while(this.gamma < 1)
		{
			while(this.delta < 1)
			{
				var forecast = this.predict();
				var error = this.computeMeanSquaredError();
				if(error < bestError || bestError == -1)
				{
					bestAlpha = this.alpha;
					bestGamma = this.gamma;
					bestDelta = this.delta;
					bestError = error;
				}
				this.delta += incr;
			}
			this.delta = 0;
			this.gamma += incr;
		}
		this.gamma = 0;
		this.alpha += incr;
	}

	this.alpha = bestAlpha;
	this.gamma = bestGamma;
	this.delta = bestDelta;
	return {"alpha":this.alpha, "gamma":this.gamma, "delta":this.delta};
}

Une fois les paramètres optimaux estimés, il suffit de générer les prédictions associées aux valeurs de ces paramètres :

//On peut se permettre de passer alpha, gamma et delta à null si on souhaite les optimiser par la suite
var hws = new HoltWintersSmoothing(data, alpha, gamma, delta, seasonLength, false);
optimizedParameters = hws.optimizeParameters(20);
var forecast = hws.predict();

Représentation graphique avec HighchartJs

Nous allons utiliser la librairie HighchartJs afin de tracer nos deux séries (celle réalisée et celle prédite). Pour cela il suffit d’inclure Jquery, ainsi que le fichier Highcharts.js. Ensuite, nous insérons le code suivant, en modifiant les différents champs en fonction de nos besoins :

<script type="text/javascript">
    $(function () {
        $('#container').highcharts({
            title: {
                text: 'Chiffre d\'affaires mensuel',
                x: -20 //center
            },
            subtitle: {
                text: 'Source: fakeSource.com',
                x: -20
            },
            xAxis: {
                categories: ['Jan', 'Fev', 'Mar', 'Avr', 'Mai', 'Juin',
                    'Juil', 'Aout', 'Sept', 'Oct', 'Nov', 'Dec', 'Jan', 'Feb']
            },
            yAxis: {
                title: {
                    text: 'CA (k€)'
                },
                plotLines: [{
                    value: 0,
                    width: 1,
                    color: '#808080'
                }]
            },
            tooltip: {
                valueSuffix: 'k€'
            },
            legend: {
                layout: 'vertical',
                align: 'right',
                verticalAlign: 'middle',
                borderWidth: 0
            },
            series: [{
                name: 'Réalisé',
                data: data
            }, {
                name: 'Prévisions',
                data: forecast
            }]
        });
    });
</script>

<div id="container" style="min-width: 310px; height: 400px; margin: 0 auto"></div>


Nous obtenons le résultat ci-dessous :


Antoine Vastel

Head of research at Datadome.