From b362d9d782c09e9bac465323148fb7c5ca6d2152 Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Wed, 11 Jan 2017 23:59:09 +0100 Subject: [PATCH] Prepare further exponential smoothing --- .../Forecasting/ExponentialSmoothing.php | 28 -- .../ExponentialSmoothing/Brown.php | 42 ++- .../ExponentialSmoothingInterface.php | 37 +++ .../ExponentialSmoothing/ForecastType.php | 36 +++ .../Forecasting/ExponentialSmoothing/Holt.php | 265 ++++++++++++++++++ 5 files changed, 372 insertions(+), 36 deletions(-) delete mode 100644 Math/Finance/Forecasting/ExponentialSmoothing.php create mode 100644 Math/Finance/Forecasting/ExponentialSmoothing/ExponentialSmoothingInterface.php create mode 100644 Math/Finance/Forecasting/ExponentialSmoothing/ForecastType.php diff --git a/Math/Finance/Forecasting/ExponentialSmoothing.php b/Math/Finance/Forecasting/ExponentialSmoothing.php deleted file mode 100644 index cb8cf442c..000000000 --- a/Math/Finance/Forecasting/ExponentialSmoothing.php +++ /dev/null @@ -1,28 +0,0 @@ - - * @author Dennis Eichhorn - * @copyright 2013 Dennis Eichhorn - * @license OMS License 1.0 - * @version 1.0.0 - * @link http://orange-management.com - */ - namespace phpOMS\Math\Finance\Forecasting; - -class ExponentialSmoothing -{ - private $data = []; - - private $cycle = []; - - public function __construct(array $data) - { - - } -} diff --git a/Math/Finance/Forecasting/ExponentialSmoothing/Brown.php b/Math/Finance/Forecasting/ExponentialSmoothing/Brown.php index 783c4a974..372e0ba41 100644 --- a/Math/Finance/Forecasting/ExponentialSmoothing/Brown.php +++ b/Math/Finance/Forecasting/ExponentialSmoothing/Brown.php @@ -19,7 +19,7 @@ use phpOMS\Math\Finance\Forecasting\SmoothingType; use phpOMS\Math\Statistic\Average; use phpOMS\Math\Statistic\Forecast\Error; -class Brown +class Brown implements ExponentialSmoothingInterface { private $data = []; @@ -27,6 +27,8 @@ class Brown private $cycle = 0; + private $type = ForecastType::LINEAR; + private $rmse = 0.0; private $mse = 0.0; @@ -35,10 +37,10 @@ class Brown private $sse = 0.0; - public function __construct(array $data, int $cycle = 0) + public function __construct(array $data, int $cycle = 0, int $type = ForecastType::LINEAR) { $this->data = $data; - $this->cycle = $cycle; + $this->type = $type; } public function setCycle(int $cycle) /* : void */ @@ -73,7 +75,7 @@ class Brown public function getForecast(int $future = 1, int $smoothing = SmoothingType::CENTERED_MOVING_AVERAGE) : array { - $trendCycle = $this->getTrendCycle($this->cycle); + $trendCycle = $this->getTrendCycle($this->cycle, $smoothing); $seasonality = $this->getSeasonality($trendCycle); $seasonalityIndexMap = $this->generateSeasonalityMap($this->cycle, $seasonality); $adjustedSeasonalityIndexMap = $this->generateAdjustedSeasonalityMap($this->cycle, $seasonalityIndexMap); @@ -83,7 +85,7 @@ class Brown return $this->getReseasonalized($this->cycle, $optimizedForecast, $adjustedSeasonalityIndexMap); } - private function getTrendCycle(int $cycle) : array + private function getTrendCycle(int $cycle, int $smoothing) : array { $centeredMovingAverage = []; @@ -152,7 +154,7 @@ class Brown return $adjusted; } - private function forecast(int $future, float $alpha, array $data, array &$error) : array + private function forecastLinear(int $future, float $alpha, array $data, array &$error) : array { $forecast = []; $dataLength = count($data); @@ -172,7 +174,25 @@ class Brown return $forecast; } - private function getOptimizedForecast(int $future, array $adjustedData) : array + private function forecastSimple(int $future, float $alpha, array $data, array &$error) : array + { + $forecast = []; + $dataLength = count($data); + $length = $dataLength + $future; + + $forecast[0] = $data[0]; + + $error[0] = 0; + + for ($i = 1; $i < $length; $i++) { + $forecast[$i] = $alpha * $data[$i-1] + (1-$alpha)*$forecast[$i-1]; + $error[$i] = $i < $dataLength ? $data[$i] - $forecast[$i] : 0; + } + + return $forecast; + } + + private function getOptimizedForecast(int $future, array $adjustedDatae) : array { $this->rmse = PHP_INT_MAX; $alpha = 0.00; @@ -180,7 +200,13 @@ class Brown while ($alpha < 1) { $error = []; - $tempForecast = $this->forecast($future, $alpha, $adjustedData, $error); + + if($this->type === ForecastType::LINEAR) { + $tempForecast = $this->forecastLinear($future, $alpha, $adjustedData, $error); + } else { + $tempForecast = $this->forecastSimple($future, $alpha, $adjustedData, $error); + } + $alpha += 0.001; $tempRMSE = Error::getRootMeanSquaredError($error); diff --git a/Math/Finance/Forecasting/ExponentialSmoothing/ExponentialSmoothingInterface.php b/Math/Finance/Forecasting/ExponentialSmoothing/ExponentialSmoothingInterface.php new file mode 100644 index 000000000..e268cdbc7 --- /dev/null +++ b/Math/Finance/Forecasting/ExponentialSmoothing/ExponentialSmoothingInterface.php @@ -0,0 +1,37 @@ + + * @author Dennis Eichhorn + * @copyright 2013 Dennis Eichhorn + * @license OMS License 1.0 + * @version 1.0.0 + * @link http://orange-management.com + */ + namespace phpOMS\Math\Finance\Forecasting\ExponentialSmoothing; + +use phpOMS\Math\Finance\Forecasting\SmoothingType; +use phpOMS\Math\Statistic\Average; +use phpOMS\Math\Statistic\Forecast\Error; + +interface ExponentialSmoothingInterface +{ + public function setCycle(int $cycle) /* : void */; + + public function getRMSE() : float; + + public function getMSE() : float; + + public function getMAE() : float; + + public function getSSE() : float; + + public function getErrors() : array; + + public function getForecast(int $future = 1, int $smoothing = SmoothingType::CENTERED_MOVING_AVERAGE) : array; +} diff --git a/Math/Finance/Forecasting/ExponentialSmoothing/ForecastType.php b/Math/Finance/Forecasting/ExponentialSmoothing/ForecastType.php new file mode 100644 index 000000000..7832e4c04 --- /dev/null +++ b/Math/Finance/Forecasting/ExponentialSmoothing/ForecastType.php @@ -0,0 +1,36 @@ + + * @author Dennis Eichhorn + * @copyright 2013 Dennis Eichhorn + * @license OMS License 1.0 + * @version 1.0.0 + * @link http://orange-management.com + */ +namespace phpOMS\Math\Finance\Forecasting\ExponentialSmoothing; + +use phpOMS\Datatypes\Enum; + +/** + * Smoothing enum. + * + * @category Framework + * @package phpOMS\Html + * @author OMS Development Team + * @author Dennis Eichhorn + * @license OMS License 1.0 + * @link http://orange-management.com + * @since 1.0.0 + */ +abstract class ForecastType extends Enum +{ + /* public */ const SIMPLE = 1; + /* public */ const LINEAR = 2; + /* public */ const EXPONENTIAL = 3; +} diff --git a/Math/Finance/Forecasting/ExponentialSmoothing/Holt.php b/Math/Finance/Forecasting/ExponentialSmoothing/Holt.php index e69de29bb..935bf097e 100644 --- a/Math/Finance/Forecasting/ExponentialSmoothing/Holt.php +++ b/Math/Finance/Forecasting/ExponentialSmoothing/Holt.php @@ -0,0 +1,265 @@ + + * @author Dennis Eichhorn + * @copyright 2013 Dennis Eichhorn + * @license OMS License 1.0 + * @version 1.0.0 + * @link http://orange-management.com + */ + namespace phpOMS\Math\Finance\Forecasting\ExponentialSmoothing; + +use phpOMS\Math\Finance\Forecasting\SmoothingType; +use phpOMS\Math\Statistic\Average; +use phpOMS\Math\Statistic\Forecast\Error; + +class Holt implements ExponentialSmoothingInterface +{ + private $data = []; + + private $errors = []; + + private $cycle = 0; + private $damping = 0; + private $type = 0; + + private $rmse = 0.0; + + private $mse = 0.0; + + private $mae = 0.0; + + private $sse = 0.0; + + public function __construct(array $data, int $cycle = 0, int $type = ForecastType::LINEAR, float $damping = 1) + { + $this->data = $data; + $this->cycle = $cycle; + $this->type = $type; + $this->damping = $damping; + } + + public function setCycle(int $cycle) /* : void */ + { + $this->cycle = $cycle; + } + + public function getRMSE() : float + { + return $this->rmse; + } + + public function getMSE() : float + { + return $this->mse; + } + + public function getMAE() : float + { + return $this->mae; + } + + public function getSSE() : float + { + return $this->sse; + } + + public function getErrors() : array + { + return $this->errors; + } + + public function getForecast(int $future = 1, int $smoothing = SmoothingType::CENTERED_MOVING_AVERAGE) : array + { + $trendCycle = $this->getTrendCycle($this->cycle, $smoothing); + $seasonality = $this->getSeasonality($trendCycle); + $seasonalityIndexMap = $this->generateSeasonalityMap($this->cycle, $seasonality); + $adjustedSeasonalityIndexMap = $this->generateAdjustedSeasonalityMap($this->cycle, $seasonalityIndexMap); + $adjustedData = $this->getAdjustedData($this->cycle, $adjustedSeasonalityIndexMap); + $optimizedForecast = $this->getOptimizedForecast($future, $adjustedData); + + return $this->getReseasonalized($this->cycle, $optimizedForecast, $adjustedSeasonalityIndexMap); + } + + private function getTrendCycle(int $cycle, int $smoothing) : array + { + $centeredMovingAverage = []; + + $length = count($this->data); + for ($i = $cycle; $i < $length - $cycle; $i++) { + $centeredMovingAverage[$i] = Average::arithmeticMean(array_slice($this->data, $i - $cycle, $cycle)); + } + + return $centeredMovingAverage; + } + + private function getSeasonality(array $trendCycle) : array + { + $seasonality = []; + foreach ($trendCycle as $key => $value) { + $seasonality[$key] = $this->data[$key] / $value; + } + + return $seasonality; + } + + private function generateSeasonalityMap(int $cycle, array $seasonality) : array + { + $map = []; + foreach ($seasonality as $key => $value) { + $map[$key % $cycle][] = $value; + } + + foreach ($map as $key => $value) { + $map[$key] = Average::arithmeticMean($value); + } + + return $map; + } + + private function generateAdjustedSeasonalityMap(int $cycle, array $seasonality) : array + { + $total = array_sum($seasonality); + + foreach ($seasonality as $key => $value) { + $seasonality[$key] = $cycle * $value / $total; + } + + return $seasonality; + } + + private function getSeasonalIndex(int $cycle, array $seasonalityMap) : array + { + $index = []; + + foreach ($this->data as $key => $value) { + $index[$key] = $seasonalityMap[$key % $cycle]; + } + + return $index; + } + + private function getAdjustedData(int $cycle, array $seasonalIndex) : array + { + $adjusted = []; + + foreach ($this->data as $key => $value) { + $adjusted[$key] = $value / ($seasonalIndex[$key % $cycle] === 0 ? 1 : $seasonalIndex[$key % $cycle]); + } + + return $adjusted; + } + + private function forecastLinear(int $future, float $alpha, float $beta, array $data, array &$error) : array + { + $forecast = []; + $dataLength = count($data); + $length = $dataLength + $future; + + $forecast[0] = $data[0]; + + $error[0] = 0; + + $level[0] = $data[0]; + $trend[0] = $data[1] - $data[0]; + + for ($i = 1; $i < $length; $i++) { + $level[$i] = $alpha * $data[$i-1] + (1 - $alpha) * ($level[$i-1] + $this->damping * $trend[$i-1]); + $trend[$i] = $beta * ($level[$i] - $level[$i-1]) + (1 - $beta) * $this->damping * $trend[$i - 1]; + $forecast[$i] = $level[$i] + $this->dampingSum($this->damping, $i) * $trend[$i]; + $error[$i] = $i < $dataLength ? $data[$i] - $forecast[$i] : 0; + } + + return $forecast; + } + + private function dampingSum(float $damping, int $future) : float + { + $sum = 0; + + for($i = 1; $i < $future; $i++) { + $sum += pow($damping, $i); + } + + return $sum; + } + + private function forecastExponential(int $future, float $alpha, float $beta, array $data, array &$error) : array + { + $forecast = []; + $dataLength = count($data); + $length = $dataLength + $future; + + $forecast[0] = $data[0]; + + $error[0] = 0; + + $level[0] = $data[0]; + $trend[0] = $data[1] - $data[0]; + + for ($i = 1; $i < $length; $i++) { + $level[$i] = $alpha * $data[$i-1] + (1 - $alpha) * $level[$i-1] * pow($trend[$i-1], $this->damping); + $trend[$i] = $beta * $level[$i] / $level[$i-1] + (1 - $beta) * pow($trend[$i - 1], $this->damping); + $forecast[$i] = $level[$i] * pow($trend[$i], $this->dampingSum($this->damping, $i)); + $error[$i] = $i < $dataLength ? $data[$i] - $forecast[$i] : 0; + } + + return $forecast; + } + + private function getOptimizedForecast(int $future, array $adjustedData) : array + { + $this->rmse = PHP_INT_MAX; + $alpha = 0.00; + $forecast = []; + + while ($alpha < 1) { + $beta = 0.00; + + while($beta < 1) { + $error = []; + + if($this->type === ForecastType::LINEAR) { + $tempForecast = $this->forecastLinear($future, $alpha, $beta, $adjustedData, $error); + } else { + $tempForecast = $this->forecastExponential($future, $alpha, $beta, $adjustedData, $error); + } + + $beta += 0.01; + + $tempRMSE = Error::getRootMeanSquaredError($error); + + if ($tempRMSE < $this->rmse) { + $this->rmse = $tempRMSE; + $forecast = $tempForecast; + } + } + + $alpha += 0.001; + } + + $this->errors = $error; + $this->mse = Error::getMeanSquaredError($error); + $this->mae = Error::getMeanAbsoulteError($error); + $this->sse = Error::getSumSquaredError($error); + + return $forecast; + } + + private function getReseasonalized(int $cycle, array $forecast, array $seasonalIndex) : array + { + $reSeasonalized = []; + + foreach ($forecast as $key => $value) { + $reSeasonalized[$key] = $value * ($seasonalIndex[$key % $cycle] === 0 ? 1 : $seasonalIndex[$key % $cycle]); + } + + return $reSeasonalized; + } +}