Search code examples
phpprojectionecommerce-sales

How do I use Holt-Winters Seasonal Dampened Method to compute a two-month sales projection in PHP?


Holt-Winters is introduced here:

http://en.wikipedia.org/wiki/Holt-Winters

The Seasonal Dampened version of it is discussed here (scroll down the page):

http://otexts.com/fpp/7/5/

In a nutshell, it basically looks at 3 things:

  • long-term trend
  • short-term trend
  • seasonal trend

It also doesn't average those together, because really what you need is weighted averaging, where seasonal and short-term are more significant than long-term trend, naturally, with financial data trends.

Given $anYear1 and $anYear2, how do I apply the Holt-Winters Seasonal Dampened Method to forecast 2 more months past the end of $anYear2? Assume $anYear1 is an array of 12 numbers. Assume $anYear2 is an array of a range of 0 to 12 numbers.

So, I can fill it with random data like so:

<?php

$anYear1 = array();
$anYear2 = array();
$nStop = 10; // so we need 11 and 12 of the year
for ($i = 1; $i <= 12; $i++) {
  $anYear1[$i] = rand(200,500);
  if ($i <= $nStop) {
    // give it a natural lift like real financial data
    $anYear2[$i] = rand(400,700); 
  }
}
$nSeasonRange = 4; // 4 months in a business quarter

Therefore, I want to create a function like so:

function forecastHoltWinters($anYear1, $anYear2, $nSeasonRange = 4) {
  ///////////////////
  // DO MAGIC HERE //
  ///////////////////

  // an array with 2 numbers, indicating 2 months forward from end of $anYear2
  return $anForecast;
}

$anForecast = forecastHoltWinters($anYear1, $anYear2, $nSeasonRange);
echo "YEAR 1\n";
print_r($anYear1);
echo "\n\nYEAR 2\n"
print_r($anYear2);
echo "\n\nTWO MONTHS FORECAST\n";
print_r($anForecast);

Note: I have found a Github example here, but it doesn't show how to do a projection. It is also discussed here.


Solution

  • I found a way to adapt Ian Barber's function to do what I needed.

    <?php
    
    error_reporting(E_ALL);
    ini_set('display_errors','On');
    
    $anYear1 = array();
    $anYear2 = array();
    $nStop = 10;
    for($i = 1; $i <= 12; $i++) {
        $anYear1[$i] = rand(100,400);
        if ($i <= $nStop) {
            $anYear2[$i+12] = rand(200,600);
        }
    }
    
    print_r($anYear1);
    print_r($anYear2);
    $anData = array_merge($anYear1,$anYear2);
    print_r(forecastHoltWinters($anData));
    
    function forecastHoltWinters($anData, $nForecast = 2, $nSeasonLength = 4, $nAlpha = 0.2, $nBeta = 0.01, $nGamma = 0.01, $nDevGamma = 0.1) {
    
        // Calculate an initial trend level
        $nTrend1 = 0;
        for($i = 0; $i < $nSeasonLength; $i++) {
          $nTrend1 += $anData[$i];
        }
        $nTrend1 /= $nSeasonLength;
    
        $nTrend2 = 0;
        for($i = $nSeasonLength; $i < 2*$nSeasonLength; $i++) {
          $nTrend2 += $anData[$i];
        }
        $nTrend2 /= $nSeasonLength;
    
        $nInitialTrend = ($nTrend2 - $nTrend1) / $nSeasonLength;
    
        // Take the first value as the initial level
        $nInitialLevel = $anData[0];
    
        // Build index
        $anIndex = array();
        foreach($anData as $nKey => $nVal) {
          $anIndex[$nKey] = $nVal / ($nInitialLevel + ($nKey + 1) * $nInitialTrend);
        }
    
        // Build season buffer
        $anSeason = array_fill(0, count($anData), 0);
        for($i = 0; $i < $nSeasonLength; $i++) {
          $anSeason[$i] = ($anIndex[$i] + $anIndex[$i+$nSeasonLength]) / 2;
        }
    
        // Normalise season
        $nSeasonFactor = $nSeasonLength / array_sum($anSeason);
        foreach($anSeason as $nKey => $nVal) {
          $anSeason[$nKey] *= $nSeasonFactor;
        }
    
        $anHoltWinters = array();
        $anDeviations = array();
        $nAlphaLevel = $nInitialLevel;
        $nBetaTrend = $nInitialTrend;
        foreach($anData as $nKey => $nVal) {
          $nTempLevel = $nAlphaLevel;
          $nTempTrend = $nBetaTrend;
    
          $nAlphaLevel = $nAlpha * $nVal / $anSeason[$nKey] + (1.0 - $nAlpha) * ($nTempLevel + $nTempTrend);
          $nBetaTrend = $nBeta * ($nAlphaLevel - $nTempLevel) + ( 1.0 - $nBeta ) * $nTempTrend;
    
          $anSeason[$nKey + $nSeasonLength] = $nGamma * $nVal / $nAlphaLevel + (1.0 - $nGamma) * $anSeason[$nKey];
    
          $anHoltWinters[$nKey] = ($nAlphaLevel + $nBetaTrend * ($nKey + 1)) * $anSeason[$nKey];
          $anDeviations[$nKey] = $nDevGamma * abs($nVal - $anHoltWinters[$nKey]) + (1-$nDevGamma) 
                      * (isset($anDeviations[$nKey - $nSeasonLength]) ? $anDeviations[$nKey - $nSeasonLength] : 0);
        }
    
        $anForecast = array();
        $nLast = end($anData);
        for($i = 1; $i <= $nForecast; $i++) {
           $nComputed = round($nAlphaLevel + $nBetaTrend * $anSeason[$nKey + $i]);
           if ($nComputed < 0) { // wildly off due to outliers
             $nComputed = $nLast;
           }
           $anForecast[] = $nComputed;
        }
    
        return $anForecast;
    }