Trend unit tests (#1899)

- Move TREND() functions into the Statistical Trends class
- Unit tests for TREND()
- Create Confidence class for Statistical Confidence functions
This commit is contained in:
Mark Baker 2021-03-06 22:50:19 +01:00 committed by GitHub
parent a79a4ddbab
commit 2d8c8c8ecf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 344 additions and 111 deletions

View File

@ -226,7 +226,7 @@ class Trends
],
[
$bestFitLinear->getSlopeSE(),
$bestFitLinear->getIntersectSE(),
($const === false) ? Functions::NA() : $bestFitLinear->getIntersectSE(),
],
[
$bestFitLinear->getGoodnessOfFit(),
@ -293,7 +293,7 @@ class Trends
],
[
$bestFitExponential->getSlopeSE(),
$bestFitExponential->getIntersectSE(),
($const === false) ? Functions::NA() : $bestFitExponential->getIntersectSE(),
],
[
$bestFitExponential->getGoodnessOfFit(),

View File

@ -348,13 +348,13 @@ class BestFit
$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 @@ class BestFit
}
$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 @@ class BestFit
}
}
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 @@ class BestFit
$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 @@ class BestFit
*
* @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;
}

View File

@ -88,20 +88,17 @@ class ExponentialBestFit extends BestFit
*
* @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 @@ class ExponentialBestFit extends BestFit
parent::__construct($yValues, $xValues);
if (!$this->error) {
$this->exponentialRegression($yValues, $xValues, $const);
$this->exponentialRegression($yValues, $xValues, (bool) $const);
}
}
}

View File

@ -56,9 +56,8 @@ class LinearBestFit extends BestFit
*
* @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 @@ class LinearBestFit extends BestFit
parent::__construct($yValues, $xValues);
if (!$this->error) {
$this->linearRegression($yValues, $xValues, $const);
$this->linearRegression($yValues, $xValues, (bool) $const);
}
}
}

View File

@ -48,7 +48,7 @@ class LogarithmicBestFit extends BestFit
$slope = $this->getSlope($dp);
$intersect = $this->getIntersect($dp);
return 'Y = ' . $intersect . ' + ' . $slope . ' * log(X)';
return 'Y = ' . $slope . ' * log(' . $intersect . ' * X)';
}
/**
@ -56,20 +56,17 @@ class LogarithmicBestFit extends BestFit
*
* @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 @@ class LogarithmicBestFit extends BestFit
parent::__construct($yValues, $xValues);
if (!$this->error) {
$this->logarithmicRegression($yValues, $xValues, $const);
$this->logarithmicRegression($yValues, $xValues, (bool) $const);
}
}
}

View File

@ -178,9 +178,8 @@ class PolynomialBestFit extends BestFit
* @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);

View File

@ -72,28 +72,23 @@ class PowerBestFit extends BestFit
*
* @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 @@ class PowerBestFit extends BestFit
parent::__construct($yValues, $xValues);
if (!$this->error) {
$this->powerRegression($yValues, $xValues, $const);
$this->powerRegression($yValues, $xValues, (bool) $const);
}
}
}

View File

@ -55,10 +55,9 @@ class Trend
$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 @@ class Trend
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 @@ class Trend
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 {

View File

@ -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);

View File

@ -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';
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Shared\Trend;
use PhpOffice\PhpSpreadsheet\Shared\Trend\LinearBestFit;
use PHPUnit\Framework\TestCase;
class LinearBestFitTest extends TestCase
{
/**
* @dataProvider providerLinearBestFit
*
* @param mixed $expectedSlope
* @param mixed $expectedIntersect
* @param mixed $expectedGoodnessOfFit
* @param mixed $yValues
* @param mixed $xValues
* @param mixed $expectedEquation
*/
public function testLinearBestFit(
$expectedSlope,
$expectedIntersect,
$expectedGoodnessOfFit,
$expectedEquation,
$yValues,
$xValues
): void {
$bestFit = new LinearBestFit($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 providerLinearBestFit()
{
return require 'tests/data/Shared/Trend/LinearBestFit.php';
}
}

View File

@ -1,6 +1,22 @@
<?php
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
return [
[
[1.0, 0.0],
[1, 2, 3, 4, 5],
[1, 2, 3, 4, 5],
false,
false,
],
[
[2.310344827586207, 0.0],
[1, 9, 5, 7],
[0, 4, 2, 3],
false,
false,
],
[
[2.0, 1.0],
[1, 9, 5, 7],
@ -8,6 +24,46 @@ return [
true,
false,
],
[
[0.600378787879, 0.0],
[3, 10, 3, 6, 8, 12, 1, 4, 9, 14],
[8, 2, 11, 6, 5, 4, 12, 9, 6, 1],
false,
false,
],
[
[-1.1064189189190, 14.081081081081],
[3, 10, 3, 6, 8, 12, 1, 4, 9, 14],
[8, 2, 11, 6, 5, 4, 12, 9, 6, 1],
true,
false,
],
[
[
[0.600378787879, 0.0],
[0.3130441135917, Functions::NA()],
[0.2901220667036, 7.193206086629],
[3.6782360429317, 9],
[190.3200757575760, 465.679924242424],
],
[3, 10, 3, 6, 8, 12, 1, 4, 9, 14],
[8, 2, 11, 6, 5, 4, 12, 9, 6, 1],
false,
true,
],
[
[
[-1.1064189189190, 14.081081081081],
[0.1491074289251, 1.083468383961],
[0.8731378215565, 1.622464237727],
[55.0605598780781, 8],
[144.9408783783780, 21.059121621622],
],
[3, 10, 3, 6, 8, 12, 1, 4, 9, 14],
[8, 2, 11, 6, 5, 4, 12, 9, 6, 1],
true,
true,
],
[
[
[56.837944664032, 11704.347826086974],
@ -21,7 +77,7 @@ return [
true,
true,
],
// [
// 'multi-series' => [
// [
// [-234.2371645, 2553.21066, 12529.76817, 27.64138737, 52317.83051],
// [13.26801148, 530.6691519, 400.0668382, 5.429374042, 12237.3616],

View File

@ -1,6 +1,20 @@
<?php
return [
[
[1.000174230092, 1.0],
[1, 2, 3, 4, 5],
[1, 10, 100, 1000, 10000],
false,
false,
],
[
[1.000091183030, 2.127357620703],
[1, 2, 3, 4, 5],
[1, 10, 100, 1000, 10000],
true,
false,
],
[
[1.463275628116, 495.304770158727],
[33100, 47300, 69000, 102000, 150000, 220000],
@ -15,4 +29,44 @@ return [
true,
false,
],
[
[1.1743674215053, 1.0],
[3, 10, 3, 6, 8, 12, 1, 4, 9, 14],
[8, 2, 11, 6, 5, 4, 12, 9, 6, 1],
false,
false,
],
[
[0.8135120728565, 20.671878197178],
[3, 10, 3, 6, 8, 12, 1, 4, 9, 14],
[8, 2, 11, 6, 5, 4, 12, 9, 6, 1],
true,
false,
],
// [
// [
// [1.174367421505266, 1.0],
// [0.0672620306083, Functions::NA()],
// [0.3881799938732, 1.545563794251],
// [5.7102087376569, 9],
// [13.6403607201119, 21.498906978904],
// ],
// [3, 10, 3, 6, 8, 12, 1, 4, 9, 14],
// [8, 2, 11, 6, 5, 4, 12, 9, 6, 1],
// false,
// true,
// ],
// [
// [
// [0.8135120728565, 20.671878197178],
// [0.0313021171611, 0.227452478657],
// [0.8445875527654, 0.340603858743],
// [43.4759283593386, 8],
// [5.0436854288511, 0.928087908723],
// ],
// [3, 10, 3, 6, 8, 12, 1, 4, 9, 14],
// [8, 2, 11, 6, 5, 4, 12, 9, 6, 1],
// true,
// true,
// ],
];

View File

@ -0,0 +1,12 @@
<?php
return [
[
'slope' => [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],
],
];

View File

@ -0,0 +1,20 @@
<?php
return [
[
'slope' => [-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],
],
];