PolynomialBestFit   A
last analyzed

Complexity

Total Complexity 28

Size/Duplication

Total Lines 203
Duplicated Lines 0 %

Test Coverage

Coverage 4.17%

Importance

Changes 0
Metric Value
wmc 28
eloc 70
dl 0
loc 203
ccs 3
cts 72
cp 0.0417
rs 10
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A getCoefficients() 0 5 1
A getOrder() 0 3 1
A getValueOfYForX() 0 15 3
A getSlope() 0 15 3
A getEquation() 0 20 4
B polynomialRegression() 0 52 9
A getValueOfXForY() 0 3 1
A __construct() 0 18 6
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Shared\Trend;
4
5
use Matrix\Matrix;
6
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
7
8
// Phpstan and Scrutinizer seem to have legitimate complaints.
9
// $this->slope is specified where an array is expected in several places.
10
// But it seems that it should always be float.
11
// This code is probably not exercised at all in unit tests.
12
// Private bool property $implemented is set to indicate
13
//     whether this implementation is correct.
14
class PolynomialBestFit extends BestFit
15
{
16
    /**
17
     * Algorithm type to use for best-fit
18
     * (Name of this Trend class).
19
     */
20
    protected string $bestFitType = 'polynomial';
21
22
    /**
23
     * Polynomial order.
24
     */
25
    protected int $order = 0;
26
27
    private bool $implemented = false;
28
29
    /**
30
     * Return the order of this polynomial.
31
     */
32
    public function getOrder(): int
33
    {
34
        return $this->order;
35
    }
36
37
    /**
38
     * Return the Y-Value for a specified value of X.
39
     *
40
     * @param float $xValue X-Value
41
     *
42
     * @return float Y-Value
43
     */
44
    public function getValueOfYForX(float $xValue): float
45
    {
46
        $retVal = $this->getIntersect();
47
        $slope = $this->getSlope();
48
        // Phpstan and Scrutinizer are both correct - getSlope returns float, not array.
49
        // @phpstan-ignore-next-line
50
        foreach ($slope as $key => $value) {
0 ignored issues
show
Bug introduced by
The expression $slope of type double is not traversable.
Loading history...
51
            /** @var float $value */
52
            if ($value != 0.0) {
53
                /** @var int $key */
54
                $retVal += $value * $xValue ** ($key + 1);
55
            }
56
        }
57
58
        return $retVal;
59
    }
60
61
    /**
62
     * Return the X-Value for a specified value of Y.
63
     *
64
     * @param float $yValue Y-Value
65
     *
66
     * @return float X-Value
67
     */
68
    public function getValueOfXForY(float $yValue): float
69
    {
70
        return ($yValue - $this->getIntersect()) / $this->getSlope();
71
    }
72
73
    /**
74
     * Return the Equation of the best-fit line.
75
     *
76
     * @param int $dp Number of places of decimal precision to display
77
     */
78
    public function getEquation(int $dp = 0): string
79
    {
80
        $slope = $this->getSlope($dp);
81
        $intersect = $this->getIntersect($dp);
82
83
        $equation = 'Y = ' . $intersect;
84
        // Phpstan and Scrutinizer are both correct - getSlope returns float, not array.
85
        // @phpstan-ignore-next-line
86
        foreach ($slope as $key => $value) {
0 ignored issues
show
Bug introduced by
The expression $slope of type double is not traversable.
Loading history...
87
            /** @var float|int $value */
88
            if ($value != 0.0) {
89
                $equation .= ' + ' . $value . ' * X';
90
                /** @var int $key */
91
                if ($key > 0) {
92
                    $equation .= '^' . ($key + 1);
93
                }
94
            }
95
        }
96
97
        return $equation;
98
    }
99
100
    /**
101
     * Return the Slope of the line.
102
     *
103
     * @param int $dp Number of places of decimal precision to display
104
     */
105
    public function getSlope(int $dp = 0): float
106
    {
107
        if ($dp != 0) {
108
            $coefficients = [];
109
            //* @phpstan-ignore-next-line
110
            foreach ($this->slope as $coefficient) {
0 ignored issues
show
Bug introduced by
The expression $this->slope of type double is not traversable.
Loading history...
111
                /** @var float|int $coefficient */
112
                $coefficients[] = round($coefficient, $dp);
113
            }
114
115
            // @phpstan-ignore-next-line
116
            return $coefficients;
117
        }
118
119
        return $this->slope;
120
    }
121
122
    /** @return array<float|int> */
123
    public function getCoefficients(int $dp = 0): array
124
    {
125
        // Phpstan and Scrutinizer are both correct - getSlope returns float, not array.
126
        // @phpstan-ignore-next-line
127
        return array_merge([$this->getIntersect($dp)], $this->getSlope($dp));
0 ignored issues
show
Bug introduced by
$this->getSlope($dp) of type double is incompatible with the type array expected by parameter $arrays of array_merge(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

127
        return array_merge([$this->getIntersect($dp)], /** @scrutinizer ignore-type */ $this->getSlope($dp));
Loading history...
128
    }
129
130
    /**
131
     * Execute the regression and calculate the goodness of fit for a set of X and Y data values.
132
     *
133
     * @param int $order Order of Polynomial for this regression
134
     * @param float[] $yValues The set of Y-values for this regression
135
     * @param float[] $xValues The set of X-values for this regression
136
     */
137
    private function polynomialRegression(int $order, array $yValues, array $xValues): void
138
    {
139
        // calculate sums
140
        $x_sum = array_sum($xValues);
141
        $y_sum = array_sum($yValues);
142
        $xx_sum = $xy_sum = $yy_sum = 0;
143
        for ($i = 0; $i < $this->valueCount; ++$i) {
144
            $xy_sum += $xValues[$i] * $yValues[$i];
145
            $xx_sum += $xValues[$i] * $xValues[$i];
146
            $yy_sum += $yValues[$i] * $yValues[$i];
147
        }
148
        /*
149
         *    This routine uses logic from the PHP port of polyfit version 0.1
150
         *    written by Michael Bommarito and Paul Meagher
151
         *
152
         *    The function fits a polynomial function of order $order through
153
         *    a series of x-y data points using least squares.
154
         *
155
         */
156
        $A = [];
157
        $B = [];
158
        for ($i = 0; $i < $this->valueCount; ++$i) {
159
            for ($j = 0; $j <= $order; ++$j) {
160
                $A[$i][$j] = $xValues[$i] ** $j;
161
            }
162
        }
163
        for ($i = 0; $i < $this->valueCount; ++$i) {
164
            $B[$i] = [$yValues[$i]];
165
        }
166
        $matrixA = new Matrix($A);
167
        $matrixB = new Matrix($B);
168
        $C = $matrixA->solve($matrixB);
169
170
        $coefficients = [];
171
        for ($i = 0; $i < $C->rows; ++$i) {
172
            $r = $C->getValue($i + 1, 1); // row and column are origin-1
173
            if (!is_numeric($r) || abs($r + 0) <= 10 ** (-9)) {
174
                $r = 0;
175
            } else {
176
                $r += 0;
177
            }
178
            $coefficients[] = $r;
179
        }
180
181
        $this->intersect = (float) array_shift($coefficients);
182
        // Phpstan is correct
183
        //* @phpstan-ignore-next-line
184
        $this->slope = $coefficients;
185
186
        $this->calculateGoodnessOfFit($x_sum, $y_sum, $xx_sum, $yy_sum, $xy_sum, 0, 0, 0);
187
        foreach ($this->xValues as $xKey => $xValue) {
188
            $this->yBestFitValues[$xKey] = $this->getValueOfYForX($xValue);
189
        }
190
    }
191
192
    /**
193
     * Define the regression and calculate the goodness of fit for a set of X and Y data values.
194
     *
195
     * @param int $order Order of Polynomial for this regression
196
     * @param float[] $yValues The set of Y-values for this regression
197
     * @param float[] $xValues The set of X-values for this regression
198
     */
199 1
    public function __construct(int $order, array $yValues, array $xValues = [])
200
    {
201 1
        if (!$this->implemented) {
202 1
            throw new SpreadsheetException('Polynomial Best Fit not yet implemented');
203
        }
204
205
        parent::__construct($yValues, $xValues);
206
207
        if (!$this->error) {
208
            if ($order < $this->valueCount) {
209
                $this->bestFitType .= '_' . $order;
210
                $this->order = $order;
211
                $this->polynomialRegression($order, $yValues, $xValues);
212
                if (($this->getGoodnessOfFit() < 0.0) || ($this->getGoodnessOfFit() > 1.0)) {
213
                    $this->error = true;
214
                }
215
            } else {
216
                $this->error = true;
217
            }
218
        }
219
    }
220
}
221