742 lines
23 KiB
Go
742 lines
23 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
package stats
|
||
|
||
import "sourcedock.dev/petrbalvin/tensor/internal/base"
|
||
|
||
import (
|
||
"math"
|
||
)
|
||
|
||
// Distribution functions: cumulative distribution functions and their
|
||
// quantiles for the distributions the library draws from, so a
|
||
// p-value or a confidence interval needs no second library.
|
||
//
|
||
// The foundations are the regularised incomplete gamma and beta
|
||
// functions: every continuous CDF here reduces to one of them, and
|
||
// the two discrete CDFs reduce through the exact identities
|
||
// P(Poisson ≤ k) = ΓUpper(k+1, λ) and P(Binomial ≤ k) =
|
||
// BetaIncomplete(1−p, n−k, k+1), which hold in floating point to the
|
||
// accuracy of the incomplete functions themselves. Quantiles invert
|
||
// the CDF by a bracketed Newton iteration through the distribution's
|
||
// density, falling back to bisection whenever the derivative step
|
||
// would leave the bracket, so the convergence stays unconditional on
|
||
// every monotone CDF.
|
||
|
||
// gammaSeriesEps bounds the series and continued-fraction iterations
|
||
// of the incomplete functions.
|
||
const gammaSeriesEps = 3e-16
|
||
|
||
// gammaIterations bounds the series and continued-fraction iterations
|
||
// for a shape a. Both converge in O(√a) rounds near x ≈ a, so a fixed
|
||
// cap would silently truncate the large-shape answers (a shape of
|
||
// 50000 at x ≈ a needs about 1800 series rounds); the budget grows
|
||
// with √a and every caller errors when it is still not enough.
|
||
func gammaIterations(a float64) int {
|
||
return 1000 + int(20*math.Sqrt(a))
|
||
}
|
||
|
||
// GammaLower returns the regularised lower incomplete gamma
|
||
// P(a, x) = γ(a, x)/Γ(a), the CDF of a Gamma(shape a, rate 1) draw.
|
||
// It evaluates the power series below x < a+1 and the continued
|
||
// fraction of the complement above, each to rounding level; for large
|
||
// shapes the series budget grows with √a, and either iteration that
|
||
// fails to converge inside its budget is an error rather than a
|
||
// silently truncated value.
|
||
func GammaLower(a, x float64) (float64, error) {
|
||
p, _, err := gammaIncomplete("GammaLower", a, x)
|
||
return p, err
|
||
}
|
||
|
||
// GammaUpper returns the regularised upper incomplete gamma
|
||
// Q(a, x) = Γ(a, x)/Γ(a) = 1 − P(a, x), computed on the same
|
||
// foundation as GammaLower.
|
||
func GammaUpper(a, x float64) (float64, error) {
|
||
_, q, err := gammaIncomplete("GammaUpper", a, x)
|
||
return q, err
|
||
}
|
||
|
||
// gammaIncomplete returns P(a, x) and Q(a, x) together, sharing the
|
||
// one exponent factor both need.
|
||
func gammaIncomplete(name string, a, x float64) (p, q float64, err error) {
|
||
if !(a > 0) {
|
||
return 0, 0, base.Errf("%s: shape a must be positive, got %g", name, a)
|
||
}
|
||
if !(x >= 0) {
|
||
return 0, 0, base.Errf("%s: x must not be negative, got %g", name, x)
|
||
}
|
||
if x == 0 {
|
||
return 0, 1, nil
|
||
}
|
||
if math.IsInf(x, 1) {
|
||
return 1, 0, nil
|
||
}
|
||
lg, _ := math.Lgamma(a)
|
||
front := math.Exp(-x + a*math.Log(x) - lg)
|
||
// The iteration budget is fixed by the shape alone, so it is
|
||
// computed once: the same bound the inline call evaluated, without
|
||
// re-deriving it on every round.
|
||
rounds := gammaIterations(a)
|
||
if x < a+1 {
|
||
// Power series for P: γ(a, x) = e^{−x}x^a Σ x^n/(a(a+1)…(a+n)).
|
||
// Every term is positive, so the sum carries no cancellation and
|
||
// stays accurate for arbitrarily large shapes given enough
|
||
// rounds.
|
||
sum := 1 / a
|
||
del := sum
|
||
ap := a
|
||
converged := false
|
||
for range rounds {
|
||
ap++
|
||
del *= x / ap
|
||
sum += del
|
||
if math.Abs(del) < math.Abs(sum)*gammaSeriesEps {
|
||
converged = true
|
||
break
|
||
}
|
||
}
|
||
if !converged {
|
||
return 0, 0, base.Errf("%s: the power series did not converge for a = %g, x = %g", name, a, x)
|
||
}
|
||
return sum * front, 1 - sum*front, nil
|
||
}
|
||
// Continued fraction for Q (Numerical Recipes form), Lentz's
|
||
// modified method with the tiny floor.
|
||
const fpmin = 1e-300
|
||
b := x + 1 - a
|
||
c := 1 / fpmin
|
||
d := 1 / b
|
||
h := d
|
||
converged := false
|
||
for i := 1; i <= rounds; i++ {
|
||
an := -float64(i) * (float64(i) - a)
|
||
b += 2
|
||
d = an*d + b
|
||
if math.Abs(d) < fpmin {
|
||
d = fpmin
|
||
}
|
||
c = b + an/c
|
||
if math.Abs(c) < fpmin {
|
||
c = fpmin
|
||
}
|
||
d = 1 / d
|
||
del := d * c
|
||
h *= del
|
||
if math.Abs(del-1) < gammaSeriesEps {
|
||
converged = true
|
||
break
|
||
}
|
||
}
|
||
if !converged {
|
||
return 0, 0, base.Errf("%s: the continued fraction did not converge for a = %g, x = %g", name, a, x)
|
||
}
|
||
return 1 - front*h, front * h, nil
|
||
}
|
||
|
||
// BetaIncomplete returns the regularised incomplete beta function
|
||
// I_x(a, b), the CDF of a Beta(a, b) draw, by the continued fraction
|
||
// with the standard symmetry switch at x > (a+1)/(a+b+2).
|
||
func BetaIncomplete(x, a, b float64) (float64, error) {
|
||
if !(a > 0 && b > 0) {
|
||
return 0, base.Errf("BetaIncomplete: shape parameters must be positive, got %g, %g", a, b)
|
||
}
|
||
if !(x >= 0 && x <= 1) {
|
||
return 0, base.Errf("BetaIncomplete: x must lie in [0, 1], got %g", x)
|
||
}
|
||
if x == 0 || x == 1 {
|
||
return x, nil
|
||
}
|
||
lab, _ := math.Lgamma(a + b)
|
||
la, _ := math.Lgamma(a)
|
||
lb, _ := math.Lgamma(b)
|
||
front := math.Exp(lab - la - lb + a*math.Log(x) + b*math.Log(1-x))
|
||
// The continued fraction converges fastest away from the skew side.
|
||
if x < (a+1)/(a+b+2) {
|
||
h, err := betaContinue(a, b, x)
|
||
if err != nil {
|
||
return 0, base.Errf("BetaIncomplete: %w", err)
|
||
}
|
||
return front * h / a, nil
|
||
}
|
||
h, err := betaContinue(b, a, 1-x)
|
||
if err != nil {
|
||
return 0, base.Errf("BetaIncomplete: %w", err)
|
||
}
|
||
return 1 - front*h/b, nil
|
||
}
|
||
|
||
// betaContinue evaluates the incomplete-beta continued fraction
|
||
// (Numerical Recipes form) by Lentz's modified method. The round
|
||
// budget grows with the shapes exactly as the incomplete gamma's does,
|
||
// and an iteration that fails to converge inside it is an error rather
|
||
// than a silently truncated value.
|
||
func betaContinue(a, b, x float64) (float64, error) {
|
||
const fpmin = 1e-300
|
||
rounds := max(1000, gammaIterations(a)+gammaIterations(b))
|
||
qab := a + b
|
||
qap := a + 1
|
||
qam := a - 1
|
||
c := 1.0
|
||
d := 1 - qab*x/qap
|
||
if math.Abs(d) < fpmin {
|
||
d = fpmin
|
||
}
|
||
d = 1 / d
|
||
h := d
|
||
converged := false
|
||
for i := 1; i <= rounds; i++ {
|
||
m := float64(i)
|
||
m2 := 2 * m
|
||
aa := m * (b - m) * x / ((qam + m2) * (a + m2))
|
||
d = 1 + aa*d
|
||
if math.Abs(d) < fpmin {
|
||
d = fpmin
|
||
}
|
||
c = 1 + aa/c
|
||
if math.Abs(c) < fpmin {
|
||
c = fpmin
|
||
}
|
||
d = 1 / d
|
||
h *= d * c
|
||
aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2))
|
||
d = 1 + aa*d
|
||
if math.Abs(d) < fpmin {
|
||
d = fpmin
|
||
}
|
||
c = 1 + aa/c
|
||
if math.Abs(c) < fpmin {
|
||
c = fpmin
|
||
}
|
||
d = 1 / d
|
||
del := d * c
|
||
h *= del
|
||
if math.Abs(del-1) < gammaSeriesEps {
|
||
converged = true
|
||
break
|
||
}
|
||
}
|
||
if !converged {
|
||
return 0, base.Errf("the continued fraction did not converge for a = %g, b = %g, x = %g", a, b, x)
|
||
}
|
||
return h, nil
|
||
}
|
||
|
||
// NormalCDF returns Φ(x), the standard normal cumulative distribution.
|
||
func NormalCDF(x float64) float64 {
|
||
return 0.5 * math.Erfc(-x/math.Sqrt2)
|
||
}
|
||
|
||
// ExponentialCDF returns P(X ≤ x) for X ~ Exponential(rate).
|
||
func ExponentialCDF(x, rate float64) (float64, error) {
|
||
if !(rate > 0) {
|
||
return 0, base.Errf("ExponentialCDF: rate must be positive, got %g", rate)
|
||
}
|
||
if math.IsNaN(x) {
|
||
// The x ≤ 0 branch below is the left tail, not a rejection, so
|
||
// NaN needs its own guard: without it the answer is a silent NaN.
|
||
return 0, base.Errf("ExponentialCDF: x must be a number, got %g", x)
|
||
}
|
||
if x <= 0 {
|
||
return 0, nil
|
||
}
|
||
// −Expm1(−rate·x) is 1 − e^{−rate·x} without the cancellation: the
|
||
// literal form loses the left tail, an 11 % relative error already
|
||
// at rate·x = 1e-16 and a total loss not far below, while Expm1
|
||
// keeps every bit of the small answer.
|
||
return -math.Expm1(-rate * x), nil
|
||
}
|
||
|
||
// GammaCDF returns P(X ≤ x) for X ~ Gamma(shape, rate), the same
|
||
// parametrisation GammaDraws samples.
|
||
func GammaCDF(x, shape, rate float64) (float64, error) {
|
||
if !(shape > 0 && rate > 0) {
|
||
return 0, base.Errf("GammaCDF: shape and rate must be positive, got %g, %g", shape, rate)
|
||
}
|
||
return GammaLower(shape, x*rate)
|
||
}
|
||
|
||
// ChiSquareCDF returns P(X ≤ x) for X ~ χ²(df).
|
||
func ChiSquareCDF(x float64, df int) (float64, error) {
|
||
if df < 1 {
|
||
return 0, base.Errf("ChiSquareCDF: df must be ≥ 1, got %d", df)
|
||
}
|
||
return GammaLower(float64(df)/2, x/2)
|
||
}
|
||
|
||
// StudentTCDF returns P(T ≤ t) for T ~ Student t(df).
|
||
func StudentTCDF(t float64, df int) (float64, error) {
|
||
if df < 1 {
|
||
return 0, base.Errf("StudentTCDF: df must be ≥ 1, got %d", df)
|
||
}
|
||
// One house tail: the upper-tail helper carries the asymptotic forms
|
||
// the heavy df ≤ 2 laws keep past t²'s overflow, where the closed
|
||
// form's z = df/(df+t²) collapses to 0 and the lower tail answered a
|
||
// silent 0 for a tail the format still holds.
|
||
upper, err := studentTUpperTail(math.Abs(t), df)
|
||
if err != nil {
|
||
return 0, base.Errf("StudentTCDF: %w", err)
|
||
}
|
||
if t >= 0 {
|
||
return 1 - upper, nil
|
||
}
|
||
return upper, nil
|
||
}
|
||
|
||
// PoissonCDF returns P(N ≤ k) for N ~ Poisson(lambda), through the
|
||
// identity P(N ≤ k) = ΓUpper(k+1, λ).
|
||
func PoissonCDF(k int, lambda float64) (float64, error) {
|
||
if !(lambda > 0) {
|
||
return 0, base.Errf("PoissonCDF: lambda must be positive, got %g", lambda)
|
||
}
|
||
if k < 0 {
|
||
return 0, nil
|
||
}
|
||
return GammaUpper(float64(k+1), lambda)
|
||
}
|
||
|
||
// BinomialCDF returns P(X ≤ k) for X ~ Binomial(trials, p), through
|
||
// the identity P(X ≤ k) = I_{1−p}(n−k, k+1).
|
||
func BinomialCDF(k, trials int, p float64) (float64, error) {
|
||
if trials < 1 {
|
||
return 0, base.Errf("BinomialCDF: trials must be ≥ 1, got %d", trials)
|
||
}
|
||
if !(p > 0 && p < 1) {
|
||
return 0, base.Errf("BinomialCDF: p must lie in (0, 1), got %g", p)
|
||
}
|
||
if k < 0 {
|
||
return 0, nil
|
||
}
|
||
if k >= trials {
|
||
return 1, nil
|
||
}
|
||
return BetaIncomplete(1-p, float64(trials-k), float64(k+1))
|
||
}
|
||
|
||
// continuousQuantile inverts a continuous CDF on the positive axis:
|
||
// the bracket starts at seed and doubles outward until the CDF
|
||
// straddles q, then the crossing is refined by a bracketed Newton
|
||
// iteration through the CDF's derivative pdf when one is given, and
|
||
// by plain bisection when it is not. The Newton walk carries the
|
||
// bracket with it and bisects whenever the derivative step would
|
||
// leave the bracket or would not cut it fast enough, so the
|
||
// convergence stays unconditional on every monotone CDF either way.
|
||
func continuousQuantile(name string, q float64, seed float64,
|
||
cdf func(float64) (float64, error), pdf func(float64) float64) (float64, error) {
|
||
// The guard is NaN-rejecting on purpose: NaN compares false against
|
||
// both bounds, so the written-out form would let it past and every
|
||
// bracket comparison below would then also be false.
|
||
if !(q >= 0 && q <= 1) {
|
||
return 0, base.Errf("%s: q must lie in [0, 1], got %g", name, q)
|
||
}
|
||
if q == 0 || q == 1 {
|
||
return 0, base.Errf("%s: q = %g has no finite quantile", name, q)
|
||
}
|
||
lo, hi := seed, seed
|
||
fLo, err := cdf(lo)
|
||
if err != nil {
|
||
return 0, base.Errf("%s: %w", name, err)
|
||
}
|
||
fHi, err := cdf(hi)
|
||
if err != nil {
|
||
return 0, base.Errf("%s: %w", name, err)
|
||
}
|
||
// Establish a bracket where the CDF crosses q.
|
||
for fLo > q {
|
||
lo /= 2
|
||
// Halving from a finite seed underflows through the denormals
|
||
// to exactly zero, which is the whole representable range: no
|
||
// CDF value can sit below a probability of that size.
|
||
if lo == 0 {
|
||
return 0, base.Errf("%s: failed to bracket q = %g from below", name, q)
|
||
}
|
||
if fLo, err = cdf(lo); err != nil {
|
||
return 0, base.Errf("%s: %w", name, err)
|
||
}
|
||
}
|
||
for fHi < q {
|
||
hi *= 2
|
||
if math.IsInf(hi, 0) {
|
||
return 0, base.Errf("%s: failed to bracket q = %g from above", name, q)
|
||
}
|
||
if fHi, err = cdf(hi); err != nil {
|
||
return 0, base.Errf("%s: %w", name, err)
|
||
}
|
||
}
|
||
if pdf == nil {
|
||
// Halve to rounding level. The interval at least halves every pass
|
||
// and the loop leaves on mid == lo || mid == hi, which a finite
|
||
// bracket always reaches (about 1100 passes from the widest one),
|
||
// so the iteration count is a guard against a non-monotone CDF,
|
||
// not a working part of the convergence: hitting it is an error,
|
||
// never a quietly unconverged midpoint.
|
||
converged := false
|
||
for range 4096 {
|
||
mid := (lo + hi) / 2
|
||
if mid == lo || mid == hi {
|
||
converged = true
|
||
break
|
||
}
|
||
f, err := cdf(mid)
|
||
if err != nil {
|
||
return 0, base.Errf("%s: %w", name, err)
|
||
}
|
||
if f < q {
|
||
lo = mid
|
||
} else {
|
||
hi = mid
|
||
}
|
||
}
|
||
if !converged {
|
||
return 0, base.Errf("%s: the bisection for q = %g did not converge", name, q)
|
||
}
|
||
return (lo + hi) / 2, nil
|
||
}
|
||
return continuousQuantileNewton(name, q, lo, hi, cdf, pdf)
|
||
}
|
||
|
||
// continuousQuantileNewton refines a bracket where the CDF crosses q
|
||
// by a safeguarded Newton walk: a Newton step on cdf(x) = q through
|
||
// the pdf derivative whenever that step lands inside the bracket and
|
||
// cuts it at least as fast as the bisection it replaces, a bisection
|
||
// step otherwise. The bisection guarantee survives untouched: the
|
||
// bracket at least halves every second round, the walk leaves when the
|
||
// bracket has no float strictly inside it, the same rounding-level
|
||
// exit the plain bisection takes, and a vanishing or non-finite
|
||
// derivative fails both Newton guards and bisects.
|
||
func continuousQuantileNewton(name string, q float64, lo, hi float64,
|
||
cdf func(float64) (float64, error), pdf func(float64) float64) (float64, error) {
|
||
x := 0.5 * (lo + hi)
|
||
f, err := cdf(x)
|
||
if err != nil {
|
||
return 0, base.Errf("%s: %w", name, err)
|
||
}
|
||
if f == q {
|
||
return x, nil
|
||
}
|
||
if f < q {
|
||
lo = x
|
||
} else {
|
||
hi = x
|
||
}
|
||
// stepSize carries the previous round's step, the yardstick the
|
||
// second Newton guard measures against: a derivative step that does
|
||
// not halve it is stalling, and the round bisects instead.
|
||
stepSize := hi - lo
|
||
converged := false
|
||
for range 4096 {
|
||
d := pdf(x)
|
||
g := f - q
|
||
// The two rtsafe guards: the derivative step is taken only when
|
||
// it lands strictly inside the bracket, x - g/d in (lo, hi),
|
||
// and cuts the last step at least in half. A vanishing or
|
||
// non-finite derivative fails the first guard and bisects.
|
||
newton := d > 0 && !math.IsInf(d, 1) &&
|
||
(x-lo)*d > g && g > (x-hi)*d &&
|
||
2*math.Abs(g) <= math.Abs(stepSize*d)
|
||
if newton {
|
||
stepSize = g / d
|
||
x -= stepSize
|
||
} else {
|
||
stepSize = 0.5 * (hi - lo)
|
||
x = 0.5 * (lo + hi)
|
||
}
|
||
if x == lo || x == hi {
|
||
converged = true
|
||
break
|
||
}
|
||
if f, err = cdf(x); err != nil {
|
||
return 0, base.Errf("%s: %w", name, err)
|
||
}
|
||
if f == q {
|
||
converged = true
|
||
break
|
||
}
|
||
if f < q {
|
||
lo = x
|
||
} else {
|
||
hi = x
|
||
}
|
||
}
|
||
if !converged {
|
||
return 0, base.Errf("%s: the quantile iteration for q = %g did not converge", name, q)
|
||
}
|
||
return x, nil
|
||
}
|
||
|
||
// continuousQuantileUpper inverts a monotone decreasing upper-tail
|
||
// probability on the positive axis: it returns the z > 0 with
|
||
// upper(z) = q, for q below what the reflected CDF can represent
|
||
// (below 2⁻⁵³, where 1−q rounds to 1). The bracket starts at [0, 1]
|
||
// and doubles outward; upper(0) = 0.5 covers every such q.
|
||
func continuousQuantileUpper(name string, q float64,
|
||
upper func(float64) (float64, error)) (float64, error) {
|
||
lo, hi := 0.0, 1.0
|
||
fHi, err := upper(hi)
|
||
if err != nil {
|
||
return 0, base.Errf("%s: %w", name, err)
|
||
}
|
||
for fHi > q {
|
||
lo = hi
|
||
hi *= 2
|
||
if math.IsInf(hi, 0) {
|
||
return 0, base.Errf("%s: failed to bracket q = %g from above", name, q)
|
||
}
|
||
if fHi, err = upper(hi); err != nil {
|
||
return 0, base.Errf("%s: %w", name, err)
|
||
}
|
||
}
|
||
converged := false
|
||
for range 4096 {
|
||
mid := (lo + hi) / 2
|
||
if mid == lo || mid == hi {
|
||
converged = true
|
||
break
|
||
}
|
||
f, err := upper(mid)
|
||
if err != nil {
|
||
return 0, base.Errf("%s: %w", name, err)
|
||
}
|
||
if f > q {
|
||
lo = mid
|
||
} else {
|
||
hi = mid
|
||
}
|
||
}
|
||
if !converged {
|
||
return 0, base.Errf("%s: the bisection for q = %g did not converge", name, q)
|
||
}
|
||
return (lo + hi) / 2, nil
|
||
}
|
||
|
||
// discreteQuantile returns the smallest integer k whose CDF reaches
|
||
// q, found by doubling the upper bound then bisecting the integer
|
||
// grid. The result is a whole number in float form.
|
||
func discreteQuantile(name string, q float64, cdf func(int) (float64, error)) (float64, error) {
|
||
// NaN-rejecting for the same reason continuousQuantile's guard is:
|
||
// a NaN q makes every comparison below false, and the doubling
|
||
// bracket would then run without end.
|
||
if !(q >= 0 && q <= 1) {
|
||
return 0, base.Errf("%s: q must lie in [0, 1], got %g", name, q)
|
||
}
|
||
if q == 0 {
|
||
return 0, nil
|
||
}
|
||
hi := 1
|
||
for {
|
||
f, err := cdf(hi)
|
||
if err != nil {
|
||
return 0, base.Errf("%s: %w", name, err)
|
||
}
|
||
if f >= q {
|
||
break
|
||
}
|
||
// A monotone CDF reaches any q > 0 at some finite k, so a bound
|
||
// this high means the CDF never will: refuse rather than let hi
|
||
// wrap to zero and the doubling loop spin for ever.
|
||
if hi > math.MaxInt/2 {
|
||
return 0, base.Errf("%s: failed to bracket q = %g", name, q)
|
||
}
|
||
hi *= 2
|
||
}
|
||
lo := 0
|
||
for lo < hi {
|
||
mid := (lo + hi) / 2
|
||
f, err := cdf(mid)
|
||
if err != nil {
|
||
return 0, base.Errf("%s: %w", name, err)
|
||
}
|
||
if f >= q {
|
||
hi = mid
|
||
} else {
|
||
lo = mid + 1
|
||
}
|
||
}
|
||
return float64(lo), nil
|
||
}
|
||
|
||
// normalPdf is the standard normal density, the derivative
|
||
// NormalQuantile's Newton walk steps through.
|
||
func normalPdf(x float64) float64 {
|
||
return math.Exp(-0.5*x*x) / math.Sqrt(2*math.Pi)
|
||
}
|
||
|
||
// gammaShapeRatePdf is the Gamma(shape, rate) density at x, evaluated
|
||
// from its logarithm so a large shape's normalising factor never
|
||
// overflows on its way to a density the format cannot hold. x at or
|
||
// below zero answers 0: the quantile walk treats a vanishing
|
||
// derivative as a signal to bisect.
|
||
func gammaShapeRatePdf(x, shape, rate float64) float64 {
|
||
if x <= 0 {
|
||
return 0
|
||
}
|
||
return math.Exp((shape-1)*math.Log(x) - rate*x + shape*math.Log(rate) - logGamma(shape))
|
||
}
|
||
|
||
// studentTPdf is the Student t(df) density at x, from its logarithm
|
||
// like the gamma density, so the wide tails' underflow lands on the 0
|
||
// the quantile walk reads as a bisection signal.
|
||
func studentTPdf(x float64, df int) float64 {
|
||
d := float64(df)
|
||
return math.Exp(logGamma(0.5*(d+1)) - logGamma(0.5*d) -
|
||
0.5*math.Log(d*math.Pi) - 0.5*(d+1)*math.Log1p(x*x/d))
|
||
}
|
||
|
||
// NormalQuantile returns the q-quantile of the standard normal
|
||
// distribution, the inverse of NormalCDF.
|
||
func NormalQuantile(q float64) (float64, error) {
|
||
if !(q >= 0 && q <= 1) {
|
||
return 0, base.Errf("NormalQuantile: q must lie in [0, 1], got %g", q)
|
||
}
|
||
if q == 0 || q == 1 {
|
||
return 0, base.Errf("NormalQuantile: q = %g has no finite quantile", q)
|
||
}
|
||
// The bracket expansion below only walks up from the seed, and
|
||
// Phi(0) = 0.5 is the infimum it can reach, so the whole lower half
|
||
// of the domain is bracketed on the reflected probability instead:
|
||
// Phi is symmetric, so the lower-tail quantile is the mirror of the
|
||
// upper one. Below 2⁻⁵³ the reflection 1−q rounds to exactly 1 and
|
||
// would be read as the q = 1 refusal, so the extreme tail bisects
|
||
// the accurate upper tail 0.5·Erfc directly, on the positive axis.
|
||
if q < 0.5 {
|
||
if 1-q == 1 {
|
||
z, err := continuousQuantileUpper("NormalQuantile", q, func(x float64) (float64, error) {
|
||
return 0.5 * math.Erfc(x/math.Sqrt2), nil
|
||
})
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return -z, nil
|
||
}
|
||
z, err := continuousQuantile("NormalQuantile", 1-q, 1, func(x float64) (float64, error) {
|
||
return NormalCDF(x), nil
|
||
}, normalPdf)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return -z, nil
|
||
}
|
||
return continuousQuantile("NormalQuantile", q, 1, func(x float64) (float64, error) {
|
||
return NormalCDF(x), nil
|
||
}, normalPdf)
|
||
}
|
||
|
||
// ExponentialQuantile returns the q-quantile of Exponential(rate).
|
||
func ExponentialQuantile(q, rate float64) (float64, error) {
|
||
return continuousQuantile("ExponentialQuantile", q, 1, func(x float64) (float64, error) {
|
||
return ExponentialCDF(x, rate)
|
||
}, func(x float64) float64 {
|
||
return rate * math.Exp(-rate*x)
|
||
})
|
||
}
|
||
|
||
// GammaQuantile returns the q-quantile of Gamma(shape, rate).
|
||
func GammaQuantile(q, shape, rate float64) (float64, error) {
|
||
return continuousQuantile("GammaQuantile", q, shape/rate, func(x float64) (float64, error) {
|
||
return GammaCDF(x, shape, rate)
|
||
}, func(x float64) float64 {
|
||
return gammaShapeRatePdf(x, shape, rate)
|
||
})
|
||
}
|
||
|
||
// ChiSquareQuantile returns the q-quantile of χ²(df), the critical
|
||
// value tables quote.
|
||
func ChiSquareQuantile(q float64, df int) (float64, error) {
|
||
shape := float64(df) / 2
|
||
return continuousQuantile("ChiSquareQuantile", q, float64(df), func(x float64) (float64, error) {
|
||
return ChiSquareCDF(x, df)
|
||
}, func(x float64) float64 {
|
||
return gammaShapeRatePdf(x, shape, 0.5)
|
||
})
|
||
}
|
||
|
||
// StudentTQuantile returns the q-quantile of Student t(df). The signed
|
||
// axis is bracketed through a two-sided bracket expansion, the crossing
|
||
// refined by the bracketed Newton walk through the t density.
|
||
func StudentTQuantile(q float64, df int) (float64, error) {
|
||
if !(q >= 0 && q <= 1) {
|
||
return 0, base.Errf("StudentTQuantile: q must lie in [0, 1], got %g", q)
|
||
}
|
||
if q == 0 || q == 1 {
|
||
return 0, base.Errf("StudentTQuantile: q = %g has no finite quantile", q)
|
||
}
|
||
// Bracket on the positive side, then mirror for q < 0.5. Below 2⁻⁵³
|
||
// the reflection 1−q rounds to exactly 1 and would be read as the
|
||
// q = 1 refusal, so the extreme tail bisects the one-sided upper
|
||
// tail directly, on the positive axis.
|
||
if q < 0.5 {
|
||
if 1-q == 1 {
|
||
t, err := continuousQuantileUpper("StudentTQuantile", q, func(x float64) (float64, error) {
|
||
return studentTUpperTail(x, df)
|
||
})
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return -t, nil
|
||
}
|
||
t, err := continuousQuantile("StudentTQuantile", 1-q, 1, func(x float64) (float64, error) {
|
||
return StudentTCDF(x, df)
|
||
}, func(x float64) float64 {
|
||
return studentTPdf(x, df)
|
||
})
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return -t, nil
|
||
}
|
||
return continuousQuantile("StudentTQuantile", q, 1, func(x float64) (float64, error) {
|
||
return StudentTCDF(x, df)
|
||
}, func(x float64) float64 {
|
||
return studentTPdf(x, df)
|
||
})
|
||
}
|
||
|
||
// studentTUpperTail returns P(T > t) for Student t(df), one tail of
|
||
// the symmetric two-sided form twoSidedT uses. Beyond t²'s overflow
|
||
// the incomplete-beta argument saturates to 0 and would report a
|
||
// phantom zero tail exactly where the heavy df ≤ 2 tails stay
|
||
// representable, so those degrees answer by their asymptotic forms.
|
||
func studentTUpperTail(t float64, df int) (float64, error) {
|
||
if t > 1.3e154 {
|
||
switch {
|
||
case df == 1:
|
||
// Cauchy: the exact tail 1/2 − atan(t)/π, in a form the
|
||
// huge t keeps accurate.
|
||
return 1 / (math.Pi * t), nil
|
||
case df == 2:
|
||
// The tail 1/(2t²) divided one t at a time: the literal
|
||
// denominator overflows past √MaxFloat64, and the quotient
|
||
// would flush the still-representable tail to zero.
|
||
return 0.5 / t / t, nil
|
||
}
|
||
}
|
||
z := float64(df) / (float64(df) + t*t)
|
||
p, err := BetaIncomplete(z, float64(df)/2, 0.5)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return p / 2, nil
|
||
}
|
||
|
||
// PoissonQuantile returns the smallest k with P(N ≤ k) ≥ q for
|
||
// N ~ Poisson(lambda).
|
||
func PoissonQuantile(q, lambda float64) (float64, error) {
|
||
if !(lambda > 0) {
|
||
return 0, base.Errf("PoissonQuantile: lambda must be positive, got %g", lambda)
|
||
}
|
||
return discreteQuantile("PoissonQuantile", q, func(k int) (float64, error) {
|
||
return PoissonCDF(k, lambda)
|
||
})
|
||
}
|
||
|
||
// BinomialQuantile returns the smallest k with P(X ≤ k) ≥ q for
|
||
// X ~ Binomial(trials, p).
|
||
func BinomialQuantile(q, p float64, trials int) (float64, error) {
|
||
if !(p > 0 && p < 1) {
|
||
return 0, base.Errf("BinomialQuantile: p must lie in (0, 1), got %g", p)
|
||
}
|
||
return discreteQuantile("BinomialQuantile", q, func(k int) (float64, error) {
|
||
return BinomialCDF(k, trials, p)
|
||
})
|
||
}
|