diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Trends.php b/src/PhpSpreadsheet/Calculation/Statistical/Trends.php index d06214de2f..a1137cefc1 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Trends.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Trends.php @@ -226,7 +226,7 @@ public static function LINEST($yValues, $xValues = null, $const = true, $stats = ], [ $bestFitLinear->getSlopeSE(), - $bestFitLinear->getIntersectSE(), + ($const === false) ? Functions::NA() : $bestFitLinear->getIntersectSE(), ], [ $bestFitLinear->getGoodnessOfFit(), @@ -293,7 +293,7 @@ public static function LOGEST($yValues, $xValues = null, $const = true, $stats = ], [ $bestFitExponential->getSlopeSE(), - $bestFitExponential->getIntersectSE(), + ($const === false) ? Functions::NA() : $bestFitExponential->getIntersectSE(), ], [ $bestFitExponential->getGoodnessOfFit(), diff --git a/src/PhpSpreadsheet/Shared/Trend/BestFit.php b/src/PhpSpreadsheet/Shared/Trend/BestFit.php index c94997226b..6d6f62838a 100644 --- a/src/PhpSpreadsheet/Shared/Trend/BestFit.php +++ b/src/PhpSpreadsheet/Shared/Trend/BestFit.php @@ -348,13 +348,13 @@ protected function calculateGoodnessOfFit($sumX, $sumY, $sumX2, $sumY2, $sumXY, $bestFitY = $this->yBestFitValues[$xKey] = $this->getValueOfYForX($xValue); $SSres += ($this->yValues[$xKey] - $bestFitY) * ($this->yValues[$xKey] - $bestFitY); - if ($const) { + if ($const === true) { $SStot += ($this->yValues[$xKey] - $meanY) * ($this->yValues[$xKey] - $meanY); } else { $SStot += $this->yValues[$xKey] * $this->yValues[$xKey]; } $SScov += ($this->xValues[$xKey] - $meanX) * ($this->yValues[$xKey] - $meanY); - if ($const) { + if ($const === true) { $SSsex += ($this->xValues[$xKey] - $meanX) * ($this->xValues[$xKey] - $meanX); } else { $SSsex += $this->xValues[$xKey] * $this->xValues[$xKey]; @@ -362,7 +362,7 @@ protected function calculateGoodnessOfFit($sumX, $sumY, $sumX2, $sumY2, $sumXY, } $this->SSResiduals = $SSres; - $this->DFResiduals = $this->valueCount - 1 - $const; + $this->DFResiduals = $this->valueCount - 1 - ($const === true ? 1 : 0); if ($this->DFResiduals == 0.0) { $this->stdevOfResiduals = 0.0; @@ -395,27 +395,39 @@ protected function calculateGoodnessOfFit($sumX, $sumY, $sumX2, $sumY2, $sumXY, } } + private function sumSquares(array $values) + { + return array_sum( + array_map( + function ($value) { + return $value ** 2; + }, + $values + ) + ); + } + /** * @param float[] $yValues * @param float[] $xValues - * @param bool $const */ - protected function leastSquareFit(array $yValues, array $xValues, $const): void + protected function leastSquareFit(array $yValues, array $xValues, bool $const): void { // calculate sums - $x_sum = array_sum($xValues); - $y_sum = array_sum($yValues); - $meanX = $x_sum / $this->valueCount; - $meanY = $y_sum / $this->valueCount; - $mBase = $mDivisor = $xx_sum = $xy_sum = $yy_sum = 0.0; + $sumValuesX = array_sum($xValues); + $sumValuesY = array_sum($yValues); + $meanValueX = $sumValuesX / $this->valueCount; + $meanValueY = $sumValuesY / $this->valueCount; + $sumSquaresX = $this->sumSquares($xValues); + $sumSquaresY = $this->sumSquares($yValues); + $mBase = $mDivisor = 0.0; + $xy_sum = 0.0; for ($i = 0; $i < $this->valueCount; ++$i) { $xy_sum += $xValues[$i] * $yValues[$i]; - $xx_sum += $xValues[$i] * $xValues[$i]; - $yy_sum += $yValues[$i] * $yValues[$i]; - if ($const) { - $mBase += ($xValues[$i] - $meanX) * ($yValues[$i] - $meanY); - $mDivisor += ($xValues[$i] - $meanX) * ($xValues[$i] - $meanX); + if ($const === true) { + $mBase += ($xValues[$i] - $meanValueX) * ($yValues[$i] - $meanValueY); + $mDivisor += ($xValues[$i] - $meanValueX) * ($xValues[$i] - $meanValueX); } else { $mBase += $xValues[$i] * $yValues[$i]; $mDivisor += $xValues[$i] * $xValues[$i]; @@ -426,13 +438,9 @@ protected function leastSquareFit(array $yValues, array $xValues, $const): void $this->slope = $mBase / $mDivisor; // calculate intersect - if ($const) { - $this->intersect = $meanY - ($this->slope * $meanX); - } else { - $this->intersect = 0; - } + $this->intersect = ($const === true) ? $meanValueY - ($this->slope * $meanValueX) : 0.0; - $this->calculateGoodnessOfFit($x_sum, $y_sum, $xx_sum, $yy_sum, $xy_sum, $meanX, $meanY, $const); + $this->calculateGoodnessOfFit($sumValuesX, $sumValuesY, $sumSquaresX, $sumSquaresY, $xy_sum, $meanValueX, $meanValueY, $const); } /** @@ -440,23 +448,22 @@ protected function leastSquareFit(array $yValues, array $xValues, $const): void * * @param float[] $yValues The set of Y-values for this regression * @param float[] $xValues The set of X-values for this regression - * @param bool $const */ - public function __construct($yValues, $xValues = [], $const = true) + public function __construct($yValues, $xValues = []) { // Calculate number of points - $nY = count($yValues); - $nX = count($xValues); + $yValueCount = count($yValues); + $xValueCount = count($xValues); // Define X Values if necessary - if ($nX == 0) { - $xValues = range(1, $nY); - } elseif ($nY != $nX) { + if ($xValueCount === 0) { + $xValues = range(1, $yValueCount); + } elseif ($yValueCount !== $xValueCount) { // Ensure both arrays of points are the same size $this->error = true; } - $this->valueCount = $nY; + $this->valueCount = $yValueCount; $this->xValues = $xValues; $this->yValues = $yValues; } diff --git a/src/PhpSpreadsheet/Shared/Trend/ExponentialBestFit.php b/src/PhpSpreadsheet/Shared/Trend/ExponentialBestFit.php index 82866dee80..eb8cd746d3 100644 --- a/src/PhpSpreadsheet/Shared/Trend/ExponentialBestFit.php +++ b/src/PhpSpreadsheet/Shared/Trend/ExponentialBestFit.php @@ -88,20 +88,17 @@ public function getIntersect($dp = 0) * * @param float[] $yValues The set of Y-values for this regression * @param float[] $xValues The set of X-values for this regression - * @param bool $const */ - private function exponentialRegression($yValues, $xValues, $const): void + private function exponentialRegression(array $yValues, array $xValues, bool $const): void { - foreach ($yValues as &$value) { - if ($value < 0.0) { - $value = 0 - log(abs($value)); - } elseif ($value > 0.0) { - $value = log($value); - } - } - unset($value); + $adjustedYValues = array_map( + function ($value) { + return ($value < 0.0) ? 0 - log(abs($value)) : log($value); + }, + $yValues + ); - $this->leastSquareFit($yValues, $xValues, $const); + $this->leastSquareFit($adjustedYValues, $xValues, $const); } /** @@ -116,7 +113,7 @@ public function __construct($yValues, $xValues = [], $const = true) parent::__construct($yValues, $xValues); if (!$this->error) { - $this->exponentialRegression($yValues, $xValues, $const); + $this->exponentialRegression($yValues, $xValues, (bool) $const); } } } diff --git a/src/PhpSpreadsheet/Shared/Trend/LinearBestFit.php b/src/PhpSpreadsheet/Shared/Trend/LinearBestFit.php index 26a562c551..65d6b4ff44 100644 --- a/src/PhpSpreadsheet/Shared/Trend/LinearBestFit.php +++ b/src/PhpSpreadsheet/Shared/Trend/LinearBestFit.php @@ -56,9 +56,8 @@ public function getEquation($dp = 0) * * @param float[] $yValues The set of Y-values for this regression * @param float[] $xValues The set of X-values for this regression - * @param bool $const */ - private function linearRegression($yValues, $xValues, $const): void + private function linearRegression(array $yValues, array $xValues, bool $const): void { $this->leastSquareFit($yValues, $xValues, $const); } @@ -75,7 +74,7 @@ public function __construct($yValues, $xValues = [], $const = true) parent::__construct($yValues, $xValues); if (!$this->error) { - $this->linearRegression($yValues, $xValues, $const); + $this->linearRegression($yValues, $xValues, (bool) $const); } } } diff --git a/src/PhpSpreadsheet/Shared/Trend/LogarithmicBestFit.php b/src/PhpSpreadsheet/Shared/Trend/LogarithmicBestFit.php index c469067d47..2366dc636a 100644 --- a/src/PhpSpreadsheet/Shared/Trend/LogarithmicBestFit.php +++ b/src/PhpSpreadsheet/Shared/Trend/LogarithmicBestFit.php @@ -48,7 +48,7 @@ public function getEquation($dp = 0) $slope = $this->getSlope($dp); $intersect = $this->getIntersect($dp); - return 'Y = ' . $intersect . ' + ' . $slope . ' * log(X)'; + return 'Y = ' . $slope . ' * log(' . $intersect . ' * X)'; } /** @@ -56,20 +56,17 @@ public function getEquation($dp = 0) * * @param float[] $yValues The set of Y-values for this regression * @param float[] $xValues The set of X-values for this regression - * @param bool $const */ - private function logarithmicRegression($yValues, $xValues, $const): void + private function logarithmicRegression(array $yValues, array $xValues, bool $const): void { - foreach ($xValues as &$value) { - if ($value < 0.0) { - $value = 0 - log(abs($value)); - } elseif ($value > 0.0) { - $value = log($value); - } - } - unset($value); + $adjustedYValues = array_map( + function ($value) { + return ($value < 0.0) ? 0 - log(abs($value)) : log($value); + }, + $yValues + ); - $this->leastSquareFit($yValues, $xValues, $const); + $this->leastSquareFit($adjustedYValues, $xValues, $const); } /** @@ -84,7 +81,7 @@ public function __construct($yValues, $xValues = [], $const = true) parent::__construct($yValues, $xValues); if (!$this->error) { - $this->logarithmicRegression($yValues, $xValues, $const); + $this->logarithmicRegression($yValues, $xValues, (bool) $const); } } } diff --git a/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php b/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php index d959eddb75..1d34e81c19 100644 --- a/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php +++ b/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php @@ -178,9 +178,8 @@ private function polynomialRegression($order, $yValues, $xValues): void * @param int $order Order of Polynomial for this regression * @param float[] $yValues The set of Y-values for this regression * @param float[] $xValues The set of X-values for this regression - * @param bool $const */ - public function __construct($order, $yValues, $xValues = [], $const = true) + public function __construct($order, $yValues, $xValues = []) { parent::__construct($yValues, $xValues); diff --git a/src/PhpSpreadsheet/Shared/Trend/PowerBestFit.php b/src/PhpSpreadsheet/Shared/Trend/PowerBestFit.php index c53eab6381..cafd01158e 100644 --- a/src/PhpSpreadsheet/Shared/Trend/PowerBestFit.php +++ b/src/PhpSpreadsheet/Shared/Trend/PowerBestFit.php @@ -72,28 +72,23 @@ public function getIntersect($dp = 0) * * @param float[] $yValues The set of Y-values for this regression * @param float[] $xValues The set of X-values for this regression - * @param bool $const */ - private function powerRegression($yValues, $xValues, $const): void + private function powerRegression(array $yValues, array $xValues, bool $const): void { - foreach ($xValues as &$value) { - if ($value < 0.0) { - $value = 0 - log(abs($value)); - } elseif ($value > 0.0) { - $value = log($value); - } - } - unset($value); - foreach ($yValues as &$value) { - if ($value < 0.0) { - $value = 0 - log(abs($value)); - } elseif ($value > 0.0) { - $value = log($value); - } - } - unset($value); + $adjustedYValues = array_map( + function ($value) { + return ($value < 0.0) ? 0 - log(abs($value)) : log($value); + }, + $yValues + ); + $adjustedXValues = array_map( + function ($value) { + return ($value < 0.0) ? 0 - log(abs($value)) : log($value); + }, + $xValues + ); - $this->leastSquareFit($yValues, $xValues, $const); + $this->leastSquareFit($adjustedYValues, $adjustedXValues, $const); } /** @@ -108,7 +103,7 @@ public function __construct($yValues, $xValues = [], $const = true) parent::__construct($yValues, $xValues); if (!$this->error) { - $this->powerRegression($yValues, $xValues, $const); + $this->powerRegression($yValues, $xValues, (bool) $const); } } } diff --git a/src/PhpSpreadsheet/Shared/Trend/Trend.php b/src/PhpSpreadsheet/Shared/Trend/Trend.php index 1b7b390107..d0a117cbde 100644 --- a/src/PhpSpreadsheet/Shared/Trend/Trend.php +++ b/src/PhpSpreadsheet/Shared/Trend/Trend.php @@ -55,10 +55,9 @@ public static function calculate($trendType = self::TREND_BEST_FIT, $yValues = [ $nX = count($xValues); // Define X Values if necessary - if ($nX == 0) { + if ($nX === 0) { $xValues = range(1, $nY); - $nX = $nY; - } elseif ($nY != $nX) { + } elseif ($nY !== $nX) { // Ensure both arrays of points are the same size trigger_error('Trend(): Number of elements in coordinate arrays do not match.', E_USER_ERROR); } @@ -84,7 +83,7 @@ public static function calculate($trendType = self::TREND_BEST_FIT, $yValues = [ case self::TREND_POLYNOMIAL_6: if (!isset(self::$trendCache[$key])) { $order = substr($trendType, -1); - self::$trendCache[$key] = new PolynomialBestFit($order, $yValues, $xValues, $const); + self::$trendCache[$key] = new PolynomialBestFit($order, $yValues, $xValues); } return self::$trendCache[$key]; @@ -100,7 +99,7 @@ public static function calculate($trendType = self::TREND_BEST_FIT, $yValues = [ if ($trendType != self::TREND_BEST_FIT_NO_POLY) { foreach (self::$trendTypePolynomialOrders as $trendMethod) { $order = substr($trendMethod, -1); - $bestFit[$trendMethod] = new PolynomialBestFit($order, $yValues, $xValues, $const); + $bestFit[$trendMethod] = new PolynomialBestFit($order, $yValues, $xValues); if ($bestFit[$trendMethod]->getError()) { unset($bestFit[$trendMethod]); } else { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/LogEstTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/LogEstTest.php index 4d926f7620..2b2d1ecf12 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/LogEstTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/LogEstTest.php @@ -19,7 +19,7 @@ class LogEstTest extends TestCase public function testLOGEST($expectedResult, $yValues, $xValues, $const, $stats): void { $result = Statistical::LOGEST($yValues, $xValues, $const, $stats); - + //var_dump($result); $elements = count($expectedResult); for ($element = 0; $element < $elements; ++$element) { self::assertEqualsWithDelta($expectedResult[$element], $result[$element], 1E-12); diff --git a/tests/PhpSpreadsheetTests/Shared/Trend/ExponentialBestFitTest.php b/tests/PhpSpreadsheetTests/Shared/Trend/ExponentialBestFitTest.php new file mode 100644 index 0000000000..32fa9d3187 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Shared/Trend/ExponentialBestFitTest.php @@ -0,0 +1,49 @@ +getSlope(1); + self::assertEquals($expectedSlope[0], $slope); + $slope = $bestFit->getSlope(); + self::assertEquals($expectedSlope[1], $slope); + $intersect = $bestFit->getIntersect(1); + self::assertEquals($expectedIntersect[0], $intersect); + $intersect = $bestFit->getIntersect(); + self::assertEquals($expectedIntersect[1], $intersect); + + $equation = $bestFit->getEquation(2); + self::assertEquals($expectedEquation, $equation); + + self::assertSame($expectedGoodnessOfFit[0], $bestFit->getGoodnessOfFit(6)); + self::assertSame($expectedGoodnessOfFit[1], $bestFit->getGoodnessOfFit()); + } + + public function providerExponentialBestFit() + { + return require 'tests/data/Shared/Trend/ExponentialBestFit.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Shared/Trend/LinearBestFitTest.php b/tests/PhpSpreadsheetTests/Shared/Trend/LinearBestFitTest.php new file mode 100644 index 0000000000..02b82038c0 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Shared/Trend/LinearBestFitTest.php @@ -0,0 +1,49 @@ +getSlope(1); + self::assertEquals($expectedSlope[0], $slope); + $slope = $bestFit->getSlope(); + self::assertEquals($expectedSlope[1], $slope); + $intersect = $bestFit->getIntersect(1); + self::assertEquals($expectedIntersect[0], $intersect); + $intersect = $bestFit->getIntersect(); + self::assertEquals($expectedIntersect[1], $intersect); + + $equation = $bestFit->getEquation(2); + self::assertEquals($expectedEquation, $equation); + + self::assertSame($expectedGoodnessOfFit[0], $bestFit->getGoodnessOfFit(6)); + self::assertSame($expectedGoodnessOfFit[1], $bestFit->getGoodnessOfFit()); + } + + public function providerLinearBestFit() + { + return require 'tests/data/Shared/Trend/LinearBestFit.php'; + } +} diff --git a/tests/data/Calculation/Statistical/LINEST.php b/tests/data/Calculation/Statistical/LINEST.php index 9bd28ffe29..ee5fdb0bde 100644 --- a/tests/data/Calculation/Statistical/LINEST.php +++ b/tests/data/Calculation/Statistical/LINEST.php @@ -1,6 +1,22 @@ [ + // [ + // [-234.2371645, 2553.21066, 12529.76817, 27.64138737, 52317.83051], + // [13.26801148, 530.6691519, 400.0668382, 5.429374042, 12237.3616], + // [0.996747993, 970.5784629, '#N/A', '#N/A', '#N/A'], + // [459.7536742, 6, '#N/A', '#N/A', '#N/A'], + // [1732393319, 5652135.316, '#N/A', '#N/A', '#N/A'], + // ], + // [142000, 144000, 151000, 150000, 139000, 169000, 126000, 142900, 163000, 169000, 149000], + // [ + // [2310, 2, 2, 20], + // [2333, 2, 2, 12], + // [2356, 3, 1.5, 33], + // [2379, 3, 2, 43], + // [2402, 2, 3, 53], + // [2425, 4, 2, 23], + // [2448, 2, 1.5, 99], + // [2471, 2, 2, 34], + // [2494, 3, 3, 23], + // [2517, 4, 4, 55], + // [2540, 2, 3, 22], + // ], + // true, + // true, // ], - // true, - // true, - // ], ]; diff --git a/tests/data/Calculation/Statistical/LOGEST.php b/tests/data/Calculation/Statistical/LOGEST.php index be9e4d7252..bba7487b97 100644 --- a/tests/data/Calculation/Statistical/LOGEST.php +++ b/tests/data/Calculation/Statistical/LOGEST.php @@ -1,6 +1,20 @@ [0.8, 0.813512072856517], + 'intersect' => [20.7, 20.671878197177865], + 'goodnessOfFit' => [0.904868, 0.9048681877346413], + 'equation' => 'Y = 20.67 * 0.81^X', + [3, 10, 3, 6, 8, 12, 1, 4, 9, 14], + [8, 2, 11, 6, 5, 4, 12, 9, 6, 1], + ], +]; diff --git a/tests/data/Shared/Trend/LinearBestFit.php b/tests/data/Shared/Trend/LinearBestFit.php new file mode 100644 index 0000000000..b1be2f9a57 --- /dev/null +++ b/tests/data/Shared/Trend/LinearBestFit.php @@ -0,0 +1,20 @@ + [-1.1, -1.1064189189190], + 'intersect' => [14.1, 14.081081081081], + 'goodnessOfFit' => [0.873138, 0.8731378215564962], + 'equation' => 'Y = 14.08 + -1.11 * X', + [3, 10, 3, 6, 8, 12, 1, 4, 9, 14], + [8, 2, 11, 6, 5, 4, 12, 9, 6, 1], + ], + [ + 'slope' => [1.0, 1.0], + 'intersect' => [-2.0, -2.0], + 'goodnessOfFit' => [1.0, 1.0], + 'equation' => 'Y = -2 + 1 * X', + [1, 2, 3, 4, 5], + [3, 4, 5, 6, 7], + ], +];