61 lines
2.4 KiB
Go
61 lines
2.4 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
|
|
// SPDX-License-Identifier: MIT
|
|||
|
|
|
|||
|
|
// Pin for the negative binomial's far regime: when the summation's seed
|
|||
|
|
// p^r underflows to an exact zero the recurrence can no longer start,
|
|||
|
|
// and the CDF answers through the regularised beta identity instead.
|
|||
|
|
// The law with r = 100 and p = 1e-4 has its mean at 999 900, so the CDF
|
|||
|
|
// at the mean is near one half; the seed 1e-400 underflows outright, and
|
|||
|
|
// the summation that ignored that answered an exact zero at every k.
|
|||
|
|
|
|||
|
|
package stats
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"math"
|
|||
|
|
"testing"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func TestNegativeBinomialFarTail(t *testing.T) {
|
|||
|
|
atMean, err := NegativeBinomialCDF(999900, 100, 1e-4)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("NegativeBinomialCDF(999900, 100, 1e-4): %v", err)
|
|||
|
|
}
|
|||
|
|
// The mean is r(1−p)/p = 999 900 and the spread is near 100 000, so
|
|||
|
|
// the true value at the mean sits within a few 1e-3 of one half
|
|||
|
|
// (exactly 0.5133 by the exact referent). The identity's accuracy in
|
|||
|
|
// this regime is bounded by the lgamma noise of its front factor, so
|
|||
|
|
// the pin holds a loose band and a strict ordering.
|
|||
|
|
if math.Abs(atMean-0.5133007914) > 1e-6 {
|
|||
|
|
t.Fatalf("NegativeBinomialCDF at the mean = %.17g, want 0.5133007914 to within 1e-6", atMean)
|
|||
|
|
}
|
|||
|
|
below, err := NegativeBinomialCDF(950000, 100, 1e-4)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("NegativeBinomialCDF(950000, 100, 1e-4): %v", err)
|
|||
|
|
}
|
|||
|
|
above, err := NegativeBinomialCDF(1050000, 100, 1e-4)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("NegativeBinomialCDF(1050000, 100, 1e-4): %v", err)
|
|||
|
|
}
|
|||
|
|
if !(below < atMean && atMean < above) {
|
|||
|
|
t.Fatalf("the CDF is not increasing through the mean: %.17g, %.17g, %.17g", below, atMean, above)
|
|||
|
|
}
|
|||
|
|
far, err := NegativeBinomialCDF(2000000, 100, 1e-4)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("NegativeBinomialCDF(2000000, 100, 1e-4): %v", err)
|
|||
|
|
}
|
|||
|
|
if far < 0.9999 {
|
|||
|
|
t.Fatalf("NegativeBinomialCDF two hundred standard deviations out = %.17g, want ~1", far)
|
|||
|
|
}
|
|||
|
|
// The quantile route runs on the same CDF: the median must sit near
|
|||
|
|
// the mean the law's own moments give, a fraction of the spread
|
|||
|
|
// below it on the skewed side. The exact referent puts the median at
|
|||
|
|
// 996 569, and the pin holds it within a thousandth of the spread.
|
|||
|
|
median, err := NegativeBinomialQuantile(0.5, 1e-4, 100)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("NegativeBinomialQuantile: %v", err)
|
|||
|
|
}
|
|||
|
|
if math.Abs(median-996569) > 100 {
|
|||
|
|
t.Fatalf("NegativeBinomialQuantile(0.5) = %v, want within 100 of the exact median 996569", median)
|
|||
|
|
}
|
|||
|
|
}
|