diff --git a/src/LinearAlgebra/MatrixFactory.php b/src/LinearAlgebra/MatrixFactory.php index 6e2c90cbb..aaaae19a8 100644 --- a/src/LinearAlgebra/MatrixFactory.php +++ b/src/LinearAlgebra/MatrixFactory.php @@ -5,6 +5,7 @@ use MathPHP\Exception; use MathPHP\Number\Complex; use MathPHP\Number\ObjectArithmetic; +use MathPHP\Polynomials\MonomialExponentGenerator; /** * Matrix factory to create matrices of all types. @@ -524,7 +525,7 @@ public static function hilbert(int $n): NumericMatrix /** * Create the Vandermonde Matrix from a simple array. * - * @param array $M (α₁, α₂, α₃ ⋯ αm) + * @param array|array> $M (α₁, α₂, α₃ ⋯ αm) * @param int $n * * @return NumericMatrix @@ -537,9 +538,28 @@ public static function hilbert(int $n): NumericMatrix public static function vandermonde(array $M, int $n): NumericMatrix { $A = []; - foreach ($M as $row => $α) { - for ($i = 0; $i < $n; $i++) { - $A[$row][$i] = $α ** $i; + if (!empty($M)) { + // Create at least a one-column matrix. + $M = \array_map(function ($value) { + return \is_array($value) ? $value : [$value]; + }, $M); + + $dimension = \count(\reset($M)); + $degree = $n - 1; + $exponentTuples = MonomialExponentGenerator::all($dimension, $degree, true); + + foreach ($M as $row) { + $values = []; + foreach ($exponentTuples as $exponents) { + $value = 1; // start as int + \reset($row); + foreach ($exponents as $exponent) { + $value *= \current($row) ** $exponent; + \next($row); + } + $values[] = $value; + } + $A[] = $values; } } diff --git a/src/Polynomials/MonomialExponentGenerator.php b/src/Polynomials/MonomialExponentGenerator.php new file mode 100644 index 000000000..f1b3639ce --- /dev/null +++ b/src/Polynomials/MonomialExponentGenerator.php @@ -0,0 +1,115 @@ += 1 + * @param int $degree p >= 0 + * @param bool $reverse + * @return list> + */ + public static function all(int $dimension, int $degree, bool $reverse): array + { + $gen = self::iterate($dimension, $degree, $reverse); + return \iterator_to_array($gen, false); + } + + /** + * Generator over all exponent tuples with total degree <= $degree. + * Uses generators to keep memory usage low for large d/p. + * + * @param int $dimension + * @param int $degree + * @param bool $reverse + * @return Generator> yields int[] (length = $dimension) + */ + public static function iterate(int $dimension, int $degree, bool $reverse): Generator + { + if ($dimension < 1) { + throw new InvalidArgumentException("dimension must be >= 1."); + } + if ($degree < 0) { + throw new InvalidArgumentException("degree must be >= 0."); + } + + $current = \array_fill(0, $dimension, 0); + + if ($reverse) { + // Degrees 0..p; within each degree use lexicographic order + for ($g = 0; $g <= $degree; $g++) { + yield from self::recursiveDistributeRevLex($dimension, $g, 0, $current); + } + } else { + // Degrees 0..p; within each degree use lexicographic order + for ($g = 0; $g <= $degree; $g++) { + yield from self::recursiveDistributeLex($dimension, $g, 0, $current); + } + } + } + + /** + * Recursive helper: distributes `remaining` units across positions in lexicographic order. + * + * @param int $dimension + * @param int $remaining + * @param int $pos + * @param list $current Variable reference for performance. + * @return Generator> + */ + private static function recursiveDistributeLex(int $dimension, int $remaining, int $pos, array &$current): Generator + { + if ($pos === $dimension - 1) { + $current[$pos] = $remaining; + yield $current; + return; + } + for ($e = 0; $e <= $remaining; $e++) { + $current[$pos] = $e; + yield from self::recursiveDistributeLex($dimension, $remaining - $e, $pos + 1, $current); + } + } + + /** + * Recursive helper: distributes `remaining` units across positions in reverse-lex order. + * + * @param int $dimension + * @param int $remaining + * @param int $pos + * @param list $current Variable reference for performance. + * @return Generator> + */ + private static function recursiveDistributeRevLex(int $dimension, int $remaining, int $pos, array &$current): Generator + { + if ($pos === $dimension - 1) { + $current[$pos] = $remaining; + yield $current; + return; + } + // reverse-lex: prioritize larger exponents at earlier positions + for ($e = $remaining; $e >= 0; $e--) { + $current[$pos] = $e; + yield from self::recursiveDistributeRevLex($dimension, $remaining - $e, $pos + 1, $current); + } + } +} diff --git a/src/Statistics/Regression/HanesWoolf.php b/src/Statistics/Regression/HanesWoolf.php index c413c49bf..826a18eb2 100644 --- a/src/Statistics/Regression/HanesWoolf.php +++ b/src/Statistics/Regression/HanesWoolf.php @@ -12,12 +12,25 @@ * K + x * * The equation is linearized and fit using Least Squares + * + * @phpstan-import-type SimpleLinearResultModel from Methods\LeastSquares + * @phpstan-import-type PolynomialResultModel from Methods\LeastSquares */ class HanesWoolf extends ParametricRegression { + /** @use Methods\LeastSquares */ use Methods\LeastSquares; use Models\MichaelisMenten; + /** + * @param list $array + * @return SimpleLinearResultModel + */ + protected function createResultModel(array $array): array + { + return $this->createSimpleLinearResultModel($array); + } + /** * Calculate the regression parameters by least squares on linearized data * x / y = x / V + K / V diff --git a/src/Statistics/Regression/Linear.php b/src/Statistics/Regression/Linear.php index d9e91c871..3d0ff712d 100644 --- a/src/Statistics/Regression/Linear.php +++ b/src/Statistics/Regression/Linear.php @@ -24,12 +24,25 @@ * * _ _ * b = y - mx + * + * @phpstan-import-type SimpleLinearResultModel from Methods\LeastSquares + * @phpstan-import-type PolynomialResultModel from Methods\LeastSquares */ class Linear extends ParametricRegression { + /** @use Methods\LeastSquares */ use Methods\LeastSquares; use Models\LinearModel; + /** + * @param list $array + * @return SimpleLinearResultModel + */ + protected function createResultModel(array $array): array + { + return $this->createSimpleLinearResultModel($array); + } + /** * Calculates the regression parameters. * diff --git a/src/Statistics/Regression/LinearThroughPoint.php b/src/Statistics/Regression/LinearThroughPoint.php index 55e6b81aa..970d2176f 100644 --- a/src/Statistics/Regression/LinearThroughPoint.php +++ b/src/Statistics/Regression/LinearThroughPoint.php @@ -23,9 +23,13 @@ * ∑(x-v)² * * b = w - m * v + * + * @phpstan-import-type SimpleLinearResultModel from Methods\LeastSquares + * @phpstan-import-type PolynomialResultModel from Methods\LeastSquares */ class LinearThroughPoint extends ParametricRegression { + /** @use Methods\LeastSquares */ use Methods\LeastSquares; use Models\LinearModel; @@ -53,6 +57,15 @@ public function __construct(array $points, array $force = [0,0]) parent::__construct($points); } + /** + * @param list $array + * @return SimpleLinearResultModel + */ + protected function createResultModel(array $array): array + { + return $this->createSimpleLinearResultModel($array); + } + /** * Calculates the regression parameters. * diff --git a/src/Statistics/Regression/LineweaverBurk.php b/src/Statistics/Regression/LineweaverBurk.php index 33f952ca7..35840a2df 100644 --- a/src/Statistics/Regression/LineweaverBurk.php +++ b/src/Statistics/Regression/LineweaverBurk.php @@ -12,12 +12,25 @@ * K + x * * The equation is linearized and fit using Least Squares + * + * @phpstan-import-type SimpleLinearResultModel from Methods\LeastSquares + * @phpstan-import-type PolynomialResultModel from Methods\LeastSquares */ class LineweaverBurk extends ParametricRegression { use Models\MichaelisMenten; + /** @use Methods\LeastSquares */ use Methods\LeastSquares; + /** + * @param list $array + * @return SimpleLinearResultModel + */ + protected function createResultModel(array $array): array + { + return $this->createSimpleLinearResultModel($array); + } + /** * Calculate the regression parameters by least squares on linearized data * y⁻¹ = K * V⁻¹ * x⁻¹ + V⁻¹ diff --git a/src/Statistics/Regression/Methods/LeastSquares.php b/src/Statistics/Regression/Methods/LeastSquares.php index dfd922b55..ba1a4ecdc 100644 --- a/src/Statistics/Regression/Methods/LeastSquares.php +++ b/src/Statistics/Regression/Methods/LeastSquares.php @@ -2,18 +2,28 @@ namespace MathPHP\Statistics\Regression\Methods; +use LogicException; +use MathPHP\Exception; use MathPHP\Exception\BadDataException; use MathPHP\Exception\BadParameterException; use MathPHP\Exception\IncorrectTypeException; -use MathPHP\LinearAlgebra\MatrixFactory; -use MathPHP\Statistics\RandomVariable; -use MathPHP\Functions\Map\Single; use MathPHP\Functions\Map\Multi; +use MathPHP\Functions\Map\Single; +use MathPHP\LinearAlgebra\MatrixFactory; +use MathPHP\LinearAlgebra\NumericMatrix; +use MathPHP\Polynomials\MonomialExponentGenerator; use MathPHP\Probability\Distribution\Continuous\F; use MathPHP\Probability\Distribution\Continuous\StudentT; -use MathPHP\LinearAlgebra\NumericMatrix; -use MathPHP\Exception; - +use MathPHP\Statistics\RandomVariable; +use MathPHP\Statistics\Regression\Regression; +use MathPHP\Util\Script; + +/** + * @phpstan-type SimpleLinearResultModel array{m: float, b: float} + * @phpstan-type PolynomialResultModel array + * + * @template-covariant TResultModel of array // SimpleLinearResultModel|PolynomialResultModel + */ trait LeastSquares { /** @@ -25,10 +35,10 @@ trait LeastSquares private $reg_ys; /** - * Regression xs + * Regression xs or xss * Since the actual xs may be translated for regression, we need to keep these * handy for regression statistics. - * @var array + * @var array|array> */ private $reg_xs; @@ -43,12 +53,19 @@ trait LeastSquares * Projection Matrix * https://en.wikipedia.org/wiki/Projection_matrix * - * @var NumericMatrix + * @var NumericMatrix|null */ - private $reg_P; + private $reg_P = null; - /** @var float */ - private $fit_constant; + /** @var int */ + private $fit_constant = 1; + + /** + * Number of columns in xss. (Field definition must match Regression::$k.) + * @see Regression::$k + * @var int + */ + protected $k; /** @var int */ private $p; @@ -56,6 +73,9 @@ trait LeastSquares /** @var int Degrees of freedom */ private $ν; + /** @var int Number of terms */ + private $t; + /** @var NumericMatrix */ private $⟮XᵀX⟯⁻¹; @@ -96,21 +116,28 @@ trait LeastSquares * (x)² - x² * * @param array $ys y values - * @param array $xs x values + * @param array|array> $xs x values * @param int $order The order of the polynomial. 1 = linear, 2 = x², etc * @param int $fit_constant '1' if we are fitting a constant to the regression. + * @param bool $calculate_projection true whether to calculate the projection matrix. * * @return NumericMatrix [[m], [b]] * * @throws Exception\MathException */ - public function leastSquares(array $ys, array $xs, int $order = 1, int $fit_constant = 1): NumericMatrix + public function leastSquares(array $ys, array $xs, int $order = 1, int $fit_constant = 1, bool $calculate_projection = true): NumericMatrix { $this->reg_ys = $ys; $this->reg_xs = $xs; $this->fit_constant = $fit_constant; $this->p = $order; - $this->ν = $this->n - $this->p - $this->fit_constant; + + $number_of_terms = MonomialExponentGenerator::getNumberOfTerms($this->k, $order); + if ($fit_constant === 0) { + $number_of_terms--; + } + $this->ν = $this->n - $number_of_terms; + $this->t = $number_of_terms; if ($this->ν <= 0) { throw new Exception\BadDataException('Degrees of freedom ν must be > 0. Computed to be ' . $this->ν); @@ -125,7 +152,7 @@ public function leastSquares(array $ys, array $xs, int $order = 1, int $fit_cons $Xᵀ = $X->transpose(); $this->⟮XᵀX⟯⁻¹ = $Xᵀ->multiply($X)->inverse(); $temp_matrix = $this->⟮XᵀX⟯⁻¹->multiply($Xᵀ); - $this->reg_P = $X->multiply($temp_matrix); + $this->reg_P = $calculate_projection ? $X->multiply($temp_matrix) : null; $β_hat = $temp_matrix->multiply($y); $this->reg_Yhat = $X->multiply($β_hat)->getColumn(0); @@ -133,6 +160,40 @@ public function leastSquares(array $ys, array $xs, int $order = 1, int $fit_cons return $β_hat; } + /** + * @param list $array + * @return TResultModel + */ + abstract protected function createResultModel(array $array): array; + + /** + * Creates a result model of shape [m, b] for simple linear regression. + * + * @param list $array + * @return SimpleLinearResultModel + */ + protected function createSimpleLinearResultModel(array $array): array + { + return [ + 'm' => $array[1], + 'b' => $array[0], + ]; + } + + /** + * Creates a result model of shape [β₀, β₁, ..., βn]. + * + * @param list $array + * @return PolynomialResultModel + */ + protected function createPolynomialResultModel(array $array): array + { + $keys = \array_map(static function (int $i) { + return 'β' . Script::getSubscript($i); + }, \range(0, \count($array) - 1)); + return \array_combine($keys, $array); + } + /** * The Design Matrix contains all the independent variables needed for the least squares regression * @@ -175,7 +236,11 @@ public function createDesignMatrix($xs): NumericMatrix */ public function getProjectionMatrix(): NumericMatrix { - return $this->reg_P; + $reg_P = $this->reg_P; + if ($reg_P === null) { + throw new LogicException('Projection matrix is not calculated. Call leastSquares() with calculate_projection=true first.'); + } + return $reg_P; } /** @@ -192,7 +257,7 @@ public function getProjectionMatrix(): NumericMatrix */ public function leverages(): array { - return $this->reg_P->getDiagonalElements(); + return $this->getProjectionMatrix()->getDiagonalElements(); } /************************************************************************** @@ -289,7 +354,7 @@ public function sumOfSquaresTotal(): float /** * Mean square regression - * MSR = SSᵣ / p + * MSR = SSᵣ / k * * @return float * @@ -297,9 +362,9 @@ public function sumOfSquaresTotal(): float */ public function meanSquareRegression(): float { - $p = $this->p; + $k = $this->k; $SSᵣ = $this->sumOfSquaresRegression(); - $MSR = $SSᵣ / $p; + $MSR = $SSᵣ / $k; return $MSR; } @@ -375,23 +440,28 @@ public function degreesOfFreedom(): float * se(b) = / ---- * √ n * - * @return array{m: float, b: float} [m => se(m), b => se(b)] + * @return TResultModel E.g., [m => se(m), b => se(b)] for simple linear regression * * @throws Exception\BadParameterException * @throws Exception\IncorrectTypeException */ public function standardErrors(): array + { + return $this->createResultModel($this->standardErrorsArray()); + } + + /** + * @return list + * @throws BadParameterException + * @throws IncorrectTypeException + */ + public function standardErrorsArray(): array { $⟮XᵀX⟯⁻¹ = $this->⟮XᵀX⟯⁻¹; $σ² = $this->meanSquareResidual(); $standard_error_matrix = $⟮XᵀX⟯⁻¹->scalarMultiply($σ²); - $standard_error_array = Single::sqrt($standard_error_matrix->getDiagonalElements()); - - return [ - 'm' => $standard_error_array[1], - 'b' => $standard_error_array[0], - ]; + return Single::sqrt($standard_error_matrix->getDiagonalElements()); } /** @@ -452,11 +522,14 @@ public function cooksD(): array $e = $this->residuals(); $h = $this->leverages(); $mse = $this->meanSquareResidual(); - $p = $this->p + $this->fit_constant; + $t = $this->t; return \array_map( - function ($eᵢ, $hᵢ) use ($mse, $p) { - return ($eᵢ ** 2 / $mse / $p) * ($hᵢ / (1 - $hᵢ) ** 2); + function ($eᵢ, $hᵢ) use ($mse, $t) { + if ((1 - $hᵢ) == 0) { + return INF; + } + return ($eᵢ ** 2 / $mse / $t) * ($hᵢ / (1 - $hᵢ) ** 2); }, $e, $h @@ -622,21 +695,33 @@ public function r2(): float * β = regression parameter (coefficient) * se(β) = standard error of the regression parameter (coefficient) * - * @return array{m: float, b: float} [m => t, b => t] + * @return TResultModel E.g., [m => t, b => t] for simple linear regression * * @throws BadParameterException * @throws IncorrectTypeException */ public function tValues(): array { - $se = $this->standardErrors(); - $m = $this->parameters[1]; - $b = $this->parameters[0]; + return $this->createResultModel($this->tValuesArray()); + } - return [ - 'm' => $m / $se['m'], - 'b' => $b / $se['b'], - ]; + /** + * @return list + * @throws BadParameterException + * @throws IncorrectTypeException + */ + public function tValuesArray(): array + { + $se = $this->standardErrorsArray(); + $parameters = $this->parameters; + + return \array_map( + static function (float $parameter, float $se) { + return $parameter / $se; + }, + $parameters, + $se + ); } /** @@ -650,21 +735,33 @@ public function tValues(): array * * alpha = 1 if the regression includes a constant term * - * @return array{m: float, b: float} [m => p, b => p] + * @return TResultModel E.g., [m => p, b => p] for simple linear regression * * @throws BadParameterException * @throws IncorrectTypeException */ public function tProbability(): array + { + return $this->createResultModel($this->tProbabilityArray()); + } + + /** + * @return list + * @throws BadParameterException + * @throws IncorrectTypeException + */ + public function tProbabilityArray(): array { $ν = $this->ν; - $t = $this->tValues(); + $t = $this->tValuesArray(); $studentT = new StudentT($ν); - return [ - 'm' => $studentT->cdf($t['m']), - 'b' => $studentT->cdf($t['b']), - ]; + return \array_map( + static function (float $value) use ($studentT) { + return $studentT->cdf($value); + }, + $t + ); } /** diff --git a/src/Statistics/Regression/Models/PolynomialModel.php b/src/Statistics/Regression/Models/PolynomialModel.php new file mode 100644 index 000000000..42bce69ff --- /dev/null +++ b/src/Statistics/Regression/Models/PolynomialModel.php @@ -0,0 +1,110 @@ + $vector + * @param non-empty-list $params + * + * @return float y evaluated + * + * @throws BadDataException + * @throws IncorrectTypeException + * @throws MathException + * @throws MatrixException + */ + public function evaluateModel(array $vector, array $params): float + { + $M = [$vector]; + $X = MatrixFactory::vandermonde($M, $this->order + 1); + if ($this->fit_constant == 0) { + $X = $X->columnExclude(0); + } + + $row = $X->getRow(0); + $y = 0; + foreach ($row as $i => $x_val) { + $y += $x_val * $params[$i]; + } + return $y; + } + + /** + * Get regression parameters (coefficients). + * + * Use array_values() on the result to get a list of coefficients. + * + * @param array $params + * + * @return array [β₀ + β₁ + β₂ + … + βₖ] + */ + public function getModelParameters(array $params): array + { + $result = []; + for ($i = 0, $len = \count($params); $i < $len; $i++) { + $result['β' . Script::getSubscript($i)] = $params[$i]; + } + return $result; + } + + /** + * Get regression equation + * + * @param non-empty-list $params + * + * @return string + */ + public function getModelEquation(array $params): string + { + $result = ''; + $exponentTuples = $this->getExponentTuples(\count($params) - 1); + if ($this->fit_constant == 0) { + \array_shift($exponentTuples); + } + foreach ($exponentTuples as $i => $exponents) { + $result .= $result === '' ? 'y = ' : ' + '; + $result .= \sprintf('%f', $params[$i]); + foreach ($exponents as $j => $exponent) { + if ($exponent === 0) { + continue; + } + $superscript = $exponent === 1 ? '' : Script::getSuperscript($exponent); + $result .= \sprintf(' * x%s%s', Script::getSubscript($j + 1), $superscript); + } + } + return $result; + } + + /** + * @param int $dimension + * @return list> + */ + public function getExponentTuples(int $dimension): array + { + return MonomialExponentGenerator::all($dimension, $this->order, true); + } +} diff --git a/src/Statistics/Regression/Multilinear.php b/src/Statistics/Regression/Multilinear.php new file mode 100644 index 000000000..a109fd79f --- /dev/null +++ b/src/Statistics/Regression/Multilinear.php @@ -0,0 +1,15 @@ +, float}> $points [ [ x₁ | [ x₁₁, x₁₂, x₁ₖ ], y₁ ], [ x₂ | [ x₂₁, x₂₂, x₂ₖ ], y₂ ], ... ] + * @param bool $calculate_projection true whether to calculate the projection matrix. + */ + public function __construct(array $points, bool $calculate_projection = true) + { + parent::__construct($points, 1, 1, $calculate_projection); + } +} diff --git a/src/Statistics/Regression/ParametricRegression.php b/src/Statistics/Regression/ParametricRegression.php index 23c5d8bb8..3870d2d98 100644 --- a/src/Statistics/Regression/ParametricRegression.php +++ b/src/Statistics/Regression/ParametricRegression.php @@ -14,7 +14,7 @@ abstract class ParametricRegression extends Regression * Have the parent separate the points into xs and ys. * Calculate the regression parameters * - * @param array $points + * @param list, float}> $points [ [ x₁ | [ x₁₁, x₁₂, x₁ₖ ], y₁ ], [ x₂ | [ x₂₁, x₂₂, x₂ₖ ], y₂ ], ... ] */ public function __construct(array $points) { diff --git a/src/Statistics/Regression/Polynomial.php b/src/Statistics/Regression/Polynomial.php new file mode 100644 index 000000000..97f3bb237 --- /dev/null +++ b/src/Statistics/Regression/Polynomial.php @@ -0,0 +1,103 @@ + */ + use Methods\LeastSquares; + use Models\PolynomialModel; + + /** + * @inheritdoc + */ + protected $multipleExplanatoryVariablesSupported = true; + + /** @var bool */ + protected $calculate_projection = true; + + /** + * @param list, float}> $points [ [ x₁ | [ x₁₁, x₁₂, x₁ₖ ], y₁ ], [ x₂ | [ x₂₁, x₂₂, x₂ₖ ], y₂ ], ... ] + * @param int $order + * @param int $fit_constant + * @param bool $calculate_projection true whether to calculate the projection matrix. + */ + public function __construct(array $points, int $order = 1, int $fit_constant = 1, bool $calculate_projection = true) + { + $this->order = $order; + $this->fit_constant = $fit_constant; + $this->calculate_projection = $calculate_projection; + parent::__construct($points); + } + + /** + * @param list $array + * @return PolynomialResultModel + */ + protected function createResultModel(array $array): array + { + return $this->createPolynomialResultModel($array); + } + + /** + * Calculates the regression parameters. + * + * @throws Exception\BadDataException + * @throws Exception\IncorrectTypeException + * @throws Exception\MatrixException + * @throws Exception\MathException + */ + public function calculate(): void + { + $this->parameters = $this->leastSquares($this->ys, $this->xss, $this->order, $this->fit_constant, $this->calculate_projection)->getColumn(0); + } + + /** + * Evaluate the simple regression equation at x. + * Uses the instance model's evaluateModel method. + * + * @param float $x + * + * @return float + * + * @throws BadDataException + * @throws IncorrectTypeException + * @throws MathException + * @throws MatrixException + */ + public function evaluate(float $x): float + { + return $this->evaluateVector([$x]); + } + + /** + * Evaluate the regression equation at x vector. + * Uses the instance model's evaluateModel method. + * + * @param non-empty-list $vector + * + * @return float + * + * @throws BadDataException + * @throws IncorrectTypeException + * @throws MathException + * @throws MatrixException + */ + public function evaluateVector(array $vector): float + { + if (empty($this->parameters)) { + throw new Exception\BadDataException('Regression parameters are not calculated'); + } + return $this->evaluateModel($vector, $this->parameters); + } +} diff --git a/src/Statistics/Regression/PowerLaw.php b/src/Statistics/Regression/PowerLaw.php index c9ecca324..9109e1e4c 100644 --- a/src/Statistics/Regression/PowerLaw.php +++ b/src/Statistics/Regression/PowerLaw.php @@ -26,10 +26,14 @@ * | ∑⟮ln yᵢ⟯ − b∑⟮ln xᵢ⟯ | * a = exp| ------------------ | * |_ n _| + * + * @phpstan-import-type SimpleLinearResultModel from Methods\LeastSquares + * @phpstan-import-type PolynomialResultModel from Methods\LeastSquares */ class PowerLaw extends ParametricRegression { use Models\PowerModel; + /** @use Methods\LeastSquares */ use Methods\LeastSquares; /** @var float */ @@ -38,6 +42,15 @@ class PowerLaw extends ParametricRegression /** @var float */ protected $b; + /** + * @param list $array + * @return SimpleLinearResultModel + */ + protected function createResultModel(array $array): array + { + return $this->createSimpleLinearResultModel($array); + } + /** * Calculate the regression parameters by least squares on linearized data * ln(y) = ln(A) + B*ln(x) diff --git a/src/Statistics/Regression/Regression.php b/src/Statistics/Regression/Regression.php index 3f449fe69..81c974521 100644 --- a/src/Statistics/Regression/Regression.php +++ b/src/Statistics/Regression/Regression.php @@ -2,52 +2,91 @@ namespace MathPHP\Statistics\Regression; +use InvalidArgumentException; + /** * Base class for regressions. */ abstract class Regression { /** - * Array of x and y points: [ [x, y], [x, y], ... ] - * @var array + * Array of x and y points: [ [ x₁ | [ x₁₁, x₁₂, x₁ₖ ], y₁ ], [ x₂ | [ x₂₁, x₂₂, x₂ₖ ], y₂ ], ... ] + * @var list, float}> */ protected $points; /** * X values of the original points - * @var array + * @var list */ protected $xs; /** * Y values of the original points - * @var array + * @var list */ protected $ys; + /** + * X row values of the original points + * @var list> + */ + protected $xss; + /** * Number of points * @var int */ protected $n; + /** + * Number of columns in xss. + * @var int + */ + protected $k; + + /** + * Whether the regression supports multiple explanatory variables. + * @var bool + */ + protected $multipleExplanatoryVariablesSupported = false; + /** * Constructor - Prepares the data arrays for regression analysis * - * @param array $points [ [x, y], [x, y], ... ] + * @param list, float}> $points [ [ x₁ | [ x₁₁, x₁₂, x₁ₖ ], y₁ ], [ x₂ | [ x₂₁, x₂₂, x₂ₖ ], y₂ ], ... ] */ public function __construct(array $points) { $this->points = $points; $this->n = \count($points); + $this->k = empty($points) ? 0 : (\is_array($points[0][0]) ? \count($points[0][0]) : 1); + + // For the multi regression, the format is a list of x for each point. + // Also do some validation. + $this->xss = \array_map(function (array $point) { + $row = \is_array($point[0]) ? $point[0] : [$point[0]]; + $count = \count($row); + if ($this->multipleExplanatoryVariablesSupported) { + if ($count === 0) { + throw new InvalidArgumentException('For multi regression, the x values of each row must be non-empty.'); + } + if ($count !== $this->k) { + throw new InvalidArgumentException('For multi regression, the x values of each row must be of the same length.'); + } + } else { + if ($count !== 1) { + throw new InvalidArgumentException('For simple regression, the x values of each row must be a single value.'); + } + } + return $row; + }, $points); - // Get list of x points and y points. - // This will be fine for linear or polynomial regression, where there is only one x, - // but if expanding to multiple linear, the format will have to change. - $this->xs = \array_map(function ($point) { - return $point[0]; + // Get a list of x points and y points for the simple regression. + $this->xs = \array_map(function (array $point) { + return \is_array($point[0]) ? $point[0][0] : $point[0]; }, $points); - $this->ys = \array_map(function ($point) { + $this->ys = \array_map(function (array $point) { return $point[1]; }, $points); } @@ -61,10 +100,21 @@ public function __construct(array $points) */ abstract public function evaluate(float $x): float; + /** + * Evaluate the regression equation at x vector + * + * @param non-empty-list $vector + * @return float + */ + public function evaluateVector(array $vector): float + { + return $this->evaluate($vector[0]); + } + /** * Get points * - * @return array + * @return list, float}> */ public function getPoints(): array { @@ -74,7 +124,7 @@ public function getPoints(): array /** * Get Xs (x values of each point) * - * @return array of x values + * @return list of x values */ public function getXs(): array { @@ -84,13 +134,23 @@ public function getXs(): array /** * Get Ys (y values of each point) * - * @return array of y values + * @return list of y values */ public function getYs(): array { return $this->ys; } + /** + * Get Xss (x vector of each point) + * + * @return list> of x values + */ + public function getXss(): array + { + return $this->xss; + } + /** * Get sample size (number of points) * @@ -105,10 +165,10 @@ public function getSampleSize(): int * Ŷ (yhat) * A list of the predicted values of Y given the regression. * - * @return array + * @return list */ public function yHat(): array { - return \array_map([$this, 'evaluate'], $this->xs); + return \array_map([$this, 'evaluateVector'], $this->xss); } } diff --git a/src/Statistics/Regression/TheilSen.php b/src/Statistics/Regression/TheilSen.php index e6b9ef27a..a5ef0bde5 100644 --- a/src/Statistics/Regression/TheilSen.php +++ b/src/Statistics/Regression/TheilSen.php @@ -39,13 +39,15 @@ public function calculate(): void { // The slopes array will be a list of slopes between all pairs of points $slopes = []; - $n = \count($this->points); + $n = $this->n; for ($i = 0; $i < $n; $i++) { for ($j = $i + 1; $j < $n; $j++) { - $pointi = $this->points[$i]; - $pointj = $this->points[$j]; - if ($pointj[0] != $pointi[0]) { - $slopes[] = ($pointj[1] - $pointi[1]) / ($pointj[0] - $pointi[0]); + $pointiX = $this->xs[$i]; + $pointiY = $this->ys[$i]; + $pointjX = $this->xs[$j]; + $pointjY = $this->ys[$j]; + if ($pointjX != $pointiX) { + $slopes[] = ($pointjY - $pointiY) / ($pointjX - $pointiX); } } } diff --git a/src/Util/Script.php b/src/Util/Script.php new file mode 100644 index 000000000..5c6d6f450 --- /dev/null +++ b/src/Util/Script.php @@ -0,0 +1,52 @@ + + */ + private static $subscripts = ['₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉']; + + /** + * @var list + */ + private static $superscripts = ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹']; + + /** + * @param int $n + * @return string + */ + public static function getSubscript(int $n): string + { + return self::get($n, self::$subscripts); + } + + /** + * @param int $n + * @return string + */ + public static function getSuperscript(int $n): string + { + return self::get($n, self::$superscripts); + } + + /** + * @param int $n + * @param list $digits + * @return string + */ + private static function get(int $n, array $digits): string + { + return \implode( + '', + \array_map( + static function ($c) use ($digits) { + return $digits[$c]; + }, + \str_split((string)$n) + ) + ); + } +} diff --git a/tests/Polynominals/MonomialExponentGeneratorTest.php b/tests/Polynominals/MonomialExponentGeneratorTest.php new file mode 100644 index 000000000..9d0df4703 --- /dev/null +++ b/tests/Polynominals/MonomialExponentGeneratorTest.php @@ -0,0 +1,190 @@ +assertEquals($expected, $result); + } + + /** + * @return array [dimension, degree, reverse, expected] + */ + public function dataProviderForAll(): array + { + return [ + [ + 1, 0, false, + [[0]] + ], + [ + 1, 1, false, + [[0], [1]] + ], + [ + 1, 2, false, + [[0], [1], [2]] + ], + [ + 2, 0, false, + [[0, 0]] + ], + [ + 2, 1, false, + [ + [0, 0], + [0, 1], [1, 0] + ] + ], + [ + 2, 2, false, + [ + [0, 0], + [0, 1], [1, 0], + [0, 2], [1, 1], [2, 0] + ] + ], + [ + 3, 1, false, + [ + [0, 0, 0], + [0, 0, 1], [0, 1, 0], [1, 0, 0] + ] + ], + [ + 3, 2, false, + [ + [0, 0, 0], + [0, 0, 1], [0, 1, 0], [1, 0, 0], + [0, 0, 2], [0, 1, 1], [0, 2, 0], [1, 0, 1], [1, 1, 0], [2, 0, 0], + ] + ], + [ + 1, 0, true, + [[0]] + ], + [ + 1, 1, true, + [[0], [1]] + ], + [ + 1, 2, true, + [[0], [1], [2]] + ], + [ + 2, 0, true, + [[0, 0]] + ], + [ + 2, 1, true, + [ + [0, 0], + [1, 0], [0, 1] + ] + ], + [ + 2, 2, true, + [ + [0, 0], + [1, 0], [0, 1], + [2, 0], [1, 1], [0, 2] + ] + ], + [ + 3, 1, true, + [ + [0, 0, 0], + [1, 0, 0], [0, 1, 0], [0, 0, 1] + ] + ], + [ + 3, 2, true, + [ + [0, 0, 0], + [1, 0, 0], [0, 1, 0], [0, 0, 1], + [2, 0, 0], [1, 1, 0], [1, 0, 1], [0, 2, 0], [0, 1, 1], [0, 0, 2], + ] + ], + ]; + } + + /** + * @test iterate returns a generator over all exponent tuples + * @dataProvider dataProviderForAll + * @param int $dimension + * @param int $degree + * @param bool $reverse + * @param array $expected + */ + public function testIterate(int $dimension, int $degree, bool $reverse, array $expected): void + { + // When + $generator = MonomialExponentGenerator::iterate($dimension, $degree, $reverse); + $result = \iterator_to_array($generator, false); + + // Then + $this->assertEquals($expected, $result); + } + + /** + * @test iterate throws InvalidArgumentException when dimension < 1 + */ + public function testAllExceptionDimensionLessThanOne(): void + { + // Then + $this->expectException(InvalidArgumentException::class); + + // When + MonomialExponentGenerator::all(0, 0, false); + } + + /** + * @test iterate throws InvalidArgumentException when degree < 0 + */ + public function testAllExceptionDegreeLessThanZero(): void + { + // Then + $this->expectException(InvalidArgumentException::class); + + // When + MonomialExponentGenerator::all(1, -1, false); + } + + /** + * @test getNumberOfTerms returns the correct number of terms + * @dataProvider dataProviderForAll + * @param int $dimension + * @param int $degree + * @param bool $reverse + * @param array $expected + */ + public function testGetNumberOfTerms(int $dimension, int $degree, bool $reverse, array $expected): void + { + // Given + $generator = new MonomialExponentGenerator(); + + // When + $numTerms = $generator->getNumberOfTerms($dimension, $degree); + + // Then + $this->assertEquals(count($expected), $numTerms); + } +} diff --git a/tests/Statistics/Regression/Methods/CooksDTest.php b/tests/Statistics/Regression/Methods/CooksDTest.php new file mode 100644 index 000000000..e995b5d84 --- /dev/null +++ b/tests/Statistics/Regression/Methods/CooksDTest.php @@ -0,0 +1,109 @@ +cooksD(); + + // Points: (1, 1), (2, 3), (3, 3), (4, 5), (5, 5) + // y_hat = [1.4, 2.4, 3.4, 4.4, 5.4] + // residuals = [-0.4, 0.6, -0.4, 0.6, -0.4] + // MSE = (0.16 + 0.36 + 0.16 + 0.36 + 0.16) / 3 = 1.2 / 3 = 0.4 + // p = 2 + // leverages = [0.6, 0.3, 0.2, 0.3, 0.6] + // D1 = ((-0.4)^2 / (0.4 * 2)) * (0.6 / (1-0.6)^2) = (0.16 / 0.8) * (0.6 / 0.16) = 0.2 * 3.75 = 0.75 + // D2 = ((0.6)^2 / (0.4 * 2)) * (0.3 / (1-0.3)^2) = (0.36 / 0.8) * (0.3 / 0.49) = 0.45 * 0.6122... = 0.27551... + // D3 = ((-0.4)^2 / (0.4 * 2)) * (0.2 / (1-0.2)^2) = (0.16 / 0.8) * (0.2 / 0.64) = 0.2 * 0.3125 = 0.0625 + + $expected = [0.75, 0.2755, 0.0625, 0.2755, 0.75]; + + foreach ($expected as $i => $val) { + $this->assertEqualsWithDelta($val, $cooksD[$i], 0.001); + } + } + + /** + * @test CooksD calculation for Multilinear Regression + */ + public function testMultilinearCooksD(): void + { + // Smaller n=4, p=3 case for easy verification. + $points = [ + [[1, 1], 4], + [[2, 1], 7], + [[1, 2], 8], + [[2, 2], 10], + ]; + $regression = new Multilinear($points); + $cooksD = $regression->cooksD(); + + // Verification: + // Leverages: [0.75, 0.75, 0.75, 0.75] + // Residuals: [-0.25, 0.25, 0.25, -0.25] + // MSE: 0.25 + // p = 3 + // D1 = ((-0.25)^2 / (0.25 * 3)) * (0.75 / (1-0.75)^2) + // = (0.0625 / 0.75) * (0.75 / 0.0625) + // = 1/12 * 12 = 1.0 + + $expected = [1.0, 1.0, 1.0, 1.0]; + foreach ($expected as $i => $val) { + $this->assertEqualsWithDelta($val, $cooksD[$i], 0.001); + } + } + + /** + * @test CooksD for Polynomial order 2 without constant + */ + public function testQuadraticNoConstantCooksD(): void + { + $points = [[1, 2], [2, 8], [3, 18], [4, 32], [5, 51]]; + // y = 2x^2 + noise + $regression = new Polynomial($points, 2, 0); // order=2, fit_constant=0 + $cooksD = $regression->cooksD(); + + // ν = n - numberOfTerms + // numberOfTerms = getNumberOfTerms(1, 2) = 3. + // But fit_constant=0, so numberOfTerms = 3 - 1 = 2 (x^2, x) + // ν = 5 - 2 = 3. p = 5 - 3 = 2. + $this->assertEquals(3, $regression->degreesOfFreedom()); + + // Just check if it's computable and positive + foreach ($cooksD as $val) { + $this->assertGreaterThan(0, $val); + } + } +} diff --git a/tests/Statistics/Regression/Methods/MultiLeastSquaresTest.php b/tests/Statistics/Regression/Methods/MultiLeastSquaresTest.php new file mode 100644 index 000000000..86192a00b --- /dev/null +++ b/tests/Statistics/Regression/Methods/MultiLeastSquaresTest.php @@ -0,0 +1,84 @@ + 1. + * Oracle: statsmodels OLS, scikit-learn LinearRegression + */ + public function testDegreesOfFreedomAndStatistics(): void + { + // y ≈ 2x₁ + 3x₂ + 5 with noise, k=2 features, n=10 points + $points = [ + [[1.0, 2.0], 13.3], [[3.0, 1.0], 13.8], + [[2.0, 4.0], 21.5], [[5.0, 2.0], 20.9], + [[4.0, 3.0], 22.4], [[1.0, 5.0], 21.7], + [[6.0, 1.0], 20.2], [[3.0, 3.0], 20.1], + [[2.0, 6.0], 26.6], [[4.0, 4.0], 25.3], + ]; + $regression = new Multilinear($points); + + // Degrees of freedom: should be n - k - 1 = 10 - 2 - 1 = 7 + $this->assertEquals(7, $regression->degreesOfFreedom()); + + // F-statistic: should be 705.22 (statsmodels OLS) + $this->assertEqualsWithDelta(705.2243, $regression->fStatistic(), 0.001); + } + + /** + * Tests polynomial regression of degree 2. + * Equation: y = 2x² + 3x + 5 + * Points: (1, 10), (2, 19), (3, 32), (4, 49), (5, 70) + */ + public function testQuadraticRegression(): void + { + $points = [ + [1, 10], [2, 19], [3, 32], [4, 49], [5, 70] + ]; + $order = 2; // Quadratic + $regression = new Polynomial($points, $order); + + // Parameters: [5, 3, 2] (constant first due to Vandermonde) + $params = $regression->getParameters(); + $this->assertEqualsWithDelta(5.0, $params['β₀'], 0.001); + $this->assertEqualsWithDelta(3.0, $params['β₁'], 0.001); + $this->assertEqualsWithDelta(2.0, $params['β₂'], 0.001); + + // Degrees of freedom: n - (order + 1) = 5 - (2 + 1) = 2 + $this->assertEquals(2, $regression->degreesOfFreedom()); + + // Prediction + $this->assertEqualsWithDelta(95.0, $regression->evaluate(6), 0.001); + } + + /** + * Tests polynomial regression of degree 3. + * Equation: y = x³ - 2x² + 5x + 10 + * Points: (0, 10), (1, 14), (2, 20), (3, 34), (4, 62) + */ + public function testCubicRegression(): void + { + $points = [ + [0, 10], [1, 14], [2, 20], [3, 34], [4, 62] + ]; + $order = 3; + $regression = new Polynomial($points, $order); + + $params = $regression->getParameters(); + $this->assertEqualsWithDelta(10.0, $params['β₀'], 0.001); + $this->assertEqualsWithDelta(5.0, $params['β₁'], 0.001); + $this->assertEqualsWithDelta(-2.0, $params['β₂'], 0.001); + $this->assertEqualsWithDelta(1.0, $params['β₃'], 0.001); + + // Degrees of freedom: n - (order + 1) = 5 - (3 + 1) = 1 + $this->assertEquals(1, $regression->degreesOfFreedom()); + + // Prediction: 5³ - 2(5²) + 5(5) + 10 = 125 - 50 + 25 + 10 = 110 + $this->assertEqualsWithDelta(110.0, $regression->evaluate(5), 0.001); + } +} diff --git a/tests/Statistics/Regression/MultilinearTest.php b/tests/Statistics/Regression/MultilinearTest.php new file mode 100644 index 000000000..803b33307 --- /dev/null +++ b/tests/Statistics/Regression/MultilinearTest.php @@ -0,0 +1,284 @@ +assertEqualsWithDelta($expectedXss, $regression->getXss(), 0.00001); + } + + /** + * @return array + */ + public function dataProviderForMultiXss(): array + { + return [ + [ + [[1, 9.5], [2, 10], [3, 15]], + [[1], [2], [3]], + ], + [ + [[[1], 9.5], [[2], 10], [[3], 15]], + [[1], [2], [3]], + ], + [ + [ + [[1, 5], 9.5], + [[2, 8], 10], + [[3, 2], 15], + [[0, 0], 10], + ], + [ + [1, 5], + [2, 8], + [3, 2], + [0, 0], + ], + ], + ]; + } + + /** + * @test getXss + * @dataProvider dataProviderForMultiXssBadData + * @param array $points + * @param array $expectedXss + */ + public function testMultiXssBadData(array $points, array $expectedXss): void + { + // Then + $this->expectException(Exception\BadDataException::class); + + // When + $regression = new Multilinear($points); + } + + /** + * @return array + */ + public function dataProviderForMultiXssBadData(): array + { + return [ + [ + [ + [[1, 5], 9.5], + [[2, 8], 10], + [[3, 2], 15], + ], + [ + [1, 5], + [2, 8], + [3, 2], + ], + ], + ]; + } + + /** + * @test getParameters + * @dataProvider dataProviderForParameters + * @param array $points + * @param array $expected_parameters + */ + public function testGetParameters(array $points, array $expected_parameters) + { + // Given + $regression = new Multilinear($points); + + // When + $parameters = $regression->getParameters(); + + // Then + foreach ($expected_parameters as $i => $value) { + $this->assertEqualsWithDelta($value, $parameters[$i], 0.0001); + } + } + + /** + * @return array [points, expected_parameters] + */ + public function dataProviderForParameters(): array + { + return [ + [ + // y = 2x₁ + 3x₂ + 5 + // Vandermonde (d=2, p=1): [0,0], [1,0], [0,1] + // β₀ = 5 + // β₁ = x₁ coeff = 2 + // β₂ = x₂ coeff = 3 + [ + [[1, 1], 10], + [[2, 1], 12], + [[1, 2], 13], + [[2, 2], 15], + [[0, 0], 5], + ], + [ + 'β₀' => 5, + 'β₁' => 2, + 'β₂' => 3, + ], + ], + [ + // Example from real data or calculated elsewhere + // y = 2x₁ - 0.5x₂ + 10 + // Vandermonde (d=2, p=1): [0,0], [1,0], [0,1] + // β₀ = 10 + // β₁ = x₁ coeff = 2 + // β₂ = x₂ coeff = -0.5 + [ + [[1, 5], 9.5], // 2(1) - 0.5(5) + 10 = 2 - 2.5 + 10 = 9.5 + [[2, 8], 10], // 2(2) - 0.5(8) + 10 = 4 - 4 + 10 = 10 + [[3, 2], 15], // 2(3) - 0.5(2) + 10 = 6 - 1 + 10 = 15 + [[0, 0], 10], + ], + [ + 'β₀' => 10, + 'β₁' => 2, + 'β₂' => -0.5, + ], + ], + ]; + } + + /** + * @test evaluateVector + * @dataProvider dataProviderForEvaluateVector + * @param array $points + * @param array $vector + * @param float $expected_y + */ + public function testEvaluateVector(array $points, array $vector, float $expected_y) + { + // Given + $regression = new Multilinear($points); + + // When + $y = $regression->evaluateVector($vector); + + // Then + $this->assertEqualsWithDelta($expected_y, $y, 0.0001); + } + + /** + * @return array [points, vector, expected_y] + */ + public function dataProviderForEvaluateVector(): array + { + return [ + [ + // y = 2x₁ + 3x₂ + 5 + [ + [[1, 1], 10], + [[2, 1], 12], + [[1, 2], 13], + [[2, 2], 15], + [[0, 0], 5], + ], + [3, 4], // 2(3) + 3(4) + 5 = 6 + 12 + 5 = 23 + 23, + ], + ]; + } + + /** + * @test evaluate + */ + public function testEvaluate() + { + // Given + $points = [ + [[1, 1], 10], + [[2, 5], 12], + [[3, 2], 14], + [[4, 8], 16], + ]; + $regression = new Multilinear($points); + + // When + $y = $regression->evaluate(1.0); + + // Then + $this->assertEqualsWithDelta(10, $y, 0.0001); + } + + /** + * @test getEquation + * @dataProvider dataProviderForEquation + * @param array $points + * @param string $expected_equation + */ + public function testGetEquation(array $points, string $expected_equation) + { + // Given + $regression = new Multilinear($points); + + // When + $equation = $regression->getEquation(); + + // Then + $this->assertEquals($expected_equation, $equation); + } + + /** + * @return array [points, expected_equation] + */ + public function dataProviderForEquation(): array + { + return [ + [ + [ + [[1, 1], 10], + [[2, 1], 12], + [[1, 2], 13], + [[2, 2], 15], + [[0, 0], 5], + ], + 'y = 5.000000 + 2.000000 * x₁ + 3.000000 * x₂', + ], + ]; + } + + /** + * @test yHat + */ + public function testYHat() + { + // Given + $points = [ + [[1, 1], 10], + [[2, 1], 12], + [[1, 2], 13], + [[2, 2], 15], + [[0, 0], 5], + ]; + $regression = new Multilinear($points); + + // When + $yHat = $regression->yHat(); + + // Then + $this->assertCount(5, $yHat); + $this->assertEqualsWithDelta(10, $yHat[0], 0.0001); + $this->assertEqualsWithDelta(12, $yHat[1], 0.0001); + $this->assertEqualsWithDelta(13, $yHat[2], 0.0001); + $this->assertEqualsWithDelta(15, $yHat[3], 0.0001); + $this->assertEqualsWithDelta(5, $yHat[4], 0.0001); + } +} diff --git a/tests/Statistics/Regression/RegressionTest.php b/tests/Statistics/Regression/RegressionTest.php index c2171f3ea..c481b1acd 100644 --- a/tests/Statistics/Regression/RegressionTest.php +++ b/tests/Statistics/Regression/RegressionTest.php @@ -263,4 +263,64 @@ public function dataProviderForSumOfSquaresEqualsSumOfSQuaresRegressionPlusSumOf [ [[61,105], [62,120], [63,120], [65,160], [65,120], [68,145], [69,175], [70,160], [72,185], [75,210]] ], ]; } + + /** + * @test getXss + * @dataProvider dataProviderForSimpleXss + * @param array $points + * @param array $expectedXss + */ + public function testSimpleXss(array $points, array $expectedXss): void + { + // Given + $regression = new Linear($points); + + // Then + $this->assertEqualsWithDelta($expectedXss, $regression->getXss(), 0.00001); + } + + /** + * @return array + */ + public function dataProviderForSimpleXss(): array + { + $y = 0; + return [ + [ + [[1, $y], [2, $y], [3, $y]], + [[1], [2], [3]], + ], + [ + [[[1], $y], [[2], $y], [[3], $y]], + [[1], [2], [3]], + ], + ]; + } + + /** + * @test getXss + * @dataProvider dataProviderForSimpleXssInvalidArgument + * @param array $points + */ + public function testSimpleXssInvalidArgument(array $points): void + { + // Then + $this->expectException(\InvalidArgumentException::class); + + // When + $regression = new Linear($points); + } + + /** + * @return array + */ + public function dataProviderForSimpleXssInvalidArgument(): array + { + $y = 0; + return [ + [ + [[[1, 10], $y], [[2, 20], $y], [[3, 30], $y]], + ], + ]; + } } diff --git a/tests/Statistics/Regression/TheilSenTest.php b/tests/Statistics/Regression/TheilSenTest.php index a09c36c0e..42b2f035d 100644 --- a/tests/Statistics/Regression/TheilSenTest.php +++ b/tests/Statistics/Regression/TheilSenTest.php @@ -24,12 +24,11 @@ public function testConstructor() /** * @test getPoints + * @dataProvider dataProviderForGetPoints + * @param array $points */ - public function testGetPoints() + public function testGetPoints(array $points): void { - // Given - $points = [ [1,2], [2,3], [4,5], [5,7], [6,8] ]; - // When $regression = new TheilSen($points); @@ -37,6 +36,21 @@ public function testGetPoints() $this->assertEquals($points, $regression->getPoints()); } + /** + * @return array [points] + */ + public function dataProviderForGetPoints(): array + { + return [ + [ + [[1, 2], [2, 3], [4, 5], [5, 7], [6, 8]], + ], + [ + [[[1], 2], [[2], 3], [[4], 5], [[5], 7], [[6], 8]], + ], + ]; + } + /** * @test getXs */