Passed
Push — master ( 20aac0...5868f8 )
by
unknown
20:33 queued 08:57
created

PolynomialBestFit::__construct()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 21.1875

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 18
ccs 3
cts 12
cp 0.25
rs 9.2222
c 0
b 0
f 0
cc 6
nc 5
nop 3
crap 21.1875
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
            if ($value != 0.0) {
52
                $retVal += $value * $xValue ** ($key + 1);
53
            }
54
        }
55
56
        return $retVal;
57
    }
58
59
    /**
60
     * Return the X-Value for a specified value of Y.
61
     *
62
     * @param float $yValue Y-Value
63
     *
64
     * @return float X-Value
65
     */
66
    public function getValueOfXForY(float $yValue): float
67
    {
68
        return ($yValue - $this->getIntersect()) / $this->getSlope();
69
    }
70
71
    /**
72
     * Return the Equation of the best-fit line.
73
     *
74
     * @param int $dp Number of places of decimal precision to display
75
     */
76
    public function getEquation(int $dp = 0): string
77
    {
78
        $slope = $this->getSlope($dp);
79
        $intersect = $this->getIntersect($dp);
80
81
        $equation = 'Y = ' . $intersect;
82
        // Phpstan and Scrutinizer are both correct - getSlope returns float, not array.
83
        // @phpstan-ignore-next-line
84
        foreach ($slope as $key => $value) {
0 ignored issues
show
Bug introduced by
The expression $slope of type double is not traversable.
Loading history...
85
            if ($value != 0.0) {
86
                $equation .= ' + ' . $value . ' * X';
87
                if ($key > 0) {
88
                    $equation .= '^' . ($key + 1);
89
                }
90
            }
91
        }
92
93
        return $equation;
94
    }
95
96
    /**
97
     * Return the Slope of the line.
98
     *
99
     * @param int $dp Number of places of decimal precision to display
100
     */
101
    public function getSlope(int $dp = 0): float
102
    {
103
        if ($dp != 0) {
104
            $coefficients = [];
105
            //* @phpstan-ignore-next-line
106
            foreach ($this->slope as $coefficient) {
0 ignored issues
show
Bug introduced by
The expression $this->slope of type double is not traversable.
Loading history...
107
                $coefficients[] = round($coefficient, $dp);
108
            }
109
110
            // @phpstan-ignore-next-line
111
            return $coefficients;
112
        }
113
114
        return $this->slope;
115
    }
116
117
    public function getCoefficients(int $dp = 0): array
118
    {
119
        // Phpstan and Scrutinizer are both correct - getSlope returns float, not array.
120
        // @phpstan-ignore-next-line
121
        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

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