feat: initial release
Release / gates (push) Successful in 4m38s
Test / test (push) Successful in 5m16s
Release / release (push) Successful in 35s

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-03 10:00:00 +02:00
commit af4ee19703
617 changed files with 191195 additions and 0 deletions
+296
View File
@@ -0,0 +1,296 @@
// 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"
)
// quantileSubgradient evaluates the check loss's subgradient at a fit
// and the slack the zero-band residuals buy it. At the optimum of
// Σρ_τ(y − Xβ) there must be a choice of s ∈ [τ−1, τ] per observation
// with Xᵀs = 0; residuals above the band fix their s (τ above the
// fit, τ−1 below it), and residuals within 1e-6 of zero, where no
// floating point fit can place them exactly, are free to slide in
// [τ−1, τ]. The certificate passes when every column's forced
// gradient is within the slack its zero-band residuals can absorb.
func quantileSubgradient(t *testing.T, x, y *core.Array, beta []float64, tau float64) (grad, free []float64) {
t.Helper()
const band = 1e-6
n, p := x.Shape()[0], x.Shape()[1]
grad = make([]float64, p)
free = make([]float64, p)
for i := range n {
r := y.FloatAt(i)
for j := range p {
r -= beta[j] * x.FloatAt(i*p+j)
}
var s float64
switch {
case r > band:
s = tau
case r < -band:
s = tau - 1
default:
for j := range p {
free[j] += math.Abs(x.FloatAt(i*p + j))
}
continue
}
for j := range p {
grad[j] += x.FloatAt(i*p+j) * s
}
}
for j := range p {
free[j] *= math.Max(tau, 1-tau)
}
return grad, free
}
// quantileHeteroFixture builds the seeded heteroscedastic model the
// coverage and monotonicity pins share: x uniform on [−2, 2], the
// response moved by 1 + 2x and widened by a scale that grows with
// |x|, so the tau-th conditional quantile is a genuinely different
// line from the mean's and no single slope fits every tau.
func quantileHeteroFixture(t *testing.T, n int, seed int64) (*core.Array, *core.Array) {
t.Helper()
g := core.NewGenerator(seed)
vals := make([]float64, 0, 2*n)
yv := make([]float64, 0, n)
for range n {
x := 4*g.Unit() - 2
y := 1 + 2*x + (0.3+0.3*math.Abs(x))*g.NormalUnit()
vals = append(vals, 1, x)
yv = append(yv, y)
}
return mustFromFloats(t, vals, n, 2), mustFromFloats(t, yv, n)
}
// TestQuantileMedianCertificate pins tau = 0.5 against the L1 median
// regression answer, twice over. An intercept-only design has the
// sample median for its answer, and the general design is verified
// against the subgradient optimality certificate, which is the exact
// first-order condition of the check loss rather than another
// algorithm's output.
func TestQuantileMedianCertificate(t *testing.T) {
// Intercept only, odd sample: the fit is the median, the single
// point every absolute deviation bends toward.
const n = 25
g := core.NewGenerator(19)
ys := make([]float64, n)
for i := range n {
ys[i] = math.Round(40*g.Unit() - 20)
}
y := mustFromFloats(t, ys, n)
ones := make([]float64, n)
for i := range n {
ones[i] = 1
}
design := mustFromFloats(t, ones, n, 1)
res, err := QuantileRegression(design, y, 0.5)
if err != nil {
t.Fatalf("QuantileRegression: %v", err)
}
if !res.Converged {
t.Fatalf("the intercept-only fit did not converge")
}
median, err := Median(y)
if err != nil {
t.Fatalf("Median: %v", err)
}
if math.Abs(res.Coefficients[0]-median) > 1e-7 {
t.Fatalf("the tau = 0.5 intercept-only fit is %.12g, want the median %.12g", res.Coefficients[0], median)
}
// A general design against the subgradient certificate.
xs := make([]float64, 0, 2*n)
yv := make([]float64, 0, n)
for range n {
x := 4*g.Unit() - 2
yv = append(yv, 1+2*x+0.4*g.NormalUnit())
xs = append(xs, 1, x)
}
design2 := mustFromFloats(t, xs, n, 2)
y2 := mustFromFloats(t, yv, n)
fit, err := QuantileRegression(design2, y2, 0.5)
if err != nil {
t.Fatalf("QuantileRegression: %v", err)
}
grad, free := quantileSubgradient(t, design2, y2, fit.Coefficients, 0.5)
for j := range 2 {
t.Logf("tau = 0.5 column %d: forced gradient %.3g against slack %.3g", j, grad[j], free[j])
if math.Abs(grad[j]) > free[j]+1e-6 {
t.Fatalf("column %d fails the subgradient certificate: %.3g against slack %.3g", j, grad[j], free[j])
}
}
}
// TestQuantileObjectiveMonotone instruments the interior-point run on
// the heteroscedastic fixture: the recorded objective is the running
// best check loss, monotone non-increasing from the ordinary least
// squares start, and the record has to show real descent: the
// tau = 0.9 quantile line is not the mean line, and the check loss at
// the optimum must sit measurably below the start.
func TestQuantileObjectiveMonotone(t *testing.T) {
design, y := quantileHeteroFixture(t, 400, 23)
fit, err := QuantileRegression(design, y, 0.9)
if err != nil {
t.Fatalf("QuantileRegression: %v", err)
}
if !fit.Converged {
t.Fatalf("the interior-point run did not converge")
}
if len(fit.Objective) < 3 {
t.Fatalf("the run recorded only %d objectives, too few to show a descent", len(fit.Objective))
}
for k := 1; k < len(fit.Objective); k++ {
if fit.Objective[k] > fit.Objective[k-1] {
t.Fatalf("the objective rose at record %d: %.12g after %.12g",
k, fit.Objective[k], fit.Objective[k-1])
}
}
if math.Abs(fit.Objective[len(fit.Objective)-1]-fit.CheckLoss) > 1e-9 {
t.Fatalf("the record ends at %.12g but the fit reports %.12g",
fit.Objective[len(fit.Objective)-1], fit.CheckLoss)
}
t.Logf("check loss descended from %.6f to %.6f over %d iterations",
fit.Objective[0], fit.CheckLoss, fit.Iterations)
if fit.Objective[0]-fit.CheckLoss < 1 {
t.Fatalf("the tau = 0.9 fit barely left the mean fit: descent %.6g", fit.Objective[0]-fit.CheckLoss)
}
}
// TestQuantileTracksConditionalQuantile measures the fit where it
// matters: on six hundred held-out draws of the heteroscedastic model,
// the tau = 0.9 line must cover ninety percent of the responses and
// the tau = 0.1 line ten percent, the defining property of a
// conditional quantile estimate.
func TestQuantileTracksConditionalQuantile(t *testing.T) {
design, y := quantileHeteroFixture(t, 400, 23)
fit90, err := QuantileRegression(design, y, 0.9)
if err != nil {
t.Fatalf("QuantileRegression tau 0.9: %v", err)
}
fit10, err := QuantileRegression(design, y, 0.1)
if err != nil {
t.Fatalf("QuantileRegression tau 0.1: %v", err)
}
if !fit90.Converged || !fit10.Converged {
t.Fatalf("a coverage fit did not converge")
}
// The fitted slopes track the true conditional quantile slope 2.
if math.Abs(fit90.Coefficients[1]-2) > 0.5 {
t.Fatalf("the tau = 0.9 slope is %.4f, far from the truth 2", fit90.Coefficients[1])
}
g := core.NewGenerator(97)
const held = 600
below90, below10 := 0, 0
for range held {
x := 4*g.Unit() - 2
yi := 1 + 2*x + (0.3+0.3*math.Abs(x))*g.NormalUnit()
q90 := fit90.Coefficients[0] + fit90.Coefficients[1]*x
q10 := fit10.Coefficients[0] + fit10.Coefficients[1]*x
if yi <= q90 {
below90++
}
if yi <= q10 {
below10++
}
}
coverage90 := float64(below90) / held
coverage10 := float64(below10) / held
t.Logf("held-out coverage: tau = 0.9 covers %.3f, tau = 0.1 covers %.3f", coverage90, coverage10)
if coverage90 < 0.84 || coverage90 > 0.96 {
t.Fatalf("the tau = 0.9 fit covers %.3f of held-out draws, want near 0.9", coverage90)
}
if coverage10 < 0.04 || coverage10 > 0.16 {
t.Fatalf("the tau = 0.1 fit covers %.3f of held-out draws, want near 0.1", coverage10)
}
}
// TestQuantileExactFit walks the zero-loss corner: a constant response
// is reproduced exactly by the start, the check loss is zero, and the
// run is settled before the first interior-point iteration.
func TestQuantileExactFit(t *testing.T) {
design := mustFromFloats(t, []float64{1, 0, 1, 1, 1, 2, 1, 3, 1, 4}, 5, 2)
y := mustFromFloats(t, []float64{4.2, 4.2, 4.2, 4.2, 4.2}, 5)
res, err := QuantileRegression(design, y, 0.3)
if err != nil {
t.Fatalf("QuantileRegression: %v", err)
}
if !res.Converged {
t.Fatalf("the exact fit did not report convergence")
}
if res.CheckLoss != 0 {
t.Fatalf("the exact fit reports check loss %g, want 0", res.CheckLoss)
}
if res.Iterations != 0 {
t.Fatalf("the exact fit spent %d iterations, want 0", res.Iterations)
}
for i := range 5 {
if math.Abs(res.Fitted[i]-4.2) > 1e-9 || math.Abs(res.Residuals[i]) > 1e-9 {
t.Fatalf("the exact fit moved row %d: fitted %.12g", i, res.Fitted[i])
}
}
}
// TestQuantileOnIntegerArrays exercises the widening accessor's
// fallback paths in the loss and the recovery: an integer design and
// response reach the fit through FloatAt rather than a raw float
// payload.
func TestQuantileOnIntegerArrays(t *testing.T) {
design := mustFromInts(t, []int64{1, 0, 1, 1, 1, 2, 1, 3, 1, 4, 1, 5, 1, 6}, 7, 2)
y := mustFromInts(t, []int64{2, 3, 4, 5, 6, 7, 8}, 7)
res, err := QuantileRegression(design, y, 0.75)
if err != nil {
t.Fatalf("QuantileRegression on integer input: %v", err)
}
if !res.Converged {
t.Fatalf("the integer-input fit did not converge")
}
grad, free := quantileSubgradient(t, design, y, res.Coefficients, 0.75)
for j := range 2 {
if math.Abs(grad[j]) > free[j]+1e-6 {
t.Fatalf("column %d fails the subgradient certificate: %.3g against slack %.3g", j, grad[j], free[j])
}
}
}
// TestQuantileValidation refuses the malformed inputs: tau outside the
// open interval, wrong shapes, non-finite samples and a rank-deficient
// design.
func TestQuantileValidation(t *testing.T) {
design := mustFromFloats(t, []float64{1, 0, 1, 1, 1, 2, 1, 3, 1, 4, 1, 5}, 6, 2)
y := mustFromFloats(t, []float64{1, 3, 2, 5, 4, 7}, 6)
for _, tau := range []float64{0, 1, -0.5, 1.5, math.NaN()} {
if _, err := QuantileRegression(design, y, tau); err == nil || !strings.Contains(err.Error(), "strictly inside") {
t.Fatalf("tau = %g: got %v, want the tau refusal", tau, err)
}
}
if _, err := QuantileRegression(mustFromFloats(t, []float64{1, 2, 3, 4, 5, 6}, 6), y, 0.5); err == nil || !strings.Contains(err.Error(), "must be rank 2") {
t.Fatalf("a rank 1 design: got %v, want the rank refusal", err)
}
if _, err := QuantileRegression(design, mustFromFloats(t, []float64{1, 2, 3, 4, 5, 6}, 3, 2), 0.5); err == nil || !strings.Contains(err.Error(), "must be rank 1") {
t.Fatalf("a rank 2 response: got %v, want the rank refusal", err)
}
if _, err := QuantileRegression(design, mustFromFloats(t, []float64{1, 2, 3}, 3), 0.5); err == nil || !strings.Contains(err.Error(), "rows but the response") {
t.Fatalf("a length mismatch: got %v, want the length refusal", err)
}
if _, err := QuantileRegression(mustFromFloats(t, []float64{1, 0, 1, 1}, 2, 2), mustFromFloats(t, []float64{1, 2}, 2), 0.5); err == nil || !strings.Contains(err.Error(), "n > p") {
t.Fatalf("n = p: got %v, want the n > p refusal", err)
}
if _, err := QuantileRegression(design, mustFromFloats(t, []float64{1, 2, 3, math.NaN(), 5, 7}, 6), 0.5); err == nil || !strings.Contains(err.Error(), "non-finite") {
t.Fatalf("a non-finite response: got %v, want the non-finite refusal", err)
}
if _, err := QuantileRegression(core.New(core.Complex, 6, 2), y, 0.5); err == nil || !strings.Contains(err.Error(), "complex") {
t.Fatalf("complex input: got %v, want the complex refusal", err)
}
singular := mustFromFloats(t, []float64{1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3}, 4, 3)
if _, err := QuantileRegression(singular, mustFromFloats(t, []float64{1, 2, 3, 4}, 4), 0.5); err == nil || !strings.Contains(err.Error(), "singular") {
t.Fatalf("a rank-deficient design: got %v, want the singular-system refusal", err)
}
}