192 lines
6.1 KiB
Go
192 lines
6.1 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|||
|
|
// SPDX-License-Identifier: MIT
|
||
|
|
|
||
|
|
// Pins for the rebased-view walks: a contiguous Slice shares its
|
||
|
|
// source's storage, so its payload runs past the view's own element
|
||
|
|
// count, and every raw-payload walk in the package is bounded by the
|
||
|
|
// visible elements. Each pin holds a view's answer against the same
|
||
|
|
// computation on fresh arrays carrying nothing but the visible
|
||
|
|
// elements, with junk parked in the invisible tail: a walk that reads
|
||
|
|
// one payload slot past its count fails the pin, and a walk that
|
||
|
|
// writes one panics outright.
|
||
|
|
|
||
|
|
package stats
|
||
|
|
|
||
|
|
import (
|
||
|
|
"math"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"sourcedock.dev/petrbalvin/tensor/internal/core"
|
||
|
|
)
|
||
|
|
|
||
|
|
// viewTailPool builds a 60-sample pool whose invisible tail, from
|
||
|
|
// position 40 on, holds values no caller can see, one of them a NaN.
|
||
|
|
func viewTailPool(t *testing.T) *core.Array {
|
||
|
|
t.Helper()
|
||
|
|
pool := make([]float64, 60)
|
||
|
|
for i := range pool {
|
||
|
|
pool[i] = float64(i%9) + 1
|
||
|
|
}
|
||
|
|
pool[55] = math.NaN() // inside the invisible tail: must be ignored
|
||
|
|
a, err := core.FromFloats(pool, len(pool))
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("FromFloats: %v", err)
|
||
|
|
}
|
||
|
|
return a
|
||
|
|
}
|
||
|
|
|
||
|
|
// viewTailHalves slices the pool's head into two disjoint views of ten,
|
||
|
|
// beside fresh arrays holding exactly the visible elements.
|
||
|
|
func viewTailHalves(t *testing.T) (va, vb, fa, fb *core.Array) {
|
||
|
|
t.Helper()
|
||
|
|
pool := viewTailPool(t)
|
||
|
|
va, err := core.Slice(pool, 0, 0, 10)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Slice: %v", err)
|
||
|
|
}
|
||
|
|
vb, err = core.Slice(pool, 0, 10, 20)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Slice: %v", err)
|
||
|
|
}
|
||
|
|
visible := pool.RawFloats()[:20]
|
||
|
|
fa, err = core.FromFloats(visible[:10], 10)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("FromFloats: %v", err)
|
||
|
|
}
|
||
|
|
fb, err = core.FromFloats(visible[10:], 10)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("FromFloats: %v", err)
|
||
|
|
}
|
||
|
|
return va, vb, fa, fb
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestViewTailANOVAOneWay pins the analysis of variance on rebased
|
||
|
|
// views: the group sums, the finite scan and the within sum of squares
|
||
|
|
// read the visible elements only.
|
||
|
|
func TestViewTailANOVAOneWay(t *testing.T) {
|
||
|
|
va, vb, fa, fb := viewTailHalves(t)
|
||
|
|
f1, p1, err1 := ANOVAOneWay([]*core.Array{va, vb})
|
||
|
|
f2, p2, err2 := ANOVAOneWay([]*core.Array{fa, fb})
|
||
|
|
if (err1 == nil) != (err2 == nil) {
|
||
|
|
t.Fatalf("ANOVAOneWay on views errored %v, on fresh arrays %v", err1, err2)
|
||
|
|
}
|
||
|
|
if err1 == nil && (f1 != f2 || p1 != p2) {
|
||
|
|
t.Fatalf("ANOVAOneWay on views = (%v, %v), fresh arrays answer (%v, %v)", f1, p1, f2, p2)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestViewTailWelchTTest pins Welch's t-test on rebased views: the
|
||
|
|
// sample mean and variance read the visible elements only.
|
||
|
|
func TestViewTailWelchTTest(t *testing.T) {
|
||
|
|
va, vb, fa, fb := viewTailHalves(t)
|
||
|
|
t1, df1, p1, err1 := WelchTTest(va, vb)
|
||
|
|
t2, df2, p2, err2 := WelchTTest(fa, fb)
|
||
|
|
if (err1 == nil) != (err2 == nil) {
|
||
|
|
t.Fatalf("WelchTTest on views errored %v, on fresh arrays %v", err1, err2)
|
||
|
|
}
|
||
|
|
if err1 == nil && (t1 != t2 || df1 != df2 || p1 != p2) {
|
||
|
|
t.Fatalf("WelchTTest on views = (%v, %v, %v), fresh arrays answer (%v, %v, %v)",
|
||
|
|
t1, df1, p1, t2, df2, p2)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestViewTailMAD pins the median absolute deviation on a rebased
|
||
|
|
// view: the deviation walk writes one cell per visible element, never
|
||
|
|
// one per payload slot.
|
||
|
|
func TestViewTailMAD(t *testing.T) {
|
||
|
|
pool := viewTailPool(t)
|
||
|
|
va, err := core.Slice(pool, 0, 0, 10)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Slice: %v", err)
|
||
|
|
}
|
||
|
|
visible := pool.RawFloats()[:10]
|
||
|
|
fa, err := core.FromFloats(visible, 10)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("FromFloats: %v", err)
|
||
|
|
}
|
||
|
|
got, err1 := MedianAbsoluteDeviation(va)
|
||
|
|
want, err2 := MedianAbsoluteDeviation(fa)
|
||
|
|
if err1 != nil || err2 != nil {
|
||
|
|
t.Fatalf("MedianAbsoluteDeviation: %v against %v", err1, err2)
|
||
|
|
}
|
||
|
|
if got != want {
|
||
|
|
t.Fatalf("MedianAbsoluteDeviation on a view = %v, fresh array answers %v", got, want)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestViewTailChiSquareGoodnessOfFit pins the goodness-of-fit test on
|
||
|
|
// rebased views: both frequency walks are bounded by the bin count,
|
||
|
|
// where an unbounded index into the other payload was a panic before it
|
||
|
|
// was a wrong statistic.
|
||
|
|
func TestViewTailChiSquareGoodnessOfFit(t *testing.T) {
|
||
|
|
pool := viewTailPool(t)
|
||
|
|
obsV, err := core.Slice(pool, 0, 20, 30)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Slice: %v", err)
|
||
|
|
}
|
||
|
|
expV, err := core.Slice(pool, 0, 30, 40)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Slice: %v", err)
|
||
|
|
}
|
||
|
|
visible := pool.RawFloats()[:40]
|
||
|
|
fObs, err := core.FromFloats(visible[20:30], 10)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("FromFloats: %v", err)
|
||
|
|
}
|
||
|
|
fExp, err := core.FromFloats(visible[30:40], 10)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("FromFloats: %v", err)
|
||
|
|
}
|
||
|
|
c1, df1, p1, errV := ChiSquareGoodnessOfFit(obsV, expV)
|
||
|
|
c2, df2, p2, errF := ChiSquareGoodnessOfFit(fObs, fExp)
|
||
|
|
if (errV == nil) != (errF == nil) {
|
||
|
|
t.Fatalf("ChiSquareGoodnessOfFit on views errored %v, on fresh arrays %v", errV, errF)
|
||
|
|
}
|
||
|
|
if errV == nil && (c1 != c2 || df1 != df2 || p1 != p2) {
|
||
|
|
t.Fatalf("ChiSquareGoodnessOfFit on views = (%v, %d, %v), fresh arrays answer (%v, %d, %v)",
|
||
|
|
c1, df1, p1, c2, df2, p2)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestViewTailLinearRegression pins the fit on rebased views: the
|
||
|
|
// design and response scans refuse only what the visible rows hold, so
|
||
|
|
// junk in the invisible tail cannot reject a clean fit.
|
||
|
|
func TestViewTailLinearRegression(t *testing.T) {
|
||
|
|
design := make([]float64, 40)
|
||
|
|
for i := range design {
|
||
|
|
design[i] = float64(i % 7)
|
||
|
|
}
|
||
|
|
resp := make([]float64, 20)
|
||
|
|
for i := range resp {
|
||
|
|
resp[i] = float64(i%5) * 0.5
|
||
|
|
}
|
||
|
|
// Both junk slots sit past their views' visible rows: design row 15
|
||
|
|
// is invisible to the ten-row design view, response element 15 to
|
||
|
|
// the ten-row response view.
|
||
|
|
design[30] = math.NaN()
|
||
|
|
resp[15] = math.NaN()
|
||
|
|
designArr, err := core.FromFloats(design, len(design))
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("FromFloats: %v", err)
|
||
|
|
}
|
||
|
|
respArr, err := core.FromFloats(resp, len(resp))
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("FromFloats: %v", err)
|
||
|
|
}
|
||
|
|
design2d, err := core.Reshape(designArr, 20, 2)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Reshape: %v", err)
|
||
|
|
}
|
||
|
|
designView, err := core.Slice(design2d, 0, 0, 10)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Slice: %v", err)
|
||
|
|
}
|
||
|
|
respView, err := core.Slice(respArr, 0, 0, 10)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Slice: %v", err)
|
||
|
|
}
|
||
|
|
if _, err := LinearRegression(designView, respView); err != nil {
|
||
|
|
t.Fatalf("LinearRegression refused clean visible rows: %v", err)
|
||
|
|
}
|
||
|
|
}
|