Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trend unit tests #1907

Draft
wants to merge 20 commits into
base: master
Choose a base branch
from
160 changes: 67 additions & 93 deletions src/PhpSpreadsheet/Shared/Trend/BestFit.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace PhpOffice\PhpSpreadsheet\Shared\Trend;

class BestFit
abstract class BestFit
{
/**
* Indicator flag for a calculation error.
Expand Down Expand Up @@ -39,13 +39,6 @@ class BestFit
*/
protected $yValues = [];

/**
* Flag indicating whether values should be adjusted to Y=0.
*
* @var bool
*/
protected $adjustToZero = false;

/**
* Y-value series of best-fit values.
*
Expand Down Expand Up @@ -96,24 +89,18 @@ public function getBestFitType()
*
* @param float $xValue X-Value
*
* @return bool Y-Value
* @return float Y-Value
*/
public function getValueOfYForX($xValue)
{
return false;
}
abstract public function getValueOfYForX($xValue);

/**
* Return the X-Value for a specified value of Y.
*
* @param float $yValue Y-Value
*
* @return bool X-Value
* @return float X-Value
*/
public function getValueOfXForY($yValue)
{
return false;
}
abstract public function getValueOfXForY($yValue);

/**
* Return the original set of X-Values.
Expand All @@ -130,12 +117,9 @@ public function getXValues()
*
* @param int $dp Number of places of decimal precision to display
*
* @return bool
* @return string
*/
public function getEquation($dp = 0)
{
return false;
}
abstract public function getEquation($dp = 0);

/**
* Return the Slope of the line.
Expand Down Expand Up @@ -278,16 +262,10 @@ public function getSSResiduals($dp = 0)
}

/**
* @param int $dp Number of places of decimal precision to return
*
* @return float
* @return int
*/
public function getDFResiduals($dp = 0)
public function getDFResiduals()
{
if ($dp != 0) {
return round($this->DFResiduals, $dp);
}

return $this->DFResiduals;
}

Expand Down Expand Up @@ -341,70 +319,68 @@ public function getYBestFitValues()
return $this->yBestFitValues;
}

protected function calculateGoodnessOfFit($sumX, $sumY, $sumX2, $sumY2, $sumXY, $meanX, $meanY, $const): void
private function sumSquares(array $values): float
{
$SSres = $SScov = $SScor = $SStot = $SSsex = 0.0;
foreach ($this->xValues as $xKey => $xValue) {
$bestFitY = $this->yBestFitValues[$xKey] = $this->getValueOfYForX($xValue);

$SSres += ($this->yValues[$xKey] - $bestFitY) * ($this->yValues[$xKey] - $bestFitY);
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 === true) {
$SSsex += ($this->xValues[$xKey] - $meanX) * ($this->xValues[$xKey] - $meanX);
} else {
$SSsex += $this->xValues[$xKey] * $this->xValues[$xKey];
}
}
return array_sum(
array_map(
function ($value) {
return $value ** 2;
},
$values
)
);
}

$this->SSResiduals = $SSres;
$this->DFResiduals = $this->valueCount - 1 - ($const === true ? 1 : 0);
/**
* @param $sumXY
* @param $sumValuesX
* @param $sumValuesY
*/
protected function calculateGoodnessOfFit(float $SStot, $sumXY, $sumValuesX, $sumValuesY, float $SSsex): void
{
$sumSquaresX = $this->sumSquares($this->xValues);
$sumSquaresY = $this->sumSquares($this->yValues);

if ($this->DFResiduals == 0.0) {
$this->stdevOfResiduals = 0.0;
} else {
$this->stdevOfResiduals = sqrt($SSres / $this->DFResiduals);
}
if (($SStot == 0.0) || ($SSres == $SStot)) {
$this->goodnessOfFit = 1;
} else {
$this->goodnessOfFit = 1 - ($SSres / $SStot);
}
$this->stdevOfResiduals = ($this->DFResiduals == 0.0) ? 0.0 : sqrt($this->SSResiduals / $this->DFResiduals);
$this->goodnessOfFit = (($SStot == 0.0) || ($this->SSResiduals == $SStot))
? 1.0
: 1.0 - ($this->SSResiduals / $SStot);

$this->SSRegression = $this->goodnessOfFit * $SStot;
$this->covariance = $SScov / $this->valueCount;
$this->correlation = ($this->valueCount * $sumXY - $sumX * $sumY) / sqrt(($this->valueCount * $sumX2 - $sumX ** 2) * ($this->valueCount * $sumY2 - $sumY ** 2));
$this->correlation = ($this->valueCount * $sumXY - $sumValuesX * $sumValuesY) /
sqrt(($this->valueCount * $sumSquaresX - $sumValuesX ** 2) * ($this->valueCount * $sumSquaresY - $sumValuesY ** 2));
$this->slopeSE = $this->stdevOfResiduals / sqrt($SSsex);
$this->intersectSE = $this->stdevOfResiduals * sqrt(1 / ($this->valueCount - ($sumX * $sumX) / $sumX2));
$this->intersectSE = $this->stdevOfResiduals *
sqrt(1 / ($this->valueCount - ($sumValuesX * $sumValuesX) / $sumSquaresX));

if ($this->SSResiduals != 0.0) {
if ($this->DFResiduals == 0.0) {
$this->f = 0.0;
} else {
$this->f = $this->SSRegression / ($this->SSResiduals / $this->DFResiduals);
}
$this->f = ($this->DFResiduals == 0.0) ? 0.0 : $this->SSRegression / ($this->SSResiduals / $this->DFResiduals);
} else {
if ($this->DFResiduals == 0.0) {
$this->f = 0.0;
} else {
$this->f = $this->SSRegression / $this->DFResiduals;
}
$this->f = ($this->DFResiduals == 0.0) ? 0.0 : $this->SSRegression / $this->DFResiduals;
}
}

private function sumSquares(array $values)
protected function calculateResiduals($sumValuesX, $sumValuesY, $sumXY, $meanValueX, $meanYmeanValueY, $const): void
{
return array_sum(
array_map(
function ($value) {
return $value ** 2;
},
$values
)
);
$SSresiduals = $SScovariance = $SStot = $SSsex = 0.0;
foreach ($this->xValues as $xKey => $xValue) {
$bestFitY = $this->yBestFitValues[$xKey] = $this->getValueOfYForX($xValue);

$SSresiduals += ($this->yValues[$xKey] - $bestFitY) * ($this->yValues[$xKey] - $bestFitY);
$SStot += ($const === true)
? ($this->yValues[$xKey] - $meanYmeanValueY) * ($this->yValues[$xKey] - $meanYmeanValueY)
: $this->yValues[$xKey] * $this->yValues[$xKey];
$SScovariance += ($this->xValues[$xKey] - $meanValueX) * ($this->yValues[$xKey] - $meanYmeanValueY);
$SSsex += ($const === true)
? ($this->xValues[$xKey] - $meanValueX) * ($this->xValues[$xKey] - $meanValueX)
: $this->xValues[$xKey] * $this->xValues[$xKey];
}

$this->DFResiduals = $this->valueCount - 1 - ($const === true ? 1 : 0);
$this->SSResiduals = $SSresiduals;
$this->covariance = $SScovariance / $this->valueCount;

$this->calculateGoodnessOfFit($SStot, $sumXY, $sumValuesX, $sumValuesY, $SSsex);
}

/**
Expand All @@ -418,20 +394,18 @@ protected function leastSquareFit(array $yValues, array $xValues, bool $const):
$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];

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];
}
$mBase += ($const === true)
? ($xValues[$i] - $meanValueX) * ($yValues[$i] - $meanValueY)
: $xValues[$i] * $yValues[$i];
$mDivisor += ($const === true)
? ($xValues[$i] - $meanValueX) * ($xValues[$i] - $meanValueX)
: $xValues[$i] * $xValues[$i];
}

// calculate slope
Expand All @@ -440,7 +414,7 @@ protected function leastSquareFit(array $yValues, array $xValues, bool $const):
// calculate intersect
$this->intersect = ($const === true) ? $meanValueY - ($this->slope * $meanValueX) : 0.0;

$this->calculateGoodnessOfFit($sumValuesX, $sumValuesY, $sumSquaresX, $sumSquaresY, $xy_sum, $meanValueX, $meanValueY, $const);
$this->calculateResiduals($sumValuesX, $sumValuesY, $xy_sum, $meanValueX, $meanValueY, $const);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ private function polynomialRegression($order, $yValues, $xValues): void
$this->intersect = array_shift($coefficients);
$this->slope = $coefficients;

$this->calculateGoodnessOfFit($x_sum, $y_sum, $xx_sum, $yy_sum, $xy_sum, 0, 0, 0);
$this->calculateResiduals($x_sum, $y_sum, $xx_sum, $yy_sum, $xy_sum, 0, 0, 0);
foreach ($this->xValues as $xKey => $xValue) {
$this->yBestFitValues[$xKey] = $this->getValueOfYForX($xValue);
}
Expand Down
2 changes: 1 addition & 1 deletion src/PhpSpreadsheet/Shared/Trend/Trend.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public static function calculate($trendType = self::TREND_BEST_FIT, $yValues = [
$bestFit[$trendMethod] = new $className($yValues, $xValues, $const);
$bestFitValue[$trendMethod] = $bestFit[$trendMethod]->getGoodnessOfFit();
}
if ($trendType != self::TREND_BEST_FIT_NO_POLY) {
if ($trendType !== self::TREND_BEST_FIT_NO_POLY) {
foreach (self::$trendTypePolynomialOrders as $trendMethod) {
$order = substr($trendMethod, -1);
$bestFit[$trendMethod] = new PolynomialBestFit($order, $yValues, $xValues);
Expand Down