Files

155 lines
5.4 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 (
"math"
"strings"
"testing"
"sourcedock.dev/petrbalvin/tensor/internal/core"
)
// TestQuantileDeepTails pins the deep-left-tail quantiles that
// the old 200-pass bisection silently mis-answered and the 1e300
// bracket floors refused: the answers must be within a rounding of the
// exact values, never an unconverged midpoint with a nil error.
func TestQuantileDeepTails(t *testing.T) {
v, err := ExponentialQuantile(1e-100, 1)
if err != nil {
t.Fatalf("ExponentialQuantile: %v", err)
}
if relErr(v, 1e-100) > 1e-6 {
t.Fatalf("ExponentialQuantile(1e-100, 1) = %g, want 1e-100", v)
}
v, err = ChiSquareQuantile(1e-30, 1)
if err != nil {
t.Fatalf("ChiSquareQuantile: %v", err)
}
// The exact χ²(1) deep lower tail: x = z² with Φ(z) = 0.5 + q/2,
// so z ≈ (q/2)/φ(0) and x ≈ (π/2)·q².
want := math.Pi / 2 * 1e-60
if relErr(v, want) > 1e-4 {
t.Fatalf("ChiSquareQuantile(1e-30, 1) = %g, want %g", v, want)
}
v, err = GammaQuantile(1e-100, 0.5, 0.5)
if err != nil {
t.Fatalf("GammaQuantile: %v", err)
}
if v < 0.5*1e-200 || v > 2*1e-200 {
t.Fatalf("GammaQuantile(1e-100, 0.5, 0.5) = %g, want ≈ 0.785e-200 (χ²(1)/2 at q²)", v)
}
// The denormal floor: the smallest representable q still answers
// the smallest representable scale, or refuses loudly; never a
// silent wrong number.
if _, err = ExponentialQuantile(5e-324, 1); err != nil {
t.Fatalf("ExponentialQuantile(5e-324): %v", err)
}
}
func relErr(got, want float64) float64 {
d := math.Abs(got - want)
if want == 0 {
return d
}
return d / math.Abs(want)
}
// TestNormalQuantileExtremeTail pins that a q below 2⁻⁵³, where
// 1−q rounds to exactly 1, still answers through the accurate upper
// tail instead of the bogus "q = 1" refusal.
func TestNormalQuantileExtremeTail(t *testing.T) {
v, err := NormalQuantile(1e-17)
if err != nil {
t.Fatalf("NormalQuantile(1e-17): %v", err)
}
// Φ(−8.2907496456... ) = 1e-17 to the digits that matter here.
if relErr(0.5*math.Erfc(-v/math.Sqrt2), 1e-17) > 1e-6 {
t.Fatalf("NormalQuantile(1e-17) = %g, tail = %g", v, 0.5*math.Erfc(-v/math.Sqrt2))
}
v, err = StudentTQuantile(1e-17, 5)
if err != nil {
t.Fatalf("StudentTQuantile(1e-17, 5): %v", err)
}
tail, err := studentTUpperTail(-v, 5)
if err != nil {
t.Fatalf("studentTUpperTail: %v", err)
}
// The quantile mirrors the one-sided tail: P(T > −t) = q.
if relErr(tail, 1e-17) > 1e-6 {
t.Fatalf("StudentTQuantile(1e-17, 5) = %g, one-sided tail = %g, want %g", v, tail, 1e-17)
}
// The heavy tails stay representable far past where t² overflows:
// Cauchy answers 1/(πq) exactly, df = 2 answers 1/sqrt(2q), where
// the incomplete-beta argument used to saturate to a phantom zero
// and clamp the quantile at the overflow wall.
vc, err := StudentTQuantile(1e-200, 1)
if err != nil {
t.Fatalf("StudentTQuantile(1e-200, 1): %v", err)
}
if want := -1 / (math.Pi * 1e-200); relErr(vc, want) > 1e-6 {
t.Fatalf("StudentTQuantile(1e-200, 1) = %g, want %g", vc, want)
}
v2, err := StudentTQuantile(1e-160, 2)
if err != nil {
t.Fatalf("StudentTQuantile(1e-160, 2): %v", err)
}
if want := -1 / math.Sqrt(2*1e-160); relErr(v2, want) > 1e-6 {
t.Fatalf("StudentTQuantile(1e-160, 2) = %g, want %g", v2, want)
}
}
// TestRegressionConstantResponse pins R² = 1 for a constant
// response reproduced exactly, where the 1 − 0/0 form reported NaN
// with a nil error.
func TestRegressionConstantResponse(t *testing.T) {
x := mustFloats(t, []float64{1, 0, 1, 1, 1, 2, 1, 3}, 4, 2)
y := mustFloats(t, []float64{5, 5, 5, 5}, 4)
res, err := LinearRegression(x, y)
if err != nil {
t.Fatalf("LinearRegression: %v", err)
}
if res.RSquared != 1 || res.AdjustedRSquared != 1 {
t.Fatalf("constant response: R² = %v, adj = %v, want 1 and 1", res.RSquared, res.AdjustedRSquared)
}
w := mustFloats(t, []float64{1, 2, 1, 1}, 4)
wres, err := WeightedLinearRegression(x, y, w)
if err != nil {
t.Fatalf("WeightedLinearRegression: %v", err)
}
if wres.RSquared != 1 || wres.AdjustedRSquared != 1 {
t.Fatalf("weighted constant response: R² = %v, adj = %v, want 1 and 1", wres.RSquared, wres.AdjustedRSquared)
}
}
// TestHistogramFullRange pins the refusal of a sample holding
// both float extremes, whose edges would be ±Inf and whose counts
// silently collapsed into bin 0.
func TestHistogramFullRange(t *testing.T) {
a := mustFloats(t, []float64{-math.MaxFloat64, 0, math.MaxFloat64})
if _, _, err := Histogram(a, 2); err == nil {
t.Fatal("Histogram: expected a range error")
}
b := mustFloats(t, []float64{-math.MaxFloat64, 1, math.MaxFloat64, 4}, 2, 2)
bx, err := core.Slice(b, 1, 0, 1)
if err != nil {
t.Fatalf("Slice: %v", err)
}
if _, _, _, err := Histogram2D(bx, b, 2, 2); err == nil {
t.Fatal("Histogram2D: expected a range error")
}
}
// TestChiSquareGOFRejectsInfiniteExpected pins that an
// infinite expectation is refused at the entry point, under its own
// name, rather than surfacing as a NaN inside the tail function.
func TestChiSquareGOFRejectsInfiniteExpected(t *testing.T) {
obs := mustFloats(t, []float64{10, 12, 9})
exp := mustFloats(t, []float64{math.Inf(1), 10, 10})
_, _, _, err := ChiSquareGoodnessOfFit(obs, exp)
if err == nil || !strings.Contains(err.Error(), "expected frequencies") {
t.Fatalf("ChiSquareGoodnessOfFit: err = %v, want the expected-frequencies refusal", err)
}
}