Passed
Push — master ( 9e1378...d02904 )
by Mark
16:48 queued 06:34
created

BestFit::getSlope()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 7
rs 10
ccs 4
cts 4
cp 1
crap 2
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Shared\Trend;
4
5
abstract class 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
    /** @var float */
57
    protected $goodnessOfFit = 1;
58
59
    /** @var float */
60
    protected $stdevOfResiduals = 0;
61
62
    /** @var float */
63
    protected $covariance = 0;
64
65
    /** @var float */
66
    protected $correlation = 0;
67
68
    /** @var float */
69
    protected $SSRegression = 0;
70
71
    /** @var float */
72
    protected $SSResiduals = 0;
73
74
    /** @var float */
75
    protected $DFResiduals = 0;
76
77
    /** @var float */
78
    protected $f = 0;
79
80
    /** @var float */
81
    protected $slope = 0;
82
83
    /** @var float */
84
    protected $slopeSE = 0;
85
86
    /** @var float */
87
    protected $intersect = 0;
88
89
    /** @var float */
90
    protected $intersectSE = 0;
91
92
    /** @var float */
93
    protected $xOffset = 0;
94
95
    /** @var float */
96
    protected $yOffset = 0;
97
98
    /** @return bool */
99
    public function getError()
100
    {
101
        return $this->error;
102
    }
103
104
    /** @return string */
105
    public function getBestFitType()
106
    {
107
        return $this->bestFitType;
108
    }
109
110
    /**
111
     * Return the Y-Value for a specified value of X.
112
     *
113
     * @param float $xValue X-Value
114
     *
115
     * @return float Y-Value
116
     */
117
    abstract public function getValueOfYForX($xValue);
118
119
    /**
120
     * Return the X-Value for a specified value of Y.
121
     *
122
     * @param float $yValue Y-Value
123
     *
124
     * @return float X-Value
125
     */
126
    abstract public function getValueOfXForY($yValue);
127
128
    /**
129
     * Return the original set of X-Values.
130
     *
131
     * @return float[] X-Values
132
     */
133 3
    public function getXValues()
134
    {
135 3
        return $this->xValues;
136
    }
137
138
    /**
139
     * Return the Equation of the best-fit line.
140
     *
141
     * @param int $dp Number of places of decimal precision to display
142
     *
143
     * @return string
144
     */
145
    abstract public function getEquation($dp = 0);
146
147
    /**
148
     * Return the Slope of the line.
149
     *
150
     * @param int $dp Number of places of decimal precision to display
151
     *
152
     * @return float
153
     */
154 40
    public function getSlope($dp = 0)
155
    {
156 40
        if ($dp != 0) {
157 2
            return round($this->slope, $dp);
158
        }
159
160 40
        return $this->slope;
161
    }
162
163
    /**
164
     * Return the standard error of the Slope.
165
     *
166
     * @param int $dp Number of places of decimal precision to display
167
     *
168
     * @return float
169
     */
170 3
    public function getSlopeSE($dp = 0)
171
    {
172 3
        if ($dp != 0) {
173
            return round($this->slopeSE, $dp);
174
        }
175
176 3
        return $this->slopeSE;
177
    }
178
179
    /**
180
     * Return the Value of X where it intersects Y = 0.
181
     *
182
     * @param int $dp Number of places of decimal precision to display
183
     *
184
     * @return float
185
     */
186 38
    public function getIntersect($dp = 0)
187
    {
188 38
        if ($dp != 0) {
189 2
            return round($this->intersect, $dp);
190
        }
191
192 38
        return $this->intersect;
193
    }
194
195
    /**
196
     * Return the standard error of the Intersect.
197
     *
198
     * @param int $dp Number of places of decimal precision to display
199
     *
200
     * @return float
201
     */
202 2
    public function getIntersectSE($dp = 0)
203
    {
204 2
        if ($dp != 0) {
205
            return round($this->intersectSE, $dp);
206
        }
207
208 2
        return $this->intersectSE;
209
    }
210
211
    /**
212
     * Return the goodness of fit for this regression.
213
     *
214
     * @param int $dp Number of places of decimal precision to return
215
     *
216
     * @return float
217
     */
218 10
    public function getGoodnessOfFit($dp = 0)
219
    {
220 10
        if ($dp != 0) {
221 3
            return round($this->goodnessOfFit, $dp);
222
        }
223
224 10
        return $this->goodnessOfFit;
225
    }
226
227
    /**
228
     * Return the goodness of fit for this regression.
229
     *
230
     * @param int $dp Number of places of decimal precision to return
231
     *
232
     * @return float
233
     */
234
    public function getGoodnessOfFitPercent($dp = 0)
235
    {
236
        if ($dp != 0) {
237
            return round($this->goodnessOfFit * 100, $dp);
238
        }
239
240
        return $this->goodnessOfFit * 100;
241
    }
242
243
    /**
244
     * Return the standard deviation of the residuals for this regression.
245
     *
246
     * @param int $dp Number of places of decimal precision to return
247
     *
248
     * @return float
249
     */
250 7
    public function getStdevOfResiduals($dp = 0)
251
    {
252 7
        if ($dp != 0) {
253
            return round($this->stdevOfResiduals, $dp);
254
        }
255
256 7
        return $this->stdevOfResiduals;
257
    }
258
259
    /**
260
     * @param int $dp Number of places of decimal precision to return
261
     *
262
     * @return float
263
     */
264 3
    public function getSSRegression($dp = 0)
265
    {
266 3
        if ($dp != 0) {
267
            return round($this->SSRegression, $dp);
268
        }
269
270 3
        return $this->SSRegression;
271
    }
272
273
    /**
274
     * @param int $dp Number of places of decimal precision to return
275
     *
276
     * @return float
277
     */
278 3
    public function getSSResiduals($dp = 0)
279
    {
280 3
        if ($dp != 0) {
281
            return round($this->SSResiduals, $dp);
282
        }
283
284 3
        return $this->SSResiduals;
285
    }
286
287
    /**
288
     * @param int $dp Number of places of decimal precision to return
289
     *
290
     * @return float
291
     */
292 3
    public function getDFResiduals($dp = 0)
293
    {
294 3
        if ($dp != 0) {
295
            return round($this->DFResiduals, $dp);
296
        }
297
298 3
        return $this->DFResiduals;
299
    }
300
301
    /**
302
     * @param int $dp Number of places of decimal precision to return
303
     *
304
     * @return float
305
     */
306 3
    public function getF($dp = 0)
307
    {
308 3
        if ($dp != 0) {
309
            return round($this->f, $dp);
310
        }
311
312 3
        return $this->f;
313
    }
314
315
    /**
316
     * @param int $dp Number of places of decimal precision to return
317
     *
318
     * @return float
319
     */
320 5
    public function getCovariance($dp = 0)
321
    {
322 5
        if ($dp != 0) {
323
            return round($this->covariance, $dp);
324
        }
325
326 5
        return $this->covariance;
327
    }
328
329
    /**
330
     * @param int $dp Number of places of decimal precision to return
331
     *
332
     * @return float
333
     */
334 4
    public function getCorrelation($dp = 0)
335
    {
336 4
        if ($dp != 0) {
337
            return round($this->correlation, $dp);
338
        }
339
340 4
        return $this->correlation;
341
    }
342
343
    /**
344
     * @return float[]
345
     */
346
    public function getYBestFitValues()
347
    {
348
        return $this->yBestFitValues;
349
    }
350
351
    /** @var mixed */
352
    private static $scrutinizerZeroPointZero = 0.0;
353
354
    /**
355
     * @param mixed $x
356
     * @param mixed $y
357
     */
358 36
    private static function scrutinizerLooseCompare($x, $y): bool
359
    {
360 36
        return $x == $y;
361
    }
362
363
    /**
364
     * @param float $sumX
365
     * @param float $sumY
366
     * @param float $sumX2
367
     * @param float $sumY2
368
     * @param float $sumXY
369
     * @param float $meanX
370
     * @param float $meanY
371
     * @param bool|int $const
372
     */
373 36
    protected function calculateGoodnessOfFit($sumX, $sumY, $sumX2, $sumY2, $sumXY, $meanX, $meanY, $const): void
374
    {
375 36
        $SSres = $SScov = $SStot = $SSsex = 0.0;
376 36
        foreach ($this->xValues as $xKey => $xValue) {
377 36
            $bestFitY = $this->yBestFitValues[$xKey] = $this->getValueOfYForX($xValue);
378
379 36
            $SSres += ($this->yValues[$xKey] - $bestFitY) * ($this->yValues[$xKey] - $bestFitY);
380 36
            if ($const === true) {
381 31
                $SStot += ($this->yValues[$xKey] - $meanY) * ($this->yValues[$xKey] - $meanY);
382
            } else {
383 5
                $SStot += $this->yValues[$xKey] * $this->yValues[$xKey];
384
            }
385 36
            $SScov += ($this->xValues[$xKey] - $meanX) * ($this->yValues[$xKey] - $meanY);
386 36
            if ($const === true) {
387 31
                $SSsex += ($this->xValues[$xKey] - $meanX) * ($this->xValues[$xKey] - $meanX);
388
            } else {
389 5
                $SSsex += $this->xValues[$xKey] * $this->xValues[$xKey];
390
            }
391
        }
392
393 36
        $this->SSResiduals = $SSres;
394 36
        $this->DFResiduals = $this->valueCount - 1 - ($const === true ? 1 : 0);
395
396 36
        if ($this->DFResiduals == 0.0) {
397 1
            $this->stdevOfResiduals = 0.0;
398
        } else {
399 35
            $this->stdevOfResiduals = sqrt($SSres / $this->DFResiduals);
400
        }
401
        // Scrutinizer thinks $SSres == $SStot is always true. It is wrong.
402 36
        if ($SStot == self::$scrutinizerZeroPointZero || self::scrutinizerLooseCompare($SSres, $SStot)) {
403
            $this->goodnessOfFit = 1;
404
        } else {
405 36
            $this->goodnessOfFit = 1 - ($SSres / $SStot);
406
        }
407
408 36
        $this->SSRegression = $this->goodnessOfFit * $SStot;
409 36
        $this->covariance = $SScov / $this->valueCount;
410 36
        $this->correlation = ($this->valueCount * $sumXY - $sumX * $sumY) / sqrt(($this->valueCount * $sumX2 - $sumX ** 2) * ($this->valueCount * $sumY2 - $sumY ** 2));
411 36
        $this->slopeSE = $this->stdevOfResiduals / sqrt($SSsex);
412 36
        $this->intersectSE = $this->stdevOfResiduals * sqrt(1 / ($this->valueCount - ($sumX * $sumX) / $sumX2));
413 36
        if ($this->SSResiduals != 0.0) {
414 25
            if ($this->DFResiduals == 0.0) {
415
                $this->f = 0.0;
416
            } else {
417 25
                $this->f = $this->SSRegression / ($this->SSResiduals / $this->DFResiduals);
418
            }
419
        } else {
420 12
            if ($this->DFResiduals == 0.0) {
421 1
                $this->f = 0.0;
422
            } else {
423 11
                $this->f = $this->SSRegression / $this->DFResiduals;
424
            }
425
        }
426
    }
427
428
    /** @return float|int */
429 36
    private function sumSquares(array $values)
430
    {
431 36
        return array_sum(
432 36
            array_map(
433 36
                function ($value) {
434 36
                    return $value ** 2;
435 36
                },
436 36
                $values
437 36
            )
438 36
        );
439
    }
440
441
    /**
442
     * @param float[] $yValues
443
     * @param float[] $xValues
444
     */
445 36
    protected function leastSquareFit(array $yValues, array $xValues, bool $const): void
446
    {
447
        // calculate sums
448 36
        $sumValuesX = array_sum($xValues);
449 36
        $sumValuesY = array_sum($yValues);
450 36
        $meanValueX = $sumValuesX / $this->valueCount;
451 36
        $meanValueY = $sumValuesY / $this->valueCount;
452 36
        $sumSquaresX = $this->sumSquares($xValues);
453 36
        $sumSquaresY = $this->sumSquares($yValues);
454 36
        $mBase = $mDivisor = 0.0;
455 36
        $xy_sum = 0.0;
456 36
        for ($i = 0; $i < $this->valueCount; ++$i) {
457 36
            $xy_sum += $xValues[$i] * $yValues[$i];
458
459 36
            if ($const === true) {
460 31
                $mBase += ($xValues[$i] - $meanValueX) * ($yValues[$i] - $meanValueY);
461 31
                $mDivisor += ($xValues[$i] - $meanValueX) * ($xValues[$i] - $meanValueX);
462
            } else {
463 5
                $mBase += $xValues[$i] * $yValues[$i];
464 5
                $mDivisor += $xValues[$i] * $xValues[$i];
465
            }
466
        }
467
468
        // calculate slope
469 36
        $this->slope = $mBase / $mDivisor;
470
471
        // calculate intersect
472 36
        $this->intersect = ($const === true) ? $meanValueY - ($this->slope * $meanValueX) : 0.0;
473
474 36
        $this->calculateGoodnessOfFit($sumValuesX, $sumValuesY, $sumSquaresX, $sumSquaresY, $xy_sum, $meanValueX, $meanValueY, $const);
475
    }
476
477
    /**
478
     * Define the regression.
479
     *
480
     * @param float[] $yValues The set of Y-values for this regression
481
     * @param float[] $xValues The set of X-values for this regression
482
     */
483 36
    public function __construct($yValues, $xValues = [])
484
    {
485
        //    Calculate number of points
486 36
        $yValueCount = count($yValues);
487 36
        $xValueCount = count($xValues);
488
489
        //    Define X Values if necessary
490 36
        if ($xValueCount === 0) {
491
            $xValues = range(1, $yValueCount);
492 36
        } elseif ($yValueCount !== $xValueCount) {
493
            //    Ensure both arrays of points are the same size
494
            $this->error = true;
495
        }
496
497 36
        $this->valueCount = $yValueCount;
498 36
        $this->xValues = $xValues;
499 36
        $this->yValues = $yValues;
500
    }
501
}
502