Files
Vincent Gao f687d261e8 Fix BETAINV false convergence when the Beta CDF underflows
calculateInverse() treated a CDF value of exactly 0.0 as "the guess is the
root" and collapsed the bracket with $b = $a, ending the search. inverse()
already rejects probability <= 0, so a CDF of 0.0 inside the bisection can
only be a float64 underflow at a guess far below the root - never an exact
hit. It is just an ordinary "guess too low" and belongs in the existing
else branch.

The underflow is reached whenever a probe lands many standard deviations
from the mean, so the search collapses after two or three iterations and
returns whichever midpoint it was holding. BETAINV(0.5, 5000, 5000) gave
0.25, though Beta(a, a) is symmetric and its median is exactly 0.5;
BETAINV(0.5, 20000, 3) gave 0.5 against a true 0.99986. Onset is around
alpha = 1080 for beta = 1, where the closed form 0.5 ** (1 / alpha) is
available to check against.

That test also happened to stop the search for shapes where incompleteBeta
declines to evaluate at all and returns 0 for every x, so inverse() now
rejects alpha + beta above that documented limit up front: the CDF is
identically zero there, so no quantile exists to search for.

Tests cover a shape x probability grid against scipy reference values plus
three checks that need no external oracle: the symmetric median, the
Beta(alpha, 1) closed form, and the BETADIST round trip / mirror identity.
2026-08-05 12:28:05 +02:00

116 lines
4.2 KiB
PHP

<?php
declare(strict_types=1);
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Statistical;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
class BetaInvTest extends AllSetupTeardown
{
#[\PHPUnit\Framework\Attributes\DataProvider('providerBETAINV')]
public function testBETAINV(mixed $expectedResult, mixed ...$args): void
{
$this->runTestCaseReference('BETAINV', $expectedResult, ...$args);
}
public static function providerBETAINV(): array
{
return require 'tests/data/Calculation/Statistical/BETAINV.php';
}
#[\PHPUnit\Framework\Attributes\DataProvider('providerBetaInvArray')]
public function testBetaInvArray(array $expectedResult, string $argument1, string $argument2, string $argument3): void
{
$calculation = Calculation::getInstance();
$formula = "=BETAINV({$argument1}, {$argument2}, {$argument3})";
$result = $calculation->calculateFormula($formula);
self::assertEqualsWithDelta($expectedResult, $result, 1.0e-14);
}
public static function providerBetaInvArray(): array
{
return [
'row/column vectors' => [
[[0.24709953547, 0.346789605377], [0.215382947588, 0.307844847105]],
'0.25',
'{5, 7.5}',
'{10; 12}',
],
];
}
/**
* Beta(alpha, alpha) is symmetric about 0.5, so its median is exactly 0.5.
*/
#[\PHPUnit\Framework\Attributes\DataProvider('providerSymmetricShape')]
public function testBetaInvMedianOfSymmetricDistribution(float $alpha): void
{
$result = Calculation::getInstance()->calculateFormula("=BETAINV(0.5, $alpha, $alpha)");
self::assertEqualsWithDelta(0.5, $result, 1.0e-12);
}
public static function providerSymmetricShape(): array
{
$cases = [];
foreach ([0.5, 2, 20, 200, 1000, 2000, 5000, 10000, 20000, 100000] as $alpha) {
$cases["alpha = beta = $alpha"] = [(float) $alpha];
}
return $cases;
}
/**
* The Beta(alpha, 1) CDF is x ** alpha, so its inverse is probability ** (1 / alpha).
*/
#[\PHPUnit\Framework\Attributes\DataProvider('providerClosedForm')]
public function testBetaInvClosedFormForBetaOne(float $probability, float $alpha): void
{
$result = Calculation::getInstance()->calculateFormula("=BETAINV($probability, $alpha, 1)");
self::assertEqualsWithDelta($probability ** (1 / $alpha), $result, 1.0e-12);
}
public static function providerClosedForm(): array
{
$cases = [];
foreach ([0.001, 0.05, 0.25, 0.5, 0.9, 0.999] as $probability) {
foreach ([1, 10, 500, 1100, 5000, 20000, 100000] as $alpha) {
$cases["probability $probability, alpha $alpha"] = [$probability, (float) $alpha];
}
}
return $cases;
}
/**
* BETADIST must map the quantile BETAINV produced back onto the probability
* it was asked for, and BETAINV(p, a, b) must mirror 1 - BETAINV(1 - p, b, a).
*/
#[\PHPUnit\Framework\Attributes\DataProvider('providerShapeGrid')]
public function testBetaInvAgreesWithBetaDist(float $probability, float $alpha, float $beta): void
{
$calculation = Calculation::getInstance();
$roundTrip = $calculation->calculateFormula("=BETADIST(BETAINV($probability, $alpha, $beta), $alpha, $beta)");
self::assertEqualsWithDelta($probability, $roundTrip, 1.0e-6, 'round trip through BETADIST');
$mirror = $calculation->calculateFormula(
"=BETAINV($probability, $alpha, $beta) + BETAINV(" . (1 - $probability) . ", $beta, $alpha)"
);
self::assertEqualsWithDelta(1.0, $mirror, 1.0e-12, 'mirror identity');
}
public static function providerShapeGrid(): array
{
$cases = [];
foreach ([0.01, 0.1, 0.5, 0.9, 0.99] as $probability) {
foreach ([[0.5, 0.5], [2, 5], [50, 50], [1000, 2], [5000, 3], [5000, 5000], [20000, 50], [200, 20000]] as [$alpha, $beta]) {
$cases["probability $probability, alpha $alpha, beta $beta"] = [$probability, (float) $alpha, (float) $beta];
}
}
return $cases;
}
}