Files
tensor/stats/deep_tail_pin_test.go
petrbalvin af4ee19703
Release / gates (push) Successful in 4m38s
Test / test (push) Successful in 5m16s
Release / release (push) Successful in 35s
feat: initial release
Assisted-by: GLM 5.3 Flash
2026-09-03 10:00:00 +02:00

155 lines
5.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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)
}
}