mirror of
https://github.com/Karaka-Management/phpOMS.git
synced 2026-01-11 17:58:41 +00:00
Improved Exponential smoothing implementation draft
This commit is contained in:
parent
b362d9d782
commit
9fe8aa95a5
|
|
@ -1,238 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Orange Management
|
||||
*
|
||||
* PHP Version 7.1
|
||||
*
|
||||
* @category TBD
|
||||
* @package TBD
|
||||
* @author OMS Development Team <dev@oms.com>
|
||||
* @author Dennis Eichhorn <d.eichhorn@oms.com>
|
||||
* @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 Brown implements ExponentialSmoothingInterface
|
||||
{
|
||||
private $data = [];
|
||||
|
||||
private $errors = [];
|
||||
|
||||
private $cycle = 0;
|
||||
|
||||
private $type = ForecastType::LINEAR;
|
||||
|
||||
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)
|
||||
{
|
||||
$this->data = $data;
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
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, array $data, array &$error) : array
|
||||
{
|
||||
$forecast = [];
|
||||
$dataLength = count($data);
|
||||
$length = $dataLength + $future;
|
||||
|
||||
$forecast[0] = $data[0];
|
||||
$forecast[1] = $data[1];
|
||||
|
||||
$error[0] = 0;
|
||||
$error[1] = $data[1] - $forecast[1];
|
||||
|
||||
for ($i = 2; $i < $length; $i++) {
|
||||
$forecast[$i] = 2 * ($data[$i - 1] ?? $forecast[$i - 1]) - ($data[$i - 2] ?? $forecast[$i - 2]) - 2 * (1 - $alpha) * $error[$i - 1] + pow(1 - $alpha, 2) * $error[$i - 2];
|
||||
$error[$i] = $i < $dataLength ? $data[$i] - $forecast[$i] : 0;
|
||||
}
|
||||
|
||||
return $forecast;
|
||||
}
|
||||
|
||||
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;
|
||||
$forecast = [];
|
||||
|
||||
while ($alpha < 1) {
|
||||
$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);
|
||||
|
||||
if ($tempRMSE < $this->rmse) {
|
||||
$this->rmse = $tempRMSE;
|
||||
$forecast = $tempForecast;
|
||||
}
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,285 @@
|
|||
@@ -1,265 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Orange Management
|
||||
*
|
||||
* PHP Version 7.1
|
||||
*
|
||||
* @category TBD
|
||||
* @package TBD
|
||||
* @author OMS Development Team <dev@oms.com>
|
||||
* @author Dennis Eichhorn <d.eichhorn@oms.com>
|
||||
* @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 $rmse = 0.0;
|
||||
|
||||
private $mse = 0.0;
|
||||
|
||||
private $mae = 0.0;
|
||||
|
||||
private $sse = 0.0;
|
||||
|
||||
public function __construct(array $data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
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 forecast(int $future, int $trendType = TrendType::NONE, int $seasonalType = SeasonalType::NONE, int $cycle = 12, float $damping = 1) : array
|
||||
{
|
||||
if($trendType === TrendType::ALL || $seasonalType === SeasonalType::ALL) {
|
||||
// todo: loob through all and find best
|
||||
} elseif($trendType === TrendType::NONE && $seasonalType === SeasonalType::NONE) {
|
||||
return $this->getNoneNone($future, $alpha);
|
||||
} elseif($trendType === TrendType::NONE && $seasonalType === SeasonalType::ADDITIVE) {
|
||||
return $this->getNoneAdditive($future, $alpha, $gamma, $cycle);
|
||||
} elseif($trendType === TrendType::NONE && $seasonalType === SeasonalType::MULTIPLICATIVE) {
|
||||
return $this->getNoneMultiplicative($future, $alpha, $gamma, $cycle);
|
||||
} elseif($trendType === TrendType::ADDITIVE && $seasonalType === SeasonalType::NONE) {
|
||||
return $this->getAdditiveNone($future, $alpha, $damping);
|
||||
} elseif($trendType === TrendType::ADDITIVE && $seasonalType === SeasonalType::ADDITIVE) {
|
||||
return $this->getAdditiveAdditive($future, $alpha, $beta, $gamma, $cycle, $damping);
|
||||
} elseif($trendType === TrendType::ADDITIVE && $seasonalType === SeasonalType::MULTIPLICATIVE) {
|
||||
return $this->getAdditiveMultiplicative($future, $alpha, $beta, $gamma, $cycle, $damping);
|
||||
} elseif($trendType === TrendType::MULTIPLICATIVE && $seasonalType === SeasonalType::NONE) {
|
||||
return $this->getMultiplicativeNone($future);
|
||||
} elseif($trendType === TrendType::MULTIPLICATIVE && $seasonalType === SeasonalType::ADDITIVE) {
|
||||
return $this->getMultiplicativeAdditive($future, $cycle, $damping);
|
||||
} elseif($trendType === TrendType::MULTIPLICATIVE && $seasonalType === SeasonalType::MULTIPLICATIVE) {
|
||||
return $this->getMultiplicativeMultiplicative($future, $cycle, $damping);
|
||||
}
|
||||
}
|
||||
|
||||
private dampingSum(float $damping, int $length) : float
|
||||
{
|
||||
$sum = 0;
|
||||
for($i = 0; $i < $length; $i++) {
|
||||
$sum += pow($damping, $i);
|
||||
}
|
||||
|
||||
return $sum;
|
||||
}
|
||||
|
||||
public function getNoneNone($future, $alpha) : array
|
||||
{
|
||||
$level[0] = $data[0];
|
||||
$dataLength = count($this->data) + $future;
|
||||
$forecast = [];
|
||||
|
||||
for($i = 1; $i < $dataLength; $i++) {
|
||||
$level[] = $alpha * $this->data[$i-1] + (1 - $alpha) * $level[$i-1];
|
||||
|
||||
$forecast[] = $level[$i];
|
||||
}
|
||||
|
||||
return $forecast;
|
||||
}
|
||||
|
||||
public function getNoneAdditive(int $future, float $alpha, float $gamma, int $cycle) : array
|
||||
{
|
||||
$level[0] = $data[0];
|
||||
$dataLength = count($this->data) + $future;
|
||||
$forecast = [];
|
||||
$gamma_ = $gamma * (1 - $alpha);
|
||||
|
||||
for($i = 0; $i < $cycle; $i++) {
|
||||
$seasonal[$i] = $data[$i] - $level[0];
|
||||
}
|
||||
|
||||
for($i = 1; $i < $dataLength; $i++) {
|
||||
$hm = (int) floor(($i-1) % $cycle) + 1;
|
||||
|
||||
$level[] = $alpha * ($this->data[$i-1] - $seasonal[$i]) + (1 - $alpha) * $level[$i-1];
|
||||
$seasonal[] = $gamma_*($this->data[$i-1] - $level[$i-1]) + (1 - $gamma_) * $seasonal[$i];
|
||||
|
||||
$forecast[] = $level[$i] + $seasonal[$i+$hm];
|
||||
}
|
||||
|
||||
return $forecast;
|
||||
}
|
||||
|
||||
public function getNoneMultiplicative(int $future, float $alpha, float $gamma, int $cycle) : array
|
||||
{
|
||||
$level[0] = $data[0];
|
||||
$dataLength = count($this->data) + $future;
|
||||
$forecast = [];
|
||||
$gamma_ = $gamma * (1 - $alpha);
|
||||
|
||||
for($i = 0; $i < $cycle; $i++) {
|
||||
$seasonal[$i] = $this->data[$i] / $level[0];
|
||||
}
|
||||
|
||||
for($i = 1; $i < $dataLength; $i++) {
|
||||
$hm = (int) floor(($i-1) % $cycle) + 1;
|
||||
|
||||
$level[] = $alpha * ($this->data[$i-1] / $seasonal[$i]) + (1 - $alpha) * $level[$i-1];
|
||||
$seasonal[] = $gamma_*($this->data[$i-1] / $level[$i-1]) + (1 - $gamma_) * $seasonal[$i]
|
||||
|
||||
$forecast[] = $level[$i] + $seasonal[$i+$hm];
|
||||
}
|
||||
|
||||
return $forecast;
|
||||
}
|
||||
|
||||
public function getAdditiveNone(int $future, float $alpha, float $damping) : array
|
||||
{
|
||||
$level[0] = $this->data[0];
|
||||
$trend[0] = $this->data[1] - $this->data[0];
|
||||
$dataLength = count($this->data) + $future;
|
||||
$forecast = [];
|
||||
|
||||
for($i = 1; $i < $dataLength; $i++) {
|
||||
$level[] = $alpha * $this->data[$i-1] + (1 - $alpha) * ($level[$i-1] + $damping * $trend[$i-1]);
|
||||
$trend[] = $beta * ($level[$i] - $level[$i-1]) + (1 - $beta) * $damping * $trend[$i-1];
|
||||
|
||||
$forecast[] = $level[$i] + $this->dampingSum($damping, $i) * $trend[$i];
|
||||
}
|
||||
|
||||
return $forecast;
|
||||
}
|
||||
|
||||
public function getAdditiveAdditive(int $future, float $alpha, float $beta, float $gamma, int $cycle, float $damping) : array
|
||||
{
|
||||
$level[0] = $1 / $cycle * array_sum($this->data, 0, $cycle);
|
||||
$trend[0] = 1 / $cycle;
|
||||
$dataLength = count($this->data) + $future;
|
||||
$forecast = [];
|
||||
$gamma_ = $gamma * (1 - $alpha);
|
||||
|
||||
$sum = 0;
|
||||
for($i = 0; $i < $cycle; $i++) {
|
||||
$sum += ($this->data[$cycle] - $this->data[$i]) / $cycle;
|
||||
}
|
||||
|
||||
$trend[0] *= $sum;
|
||||
|
||||
for($i = 0; $i < $cycle; $i++) {
|
||||
$seasonal[$i] = $data[$i] - $level[0];
|
||||
}
|
||||
|
||||
for($i = 1; $i < $dataLength; $i++) {
|
||||
$hm = (int) floor(($i-1) % $cycle) + 1;
|
||||
|
||||
$level[] = $alpha * ($this->data[$i-1] - $seasonal[$i]) + (1 - $alpha) * ($level[$i-1] + $damping * $trend[$i-1]);
|
||||
$trend[] = $beta * ($level[$i] - $level[$i-1]) + (1 - $beta) * $damping * $trend[$i-1];
|
||||
$seasonal[] = $gamma_*($this->data[$i-1] - $level[$i-1]) + (1 - $gamma_) * $seasonal[$i];
|
||||
|
||||
$forecast[] = $level[$i] + $this->dampingSum($damping, $i) * $trend[$i] + $seasonal[$i+$hm];
|
||||
}
|
||||
|
||||
return $forecast;
|
||||
}
|
||||
|
||||
public function getAdditiveMultiplicative(int $future, float $alpha, float $beta, float $gamma, int $cycle, float $damping) : array
|
||||
{
|
||||
$level[0] = 1 / $cycle * array_sum($this->data, 0, $cycle);
|
||||
$trend[0] = 1 / $cycle;
|
||||
$dataLength = count($this->data) + $future;
|
||||
$forecast = [];
|
||||
$gamma_ = $gamma * (1 - $alpha);
|
||||
|
||||
$sum = 0;
|
||||
for($i = 0; $i < $cycle; $i++) {
|
||||
$sum += ($this->data[$cycle] - $this->data[$i]) / $cycle;
|
||||
}
|
||||
|
||||
$trend[0] *= $sum;
|
||||
|
||||
for($i = 0; $i < $cycle; $i++) {
|
||||
$seasonal[$i] = $this->data[$i] / $level[0];
|
||||
}
|
||||
|
||||
for($i = 1; $i < $dataLength; $i++) {
|
||||
$hm = (int) floor(($i-1) % $cycle) + 1;
|
||||
|
||||
$level[] = $alpha * ($this->data[$i-1] / $seasonal[$i]) + (1 - $alpha) * ($level[$i-1] + $damping * $trend[$i-1]);
|
||||
$trend[] = $beta * ($level[$i] - $level[$i-1]) + (1 - $beta) * $damping * $trend[$i-1];
|
||||
$seasonal[] = $gamma_*($this->data[$i-1] / ($level[$i-1] + $damping * $trend[$i-1]) + (1 - $gamma_) * $seasonal[$i];
|
||||
|
||||
$forecast[] = ($level[$i] + $this->dampingSum($damping, $i) * $trend[$i-1]) * $seasonal[$i+$hm];
|
||||
}
|
||||
|
||||
return $forecast;
|
||||
}
|
||||
|
||||
public function getMultiplicativeNone() : array
|
||||
{
|
||||
$level[0] = $this->data[0];
|
||||
$trend[0] = $this->data[1] / $this->data[0];
|
||||
}
|
||||
|
||||
public function getMultiplicativeAdditive() : array
|
||||
{
|
||||
$level[0] = $this->data[0];
|
||||
$trend[0] = 1 / $cycle;
|
||||
|
||||
$sum = 0;
|
||||
for($i = 0; $i < $cycle; $i++) {
|
||||
$sum += ($this->data[$cycle] - $this->data[$i]) / $cycle;
|
||||
}
|
||||
|
||||
$trend[0] *= $sum;
|
||||
|
||||
for($i = 0; $i < $cycle; $i++) {
|
||||
$seasonal[$i] = $data[$i] - $level[0];
|
||||
}
|
||||
}
|
||||
|
||||
public function getMultiplicativeMultiplicative() : array
|
||||
{
|
||||
$level[0] = $this->data[0];
|
||||
|
||||
$trend[0] = 1 / $cycle;
|
||||
|
||||
$sum = 0;
|
||||
for($i = 0; $i < $cycle; $i++) {
|
||||
$sum += ($this->data[$cycle] - $this->data[$i]) / $cycle;
|
||||
}
|
||||
|
||||
$trend[0] *= $sum;
|
||||
|
||||
for($i = 0; $i < $cycle; $i++) {
|
||||
$seasonal[$i] = $this->data[$i] / $level[0];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Orange Management
|
||||
*
|
||||
* PHP Version 7.1
|
||||
*
|
||||
* @category TBD
|
||||
* @package TBD
|
||||
* @author OMS Development Team <dev@oms.com>
|
||||
* @author Dennis Eichhorn <d.eichhorn@oms.com>
|
||||
* @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;
|
||||
}
|
||||
|
|
@ -1,265 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Orange Management
|
||||
*
|
||||
* PHP Version 7.1
|
||||
*
|
||||
* @category TBD
|
||||
* @package TBD
|
||||
* @author OMS Development Team <dev@oms.com>
|
||||
* @author Dennis Eichhorn <d.eichhorn@oms.com>
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
/**
|
||||
* Orange Management
|
||||
*
|
||||
* PHP Version 7.1
|
||||
*
|
||||
* @category TBD
|
||||
* @package TBD
|
||||
* @author OMS Development Team <dev@oms.com>
|
||||
* @author Dennis Eichhorn <d.eichhorn@oms.com>
|
||||
* @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 <dev@oms.com>
|
||||
* @author Dennis Eichhorn <d.eichhorn@oms.com>
|
||||
* @license OMS License 1.0
|
||||
* @link http://orange-management.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
abstract class SeasonalType extends Enum
|
||||
{
|
||||
/* public */ const ALL = 0;
|
||||
/* public */ const NONE = 1;
|
||||
/* public */ const ADDITIVE = 2;
|
||||
/* public */ const MULTIPLICATIVE = 4;
|
||||
}
|
||||
|
|
@ -28,9 +28,10 @@ use phpOMS\Datatypes\Enum;
|
|||
* @link http://orange-management.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
abstract class ForecastType extends Enum
|
||||
abstract class TrendType extends Enum
|
||||
{
|
||||
/* public */ const SIMPLE = 1;
|
||||
/* public */ const LINEAR = 2;
|
||||
/* public */ const EXPONENTIAL = 3;
|
||||
/* public */ const ALL = 0;
|
||||
/* public */ const NONE = 1;
|
||||
/* public */ const ADDITIVE = 2;
|
||||
/* public */ const MULTIPLICATIVE = 4;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user