├── src ├── TimeSeriesException.php ├── Predictors │ ├── TimeSeriesPredictor.php │ ├── HoltPredictor.php │ ├── MultiplicativeTrendPredictor.php │ ├── DampedHoltPredictor.php │ └── DampedMultiplicativeTrendPredictor.php └── Factories │ ├── PredictorFactory.php │ ├── HoltFactory.php │ ├── MultiplicativeTrendFactory.php │ ├── DampedHoltFactory.php │ ├── DampedMultiplicativeTrendFactory.php │ ├── BasicExponentialSmoothingFactory.php │ └── DampedExponentialSmoothingFactory.php └── composer.json /src/TimeSeriesException.php: -------------------------------------------------------------------------------- 1 | train($dataPoints); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Factories/HoltFactory.php: -------------------------------------------------------------------------------- 1 | nStepsPerParam = (int)max(2, $nStepsPerParam); 26 | } 27 | 28 | /** 29 | * @param array $dataPoints 30 | * @return TimeSeriesPredictor 31 | */ 32 | public function train(array $dataPoints) 33 | { 34 | $dataSetSize = \count($dataPoints); 35 | if ($dataSetSize < 4) { 36 | throw new TimeSeriesException('Insufficient number of data points'); 37 | } 38 | 39 | $minError = +INF; 40 | $bestPredictor = null; 41 | 42 | // Local variables to avoid too many indirections 43 | $nStepsPerParam = $this->nStepsPerParam; 44 | $stepSize = 1./($nStepsPerParam-1); 45 | $firstLevel = $dataPoints[0]; 46 | 47 | for ($i=0; $i<$nStepsPerParam; $i++) { 48 | for ($j=0; $j<$nStepsPerParam; $j++) { 49 | $candidate = $this->getPredictor($i*$stepSize, $j*$stepSize, $firstLevel); 50 | $candidateError = 0.0; 51 | 52 | for ($k=1; $k<$dataSetSize; $k++) { 53 | $candidateError += \pow($candidate->predict()-$dataPoints[$k], 2); 54 | $candidate->ingestDataPoint($dataPoints[$k]); 55 | } 56 | 57 | if ($candidateError < $minError) { 58 | $minError = $candidateError; 59 | $bestPredictor = $candidate; 60 | } 61 | } 62 | } 63 | 64 | return $bestPredictor; 65 | } 66 | 67 | /** 68 | * @param double $alpha 69 | * @param double $beta 70 | * @param double $level 71 | * @return TimeSeriesPredictor 72 | */ 73 | protected abstract function getPredictor($alpha, $beta, $level); 74 | } 75 | -------------------------------------------------------------------------------- /src/Factories/DampedExponentialSmoothingFactory.php: -------------------------------------------------------------------------------- 1 | nStepsPerParam = (int)max(2, $nStepsPerParam); 30 | $this->predictionHorizon = is_infinite($predictionHorizon) ? 31 | +INF : (int)ceil(max(2, $predictionHorizon)); 32 | } 33 | 34 | /** 35 | * @param array $dataPoints 36 | * @return DampedHoltFactory 37 | */ 38 | public function train(array $dataPoints) 39 | { 40 | $dataSetSize = count($dataPoints); 41 | if ($dataSetSize < 6) { 42 | throw new TimeSeriesException('Insufficient number of data points'); 43 | } 44 | 45 | $minError = +INF; 46 | $bestPredictor = null; 47 | 48 | // Local variables to avoid too many indirections 49 | $nStepsPerParam = $this->nStepsPerParam; 50 | $stepSize = 1./($nStepsPerParam-1); 51 | $firstLevel = $dataPoints[0]; 52 | 53 | for ($i=0; $i<$nStepsPerParam; $i++) { 54 | for ($j=0; $j<$nStepsPerParam; $j++) { 55 | for ($k=1; $k<$nStepsPerParam; $k+=2) { 56 | $candidate = $this->getPredictor($i*$stepSize, $j*$stepSize, $k*$stepSize, $firstLevel); 57 | $candidateError = 0.0; 58 | 59 | for ($u=1; $u<$dataSetSize; $u++) { 60 | for ($v=0; $v<$this->predictionHorizon && $u+$v<$dataSetSize; $v++) { 61 | $candidateError += pow($candidate->predict($v+1)-$dataPoints[$u+$v], 2); 62 | } 63 | $candidate->ingestDataPoint($dataPoints[$u]); 64 | } 65 | 66 | if ($candidateError < $minError) { 67 | $minError = $candidateError; 68 | $bestPredictor = $candidate; 69 | } 70 | } 71 | } 72 | } 73 | 74 | return $bestPredictor; 75 | } 76 | 77 | /** 78 | * @param double $alpha 79 | * @param double $beta 80 | * @param double $theta 81 | * @param double $level 82 | * @return TimeSeriesPredictor 83 | */ 84 | abstract protected function getPredictor($alpha, $beta, $theta, $level); 85 | } 86 | -------------------------------------------------------------------------------- /src/Predictors/HoltPredictor.php: -------------------------------------------------------------------------------- 1 | alpha = (double)\min(1.0, \max(0.0, $alpha)); 34 | $this->beta = (double)\min(1.0, \max(0.0, $beta)); 35 | 36 | $this->level = (double)$level; 37 | $this->trend = (double)$trend; 38 | } 39 | 40 | 41 | /** 42 | * @param (null|double)[] $dataPoints 43 | */ 44 | public function ingestDataArray(array $dataPoints) 45 | { 46 | $this->ingestData($dataPoints); 47 | } 48 | 49 | /** 50 | * @param \Traversable $dataPoints 51 | */ 52 | public function ingestDataTraversable(\Traversable $dataPoints) 53 | { 54 | $this->ingestData($dataPoints); 55 | } 56 | 57 | /** 58 | * @param integer $nStepsToTheFuture 59 | * @return double 60 | */ 61 | public function predict($nStepsToTheFuture = 1) 62 | { 63 | return $this->level + $nStepsToTheFuture*$this->trend; 64 | } 65 | 66 | /** 67 | * @return float 68 | */ 69 | public function getLevel() 70 | { 71 | return $this->level; 72 | } 73 | 74 | /** 75 | * @return float 76 | */ 77 | public function getTrend() 78 | { 79 | return $this->trend; 80 | } 81 | 82 | /** 83 | * @return float Level learning rate 84 | */ 85 | public function getAlpha() 86 | { 87 | return $this->alpha; 88 | } 89 | 90 | /** 91 | * @return float Trend learning rate 92 | */ 93 | public function getBeta() 94 | { 95 | return $this->beta; 96 | } 97 | 98 | /** 99 | * @param double $dataPoint 100 | */ 101 | public function ingestDataPoint($dataPoint) 102 | { 103 | $chgFactor = $this->alpha*($dataPoint - $this->level - $this->trend); 104 | $this->level = $this->level + $this->trend + $chgFactor; 105 | $this->trend = $this->trend + $this->beta*$chgFactor; 106 | } 107 | 108 | /** 109 | * @param array|\Traversable $dataPoints 110 | */ 111 | private function ingestData($dataPoints) 112 | { 113 | // We don't use $this->ingestDataPoint for performance reasons 114 | foreach ($dataPoints as $dataPoint) { 115 | $chgFactor = $this->alpha*($dataPoint - $this->level - $this->trend); 116 | $this->level = $this->level + $this->trend + $chgFactor; 117 | $this->trend = $this->trend + $this->beta*$chgFactor; 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Predictors/MultiplicativeTrendPredictor.php: -------------------------------------------------------------------------------- 1 | 1.0 || $beta < 0.0 || $beta > 1.0) { 41 | throw new TimeSeriesException('Alpha and Beta must be in the [0, 1] interval'); 42 | } 43 | 44 | $this->alpha = (double)$alpha; 45 | $this->beta = (double)$beta; 46 | 47 | $this->level = (double)$level; 48 | $this->trend = (double)$trend; 49 | } 50 | 51 | 52 | /** 53 | * @param (null|double)[] $dataPoints 54 | */ 55 | public function ingestDataArray(array $dataPoints) 56 | { 57 | $this->ingestData($dataPoints); 58 | } 59 | 60 | /** 61 | * @param \Traversable $dataPoints 62 | */ 63 | public function ingestDataTraversable(\Traversable $dataPoints) 64 | { 65 | $this->ingestData($dataPoints); 66 | } 67 | 68 | /** 69 | * @param integer $nStepsToTheFuture 70 | * @return double 71 | */ 72 | public function predict($nStepsToTheFuture = 1) 73 | { 74 | return $this->level*pow($this->trend, $nStepsToTheFuture); 75 | } 76 | 77 | /** 78 | * @return float 79 | */ 80 | public function getLevel() 81 | { 82 | return $this->level; 83 | } 84 | 85 | /** 86 | * @return float 87 | */ 88 | public function getTrend() 89 | { 90 | return $this->trend; 91 | } 92 | 93 | /** 94 | * @return float Level learning rate 95 | */ 96 | public function getAlpha() 97 | { 98 | return $this->alpha; 99 | } 100 | 101 | /** 102 | * @return float Trend learning rate 103 | */ 104 | public function getBeta() 105 | { 106 | return $this->beta; 107 | } 108 | 109 | /** 110 | * @param double $dataPoint 111 | */ 112 | public function ingestDataPoint($dataPoint) 113 | { 114 | $chgFactor = $this->alpha*($dataPoint - $this->level*$this->trend); 115 | $previousLevel = $this->level; 116 | $this->level = $this->level*$this->trend + $chgFactor; 117 | $this->trend = $this->trend + $this->beta*$chgFactor/$previousLevel; 118 | } 119 | 120 | /** 121 | * @param array|\Traversable $dataPoints 122 | */ 123 | private function ingestData($dataPoints) 124 | { 125 | // We don't use $this->ingestDataPoint for performance reasons 126 | foreach ($dataPoints as $dataPoint) { 127 | $chgFactor = $this->alpha*($dataPoint - $this->level*$this->trend); 128 | $previousLevel = $this->level; 129 | $this->level = $this->level*$this->trend + $chgFactor; 130 | $this->trend = $this->trend + $this->beta*$chgFactor/$previousLevel; 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Predictors/DampedHoltPredictor.php: -------------------------------------------------------------------------------- 1 | 1.0) { 41 | throw new TimeSeriesException('Theta must be in the (0, 1] interval'); 42 | } 43 | 44 | $this->alpha = (double)min(max(0.0, $alpha), 1.0); 45 | $this->beta = (double)min(max(0.0, $beta), 1.0); 46 | $this->theta = (double)$theta; 47 | 48 | $this->level = (double)$level; 49 | $this->trend = (double)$trend; 50 | } 51 | 52 | 53 | /** 54 | * @param (null|double)[] $dataPoints 55 | */ 56 | public function ingestDataArray(array $dataPoints) 57 | { 58 | $this->ingestData($dataPoints); 59 | } 60 | 61 | /** 62 | * @param \Traversable $dataPoints 63 | */ 64 | public function ingestDataTraversable(\Traversable $dataPoints) 65 | { 66 | $this->ingestData($dataPoints); 67 | } 68 | 69 | /** 70 | * @param integer $nStepsToTheFuture 71 | * @return double 72 | */ 73 | public function predict($nStepsToTheFuture = 1) 74 | { 75 | $m = 0; 76 | $t = 1; 77 | 78 | for ($i=0; $i<$nStepsToTheFuture; $i++) { 79 | $t *= $this->theta; 80 | $m += $t; 81 | } 82 | 83 | return $this->level + $m*$this->trend; 84 | } 85 | 86 | /** 87 | * @return float 88 | */ 89 | public function getLevel() 90 | { 91 | return $this->level; 92 | } 93 | 94 | /** 95 | * @return float 96 | */ 97 | public function getTrend() 98 | { 99 | return $this->trend; 100 | } 101 | 102 | /** 103 | * @return float Level learning rate 104 | */ 105 | public function getAlpha() 106 | { 107 | return $this->alpha; 108 | } 109 | 110 | /** 111 | * @return float Trend learning rate 112 | */ 113 | public function getBeta() 114 | { 115 | return $this->beta; 116 | } 117 | 118 | /** 119 | * @return float Damping factor 120 | */ 121 | public function getTheta() 122 | { 123 | return $this->theta; 124 | } 125 | 126 | /** 127 | * @param double $dataPoint 128 | */ 129 | public function ingestDataPoint($dataPoint) 130 | { 131 | $tt = $this->theta*$this->trend; 132 | $chgFactor = $this->alpha*($dataPoint - $this->level - $tt); 133 | 134 | $this->level = $this->level + $tt + $chgFactor; 135 | $this->trend = $tt + $this->beta*$chgFactor; 136 | } 137 | 138 | /** 139 | * @param array|\Traversable $dataPoints 140 | */ 141 | private function ingestData($dataPoints) 142 | { 143 | // We don't use $this->ingestDataPoint for performance reasons 144 | foreach ($dataPoints as $dataPoint) { 145 | $tt = $this->theta*$this->trend; 146 | $chgFactor = $this->alpha*($dataPoint - $this->level - $tt); 147 | 148 | $this->level = $this->level + $tt + $chgFactor; 149 | $this->trend = $tt + $this->beta*$chgFactor; 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/Predictors/DampedMultiplicativeTrendPredictor.php: -------------------------------------------------------------------------------- 1 | 1.0) { 41 | throw new TimeSeriesException('Theta must be in the (0, 1] interval'); 42 | } 43 | 44 | $this->alpha = (double)min(max(0.0, $alpha), 1.0); 45 | $this->beta = (double)min(max(0.0, $beta), 1.0); 46 | $this->theta = (double)$theta; 47 | 48 | $this->level = (double)$level; 49 | $this->trend = (double)$trend; 50 | } 51 | 52 | 53 | /** 54 | * @param (null|double)[] $dataPoints 55 | */ 56 | public function ingestDataArray(array $dataPoints) 57 | { 58 | $this->ingestData($dataPoints); 59 | } 60 | 61 | /** 62 | * @param \Traversable $dataPoints 63 | */ 64 | public function ingestDataTraversable(\Traversable $dataPoints) 65 | { 66 | $this->ingestData($dataPoints); 67 | } 68 | 69 | /** 70 | * @param integer $nStepsToTheFuture 71 | * @return double 72 | */ 73 | public function predict($nStepsToTheFuture = 1) 74 | { 75 | $m = 0; 76 | $t = 1; 77 | 78 | for ($i=0; $i<$nStepsToTheFuture; $i++) { 79 | $t *= $this->theta; 80 | $m += $t; 81 | } 82 | 83 | return $this->level * pow($this->trend, $m); 84 | } 85 | 86 | /** 87 | * @return float 88 | */ 89 | public function getLevel() 90 | { 91 | return $this->level; 92 | } 93 | 94 | /** 95 | * @return float 96 | */ 97 | public function getTrend() 98 | { 99 | return $this->trend; 100 | } 101 | 102 | /** 103 | * @return float Level learning rate 104 | */ 105 | public function getAlpha() 106 | { 107 | return $this->alpha; 108 | } 109 | 110 | /** 111 | * @return float Trend learning rate 112 | */ 113 | public function getBeta() 114 | { 115 | return $this->beta; 116 | } 117 | 118 | /** 119 | * @return float Damping factor 120 | */ 121 | public function getTheta() 122 | { 123 | return $this->theta; 124 | } 125 | 126 | /** 127 | * @param double $dataPoint 128 | */ 129 | public function ingestDataPoint($dataPoint) 130 | { 131 | $tt = pow($this->trend, $this->theta); 132 | $chgFactor = $this->alpha*($dataPoint - $this->level*$tt); 133 | 134 | $previousLevel = $this->level; 135 | 136 | $this->level = $this->level*$tt + $chgFactor; 137 | $this->trend = $tt + $this->beta*$chgFactor/$previousLevel; 138 | } 139 | 140 | /** 141 | * @param array|\Traversable $dataPoints 142 | */ 143 | private function ingestData($dataPoints) 144 | { 145 | // We don't use $this->ingestDataPoint for performance reasons 146 | foreach ($dataPoints as $dataPoint) { 147 | $tt = pow($this->trend, $this->theta); 148 | $chgFactor = $this->alpha*($dataPoint - $this->level*$tt); 149 | 150 | $previousLevel = $this->level; 151 | 152 | $this->level = $this->level*$tt + $chgFactor; 153 | $this->trend = $tt + $this->beta*$chgFactor/$previousLevel; 154 | } 155 | } 156 | } 157 | --------------------------------------------------------------------------------