Passed
Push — master ( 10b137...2bffcf )
by Adrien
10:25
created

BestFit   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 455
Duplicated Lines 0 %

Test Coverage

Coverage 82.17%

Importance

Changes 0
Metric Value
eloc 136
dl 0
loc 455
ccs 106
cts 129
cp 0.8217
rs 8.48
c 0
b 0
f 0
wmc 49

21 Methods

Rating   Name   Duplication   Size   Complexity  
A getBestFitType() 0 3 1
A getError() 0 3 1
A getSlopeSE() 0 7 2
A getCorrelation() 0 7 2
A getCovariance() 0 7 2
A getSSResiduals() 0 7 2
A leastSquareFit() 0 30 4
A getDFResiduals() 0 7 2
A getSSRegression() 0 7 2
A getF() 0 7 2
A getYBestFitValues() 0 3 1
A getGoodnessOfFitPercent() 0 7 2
A getIntersect() 0 7 2
A getGoodnessOfFit() 0 7 2
A getXValues() 0 3 1
A getStdevOfResiduals() 0 7 2
A getSlope() 0 7 2
A getIntersectSE() 0 7 2
A __construct() 0 17 3
C calculateGoodnessOfFit() 0 50 11
A sumSquares() 0 8 1

How to fix   Complexity   

Complex Class

Complex classes like BestFit often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BestFit, and based on these observations, apply Extract Interface, too.

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
    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 float Y-Value
100
     */
101
    abstract public function getValueOfYForX($xValue);
102
103
    /**
104
     * Return the X-Value for a specified value of Y.
105
     *
106
     * @param float $yValue Y-Value
107
     *
108
     * @return float X-Value
109
     */
110
    abstract public function getValueOfXForY($yValue);
111
112
    /**
113
     * Return the original set of X-Values.
114
     *
115
     * @return float[] X-Values
116
     */
117 2
    public function getXValues()
118
    {
119 2
        return $this->xValues;
120
    }
121
122
    /**
123
     * Return the Equation of the best-fit line.
124
     *
125
     * @param int $dp Number of places of decimal precision to display
126
     *
127
     * @return string
128
     */
129
    abstract public function getEquation($dp = 0);
130
131
    /**
132
     * Return the Slope of the line.
133
     *
134
     * @param int $dp Number of places of decimal precision to display
135
     *
136
     * @return float
137
     */
138 37
    public function getSlope($dp = 0)
139
    {
140 37
        if ($dp != 0) {
141 2
            return round($this->slope, $dp);
142
        }
143
144 37
        return $this->slope;
145
    }
146
147
    /**
148
     * Return the standard error of the Slope.
149
     *
150
     * @param int $dp Number of places of decimal precision to display
151
     *
152
     * @return float
153
     */
154 3
    public function getSlopeSE($dp = 0)
155
    {
156 3
        if ($dp != 0) {
157
            return round($this->slopeSE, $dp);
158
        }
159
160 3
        return $this->slopeSE;
161
    }
162
163
    /**
164
     * Return the Value of X where it intersects Y = 0.
165
     *
166
     * @param int $dp Number of places of decimal precision to display
167
     *
168
     * @return float
169
     */
170 36
    public function getIntersect($dp = 0)
171
    {
172 36
        if ($dp != 0) {
173 2
            return round($this->intersect, $dp);
174
        }
175
176 36
        return $this->intersect;
177
    }
178
179
    /**
180
     * Return the standard error of the Intersect.
181
     *
182
     * @param int $dp Number of places of decimal precision to display
183
     *
184
     * @return float
185
     */
186 2
    public function getIntersectSE($dp = 0)
187
    {
188 2
        if ($dp != 0) {
189
            return round($this->intersectSE, $dp);
190
        }
191
192 2
        return $this->intersectSE;
193
    }
194
195
    /**
196
     * Return the goodness of fit for this regression.
197
     *
198
     * @param int $dp Number of places of decimal precision to return
199
     *
200
     * @return float
201
     */
202 8
    public function getGoodnessOfFit($dp = 0)
203
    {
204 8
        if ($dp != 0) {
205 3
            return round($this->goodnessOfFit, $dp);
206
        }
207
208 8
        return $this->goodnessOfFit;
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
    public function getGoodnessOfFitPercent($dp = 0)
219
    {
220
        if ($dp != 0) {
221
            return round($this->goodnessOfFit * 100, $dp);
222
        }
223
224
        return $this->goodnessOfFit * 100;
225
    }
226
227
    /**
228
     * Return the standard deviation of the residuals for this regression.
229
     *
230
     * @param int $dp Number of places of decimal precision to return
231
     *
232
     * @return float
233
     */
234 5
    public function getStdevOfResiduals($dp = 0)
235
    {
236 5
        if ($dp != 0) {
237
            return round($this->stdevOfResiduals, $dp);
238
        }
239
240 5
        return $this->stdevOfResiduals;
241
    }
242
243
    /**
244
     * @param int $dp Number of places of decimal precision to return
245
     *
246
     * @return float
247
     */
248 3
    public function getSSRegression($dp = 0)
249
    {
250 3
        if ($dp != 0) {
251
            return round($this->SSRegression, $dp);
252
        }
253
254 3
        return $this->SSRegression;
255
    }
256
257
    /**
258
     * @param int $dp Number of places of decimal precision to return
259
     *
260
     * @return float
261
     */
262 3
    public function getSSResiduals($dp = 0)
263
    {
264 3
        if ($dp != 0) {
265
            return round($this->SSResiduals, $dp);
266
        }
267
268 3
        return $this->SSResiduals;
269
    }
270
271
    /**
272
     * @param int $dp Number of places of decimal precision to return
273
     *
274
     * @return float
275
     */
276 3
    public function getDFResiduals($dp = 0)
277
    {
278 3
        if ($dp != 0) {
279
            return round($this->DFResiduals, $dp);
280
        }
281
282 3
        return $this->DFResiduals;
283
    }
284
285
    /**
286
     * @param int $dp Number of places of decimal precision to return
287
     *
288
     * @return float
289
     */
290 3
    public function getF($dp = 0)
291
    {
292 3
        if ($dp != 0) {
293
            return round($this->f, $dp);
294
        }
295
296 3
        return $this->f;
297
    }
298
299
    /**
300
     * @param int $dp Number of places of decimal precision to return
301
     *
302
     * @return float
303
     */
304 3
    public function getCovariance($dp = 0)
305
    {
306 3
        if ($dp != 0) {
307
            return round($this->covariance, $dp);
308
        }
309
310 3
        return $this->covariance;
311
    }
312
313
    /**
314
     * @param int $dp Number of places of decimal precision to return
315
     *
316
     * @return float
317
     */
318 2
    public function getCorrelation($dp = 0)
319
    {
320 2
        if ($dp != 0) {
321
            return round($this->correlation, $dp);
322
        }
323
324 2
        return $this->correlation;
325
    }
326
327
    /**
328
     * @return float[]
329
     */
330
    public function getYBestFitValues()
331
    {
332
        return $this->yBestFitValues;
333
    }
334
335 39
    protected function calculateGoodnessOfFit($sumX, $sumY, $sumX2, $sumY2, $sumXY, $meanX, $meanY, $const): void
336
    {
337 39
        $SSres = $SScov = $SScor = $SStot = $SSsex = 0.0;
0 ignored issues
show
Unused Code introduced by
The assignment to $SScor is dead and can be removed.
Loading history...
338 39
        foreach ($this->xValues as $xKey => $xValue) {
339 39
            $bestFitY = $this->yBestFitValues[$xKey] = $this->getValueOfYForX($xValue);
340
341 39
            $SSres += ($this->yValues[$xKey] - $bestFitY) * ($this->yValues[$xKey] - $bestFitY);
342 39
            if ($const === true) {
343 34
                $SStot += ($this->yValues[$xKey] - $meanY) * ($this->yValues[$xKey] - $meanY);
344
            } else {
345 5
                $SStot += $this->yValues[$xKey] * $this->yValues[$xKey];
346
            }
347 39
            $SScov += ($this->xValues[$xKey] - $meanX) * ($this->yValues[$xKey] - $meanY);
348 39
            if ($const === true) {
349 34
                $SSsex += ($this->xValues[$xKey] - $meanX) * ($this->xValues[$xKey] - $meanX);
350
            } else {
351 5
                $SSsex += $this->xValues[$xKey] * $this->xValues[$xKey];
352
            }
353
        }
354
355 39
        $this->SSResiduals = $SSres;
356 39
        $this->DFResiduals = $this->valueCount - 1 - ($const === true ? 1 : 0);
357
358 39
        if ($this->DFResiduals == 0.0) {
359 1
            $this->stdevOfResiduals = 0.0;
360
        } else {
361 38
            $this->stdevOfResiduals = sqrt($SSres / $this->DFResiduals);
362
        }
363 39
        if (($SStot == 0.0) || ($SSres == $SStot)) {
1 ignored issue
show
introduced by
The condition $SStot == 0.0 is always true.
Loading history...
364
            $this->goodnessOfFit = 1;
365
        } else {
366 39
            $this->goodnessOfFit = 1 - ($SSres / $SStot);
367
        }
368
369 39
        $this->SSRegression = $this->goodnessOfFit * $SStot;
370 39
        $this->covariance = $SScov / $this->valueCount;
371 39
        $this->correlation = ($this->valueCount * $sumXY - $sumX * $sumY) / sqrt(($this->valueCount * $sumX2 - $sumX ** 2) * ($this->valueCount * $sumY2 - $sumY ** 2));
372 39
        $this->slopeSE = $this->stdevOfResiduals / sqrt($SSsex);
373 39
        $this->intersectSE = $this->stdevOfResiduals * sqrt(1 / ($this->valueCount - ($sumX * $sumX) / $sumX2));
374 39
        if ($this->SSResiduals != 0.0) {
375 27
            if ($this->DFResiduals == 0.0) {
376
                $this->f = 0.0;
377
            } else {
378 27
                $this->f = $this->SSRegression / ($this->SSResiduals / $this->DFResiduals);
379
            }
380
        } else {
381 12
            if ($this->DFResiduals == 0.0) {
382 1
                $this->f = 0.0;
383
            } else {
384 11
                $this->f = $this->SSRegression / $this->DFResiduals;
385
            }
386
        }
387 39
    }
388
389 39
    private function sumSquares(array $values)
390
    {
391 39
        return array_sum(
392 39
            array_map(
393 39
                function ($value) {
394 39
                    return $value ** 2;
395 39
                },
396
                $values
397
            )
398
        );
399
    }
400
401
    /**
402
     * @param float[] $yValues
403
     * @param float[] $xValues
404
     */
405 39
    protected function leastSquareFit(array $yValues, array $xValues, bool $const): void
406
    {
407
        // calculate sums
408 39
        $sumValuesX = array_sum($xValues);
409 39
        $sumValuesY = array_sum($yValues);
410 39
        $meanValueX = $sumValuesX / $this->valueCount;
411 39
        $meanValueY = $sumValuesY / $this->valueCount;
412 39
        $sumSquaresX = $this->sumSquares($xValues);
413 39
        $sumSquaresY = $this->sumSquares($yValues);
414 39
        $mBase = $mDivisor = 0.0;
415 39
        $xy_sum = 0.0;
416 39
        for ($i = 0; $i < $this->valueCount; ++$i) {
417 39
            $xy_sum += $xValues[$i] * $yValues[$i];
418
419 39
            if ($const === true) {
420 34
                $mBase += ($xValues[$i] - $meanValueX) * ($yValues[$i] - $meanValueY);
421 34
                $mDivisor += ($xValues[$i] - $meanValueX) * ($xValues[$i] - $meanValueX);
422
            } else {
423 5
                $mBase += $xValues[$i] * $yValues[$i];
424 5
                $mDivisor += $xValues[$i] * $xValues[$i];
425
            }
426
        }
427
428
        // calculate slope
429 39
        $this->slope = $mBase / $mDivisor;
430
431
        // calculate intersect
432 39
        $this->intersect = ($const === true) ? $meanValueY - ($this->slope * $meanValueX) : 0.0;
433
434 39
        $this->calculateGoodnessOfFit($sumValuesX, $sumValuesY, $sumSquaresX, $sumSquaresY, $xy_sum, $meanValueX, $meanValueY, $const);
435 39
    }
436
437
    /**
438
     * Define the regression.
439
     *
440
     * @param float[] $yValues The set of Y-values for this regression
441
     * @param float[] $xValues The set of X-values for this regression
442
     */
443 39
    public function __construct($yValues, $xValues = [])
444
    {
445
        //    Calculate number of points
446 39
        $yValueCount = count($yValues);
447 39
        $xValueCount = count($xValues);
448
449
        //    Define X Values if necessary
450 39
        if ($xValueCount === 0) {
451
            $xValues = range(1, $yValueCount);
452 39
        } elseif ($yValueCount !== $xValueCount) {
453
            //    Ensure both arrays of points are the same size
454
            $this->error = true;
455
        }
456
457 39
        $this->valueCount = $yValueCount;
458 39
        $this->xValues = $xValues;
459 39
        $this->yValues = $yValues;
460 39
    }
461
}
462