1<?php
2
3namespace PhpOffice\PhpSpreadsheet\Shared\Trend;
4
5class BestFit
6{
7    /**
8     * Indicator flag for a calculation error.
9     *
10     * @var bool
11     */
12    protected $error = false;
13
14    /**
15     * Algorithm type to use for best-fit.
16     *
17     * @var string
18     */
19    protected $bestFitType = 'undetermined';
20
21    /**
22     * Number of entries in the sets of x- and y-value arrays.
23     *
24     * @var int
25     */
26    protected $valueCount = 0;
27
28    /**
29     * X-value dataseries of values.
30     *
31     * @var float[]
32     */
33    protected $xValues = [];
34
35    /**
36     * Y-value dataseries of values.
37     *
38     * @var float[]
39     */
40    protected $yValues = [];
41
42    /**
43     * Flag indicating whether values should be adjusted to Y=0.
44     *
45     * @var bool
46     */
47    protected $adjustToZero = false;
48
49    /**
50     * Y-value series of best-fit values.
51     *
52     * @var float[]
53     */
54    protected $yBestFitValues = [];
55
56    protected $goodnessOfFit = 1;
57
58    protected $stdevOfResiduals = 0;
59
60    protected $covariance = 0;
61
62    protected $correlation = 0;
63
64    protected $SSRegression = 0;
65
66    protected $SSResiduals = 0;
67
68    protected $DFResiduals = 0;
69
70    protected $f = 0;
71
72    protected $slope = 0;
73
74    protected $slopeSE = 0;
75
76    protected $intersect = 0;
77
78    protected $intersectSE = 0;
79
80    protected $xOffset = 0;
81
82    protected $yOffset = 0;
83
84    public function getError()
85    {
86        return $this->error;
87    }
88
89    public function getBestFitType()
90    {
91        return $this->bestFitType;
92    }
93
94    /**
95     * Return the Y-Value for a specified value of X.
96     *
97     * @param float $xValue X-Value
98     *
99     * @return bool Y-Value
100     */
101    public function getValueOfYForX($xValue)
102    {
103        return false;
104    }
105
106    /**
107     * Return the X-Value for a specified value of Y.
108     *
109     * @param float $yValue Y-Value
110     *
111     * @return bool X-Value
112     */
113    public function getValueOfXForY($yValue)
114    {
115        return false;
116    }
117
118    /**
119     * Return the original set of X-Values.
120     *
121     * @return float[] X-Values
122     */
123    public function getXValues()
124    {
125        return $this->xValues;
126    }
127
128    /**
129     * Return the Equation of the best-fit line.
130     *
131     * @param int $dp Number of places of decimal precision to display
132     *
133     * @return bool
134     */
135    public function getEquation($dp = 0)
136    {
137        return false;
138    }
139
140    /**
141     * Return the Slope of the line.
142     *
143     * @param int $dp Number of places of decimal precision to display
144     *
145     * @return float
146     */
147    public function getSlope($dp = 0)
148    {
149        if ($dp != 0) {
150            return round($this->slope, $dp);
151        }
152
153        return $this->slope;
154    }
155
156    /**
157     * Return the standard error of the Slope.
158     *
159     * @param int $dp Number of places of decimal precision to display
160     *
161     * @return float
162     */
163    public function getSlopeSE($dp = 0)
164    {
165        if ($dp != 0) {
166            return round($this->slopeSE, $dp);
167        }
168
169        return $this->slopeSE;
170    }
171
172    /**
173     * Return the Value of X where it intersects Y = 0.
174     *
175     * @param int $dp Number of places of decimal precision to display
176     *
177     * @return float
178     */
179    public function getIntersect($dp = 0)
180    {
181        if ($dp != 0) {
182            return round($this->intersect, $dp);
183        }
184
185        return $this->intersect;
186    }
187
188    /**
189     * Return the standard error of the Intersect.
190     *
191     * @param int $dp Number of places of decimal precision to display
192     *
193     * @return float
194     */
195    public function getIntersectSE($dp = 0)
196    {
197        if ($dp != 0) {
198            return round($this->intersectSE, $dp);
199        }
200
201        return $this->intersectSE;
202    }
203
204    /**
205     * Return the goodness of fit for this regression.
206     *
207     * @param int $dp Number of places of decimal precision to return
208     *
209     * @return float
210     */
211    public function getGoodnessOfFit($dp = 0)
212    {
213        if ($dp != 0) {
214            return round($this->goodnessOfFit, $dp);
215        }
216
217        return $this->goodnessOfFit;
218    }
219
220    /**
221     * Return the goodness of fit for this regression.
222     *
223     * @param int $dp Number of places of decimal precision to return
224     *
225     * @return float
226     */
227    public function getGoodnessOfFitPercent($dp = 0)
228    {
229        if ($dp != 0) {
230            return round($this->goodnessOfFit * 100, $dp);
231        }
232
233        return $this->goodnessOfFit * 100;
234    }
235
236    /**
237     * Return the standard deviation of the residuals for this regression.
238     *
239     * @param int $dp Number of places of decimal precision to return
240     *
241     * @return float
242     */
243    public function getStdevOfResiduals($dp = 0)
244    {
245        if ($dp != 0) {
246            return round($this->stdevOfResiduals, $dp);
247        }
248
249        return $this->stdevOfResiduals;
250    }
251
252    /**
253     * @param int $dp Number of places of decimal precision to return
254     *
255     * @return float
256     */
257    public function getSSRegression($dp = 0)
258    {
259        if ($dp != 0) {
260            return round($this->SSRegression, $dp);
261        }
262
263        return $this->SSRegression;
264    }
265
266    /**
267     * @param int $dp Number of places of decimal precision to return
268     *
269     * @return float
270     */
271    public function getSSResiduals($dp = 0)
272    {
273        if ($dp != 0) {
274            return round($this->SSResiduals, $dp);
275        }
276
277        return $this->SSResiduals;
278    }
279
280    /**
281     * @param int $dp Number of places of decimal precision to return
282     *
283     * @return float
284     */
285    public function getDFResiduals($dp = 0)
286    {
287        if ($dp != 0) {
288            return round($this->DFResiduals, $dp);
289        }
290
291        return $this->DFResiduals;
292    }
293
294    /**
295     * @param int $dp Number of places of decimal precision to return
296     *
297     * @return float
298     */
299    public function getF($dp = 0)
300    {
301        if ($dp != 0) {
302            return round($this->f, $dp);
303        }
304
305        return $this->f;
306    }
307
308    /**
309     * @param int $dp Number of places of decimal precision to return
310     *
311     * @return float
312     */
313    public function getCovariance($dp = 0)
314    {
315        if ($dp != 0) {
316            return round($this->covariance, $dp);
317        }
318
319        return $this->covariance;
320    }
321
322    /**
323     * @param int $dp Number of places of decimal precision to return
324     *
325     * @return float
326     */
327    public function getCorrelation($dp = 0)
328    {
329        if ($dp != 0) {
330            return round($this->correlation, $dp);
331        }
332
333        return $this->correlation;
334    }
335
336    /**
337     * @return float[]
338     */
339    public function getYBestFitValues()
340    {
341        return $this->yBestFitValues;
342    }
343
344    protected function calculateGoodnessOfFit($sumX, $sumY, $sumX2, $sumY2, $sumXY, $meanX, $meanY, $const)
345    {
346        $SSres = $SScov = $SScor = $SStot = $SSsex = 0.0;
347        foreach ($this->xValues as $xKey => $xValue) {
348            $bestFitY = $this->yBestFitValues[$xKey] = $this->getValueOfYForX($xValue);
349
350            $SSres += ($this->yValues[$xKey] - $bestFitY) * ($this->yValues[$xKey] - $bestFitY);
351            if ($const) {
352                $SStot += ($this->yValues[$xKey] - $meanY) * ($this->yValues[$xKey] - $meanY);
353            } else {
354                $SStot += $this->yValues[$xKey] * $this->yValues[$xKey];
355            }
356            $SScov += ($this->xValues[$xKey] - $meanX) * ($this->yValues[$xKey] - $meanY);
357            if ($const) {
358                $SSsex += ($this->xValues[$xKey] - $meanX) * ($this->xValues[$xKey] - $meanX);
359            } else {
360                $SSsex += $this->xValues[$xKey] * $this->xValues[$xKey];
361            }
362        }
363
364        $this->SSResiduals = $SSres;
365        $this->DFResiduals = $this->valueCount - 1 - $const;
366
367        if ($this->DFResiduals == 0.0) {
368            $this->stdevOfResiduals = 0.0;
369        } else {
370            $this->stdevOfResiduals = sqrt($SSres / $this->DFResiduals);
371        }
372        if (($SStot == 0.0) || ($SSres == $SStot)) {
373            $this->goodnessOfFit = 1;
374        } else {
375            $this->goodnessOfFit = 1 - ($SSres / $SStot);
376        }
377
378        $this->SSRegression = $this->goodnessOfFit * $SStot;
379        $this->covariance = $SScov / $this->valueCount;
380        $this->correlation = ($this->valueCount * $sumXY - $sumX * $sumY) / sqrt(($this->valueCount * $sumX2 - pow($sumX, 2)) * ($this->valueCount * $sumY2 - pow($sumY, 2)));
381        $this->slopeSE = $this->stdevOfResiduals / sqrt($SSsex);
382        $this->intersectSE = $this->stdevOfResiduals * sqrt(1 / ($this->valueCount - ($sumX * $sumX) / $sumX2));
383        if ($this->SSResiduals != 0.0) {
384            if ($this->DFResiduals == 0.0) {
385                $this->f = 0.0;
386            } else {
387                $this->f = $this->SSRegression / ($this->SSResiduals / $this->DFResiduals);
388            }
389        } else {
390            if ($this->DFResiduals == 0.0) {
391                $this->f = 0.0;
392            } else {
393                $this->f = $this->SSRegression / $this->DFResiduals;
394            }
395        }
396    }
397
398    /**
399     * @param float[] $yValues
400     * @param float[] $xValues
401     * @param bool $const
402     */
403    protected function leastSquareFit(array $yValues, array $xValues, $const)
404    {
405        // calculate sums
406        $x_sum = array_sum($xValues);
407        $y_sum = array_sum($yValues);
408        $meanX = $x_sum / $this->valueCount;
409        $meanY = $y_sum / $this->valueCount;
410        $mBase = $mDivisor = $xx_sum = $xy_sum = $yy_sum = 0.0;
411        for ($i = 0; $i < $this->valueCount; ++$i) {
412            $xy_sum += $xValues[$i] * $yValues[$i];
413            $xx_sum += $xValues[$i] * $xValues[$i];
414            $yy_sum += $yValues[$i] * $yValues[$i];
415
416            if ($const) {
417                $mBase += ($xValues[$i] - $meanX) * ($yValues[$i] - $meanY);
418                $mDivisor += ($xValues[$i] - $meanX) * ($xValues[$i] - $meanX);
419            } else {
420                $mBase += $xValues[$i] * $yValues[$i];
421                $mDivisor += $xValues[$i] * $xValues[$i];
422            }
423        }
424
425        // calculate slope
426        $this->slope = $mBase / $mDivisor;
427
428        // calculate intersect
429        if ($const) {
430            $this->intersect = $meanY - ($this->slope * $meanX);
431        } else {
432            $this->intersect = 0;
433        }
434
435        $this->calculateGoodnessOfFit($x_sum, $y_sum, $xx_sum, $yy_sum, $xy_sum, $meanX, $meanY, $const);
436    }
437
438    /**
439     * Define the regression.
440     *
441     * @param float[] $yValues The set of Y-values for this regression
442     * @param float[] $xValues The set of X-values for this regression
443     * @param bool $const
444     */
445    public function __construct($yValues, $xValues = [], $const = true)
446    {
447        //    Calculate number of points
448        $nY = count($yValues);
449        $nX = count($xValues);
450
451        //    Define X Values if necessary
452        if ($nX == 0) {
453            $xValues = range(1, $nY);
454        } elseif ($nY != $nX) {
455            //    Ensure both arrays of points are the same size
456            $this->error = true;
457        }
458
459        $this->valueCount = $nY;
460        $this->xValues = $xValues;
461        $this->yValues = $yValues;
462    }
463}
464