Files
tensor/stats/cdf.go
T

742 lines
23 KiB
Go
Raw Permalink Normal View History

2026-09-03 10:00:00 +02:00
// 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)
2026-09-03 10:00:00 +02:00
if err != nil {
return 0, base.Errf("StudentTCDF: %w", err)
}
if t >= 0 {
return 1 - upper, nil
2026-09-03 10:00:00 +02:00
}
return upper, nil
2026-09-03 10:00:00 +02:00
}
// 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
2026-09-03 10:00:00 +02:00
}
}
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)
})
}