Files

153 lines
4.6 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"
)
// Estimation guards and exactness pins: silent NaN estimates, a
// perfect-significance null model, and int64 samples that round
// through float64.
// TestCovarianceMatrixRejectsNaN: one NaN observation flowed into the
// column means and produced an all-NaN matrix with a nil error.
func TestCovarianceMatrixRejectsNaN(t *testing.T) {
obs, err := core.FromFloats([]float64{1, 2, math.NaN(), 4, 5, 6}, 3, 2)
if err != nil {
t.Fatal(err)
}
if _, err := CovarianceMatrix(obs); err == nil || !strings.Contains(err.Error(), "CovarianceMatrix") {
t.Fatalf("CovarianceMatrix on a NaN observation: err = %v", err)
}
if _, err := CorrelationMatrix(obs); err == nil || !strings.Contains(err.Error(), "CorrelationMatrix") {
t.Fatalf("CorrelationMatrix on a NaN observation: err = %v", err)
}
}
// TestRegressionInterceptOnly: an intercept-only design skipped the F
// block and left FPValue at its zero value, reporting the null model
// as maximally significant.
func TestRegressionInterceptOnly(t *testing.T) {
y, err := core.FromFloats([]float64{2, 4, 6, 8}, 4)
if err != nil {
t.Fatal(err)
}
one, err := core.FromFloats([]float64{1, 1, 1, 1}, 4, 1)
if err != nil {
t.Fatal(err)
}
for name, run := range map[string]func() (*LinearRegressionResult, error){
"LinearRegression": func() (*LinearRegressionResult, error) { return LinearRegression(one, y) },
"WeightedLinearRegression": func() (*LinearRegressionResult, error) {
w, _ := core.FromFloats([]float64{1, 1, 1, 1}, 4)
return WeightedLinearRegression(one, y, w)
},
} {
res, err := run()
if err != nil {
t.Fatalf("%s: %v", name, err)
}
if res.FPValue != 1 {
t.Fatalf("%s: intercept-only FPValue = %g, want 1", name, res.FPValue)
}
if res.FStatistic != 0 {
t.Fatalf("%s: intercept-only FStatistic = %g, want 0", name, res.FStatistic)
}
}
}
// TestHistogramInt64Exact: the samples 2^53, 2^53+1, 2^53+2 all
// widened to the same float64 and landed in the wrong bins; the exact
// integer path bins them by their true values.
func TestHistogramInt64Exact(t *testing.T) {
const base = 1 << 53
x, err := core.FromInts([]int64{base + 1, base, base + 2}, 3)
if err != nil {
t.Fatal(err)
}
counts, _, err := Histogram(x, 2)
if err != nil {
t.Fatal(err)
}
if c0, _ := core.IntAt(counts, 0); c0 != 1 {
t.Fatalf("bin 0 count = %d, want 1 (only the minimum)", c0)
}
if c1, _ := core.IntAt(counts, 1); c1 != 2 {
t.Fatalf("bin 1 count = %d, want 2", c1)
}
}
// TestMedianQuantileInt64Exact: the median of {2^53, 2^53+1} came
// back as 2^53 although 2^53+0.5 is representable; the same held for
// the quantile at 0.5, whose interpolation collapsed on the rounded
// pair.
func TestMedianQuantileInt64Exact(t *testing.T) {
const base = 1 << 53
x, err := core.FromInts([]int64{base, base + 1}, 2)
if err != nil {
t.Fatal(err)
}
m, err := Median(x)
if err != nil {
t.Fatal(err)
}
if m != base+0.5 {
t.Fatalf("Median = %g, want %g", m, base+0.5)
}
q, err := Quantile(x, []float64{0.5})
if err != nil {
t.Fatal(err)
}
if q.FloatAt(0) != base+0.5 {
t.Fatalf("Quantile(0.5) = %g, want %g", q.FloatAt(0), base+0.5)
}
}
// TestGammaUpperOwnErrorName: GammaUpper reported the GammaLower
// prefix on the shape refusal.
func TestGammaUpperOwnErrorName(t *testing.T) {
if _, err := GammaUpper(0, 1); err == nil || !strings.Contains(err.Error(), "GammaUpper:") {
t.Fatalf("GammaUpper(0, 1): err = %v", err)
}
}
// TestWelchTTestRejectsNaN: a NaN sample only surfaced when the tail
// function rejected the NaN statistic, under its own name.
func TestWelchTTestRejectsNaN(t *testing.T) {
a, err := core.FromFloats([]float64{math.NaN(), 2, 3}, 3)
if err != nil {
t.Fatal(err)
}
b, err := core.FromFloats([]float64{1, 2, 3}, 3)
if err != nil {
t.Fatal(err)
}
if _, _, _, err := WelchTTest(a, b); err == nil || !strings.Contains(err.Error(), "WelchTTest") {
t.Fatalf("WelchTTest on a NaN sample: err = %v", err)
}
}
// TestMannWhitneyUTieOverflow: a tie block of 2^21+1 observations
// overflowed the integer tie term, wrapped it negative and bypassed
// the every-observation-tied refusal.
func TestMannWhitneyUTieOverflow(t *testing.T) {
const n = 1<<21 + 1
vals := make([]float64, n)
for i := range vals {
vals[i] = 7
}
a, err := core.FromFloats(vals, n)
if err != nil {
t.Fatal(err)
}
if _, _, err := MannWhitneyU(a, a); err == nil || !strings.Contains(err.Error(), "tied") {
t.Fatalf("MannWhitneyU on one giant tie block: err = %v", err)
}
}