Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]

   1  <?php
   2  
   3  namespace PhpOffice\PhpSpreadsheet\Shared\Trend;
   4  
   5  use Matrix\Matrix;
   6  
   7  // Phpstan and Scrutinizer seem to have legitimate complaints.
   8  // $this->slope is specified where an array is expected in several places.
   9  // But it seems that it should always be float.
  10  // This code is probably not exercised at all in unit tests.
  11  class PolynomialBestFit extends BestFit
  12  {
  13      /**
  14       * Algorithm type to use for best-fit
  15       * (Name of this Trend class).
  16       *
  17       * @var string
  18       */
  19      protected $bestFitType = 'polynomial';
  20  
  21      /**
  22       * Polynomial order.
  23       *
  24       * @var int
  25       */
  26      protected $order = 0;
  27  
  28      /**
  29       * Return the order of this polynomial.
  30       *
  31       * @return int
  32       */
  33      public function getOrder()
  34      {
  35          return $this->order;
  36      }
  37  
  38      /**
  39       * Return the Y-Value for a specified value of X.
  40       *
  41       * @param float $xValue X-Value
  42       *
  43       * @return float Y-Value
  44       */
  45      public function getValueOfYForX($xValue)
  46      {
  47          $retVal = $this->getIntersect();
  48          $slope = $this->getSlope();
  49          // Phpstan and Scrutinizer are both correct - getSlope returns float, not array.
  50          // @phpstan-ignore-next-line
  51          foreach ($slope as $key => $value) {
  52              if ($value != 0.0) {
  53                  $retVal += $value * $xValue ** ($key + 1);
  54              }
  55          }
  56  
  57          return $retVal;
  58      }
  59  
  60      /**
  61       * Return the X-Value for a specified value of Y.
  62       *
  63       * @param float $yValue Y-Value
  64       *
  65       * @return float X-Value
  66       */
  67      public function getValueOfXForY($yValue)
  68      {
  69          return ($yValue - $this->getIntersect()) / $this->getSlope();
  70      }
  71  
  72      /**
  73       * Return the Equation of the best-fit line.
  74       *
  75       * @param int $dp Number of places of decimal precision to display
  76       *
  77       * @return string
  78       */
  79      public function getEquation($dp = 0)
  80      {
  81          $slope = $this->getSlope($dp);
  82          $intersect = $this->getIntersect($dp);
  83  
  84          $equation = 'Y = ' . $intersect;
  85          // Phpstan and Scrutinizer are both correct - getSlope returns float, not array.
  86          // @phpstan-ignore-next-line
  87          foreach ($slope as $key => $value) {
  88              if ($value != 0.0) {
  89                  $equation .= ' + ' . $value . ' * X';
  90                  if ($key > 0) {
  91                      $equation .= '^' . ($key + 1);
  92                  }
  93              }
  94          }
  95  
  96          return $equation;
  97      }
  98  
  99      /**
 100       * Return the Slope of the line.
 101       *
 102       * @param int $dp Number of places of decimal precision to display
 103       *
 104       * @return float
 105       */
 106      public function getSlope($dp = 0)
 107      {
 108          if ($dp != 0) {
 109              $coefficients = [];
 110              // Scrutinizer is correct - $this->slope is float, not array.
 111              //* @phpstan-ignore-next-line
 112              foreach ($this->slope as $coefficient) {
 113                  $coefficients[] = round($coefficient, $dp);
 114              }
 115  
 116              // @phpstan-ignore-next-line
 117              return $coefficients;
 118          }
 119  
 120          return $this->slope;
 121      }
 122  
 123      /**
 124       * @param int $dp
 125       *
 126       * @return array
 127       */
 128      public function getCoefficients($dp = 0)
 129      {
 130          // Phpstan and Scrutinizer are both correct - getSlope returns float, not array.
 131          // @phpstan-ignore-next-line
 132          return array_merge([$this->getIntersect($dp)], $this->getSlope($dp));
 133      }
 134  
 135      /**
 136       * Execute the regression and calculate the goodness of fit for a set of X and Y data values.
 137       *
 138       * @param int $order Order of Polynomial for this regression
 139       * @param float[] $yValues The set of Y-values for this regression
 140       * @param float[] $xValues The set of X-values for this regression
 141       */
 142      private function polynomialRegression($order, $yValues, $xValues): void
 143      {
 144          // calculate sums
 145          $x_sum = array_sum($xValues);
 146          $y_sum = array_sum($yValues);
 147          $xx_sum = $xy_sum = $yy_sum = 0;
 148          for ($i = 0; $i < $this->valueCount; ++$i) {
 149              $xy_sum += $xValues[$i] * $yValues[$i];
 150              $xx_sum += $xValues[$i] * $xValues[$i];
 151              $yy_sum += $yValues[$i] * $yValues[$i];
 152          }
 153          /*
 154           *    This routine uses logic from the PHP port of polyfit version 0.1
 155           *    written by Michael Bommarito and Paul Meagher
 156           *
 157           *    The function fits a polynomial function of order $order through
 158           *    a series of x-y data points using least squares.
 159           *
 160           */
 161          $A = [];
 162          $B = [];
 163          for ($i = 0; $i < $this->valueCount; ++$i) {
 164              for ($j = 0; $j <= $order; ++$j) {
 165                  $A[$i][$j] = $xValues[$i] ** $j;
 166              }
 167          }
 168          for ($i = 0; $i < $this->valueCount; ++$i) {
 169              $B[$i] = [$yValues[$i]];
 170          }
 171          $matrixA = new Matrix($A);
 172          $matrixB = new Matrix($B);
 173          $C = $matrixA->solve($matrixB);
 174  
 175          $coefficients = [];
 176          for ($i = 0; $i < $C->rows; ++$i) {
 177              $r = $C->getValue($i + 1, 1); // row and column are origin-1
 178              if (abs($r) <= 10 ** (-9)) {
 179                  $r = 0;
 180              }
 181              $coefficients[] = $r;
 182          }
 183  
 184          $this->intersect = array_shift($coefficients);
 185          // Phpstan (and maybe Scrutinizer) are correct
 186          //* @phpstan-ignore-next-line
 187          $this->slope = $coefficients;
 188  
 189          $this->calculateGoodnessOfFit($x_sum, $y_sum, $xx_sum, $yy_sum, $xy_sum, 0, 0, 0);
 190          foreach ($this->xValues as $xKey => $xValue) {
 191              $this->yBestFitValues[$xKey] = $this->getValueOfYForX($xValue);
 192          }
 193      }
 194  
 195      /**
 196       * Define the regression and calculate the goodness of fit for a set of X and Y data values.
 197       *
 198       * @param int $order Order of Polynomial for this regression
 199       * @param float[] $yValues The set of Y-values for this regression
 200       * @param float[] $xValues The set of X-values for this regression
 201       */
 202      public function __construct($order, $yValues, $xValues = [])
 203      {
 204          parent::__construct($yValues, $xValues);
 205  
 206          if (!$this->error) {
 207              if ($order < $this->valueCount) {
 208                  $this->bestFitType .= '_' . $order;
 209                  $this->order = $order;
 210                  $this->polynomialRegression($order, $yValues, $xValues);
 211                  if (($this->getGoodnessOfFit() < 0.0) || ($this->getGoodnessOfFit() > 1.0)) {
 212                      $this->error = true;
 213                  }
 214              } else {
 215                  $this->error = true;
 216              }
 217          }
 218      }
 219  }