Skip to content

Commit

Permalink
Trend unit tests (#1899)
Browse files Browse the repository at this point in the history
- Move TREND() functions into the Statistical Trends class
- Unit tests for TREND()
- Create Confidence class for Statistical Confidence functions
  • Loading branch information
Mark Baker authored Mar 6, 2021
1 parent a79a4dd commit 2d8c8c8
Show file tree
Hide file tree
Showing 15 changed files with 344 additions and 111 deletions.
4 changes: 2 additions & 2 deletions src/PhpSpreadsheet/Calculation/Statistical/Trends.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down
65 changes: 36 additions & 29 deletions src/PhpSpreadsheet/Shared/Trend/BestFit.php
Original file line number Diff line number Diff line change
Expand Up @@ -348,21 +348,21 @@ 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];
}
}

$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;
Expand Down Expand Up @@ -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];
Expand All @@ -426,37 +438,32 @@ 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);
}

/**
* Define the 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($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;
}
Expand Down
21 changes: 9 additions & 12 deletions src/PhpSpreadsheet/Shared/Trend/ExponentialBestFit.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand All @@ -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);
}
}
}
5 changes: 2 additions & 3 deletions src/PhpSpreadsheet/Shared/Trend/LinearBestFit.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
}
}
}
23 changes: 10 additions & 13 deletions src/PhpSpreadsheet/Shared/Trend/LogarithmicBestFit.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,28 +48,25 @@ 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)';
}

/**
* Execute the regression and calculate the goodness of fit for a set of X and Y data values.
*
* @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);
}

/**
Expand All @@ -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);
}
}
}
3 changes: 1 addition & 2 deletions src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
35 changes: 15 additions & 20 deletions src/PhpSpreadsheet/Shared/Trend/PowerBestFit.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand All @@ -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);
}
}
}
9 changes: 4 additions & 5 deletions src/PhpSpreadsheet/Shared/Trend/Trend.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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];
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
49 changes: 49 additions & 0 deletions tests/PhpSpreadsheetTests/Shared/Trend/ExponentialBestFitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace PhpOffice\PhpSpreadsheetTests\Shared\Trend;

use PhpOffice\PhpSpreadsheet\Shared\Trend\ExponentialBestFit;
use PHPUnit\Framework\TestCase;

class ExponentialBestFitTest extends TestCase
{
/**
* @dataProvider providerExponentialBestFit
*
* @param mixed $expectedSlope
* @param mixed $expectedIntersect
* @param mixed $expectedGoodnessOfFit
* @param mixed $yValues
* @param mixed $xValues
* @param mixed $expectedEquation
*/
public function testExponentialBestFit(
$expectedSlope,
$expectedIntersect,
$expectedGoodnessOfFit,
$expectedEquation,
$yValues,
$xValues
): void {
$bestFit = new ExponentialBestFit($yValues, $xValues);
$slope = $bestFit->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';
}
}
Loading

0 comments on commit 2d8c8c8

Please sign in to comment.