mirror of
https://github.com/Karaka-Management/phpOMS.git
synced 2026-01-11 09:48:40 +00:00
235 lines
6.6 KiB
PHP
235 lines
6.6 KiB
PHP
<?php
|
|
/**
|
|
* Orange Management
|
|
*
|
|
* PHP Version 7.2
|
|
*
|
|
* @package Framework
|
|
* @copyright Dennis Eichhorn
|
|
* @license OMS License 1.0
|
|
* @version 1.0.0
|
|
* @link http://website.orange-management.de
|
|
*/
|
|
declare(strict_types=1);
|
|
|
|
namespace phpOMS\Business\Finance\Forecasting;
|
|
|
|
use phpOMS\Math\Statistic\Average;
|
|
|
|
/**
|
|
* Classical decomposition class.
|
|
*
|
|
* This can be used to simplify time series patterns for forecasts.
|
|
*
|
|
* @package Framework
|
|
* @license OMS License 1.0
|
|
* @link http://website.orange-management.de
|
|
* @see https://www.otexts.org/fpp/6/1
|
|
* @since 1.0.0
|
|
*/
|
|
class ClassicalDecomposition
|
|
{
|
|
/**
|
|
* Decomposition mode.
|
|
*
|
|
* @var int
|
|
* @since 1.0.0
|
|
*/
|
|
public const ADDITIVE = 0;
|
|
|
|
/**
|
|
* Decomposition mode.
|
|
*
|
|
* @var int
|
|
* @since 1.0.0
|
|
*/
|
|
public const MULTIPLICATIVE = 1;
|
|
|
|
/**
|
|
* Decomposition mode.
|
|
*
|
|
* @var int
|
|
* @since 1.0.0
|
|
*/
|
|
private $mode = self::ADDITIVE;
|
|
|
|
/**
|
|
* Raw data.
|
|
*
|
|
* @var array
|
|
* @since 1.0.0
|
|
*/
|
|
private $data = [];
|
|
|
|
/**
|
|
* Order or seasonal period.
|
|
*
|
|
* @var int
|
|
* @since 1.0.0
|
|
*/
|
|
private $order = 0;
|
|
|
|
/**
|
|
* Raw data size.
|
|
*
|
|
* @var int
|
|
* @since 1.0.0
|
|
*/
|
|
private $dataSize = 0;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param array $data Historic data
|
|
* @param int $order Seasonal period (e.g. 4 = quarterly, 12 = monthly, 7 = weekly pattern in daily data)
|
|
* @param int $mode Decomposition mode
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
public function __construct(array $data, int $order, int $mode = self::ADDITIVE)
|
|
{
|
|
$this->mode = $mode;
|
|
$this->data = $data;
|
|
$this->order = $order;
|
|
|
|
$this->dataSize = \count($data);
|
|
}
|
|
|
|
/**
|
|
* Get decomposition.
|
|
*
|
|
* @return array Returns an array containing the trend cycle component, detrended series, seasonal component and remainder component.
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
public function getDecomposition() : array
|
|
{
|
|
$trendCycleComponent = self::computeTrendCycle($this->data, $this->order);
|
|
$detrendedSeries = self::computeDetrendedSeries($this->data, $trendCycleComponent, $this->mode);
|
|
$seasonalComponent = $this->computeSeasonalComponent($detrendedSeries, $this->order);
|
|
$remainderComponent = self::computeRemainderComponent($this->data, $trendCycleComponent, $seasonalComponent, $this->mode);
|
|
|
|
return [
|
|
'trendCycleComponent' => $trendCycleComponent,
|
|
'detrendedSeries' => $detrendedSeries,
|
|
'seasonalComponent' => $seasonalComponent,
|
|
'remainderComponent' => $remainderComponent,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Calculate trend cycle
|
|
*
|
|
* @param array $data Data to analyze
|
|
* @param int $order Seasonal period (e.g. 4 = quarterly, 12 = monthly, 7 = weekly pattern in daily data)
|
|
*
|
|
* @return array Total moving average 2 x m-MA
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
public static function computeTrendCycle(array $data, int $order) : array
|
|
{
|
|
$mMA = Average::totalMovingAverage($data, $order, null, true);
|
|
|
|
return $order % 2 === 0 ? Average::totalMovingAverage($mMA, $order, null, true) : $mMA;
|
|
}
|
|
|
|
/**
|
|
* Calculate detrended series
|
|
*
|
|
* @param array $data Data to analyze
|
|
* @param array $trendCycleComponent Trend cycle component
|
|
* @param int $mode Detrend mode
|
|
*
|
|
* @return array Detrended series / seasonal normalized data
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
public static function computeDetrendedSeries(array $data, array $trendCycleComponent, int $mode) : array
|
|
{
|
|
$detrended = [];
|
|
$count = \count($trendCycleComponent);
|
|
$start = self::getStartOfDecomposition(\count($data), $count);
|
|
|
|
for ($i = 0; $i < $count; ++$i) {
|
|
$detrended[] = $mode === self::ADDITIVE ? $data[$start + $i] - $trendCycleComponent[$i] : $data[$start + $i] / $trendCycleComponent[$i];
|
|
}
|
|
|
|
return $detrended;
|
|
}
|
|
|
|
/**
|
|
* Calculate the data start point for the decomposition
|
|
*
|
|
* By using averaging methods some initial data get's incorporated into the average which reduces the data points.
|
|
*
|
|
* @param int $dataSize Original data size
|
|
* @param int $trendCycleComponents Trend cycle component size
|
|
*
|
|
* @return int New data start index
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
public static function getStartOfDecomposition(int $dataSize, int $trendCycleComponents) : int
|
|
{
|
|
return (int) (($dataSize - $trendCycleComponents) / 2);
|
|
}
|
|
|
|
/**
|
|
* Calculate the seasonal component
|
|
*
|
|
* Average of the detrended values for every month, quarter, day etc.
|
|
*
|
|
* @param array $detrendedSeries Detrended series
|
|
* @param int $order Seasonal period (e.g. 4 = quarterly, 12 = monthly, 7 = weekly pattern in daily data)
|
|
*
|
|
* @return array
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
private function computeSeasonalComponent(array $detrendedSeries, int $order) : array
|
|
{
|
|
$seasonalComponent = [];
|
|
$count = \count($detrendedSeries);
|
|
|
|
for ($i = 0; $i < $order; ++$i) {
|
|
$temp = [];
|
|
|
|
for ($j = $i; $j < $count; $j += $order) {
|
|
$temp[] = $detrendedSeries[$j];
|
|
}
|
|
|
|
$seasonalComponent[] = Average::arithmeticMean($temp);
|
|
}
|
|
|
|
return $seasonalComponent;
|
|
}
|
|
|
|
/**
|
|
* Calculate the remainder component or error
|
|
*
|
|
* @param array $data Raw data
|
|
* @param array $trendCycleComponent Trend cycle component
|
|
* @param array $seasonalComponent Seasonal component
|
|
* @param int $mode Detrend mode
|
|
*
|
|
* @return array All remainders or absolute errors
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
public static function computeRemainderComponent(array $data, array $trendCycleComponent, array $seasonalComponent, int $mode = self::ADDITIVE) : array
|
|
{
|
|
$dataSize = \count($data);
|
|
$remainderComponent = [];
|
|
$count = \count($trendCycleComponent);
|
|
$start = self::getStartOfDecomposition($dataSize, $count);
|
|
$seasons = \count($seasonalComponent);
|
|
|
|
for ($i = 0; $i < $count; ++$i) {
|
|
$remainderComponent[] = $mode === self::ADDITIVE ? $data[$start + $i] - $trendCycleComponent[$i] - $seasonalComponent[$i % $seasons] : $data[$start + $i] / ($trendCycleComponent[$i] * $seasonalComponent[$i % $seasons]);
|
|
}
|
|
|
|
return $remainderComponent;
|
|
}
|
|
}
|