Extract Normal and Standard Normal Distributions from the Statistical Class (#1981)

* Extract Normal and Standard Normal Distributions from the Statistical Class
* Extract ZTest from the Statistical Class, and move it to the Standard Normal Distribution class
Additional unit tests for NORMINV()
* Extract LogNormal distribution functions from Statistical
This commit is contained in:
Mark Baker 2021-04-02 20:17:03 +02:00 committed by GitHub
parent a4982fd9fe
commit a2bb825bc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 563 additions and 217 deletions

View File

@ -1586,22 +1586,22 @@ class Calculation
], ],
'LOGINV' => [ 'LOGINV' => [
'category' => Category::CATEGORY_STATISTICAL, 'category' => Category::CATEGORY_STATISTICAL,
'functionCall' => [Statistical::class, 'LOGINV'], 'functionCall' => [Statistical\Distributions\LogNormal::class, 'inverse'],
'argumentCount' => '3', 'argumentCount' => '3',
], ],
'LOGNORMDIST' => [ 'LOGNORMDIST' => [
'category' => Category::CATEGORY_STATISTICAL, 'category' => Category::CATEGORY_STATISTICAL,
'functionCall' => [Statistical::class, 'LOGNORMDIST'], 'functionCall' => [Statistical\Distributions\LogNormal::class, 'cumulative'],
'argumentCount' => '3', 'argumentCount' => '3',
], ],
'LOGNORM.DIST' => [ 'LOGNORM.DIST' => [
'category' => Category::CATEGORY_STATISTICAL, 'category' => Category::CATEGORY_STATISTICAL,
'functionCall' => [Statistical::class, 'LOGNORMDIST2'], 'functionCall' => [Statistical\Distributions\LogNormal::class, 'distribution'],
'argumentCount' => '4', 'argumentCount' => '4',
], ],
'LOGNORM.INV' => [ 'LOGNORM.INV' => [
'category' => Category::CATEGORY_STATISTICAL, 'category' => Category::CATEGORY_STATISTICAL,
'functionCall' => [Statistical::class, 'LOGINV'], 'functionCall' => [Statistical\Distributions\LogNormal::class, 'inverse'],
'argumentCount' => '3', 'argumentCount' => '3',
], ],
'LOOKUP' => [ 'LOOKUP' => [
@ -1776,42 +1776,42 @@ class Calculation
], ],
'NORMDIST' => [ 'NORMDIST' => [
'category' => Category::CATEGORY_STATISTICAL, 'category' => Category::CATEGORY_STATISTICAL,
'functionCall' => [Statistical::class, 'NORMDIST'], 'functionCall' => [Statistical\Distributions\Normal::class, 'distribution'],
'argumentCount' => '4', 'argumentCount' => '4',
], ],
'NORM.DIST' => [ 'NORM.DIST' => [
'category' => Category::CATEGORY_STATISTICAL, 'category' => Category::CATEGORY_STATISTICAL,
'functionCall' => [Statistical::class, 'NORMDIST'], 'functionCall' => [Statistical\Distributions\Normal::class, 'distribution'],
'argumentCount' => '4', 'argumentCount' => '4',
], ],
'NORMINV' => [ 'NORMINV' => [
'category' => Category::CATEGORY_STATISTICAL, 'category' => Category::CATEGORY_STATISTICAL,
'functionCall' => [Statistical::class, 'NORMINV'], 'functionCall' => [Statistical\Distributions\Normal::class, 'inverse'],
'argumentCount' => '3', 'argumentCount' => '3',
], ],
'NORM.INV' => [ 'NORM.INV' => [
'category' => Category::CATEGORY_STATISTICAL, 'category' => Category::CATEGORY_STATISTICAL,
'functionCall' => [Statistical::class, 'NORMINV'], 'functionCall' => [Statistical\Distributions\Normal::class, 'inverse'],
'argumentCount' => '3', 'argumentCount' => '3',
], ],
'NORMSDIST' => [ 'NORMSDIST' => [
'category' => Category::CATEGORY_STATISTICAL, 'category' => Category::CATEGORY_STATISTICAL,
'functionCall' => [Statistical::class, 'NORMSDIST'], 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'cumulative'],
'argumentCount' => '1', 'argumentCount' => '1',
], ],
'NORM.S.DIST' => [ 'NORM.S.DIST' => [
'category' => Category::CATEGORY_STATISTICAL, 'category' => Category::CATEGORY_STATISTICAL,
'functionCall' => [Statistical::class, 'NORMSDIST2'], 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'distribution'],
'argumentCount' => '1,2', 'argumentCount' => '1,2',
], ],
'NORMSINV' => [ 'NORMSINV' => [
'category' => Category::CATEGORY_STATISTICAL, 'category' => Category::CATEGORY_STATISTICAL,
'functionCall' => [Statistical::class, 'NORMSINV'], 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'inverse'],
'argumentCount' => '1', 'argumentCount' => '1',
], ],
'NORM.S.INV' => [ 'NORM.S.INV' => [
'category' => Category::CATEGORY_STATISTICAL, 'category' => Category::CATEGORY_STATISTICAL,
'functionCall' => [Statistical::class, 'NORMSINV'], 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'inverse'],
'argumentCount' => '1', 'argumentCount' => '1',
], ],
'NOT' => [ 'NOT' => [
@ -2651,12 +2651,12 @@ class Calculation
], ],
'ZTEST' => [ 'ZTEST' => [
'category' => Category::CATEGORY_STATISTICAL, 'category' => Category::CATEGORY_STATISTICAL,
'functionCall' => [Statistical::class, 'ZTEST'], 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'zTest'],
'argumentCount' => '2-3', 'argumentCount' => '2-3',
], ],
'Z.TEST' => [ 'Z.TEST' => [
'category' => Category::CATEGORY_STATISTICAL, 'category' => Category::CATEGORY_STATISTICAL,
'functionCall' => [Statistical::class, 'ZTEST'], 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'zTest'],
'argumentCount' => '2-3', 'argumentCount' => '2-3',
], ],
]; ];

View File

@ -21,90 +21,6 @@ class Statistical
const MAX_ITERATIONS = 256; const MAX_ITERATIONS = 256;
const SQRT2PI = 2.5066282746310005024157652848110452530069867406099; const SQRT2PI = 2.5066282746310005024157652848110452530069867406099;
/*
* inverse_ncdf.php
* -------------------
* begin : Friday, January 16, 2004
* copyright : (C) 2004 Michael Nickerson
* email : nickersonm@yahoo.com
*
*/
private static function inverseNcdf($p)
{
// Inverse ncdf approximation by Peter J. Acklam, implementation adapted to
// PHP by Michael Nickerson, using Dr. Thomas Ziegler's C implementation as
// a guide. http://home.online.no/~pjacklam/notes/invnorm/index.html
// I have not checked the accuracy of this implementation. Be aware that PHP
// will truncate the coeficcients to 14 digits.
// You have permission to use and distribute this function freely for
// whatever purpose you want, but please show common courtesy and give credit
// where credit is due.
// Input paramater is $p - probability - where 0 < p < 1.
// Coefficients in rational approximations
static $a = [
1 => -3.969683028665376e+01,
2 => 2.209460984245205e+02,
3 => -2.759285104469687e+02,
4 => 1.383577518672690e+02,
5 => -3.066479806614716e+01,
6 => 2.506628277459239e+00,
];
static $b = [
1 => -5.447609879822406e+01,
2 => 1.615858368580409e+02,
3 => -1.556989798598866e+02,
4 => 6.680131188771972e+01,
5 => -1.328068155288572e+01,
];
static $c = [
1 => -7.784894002430293e-03,
2 => -3.223964580411365e-01,
3 => -2.400758277161838e+00,
4 => -2.549732539343734e+00,
5 => 4.374664141464968e+00,
6 => 2.938163982698783e+00,
];
static $d = [
1 => 7.784695709041462e-03,
2 => 3.224671290700398e-01,
3 => 2.445134137142996e+00,
4 => 3.754408661907416e+00,
];
// Define lower and upper region break-points.
$p_low = 0.02425; //Use lower region approx. below this
$p_high = 1 - $p_low; //Use upper region approx. above this
if (0 < $p && $p < $p_low) {
// Rational approximation for lower region.
$q = sqrt(-2 * log($p));
return ((((($c[1] * $q + $c[2]) * $q + $c[3]) * $q + $c[4]) * $q + $c[5]) * $q + $c[6]) /
(((($d[1] * $q + $d[2]) * $q + $d[3]) * $q + $d[4]) * $q + 1);
} elseif ($p_low <= $p && $p <= $p_high) {
// Rational approximation for central region.
$q = $p - 0.5;
$r = $q * $q;
return ((((($a[1] * $r + $a[2]) * $r + $a[3]) * $r + $a[4]) * $r + $a[5]) * $r + $a[6]) * $q /
((((($b[1] * $r + $b[2]) * $r + $b[3]) * $r + $b[4]) * $r + $b[5]) * $r + 1);
} elseif ($p_high < $p && $p < 1) {
// Rational approximation for upper region.
$q = sqrt(-2 * log(1 - $p));
return -((((($c[1] * $q + $c[2]) * $q + $c[3]) * $q + $c[4]) * $q + $c[5]) * $q + $c[6]) /
(((($d[1] * $q + $d[2]) * $q + $d[3]) * $q + $d[4]) * $q + 1);
}
// If 0 < p < 1, return a null value
return Functions::NULL();
}
/** /**
* AVEDEV. * AVEDEV.
* *
@ -766,7 +682,7 @@ class Statistical
return Functions::VALUE(); return Functions::VALUE();
} }
return self::NORMDIST($value, 0, 1, true) - 0.5; return Statistical\Distributions\Normal::distribution($value, 0, 1, true) - 0.5;
} }
/** /**
@ -1048,6 +964,11 @@ class Statistical
* *
* Returns the inverse of the normal cumulative distribution * Returns the inverse of the normal cumulative distribution
* *
* @Deprecated 1.18.0
*
* @see Statistical\Distributions\LogNormal::inverse()
* Use the inverse() method in the Statistical\Distributions\LogNormal class instead
*
* @param float $probability * @param float $probability
* @param float $mean * @param float $mean
* @param float $stdDev * @param float $stdDev
@ -1060,19 +981,7 @@ class Statistical
*/ */
public static function LOGINV($probability, $mean, $stdDev) public static function LOGINV($probability, $mean, $stdDev)
{ {
$probability = Functions::flattenSingleValue($probability); return Statistical\Distributions\LogNormal::inverse($probability, $mean, $stdDev);
$mean = Functions::flattenSingleValue($mean);
$stdDev = Functions::flattenSingleValue($stdDev);
if ((is_numeric($probability)) && (is_numeric($mean)) && (is_numeric($stdDev))) {
if (($probability < 0) || ($probability > 1) || ($stdDev <= 0)) {
return Functions::NAN();
}
return exp($mean + $stdDev * self::NORMSINV($probability));
}
return Functions::VALUE();
} }
/** /**
@ -1081,6 +990,11 @@ class Statistical
* Returns the cumulative lognormal distribution of x, where ln(x) is normally distributed * Returns the cumulative lognormal distribution of x, where ln(x) is normally distributed
* with parameters mean and standard_dev. * with parameters mean and standard_dev.
* *
* @Deprecated 1.18.0
*
* @see Statistical\Distributions\LogNormal::cumulative()
* Use the cumulative() method in the Statistical\Distributions\LogNormal class instead
*
* @param float $value * @param float $value
* @param float $mean * @param float $mean
* @param float $stdDev * @param float $stdDev
@ -1089,19 +1003,7 @@ class Statistical
*/ */
public static function LOGNORMDIST($value, $mean, $stdDev) public static function LOGNORMDIST($value, $mean, $stdDev)
{ {
$value = Functions::flattenSingleValue($value); return Statistical\Distributions\LogNormal::cumulative($value, $mean, $stdDev);
$mean = Functions::flattenSingleValue($mean);
$stdDev = Functions::flattenSingleValue($stdDev);
if ((is_numeric($value)) && (is_numeric($mean)) && (is_numeric($stdDev))) {
if (($value <= 0) || ($stdDev <= 0)) {
return Functions::NAN();
}
return self::NORMSDIST((log($value) - $mean) / $stdDev);
}
return Functions::VALUE();
} }
/** /**
@ -1110,6 +1012,11 @@ class Statistical
* Returns the lognormal distribution of x, where ln(x) is normally distributed * Returns the lognormal distribution of x, where ln(x) is normally distributed
* with parameters mean and standard_dev. * with parameters mean and standard_dev.
* *
* @Deprecated 1.18.0
*
* @see Statistical\Distributions\LogNormal::distribution()
* Use the distribution() method in the Statistical\Distributions\LogNormal class instead
*
* @param float $value * @param float $value
* @param float $mean * @param float $mean
* @param float $stdDev * @param float $stdDev
@ -1119,25 +1026,7 @@ class Statistical
*/ */
public static function LOGNORMDIST2($value, $mean, $stdDev, $cumulative = false) public static function LOGNORMDIST2($value, $mean, $stdDev, $cumulative = false)
{ {
$value = Functions::flattenSingleValue($value); return Statistical\Distributions\LogNormal::distribution($value, $mean, $stdDev, $cumulative);
$mean = Functions::flattenSingleValue($mean);
$stdDev = Functions::flattenSingleValue($stdDev);
$cumulative = (bool) Functions::flattenSingleValue($cumulative);
if ((is_numeric($value)) && (is_numeric($mean)) && (is_numeric($stdDev))) {
if (($value <= 0) || ($stdDev <= 0)) {
return Functions::NAN();
}
if ($cumulative === true) {
return self::NORMSDIST2((log($value) - $mean) / $stdDev, true);
}
return (1 / (sqrt(2 * M_PI) * $stdDev * $value)) *
exp(0 - ((log($value) - $mean) ** 2 / (2 * $stdDev ** 2)));
}
return Functions::VALUE();
} }
/** /**
@ -1329,7 +1218,7 @@ class Statistical
* *
* @Deprecated 1.18.0 * @Deprecated 1.18.0
* *
* @see Statistical\Distributions\Binomial::negative::mode() * @see Statistical\Distributions\Binomial::negative()
* Use the negative() method in the Statistical\Distributions\Binomial class instead * Use the negative() method in the Statistical\Distributions\Binomial class instead
* *
* @param mixed (float) $failures Number of Failures * @param mixed (float) $failures Number of Failures
@ -1350,6 +1239,11 @@ class Statistical
* function has a very wide range of applications in statistics, including hypothesis * function has a very wide range of applications in statistics, including hypothesis
* testing. * testing.
* *
* @Deprecated 1.18.0
*
* @see Statistical\Distributions\Normal::distribution()
* Use the distribution() method in the Statistical\Distributions\Normal class instead
*
* @param mixed (float) $value * @param mixed (float) $value
* @param mixed (float) $mean Mean Value * @param mixed (float) $mean Mean Value
* @param mixed (float) $stdDev Standard Deviation * @param mixed (float) $stdDev Standard Deviation
@ -1359,24 +1253,7 @@ class Statistical
*/ */
public static function NORMDIST($value, $mean, $stdDev, $cumulative) public static function NORMDIST($value, $mean, $stdDev, $cumulative)
{ {
$value = Functions::flattenSingleValue($value); return Statistical\Distributions\Normal::distribution($value, $mean, $stdDev, $cumulative);
$mean = Functions::flattenSingleValue($mean);
$stdDev = Functions::flattenSingleValue($stdDev);
if ((is_numeric($value)) && (is_numeric($mean)) && (is_numeric($stdDev))) {
if ($stdDev < 0) {
return Functions::NAN();
}
if ((is_numeric($cumulative)) || (is_bool($cumulative))) {
if ($cumulative) {
return 0.5 * (1 + Engineering\Erf::erfValue(($value - $mean) / ($stdDev * sqrt(2))));
}
return (1 / (self::SQRT2PI * $stdDev)) * exp(0 - (($value - $mean) ** 2 / (2 * ($stdDev * $stdDev))));
}
}
return Functions::VALUE();
} }
/** /**
@ -1384,6 +1261,11 @@ class Statistical
* *
* Returns the inverse of the normal cumulative distribution for the specified mean and standard deviation. * Returns the inverse of the normal cumulative distribution for the specified mean and standard deviation.
* *
* @Deprecated 1.18.0
*
* @see Statistical\Distributions\Normal::inverse()
* Use the inverse() method in the Statistical\Distributions\Normal class instead
*
* @param mixed (float) $probability * @param mixed (float) $probability
* @param mixed (float) $mean Mean Value * @param mixed (float) $mean Mean Value
* @param mixed (float) $stdDev Standard Deviation * @param mixed (float) $stdDev Standard Deviation
@ -1392,22 +1274,7 @@ class Statistical
*/ */
public static function NORMINV($probability, $mean, $stdDev) public static function NORMINV($probability, $mean, $stdDev)
{ {
$probability = Functions::flattenSingleValue($probability); return Statistical\Distributions\Normal::inverse($probability, $mean, $stdDev);
$mean = Functions::flattenSingleValue($mean);
$stdDev = Functions::flattenSingleValue($stdDev);
if ((is_numeric($probability)) && (is_numeric($mean)) && (is_numeric($stdDev))) {
if (($probability < 0) || ($probability > 1)) {
return Functions::NAN();
}
if ($stdDev < 0) {
return Functions::NAN();
}
return (self::inverseNcdf($probability) * $stdDev) + $mean;
}
return Functions::VALUE();
} }
/** /**
@ -1417,18 +1284,18 @@ class Statistical
* a mean of 0 (zero) and a standard deviation of one. Use this function in place of a * a mean of 0 (zero) and a standard deviation of one. Use this function in place of a
* table of standard normal curve areas. * table of standard normal curve areas.
* *
* @Deprecated 1.18.0
*
* @see Statistical\Distributions\StandardNormal::cumulative()
* Use the cumulative() method in the Statistical\Distributions\StandardNormal class instead
*
* @param mixed (float) $value * @param mixed (float) $value
* *
* @return float|string The result, or a string containing an error * @return float|string The result, or a string containing an error
*/ */
public static function NORMSDIST($value) public static function NORMSDIST($value)
{ {
$value = Functions::flattenSingleValue($value); return Statistical\Distributions\StandardNormal::cumulative($value);
if (!is_numeric($value)) {
return Functions::VALUE();
}
return self::NORMDIST($value, 0, 1, true);
} }
/** /**
@ -1438,6 +1305,11 @@ class Statistical
* a mean of 0 (zero) and a standard deviation of one. Use this function in place of a * a mean of 0 (zero) and a standard deviation of one. Use this function in place of a
* table of standard normal curve areas. * table of standard normal curve areas.
* *
* @Deprecated 1.18.0
*
* @see Statistical\Distributions\StandardNormal::distribution()
* Use the distribution() method in the Statistical\Distributions\StandardNormal class instead
*
* @param mixed (float) $value * @param mixed (float) $value
* @param mixed (bool) $cumulative * @param mixed (bool) $cumulative
* *
@ -1445,13 +1317,7 @@ class Statistical
*/ */
public static function NORMSDIST2($value, $cumulative) public static function NORMSDIST2($value, $cumulative)
{ {
$value = Functions::flattenSingleValue($value); return Statistical\Distributions\StandardNormal::distribution($value, $cumulative);
if (!is_numeric($value)) {
return Functions::VALUE();
}
$cumulative = (bool) Functions::flattenSingleValue($cumulative);
return self::NORMDIST($value, 0, 1, $cumulative);
} }
/** /**
@ -1459,13 +1325,18 @@ class Statistical
* *
* Returns the inverse of the standard normal cumulative distribution * Returns the inverse of the standard normal cumulative distribution
* *
* @Deprecated 1.18.0
*
* @see Statistical\Distributions\StandardNormal::inverse()
* Use the inverse() method in the Statistical\Distributions\StandardNormal class instead
*
* @param mixed (float) $value * @param mixed (float) $value
* *
* @return float|string The result, or a string containing an error * @return float|string The result, or a string containing an error
*/ */
public static function NORMSINV($value) public static function NORMSINV($value)
{ {
return self::NORMINV($value, 0, 1); return Statistical\Distributions\StandardNormal::inverse($value);
} }
/** /**
@ -2098,6 +1969,11 @@ class Statistical
* For a given hypothesized population mean, x, Z.TEST returns the probability that the sample mean would be * For a given hypothesized population mean, x, Z.TEST returns the probability that the sample mean would be
* greater than the average of observations in the data set (array) that is, the observed sample mean. * greater than the average of observations in the data set (array) that is, the observed sample mean.
* *
* @Deprecated 1.18.0
*
* @see Statistical\Distributions\StandardNormal::zTest()
* Use the zTest() method in the Statistical\Distributions\StandardNormal class instead
*
* @param float $dataSet * @param float $dataSet
* @param float $m0 Alpha Parameter * @param float $m0 Alpha Parameter
* @param float $sigma Beta Parameter * @param float $sigma Beta Parameter
@ -2106,15 +1982,6 @@ class Statistical
*/ */
public static function ZTEST($dataSet, $m0, $sigma = null) public static function ZTEST($dataSet, $m0, $sigma = null)
{ {
$dataSet = Functions::flattenArrayIndexed($dataSet); return Statistical\Distributions\StandardNormal::zTest($dataSet, $m0, $sigma);
$m0 = Functions::flattenSingleValue($m0);
$sigma = Functions::flattenSingleValue($sigma);
if ($sigma === null) {
$sigma = StandardDeviations::STDEV($dataSet);
}
$n = count($dataSet);
return 1 - self::NORMSDIST((Averages::average($dataSet) - $m0) / ($sigma / sqrt($n)));
} }
} }

View File

@ -4,7 +4,6 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical;
use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical;
class Confidence class Confidence
{ {
@ -39,6 +38,6 @@ class Confidence
return Functions::NAN(); return Functions::NAN();
} }
return Statistical::NORMSINV(1 - $alpha / 2) * $stdDev / sqrt($size); return Distributions\StandardNormal::inverse(1 - $alpha / 2) * $stdDev / sqrt($size);
} }
} }

View File

@ -0,0 +1,121 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class LogNormal
{
use BaseValidations;
/**
* LOGNORMDIST.
*
* Returns the cumulative lognormal distribution of x, where ln(x) is normally distributed
* with parameters mean and standard_dev.
*
* @param mixed (float) $value
* @param mixed (float) $mean
* @param mixed (float) $stdDev
*
* @return float|string The result, or a string containing an error
*/
public static function cumulative($value, $mean, $stdDev)
{
$value = Functions::flattenSingleValue($value);
$mean = Functions::flattenSingleValue($mean);
$stdDev = Functions::flattenSingleValue($stdDev);
try {
$value = self::validateFloat($value);
$mean = self::validateFloat($mean);
$stdDev = self::validateFloat($stdDev);
} catch (Exception $e) {
return $e->getMessage();
}
if (($value <= 0) || ($stdDev <= 0)) {
return Functions::NAN();
}
return StandardNormal::cumulative((log($value) - $mean) / $stdDev);
}
/**
* LOGNORM.DIST.
*
* Returns the lognormal distribution of x, where ln(x) is normally distributed
* with parameters mean and standard_dev.
*
* @param mixed (float) $value
* @param mixed (float) $mean
* @param mixed (float) $stdDev
* @param mixed (bool) $cumulative
*
* @return float|string The result, or a string containing an error
*/
public static function distribution($value, $mean, $stdDev, $cumulative = false)
{
$value = Functions::flattenSingleValue($value);
$mean = Functions::flattenSingleValue($mean);
$stdDev = Functions::flattenSingleValue($stdDev);
$cumulative = Functions::flattenSingleValue($cumulative);
try {
$value = self::validateFloat($value);
$mean = self::validateFloat($mean);
$stdDev = self::validateFloat($stdDev);
$cumulative = self::validateBool($cumulative);
} catch (Exception $e) {
return $e->getMessage();
}
if (($value <= 0) || ($stdDev <= 0)) {
return Functions::NAN();
}
if ($cumulative === true) {
return StandardNormal::distribution((log($value) - $mean) / $stdDev, true);
}
return (1 / (sqrt(2 * M_PI) * $stdDev * $value)) *
exp(0 - ((log($value) - $mean) ** 2 / (2 * $stdDev ** 2)));
}
/**
* LOGINV.
*
* Returns the inverse of the normal cumulative distribution
*
* @param mixed (float) $probability
* @param mixed (float) $mean
* @param mixed (float) $stdDev
*
* @return float|string The result, or a string containing an error
*
* @TODO Try implementing P J Acklam's refinement algorithm for greater
* accuracy if I can get my head round the mathematics
* (as described at) http://home.online.no/~pjacklam/notes/invnorm/
*/
public static function inverse($probability, $mean, $stdDev)
{
$probability = Functions::flattenSingleValue($probability);
$mean = Functions::flattenSingleValue($mean);
$stdDev = Functions::flattenSingleValue($stdDev);
try {
$probability = self::validateProbability($probability);
$mean = self::validateFloat($mean);
$stdDev = self::validateFloat($stdDev);
} catch (Exception $e) {
return $e->getMessage();
}
if ($stdDev <= 0) {
return Functions::NAN();
}
return exp($mean + $stdDev * StandardNormal::inverse($probability));
}
}

View File

@ -0,0 +1,168 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
use PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class Normal
{
use BaseValidations;
public const SQRT2PI = 2.5066282746310005024157652848110452530069867406099;
/**
* NORMDIST.
*
* Returns the normal distribution for the specified mean and standard deviation. This
* function has a very wide range of applications in statistics, including hypothesis
* testing.
*
* @param mixed (float) $value
* @param mixed (float) $mean Mean Value
* @param mixed (float) $stdDev Standard Deviation
* @param mixed (bool) $cumulative
*
* @return float|string The result, or a string containing an error
*/
public static function distribution($value, $mean, $stdDev, $cumulative)
{
$value = Functions::flattenSingleValue($value);
$mean = Functions::flattenSingleValue($mean);
$stdDev = Functions::flattenSingleValue($stdDev);
try {
$value = self::validateFloat($value);
$mean = self::validateFloat($mean);
$stdDev = self::validateFloat($stdDev);
$cumulative = self::validateBool($cumulative);
} catch (Exception $e) {
return $e->getMessage();
}
if ($stdDev < 0) {
return Functions::NAN();
}
if ($cumulative) {
return 0.5 * (1 + Engineering\Erf::erfValue(($value - $mean) / ($stdDev * sqrt(2))));
}
return (1 / (self::SQRT2PI * $stdDev)) * exp(0 - (($value - $mean) ** 2 / (2 * ($stdDev * $stdDev))));
}
/**
* NORMINV.
*
* Returns the inverse of the normal cumulative distribution for the specified mean and standard deviation.
*
* @param mixed (float) $probability
* @param mixed (float) $mean Mean Value
* @param mixed (float) $stdDev Standard Deviation
*
* @return float|string The result, or a string containing an error
*/
public static function inverse($probability, $mean, $stdDev)
{
$probability = Functions::flattenSingleValue($probability);
$mean = Functions::flattenSingleValue($mean);
$stdDev = Functions::flattenSingleValue($stdDev);
try {
$probability = self::validateProbability($probability);
$mean = self::validateFloat($mean);
$stdDev = self::validateFloat($stdDev);
} catch (Exception $e) {
return $e->getMessage();
}
if ($stdDev < 0) {
return Functions::NAN();
}
return (self::inverseNcdf($probability) * $stdDev) + $mean;
}
/*
* inverse_ncdf.php
* -------------------
* begin : Friday, January 16, 2004
* copyright : (C) 2004 Michael Nickerson
* email : nickersonm@yahoo.com
*
*/
private static function inverseNcdf($p)
{
// Inverse ncdf approximation by Peter J. Acklam, implementation adapted to
// PHP by Michael Nickerson, using Dr. Thomas Ziegler's C implementation as
// a guide. http://home.online.no/~pjacklam/notes/invnorm/index.html
// I have not checked the accuracy of this implementation. Be aware that PHP
// will truncate the coeficcients to 14 digits.
// You have permission to use and distribute this function freely for
// whatever purpose you want, but please show common courtesy and give credit
// where credit is due.
// Input paramater is $p - probability - where 0 < p < 1.
// Coefficients in rational approximations
static $a = [
1 => -3.969683028665376e+01,
2 => 2.209460984245205e+02,
3 => -2.759285104469687e+02,
4 => 1.383577518672690e+02,
5 => -3.066479806614716e+01,
6 => 2.506628277459239e+00,
];
static $b = [
1 => -5.447609879822406e+01,
2 => 1.615858368580409e+02,
3 => -1.556989798598866e+02,
4 => 6.680131188771972e+01,
5 => -1.328068155288572e+01,
];
static $c = [
1 => -7.784894002430293e-03,
2 => -3.223964580411365e-01,
3 => -2.400758277161838e+00,
4 => -2.549732539343734e+00,
5 => 4.374664141464968e+00,
6 => 2.938163982698783e+00,
];
static $d = [
1 => 7.784695709041462e-03,
2 => 3.224671290700398e-01,
3 => 2.445134137142996e+00,
4 => 3.754408661907416e+00,
];
// Define lower and upper region break-points.
$p_low = 0.02425; //Use lower region approx. below this
$p_high = 1 - $p_low; //Use upper region approx. above this
if (0 < $p && $p < $p_low) {
// Rational approximation for lower region.
$q = sqrt(-2 * log($p));
return ((((($c[1] * $q + $c[2]) * $q + $c[3]) * $q + $c[4]) * $q + $c[5]) * $q + $c[6]) /
(((($d[1] * $q + $d[2]) * $q + $d[3]) * $q + $d[4]) * $q + 1);
} elseif ($p_high < $p && $p < 1) {
// Rational approximation for upper region.
$q = sqrt(-2 * log(1 - $p));
return -((((($c[1] * $q + $c[2]) * $q + $c[3]) * $q + $c[4]) * $q + $c[5]) * $q + $c[6]) /
(((($d[1] * $q + $d[2]) * $q + $d[3]) * $q + $d[4]) * $q + 1);
}
// Rational approximation for central region.
$q = $p - 0.5;
$r = $q * $q;
return ((((($a[1] * $r + $a[2]) * $r + $a[3]) * $r + $a[4]) * $r + $a[5]) * $r + $a[6]) * $q /
((((($b[1] * $r + $b[2]) * $r + $b[3]) * $r + $b[4]) * $r + $b[5]) * $r + 1);
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Averages;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\StandardDeviations;
class StandardNormal
{
/**
* NORMSDIST.
*
* Returns the standard normal cumulative distribution function. The distribution has
* a mean of 0 (zero) and a standard deviation of one. Use this function in place of a
* table of standard normal curve areas.
*
* @param mixed (float) $value
*
* @return float|string The result, or a string containing an error
*/
public static function cumulative($value)
{
return Normal::distribution($value, 0, 1, true);
}
/**
* NORM.S.DIST.
*
* Returns the standard normal cumulative distribution function. The distribution has
* a mean of 0 (zero) and a standard deviation of one. Use this function in place of a
* table of standard normal curve areas.
*
* @param mixed (float) $value
* @param mixed (bool) $cumulative
*
* @return float|string The result, or a string containing an error
*/
public static function distribution($value, $cumulative)
{
return Normal::distribution($value, 0, 1, $cumulative);
}
/**
* NORMSINV.
*
* Returns the inverse of the standard normal cumulative distribution
*
* @param mixed (float) $value
*
* @return float|string The result, or a string containing an error
*/
public static function inverse($value)
{
return Normal::inverse($value, 0, 1);
}
/**
* ZTEST.
*
* Returns the one-tailed P-value of a z-test.
*
* For a given hypothesized population mean, x, Z.TEST returns the probability that the sample mean would be
* greater than the average of observations in the data set (array) that is, the observed sample mean.
*
* @param mixed (float) $dataSet
* @param mixed (float) $m0 Alpha Parameter
* @param mixed (null|float) $sigma Beta Parameter
*
* @return float|string (string if result is an error)
*/
public static function zTest($dataSet, $m0, $sigma = null)
{
$dataSet = Functions::flattenArrayIndexed($dataSet);
$m0 = Functions::flattenSingleValue($m0);
$sigma = Functions::flattenSingleValue($sigma);
if (!is_numeric($m0) || ($sigma !== null && !is_numeric($sigma))) {
return Functions::VALUE();
}
if ($sigma === null) {
$sigma = StandardDeviations::STDEV($dataSet);
}
$n = count($dataSet);
return 1 - self::cumulative((Averages::average($dataSet) - $m0) / ($sigma / sqrt($n)));
}
}

View File

@ -15,7 +15,7 @@ class NormInvTest extends TestCase
public function testNORMINV($expectedResult, ...$args): void public function testNORMINV($expectedResult, ...$args): void
{ {
$result = Statistical::NORMINV(...$args); $result = Statistical::NORMINV(...$args);
self::assertEqualsWithDelta($expectedResult, $result, 1E-12); self::assertEqualsWithDelta($expectedResult, $result, 1E-8);
} }
public function providerNORMINV() public function providerNORMINV()

View File

@ -0,0 +1,28 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Statistical;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical;
use PHPUnit\Framework\TestCase;
class ZTestTest extends TestCase
{
/**
* @dataProvider providerZTEST
*
* @param mixed $expectedResult
* @param mixed $value
* @param mixed $dataSet
* @param null|mixed $sigma
*/
public function testZTEST($expectedResult, $dataSet, $value, $sigma = null): void
{
$result = Statistical::ZTEST($dataSet, $value, $sigma);
self::assertEqualsWithDelta($expectedResult, $result, 1E-12);
}
public function providerZTEST()
{
return require 'tests/data/Calculation/Statistical/ZTEST.php';
}
}

View File

@ -15,10 +15,26 @@ return [
], ],
[ [
'#VALUE!', '#VALUE!',
1.1, 2.2, 'NAN', 'NaN', 2.2, 3.3,
],
[
'#VALUE!',
0.1, 'NaN', 3.3,
],
[
'#VALUE!',
0.1, 2.2, 'NaN',
],
[
'#NUM!',
-0.1, 2.2, 3.3,
], ],
[ [
'#NUM!', '#NUM!',
1.1, 2.2, 3.3, 1.1, 2.2, 3.3,
], ],
[
'#NUM!',
0.1, 2.2, -3.3,
],
]; ];

View File

@ -3,7 +3,9 @@
return[ return[
[0.0390835557068, 4, 3.5, 1.2], [0.0390835557068, 4, 3.5, 1.2],
[0.066417114799, 12, 10, 5], [0.066417114799, 12, 10, 5],
['#NUM!', -1.1, -2.2, 3.3], ['#NUM!', -1.1, 2.2, 3.3],
['#NUM!', 1.1, -2.2, -3.3], ['#NUM!', 1.1, 2.2, -3.3],
['#VALUE!', 'NAN', 0.1, 0.2], ['#VALUE!', 'NaN', 0.1, 0.2],
['#VALUE!', 1.1, 'NAN', 0.2],
['#VALUE!', 1.1, 0.1, 'NAN'],
]; ];

View File

@ -17,7 +17,10 @@ return[
[0.0896756593248, 1.1, 2.2, 3.3, false], [0.0896756593248, 1.1, 2.2, 3.3, false],
[0.7566441984111, 1.1, -2.2, 3.3, true], [0.7566441984111, 1.1, -2.2, 3.3, true],
[0.0862879718374, 1.1, -2.2, 3.3, false], [0.0862879718374, 1.1, -2.2, 3.3, false],
['#NUM!', -1.1, -2.2, 3.3, true], ['#NUM!', -1.1, 2.2, 3.3, true],
['#NUM!', 1.1, -2.2, -3.3, true], ['#NUM!', 1.1, 2.2, -3.3, true],
['#VALUE!', 'NAN', 0.1, 0.2, true], ['#VALUE!', 'NaN', 0.1, 0.2, true],
['#VALUE!', 1.1, 'NaN', 0.2, true],
['#VALUE!', 1.1, 0.1, 'NaN', true],
['#VALUE!', 1.1, 0.1, 0.2, 'NaN'],
]; ];

View File

@ -7,5 +7,8 @@ return [
[0.2524925375469, 0.8, 1, 0.3, true], [0.2524925375469, 0.8, 1, 0.3, true],
[0.8413447460685, 68, 65.5, 2.5, true], [0.8413447460685, 68, 65.5, 2.5, true],
['#NUM!', 42, 40, -1.5, true], ['#NUM!', 42, 40, -1.5, true],
['#VALUE!', 42, 'ALPHA', 1.5, true], ['#VALUE!', 'NaN', 40, 1.5, true],
['#VALUE!', 42, 'NaN', 1.5, true],
['#VALUE!', 42, 40, 'NaN', true],
['#VALUE!', 42, 40, 1.5, 'NaN'],
]; ];

View File

@ -4,8 +4,12 @@ return [
[42.000002008416, 0.908789, 40, 1.5], [42.000002008416, 0.908789, 40, 1.5],
[5.50669420572, 0.6, 5, 2], [5.50669420572, 0.6, 5, 2],
[63.813775624441, 0.25, 65.5, 2.5], [63.813775624441, 0.25, 65.5, 2.5],
[7.074774056039, 0.015, 12.5, 2.5],
[17.925225943961, 0.985, 12.5, 2.5],
['#NUM!', -0.5, 2.2, 3.3], ['#NUM!', -0.5, 2.2, 3.3],
['#NUM!', 1.5, 2.2, 3.3], ['#NUM!', 1.5, 2.2, 3.3],
['#NUM!', 0.5, 2.2, -3.3], ['#NUM!', 0.5, 2.2, -3.3],
['#VALUE!', 'NAN', 0.1, 0.2], ['#VALUE!', 'NaN', 2.5, 0.2],
['#VALUE!', 0.25, 'NaN', 0.2],
['#VALUE!', 0.25, 2.5, 'NaN'],
]; ];

View File

@ -8,5 +8,5 @@ return [
[0.066807201269, -1.5], [0.066807201269, -1.5],
[0.5, 0], [0.5, 0],
[0.989275889978, 2.3], [0.989275889978, 2.3],
['#VALUE!', 'NAN'], ['#VALUE!', 'NaN'],
]; ];

View File

@ -13,5 +13,6 @@ return [
[0.398942280401, 0, false], [0.398942280401, 0, false],
[0.989275889978, 2.3, true], [0.989275889978, 2.3, true],
[0.028327037742, 2.3, false], [0.028327037742, 2.3, false],
['#VALUE!', 'NAN', true], ['#VALUE!', 'NaN', true],
['#VALUE!', 2.3, 'NaN'],
]; ];

View File

@ -5,4 +5,7 @@ return [
[-0.67448975022342, 0.25], [-0.67448975022342, 0.25],
[0.12566134687610, 0.55], [0.12566134687610, 0.55],
[1.28155156414015, 0.9], [1.28155156414015, 0.9],
['#VALUE!', 'NaN'],
['#NUM!', -0.9],
['#NUM!', 1.9],
]; ];

View File

@ -0,0 +1,42 @@
<?php
return [
[
0.090574196851,
[3, 6, 7, 8, 6, 5, 4, 2, 1, 9],
4,
],
[
0.863043389130,
[3, 6, 7, 8, 6, 5, 4, 2, 1, 9],
6,
],
[
0.371103278559,
[4, 5, 2, 5, 8, 9, 3, 2, 3, 8, 9, 5],
5,
],
[
0.83812918702,
[4, 5, 2, 5, 8, 9, 3, 2, 3, 8, 9, 5],
6,
],
[
0.412070447871,
[1, 2, 3, 3, 4, 4, 8, 10, 12],
5,
3,
],
[
'#VALUE!',
[1, 2, 3, 3, 4, 4, 8, 10, 12],
'NaN',
3,
],
[
'#VALUE!',
[1, 2, 3, 3, 4, 4, 8, 10, 12],
5,
'NaN',
],
];