639 lines
18 KiB
Go
639 lines
18 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
|
|
// SPDX-License-Identifier: MIT
|
|||
|
|
|
|||
|
|
package stats
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"fmt"
|
|||
|
|
"math"
|
|||
|
|
"testing"
|
|||
|
|
|
|||
|
|
"sourcedock.dev/petrbalvin/tensor/internal/base"
|
|||
|
|
"sourcedock.dev/petrbalvin/tensor/internal/core"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// The randomised and cross-checked tests that guard the pair counting of
|
|||
|
|
// KendallTau, the covariance diagonals of the regression entries and the
|
|||
|
|
// pairwise-slope walk of Theil-Sen against the implementations they
|
|||
|
|
// replace, followed by the benchmarks of those paths.
|
|||
|
|
|
|||
|
|
// randSource is a deterministic linear congruential generator for the
|
|||
|
|
// randomised checks: a fixed recurrence makes every failing case
|
|||
|
|
// reproducible from its seed alone.
|
|||
|
|
type randSource struct{ state uint64 }
|
|||
|
|
|
|||
|
|
// next returns the next draw in [0, 1).
|
|||
|
|
func (r *randSource) next() float64 {
|
|||
|
|
r.state = r.state*6364136223846793005 + 1442695040888963407
|
|||
|
|
return float64(r.state>>11) / float64(uint64(1)<<53)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// intn returns the next draw in [0, n).
|
|||
|
|
func (r *randSource) intn(n int) int { return int(r.next() * float64(n)) }
|
|||
|
|
|
|||
|
|
// bruteForceTally counts the pairs of two paired samples the way the
|
|||
|
|
// definition reads: every ordered pair of positions, its two differences
|
|||
|
|
// multiplied, the sign of the product deciding the side, and the ties of
|
|||
|
|
// a sample counted over the same pair walk. It is the oracle the
|
|||
|
|
// merge-sort pair counting is checked against.
|
|||
|
|
func bruteForceTally(xs, ys []float64) pairTally {
|
|||
|
|
var tally pairTally
|
|||
|
|
for i := 1; i < len(xs); i++ {
|
|||
|
|
for j := 0; j < i; j++ {
|
|||
|
|
switch prod := (xs[i] - xs[j]) * (ys[i] - ys[j]); {
|
|||
|
|
case prod > 0:
|
|||
|
|
tally.concordant++
|
|||
|
|
case prod < 0:
|
|||
|
|
tally.discordant++
|
|||
|
|
}
|
|||
|
|
if xs[i] == xs[j] {
|
|||
|
|
tally.tiedX++
|
|||
|
|
}
|
|||
|
|
if ys[i] == ys[j] {
|
|||
|
|
tally.tiedY++
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return tally
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// bruteForceTau is the O(n²) τ-b of the definition, the oracle the
|
|||
|
|
// returned correlation is checked against.
|
|||
|
|
func bruteForceTau(xs, ys []float64) float64 {
|
|||
|
|
tally := bruteForceTally(xs, ys)
|
|||
|
|
n := float64(len(xs))
|
|||
|
|
n0 := n * (n - 1) / 2
|
|||
|
|
return (float64(tally.concordant) - float64(tally.discordant)) /
|
|||
|
|
math.Sqrt((n0-float64(tally.tiedX))*(n0-float64(tally.tiedY)))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestKendallTauCountsMatchBruteForce checks the merge-sort pair
|
|||
|
|
// counting against the pair enumeration it replaces. The values are
|
|||
|
|
// drawn from a handful of levels so that ties in either sample are the
|
|||
|
|
// common case, which is where the block scan and the tie accounting
|
|||
|
|
// carry the work; continuous draws, degenerate ranges and a constant
|
|||
|
|
// second sample cover the rest. Both sides are exact integers, so the
|
|||
|
|
// comparison is on the returned bits rather than on a tolerance.
|
|||
|
|
func TestKendallTauCountsMatchBruteForce(t *testing.T) {
|
|||
|
|
src := &randSource{state: 0x9e3779b97f4a7c15}
|
|||
|
|
levels := []float64{-1, 0, 1, 2, 3}
|
|||
|
|
for trial := range 400 {
|
|||
|
|
n := 2 + src.intn(38)
|
|||
|
|
xs := make([]float64, n)
|
|||
|
|
ys := make([]float64, n)
|
|||
|
|
// A third of the draws are continuous, the rest come from the
|
|||
|
|
// levels, so both samples are tie-heavy on average.
|
|||
|
|
draw := func() float64 {
|
|||
|
|
if src.intn(3) == 0 {
|
|||
|
|
return src.next()*4 - 2
|
|||
|
|
}
|
|||
|
|
return levels[src.intn(len(levels))]
|
|||
|
|
}
|
|||
|
|
for i := range n {
|
|||
|
|
xs[i] = draw()
|
|||
|
|
ys[i] = draw()
|
|||
|
|
}
|
|||
|
|
if trial%4 == 0 {
|
|||
|
|
// The pairing is perfectly monotone, its τ exactly 1.
|
|||
|
|
ys = append([]float64(nil), xs...)
|
|||
|
|
}
|
|||
|
|
if trial%9 == 0 {
|
|||
|
|
// A constant chunk inside otherwise varying samples: the
|
|||
|
|
// widest blocks of equal values the scan has to handle.
|
|||
|
|
for i := n / 3; i < 2*n/3; i++ {
|
|||
|
|
xs[i] = xs[0]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
want := bruteForceTally(xs, ys)
|
|||
|
|
got := tallyPairs(append([]float64(nil), xs...), append([]float64(nil), ys...))
|
|||
|
|
if got != want {
|
|||
|
|
t.Fatalf("trial %d, n %d: pair counts %+v, want %+v\nx = %v\ny = %v", trial, n, got, want, xs, ys)
|
|||
|
|
}
|
|||
|
|
if want.tiedX == int64(n*(n-1)/2) || want.tiedY == int64(n*(n-1)/2) {
|
|||
|
|
continue // a constant sample has no τ-b to compare
|
|||
|
|
}
|
|||
|
|
gotTau, err := KendallTau(mustFloats(t, xs), mustFloats(t, ys))
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("trial %d, n %d: KendallTau: %v", trial, n, err)
|
|||
|
|
}
|
|||
|
|
wantTau := bruteForceTau(xs, ys)
|
|||
|
|
if gotTau != wantTau {
|
|||
|
|
t.Fatalf("trial %d, n %d: τ = %v (%#x), want %v (%#x)\nx = %v\ny = %v",
|
|||
|
|
trial, n, gotTau, math.Float64bits(gotTau), wantTau, math.Float64bits(wantTau), xs, ys)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// invertDiagonal returns the diagonal of the inverse of a dense matrix
|
|||
|
|
// by Gauss-Jordan elimination with partial pivoting: an independent
|
|||
|
|
// route to the covariance diagonals the fits read through the shared LU
|
|||
|
|
// solve. It is accurate to rounding, which is all the cross-checks below
|
|||
|
|
// need from it.
|
|||
|
|
func invertDiagonal(m [][]float64) []float64 {
|
|||
|
|
p := len(m)
|
|||
|
|
aug := make([][]float64, p)
|
|||
|
|
for i := range p {
|
|||
|
|
aug[i] = make([]float64, 2*p)
|
|||
|
|
copy(aug[i], m[i])
|
|||
|
|
aug[i][p+i] = 1
|
|||
|
|
}
|
|||
|
|
for k := range p {
|
|||
|
|
piv := k
|
|||
|
|
for i := k + 1; i < p; i++ {
|
|||
|
|
if math.Abs(aug[i][k]) > math.Abs(aug[piv][k]) {
|
|||
|
|
piv = i
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
aug[k], aug[piv] = aug[piv], aug[k]
|
|||
|
|
d := aug[k][k]
|
|||
|
|
for j := range 2 * p {
|
|||
|
|
aug[k][j] /= d
|
|||
|
|
}
|
|||
|
|
for i := range p {
|
|||
|
|
if i == k {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
f := aug[i][k]
|
|||
|
|
if f == 0 {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
for j := range 2 * p {
|
|||
|
|
aug[i][j] -= f * aug[k][j]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
diag := make([]float64, p)
|
|||
|
|
for i := range p {
|
|||
|
|
diag[i] = aug[i][p+i]
|
|||
|
|
}
|
|||
|
|
return diag
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// copyMatrix copies a p×p system, the shape SolveSystem factors in place.
|
|||
|
|
func copyMatrix(m [][]float64) [][]float64 {
|
|||
|
|
out := make([][]float64, len(m))
|
|||
|
|
for i, row := range m {
|
|||
|
|
out[i] = append([]float64(nil), row...)
|
|||
|
|
}
|
|||
|
|
return out
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestSolveSystemUnitColumnsMatchPerColumnSolves checks the shared
|
|||
|
|
// factorisation the covariance diagonals read against the
|
|||
|
|
// per-coefficient solves it replaced: the same normal-equations matrix,
|
|||
|
|
// once against all p unit columns and once against each column alone, on
|
|||
|
|
// a positive-definite system built from a deterministic design. The
|
|||
|
|
// diagonal entries are compared bit for bit.
|
|||
|
|
func TestSolveSystemUnitColumnsMatchPerColumnSolves(t *testing.T) {
|
|||
|
|
const p = 9
|
|||
|
|
src := &randSource{state: 0xda3e39cb94b95bdb}
|
|||
|
|
m := make([][]float64, p)
|
|||
|
|
for i := range p {
|
|||
|
|
m[i] = make([]float64, p)
|
|||
|
|
}
|
|||
|
|
row := make([]float64, p)
|
|||
|
|
for range 40 {
|
|||
|
|
row[0] = 1
|
|||
|
|
for c := 1; c < p; c++ {
|
|||
|
|
row[c] = src.next() - 0.5
|
|||
|
|
}
|
|||
|
|
for i := range p {
|
|||
|
|
for j := range p {
|
|||
|
|
m[i][j] += row[i] * row[j]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
units := make([][]float64, p)
|
|||
|
|
for j := range p {
|
|||
|
|
units[j] = make([]float64, p)
|
|||
|
|
units[j][j] = 1
|
|||
|
|
}
|
|||
|
|
together, err := base.SolveSystem("test", copyMatrix(m), units)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatal(err)
|
|||
|
|
}
|
|||
|
|
for j := range p {
|
|||
|
|
e := make([]float64, p)
|
|||
|
|
e[j] = 1
|
|||
|
|
alone, err := base.SolveSystem("test", copyMatrix(m), [][]float64{e})
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatal(err)
|
|||
|
|
}
|
|||
|
|
if got, want := together[j][j], alone[0][j]; got != want {
|
|||
|
|
t.Fatalf("diagonal %d of the batched solve = %v (%#x), want %v (%#x) from the single-column solve",
|
|||
|
|
j, got, math.Float64bits(got), want, math.Float64bits(want))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestLogisticRegressionWaldStandardErrors rebuilds the Fisher
|
|||
|
|
// information at the reported fit from the reported probabilities and
|
|||
|
|
// inverts it by an independent elimination: the Wald standard errors are
|
|||
|
|
// that inverse's diagonal, so the check covers the shared factorisation
|
|||
|
|
// the entry now reads them through on a design no digest pins.
|
|||
|
|
func TestLogisticRegressionWaldStandardErrors(t *testing.T) {
|
|||
|
|
const n, p = 300, 6
|
|||
|
|
src := &randSource{state: 0x2545f4914f6cdd1d}
|
|||
|
|
design := make([]float64, n*p)
|
|||
|
|
response := make([]float64, n)
|
|||
|
|
for r := range n {
|
|||
|
|
eta := 0.3
|
|||
|
|
for c := range p {
|
|||
|
|
v := src.next() - 0.5
|
|||
|
|
design[r*p+c] = v
|
|||
|
|
eta += 0.8 * v
|
|||
|
|
}
|
|||
|
|
design[r*p] = 1
|
|||
|
|
if src.next() < 1/(1+math.Exp(-eta)) {
|
|||
|
|
response[r] = 1
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
res, err := LogisticRegression(mustFloats(t, design, n, p), mustFloats(t, response, n))
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatal(err)
|
|||
|
|
}
|
|||
|
|
fisher := make([][]float64, p)
|
|||
|
|
for i := range p {
|
|||
|
|
fisher[i] = make([]float64, p)
|
|||
|
|
}
|
|||
|
|
for r := range n {
|
|||
|
|
w := res.Fitted[r] * (1 - res.Fitted[r])
|
|||
|
|
for i := range p {
|
|||
|
|
for j := range p {
|
|||
|
|
fisher[i][j] += design[r*p+i] * design[r*p+j] * w
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
diag := invertDiagonal(fisher)
|
|||
|
|
for j := range p {
|
|||
|
|
want := math.Sqrt(diag[j])
|
|||
|
|
if rel := math.Abs(res.StandardErrors[j]-want) / want; rel > 1e-9 {
|
|||
|
|
t.Fatalf("standard error %d = %.15g, want %.15g (relative %.3g)",
|
|||
|
|
j, res.StandardErrors[j], want, rel)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestHuberRegressionStandardErrors does the same for the Huber fit:
|
|||
|
|
// σ²·(XᵀWX)⁻¹ with σ the reported robust scale and W the reported final
|
|||
|
|
// weights, its diagonal checked against an independent elimination of
|
|||
|
|
// the weighted normal equations the fit writes its own weights into.
|
|||
|
|
func TestHuberRegressionStandardErrors(t *testing.T) {
|
|||
|
|
const n, p = 300, 6
|
|||
|
|
src := &randSource{state: 0x853c49e6748fea9b}
|
|||
|
|
design := make([]float64, n*p)
|
|||
|
|
response := make([]float64, n)
|
|||
|
|
for r := range n {
|
|||
|
|
design[r*p] = 1
|
|||
|
|
response[r] = 1
|
|||
|
|
for c := 1; c < p; c++ {
|
|||
|
|
v := src.next() - 0.5
|
|||
|
|
design[r*p+c] = v
|
|||
|
|
response[r] += float64(c) * 0.5 * v
|
|||
|
|
}
|
|||
|
|
response[r] += 0.2 * (src.next() - 0.5)
|
|||
|
|
}
|
|||
|
|
response[7] += 5 // the gross point the robust fit exists for
|
|||
|
|
res, err := HuberRegression(mustFloats(t, design, n, p), mustFloats(t, response, n))
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatal(err)
|
|||
|
|
}
|
|||
|
|
if !(res.Scale > 0) {
|
|||
|
|
t.Fatalf("the robust scale collapsed to %g on a contaminated sample", res.Scale)
|
|||
|
|
}
|
|||
|
|
wxx := make([][]float64, p)
|
|||
|
|
for i := range p {
|
|||
|
|
wxx[i] = make([]float64, p)
|
|||
|
|
}
|
|||
|
|
for r := range n {
|
|||
|
|
w := res.Weights[r]
|
|||
|
|
for i := range p {
|
|||
|
|
for j := range p {
|
|||
|
|
wxx[i][j] += design[r*p+i] * design[r*p+j] * w
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
diag := invertDiagonal(wxx)
|
|||
|
|
for j := range p {
|
|||
|
|
want := math.Sqrt(res.Scale * res.Scale * diag[j])
|
|||
|
|
if rel := math.Abs(res.StandardErrors[j]-want) / want; rel > 1e-9 {
|
|||
|
|
t.Fatalf("standard error %d = %.15g, want %.15g (relative %.3g)",
|
|||
|
|
j, res.StandardErrors[j], want, rel)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestTheilSenParallelPairs checks the pairwise-slope walk against the
|
|||
|
|
// serial enumeration it replaces, element for element and bit for bit,
|
|||
|
|
// on a sample large enough to take the parallel path and with repeated
|
|||
|
|
// predictors, which are the pairs the walk has to skip. The reference
|
|||
|
|
// walk reproduces the original append order, so a slope landing in the
|
|||
|
|
// wrong buffer slot moves the median and fails the test.
|
|||
|
|
func TestTheilSenParallelPairs(t *testing.T) {
|
|||
|
|
const n = 1400
|
|||
|
|
src := &randSource{state: 0xc0ffee123456789a}
|
|||
|
|
xs := make([]float64, n)
|
|||
|
|
ys := make([]float64, n)
|
|||
|
|
for i := range n {
|
|||
|
|
// A tenth of a unit apart, so a tenth of the pairs share a
|
|||
|
|
// predictor and carry no slope.
|
|||
|
|
xs[i] = math.Floor(src.next()*300) / 10
|
|||
|
|
ys[i] = 2*xs[i] + src.next()
|
|||
|
|
}
|
|||
|
|
gotIntercept, gotSlope, err := TheilSenRegression(mustFloats(t, xs), mustFloats(t, ys))
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatal(err)
|
|||
|
|
}
|
|||
|
|
slopes := make([]float64, 0, n*(n-1)/2)
|
|||
|
|
for i := range n {
|
|||
|
|
for j := i + 1; j < n; j++ {
|
|||
|
|
if dx := xs[j] - xs[i]; dx != 0 {
|
|||
|
|
slopes = append(slopes, (ys[j]-ys[i])/dx)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
slope := medianSlice(slopes)
|
|||
|
|
intercepts := make([]float64, n)
|
|||
|
|
for i := range n {
|
|||
|
|
intercepts[i] = ys[i] - slope*xs[i]
|
|||
|
|
}
|
|||
|
|
intercept := medianSlice(intercepts)
|
|||
|
|
if gotSlope != slope || gotIntercept != intercept {
|
|||
|
|
t.Fatalf("Theil-Sen = (%v, %v), want (%v, %v) from the serial walk",
|
|||
|
|
gotIntercept, gotSlope, intercept, slope)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// regDesign builds a deterministic (n, p) design with an intercept column
|
|||
|
|
// and covariates in (−0.5, 0.5), plus the row-major values for the
|
|||
|
|
// cross-checks that rebuild a normal-equations matrix from them.
|
|||
|
|
func regDesign(tb testing.TB, n, p int, seed uint64) (*core.Array, []float64) {
|
|||
|
|
tb.Helper()
|
|||
|
|
src := &randSource{state: seed}
|
|||
|
|
vals := make([]float64, n*p)
|
|||
|
|
for r := range n {
|
|||
|
|
vals[r*p] = 1
|
|||
|
|
for c := 1; c < p; c++ {
|
|||
|
|
vals[r*p+c] = src.next() - 0.5
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
a, err := core.FromFloats(vals, n, p)
|
|||
|
|
if err != nil {
|
|||
|
|
tb.Fatal(err)
|
|||
|
|
}
|
|||
|
|
return a, vals
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// BenchmarkKendallTau measures the rank correlation at a size where the
|
|||
|
|
// pair enumeration it replaced costs milliseconds: 8.4 million pairs,
|
|||
|
|
// counted by the merge sort in a fraction of that.
|
|||
|
|
func BenchmarkKendallTau(b *testing.B) {
|
|||
|
|
const n = 4096
|
|||
|
|
src := &randSource{state: 0x123456789abcdef}
|
|||
|
|
xv := make([]float64, n)
|
|||
|
|
yv := make([]float64, n)
|
|||
|
|
for i := range n {
|
|||
|
|
// Rounded draws, so ties in both samples are part of the walk.
|
|||
|
|
xv[i] = math.Floor(src.next()*512) / 512
|
|||
|
|
yv[i] = math.Floor(src.next()*512) / 512
|
|||
|
|
}
|
|||
|
|
x, err := core.FromFloats(xv, n)
|
|||
|
|
if err != nil {
|
|||
|
|
b.Fatal(err)
|
|||
|
|
}
|
|||
|
|
y, err := core.FromFloats(yv, n)
|
|||
|
|
if err != nil {
|
|||
|
|
b.Fatal(err)
|
|||
|
|
}
|
|||
|
|
b.ReportAllocs()
|
|||
|
|
for b.Loop() {
|
|||
|
|
if _, err := KendallTau(x, y); err != nil {
|
|||
|
|
b.Fatal(err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// BenchmarkLinearRegressionCovariance measures the least-squares fit with
|
|||
|
|
// its inference on a wide design, where the covariance diagonal is a
|
|||
|
|
// visible share of the work: p is large enough that the p solves it needs
|
|||
|
|
// outgrow the normal equations themselves.
|
|||
|
|
func BenchmarkLinearRegressionCovariance(b *testing.B) {
|
|||
|
|
const n, p = 400, 48
|
|||
|
|
x, _ := regDesign(b, n, p, 0x5eed0001)
|
|||
|
|
src := &randSource{state: 0x5eed0002}
|
|||
|
|
yv := make([]float64, n)
|
|||
|
|
for i := range n {
|
|||
|
|
yv[i] = src.next()*2 - 1
|
|||
|
|
}
|
|||
|
|
y, err := core.FromFloats(yv, n)
|
|||
|
|
if err != nil {
|
|||
|
|
b.Fatal(err)
|
|||
|
|
}
|
|||
|
|
b.ReportAllocs()
|
|||
|
|
for b.Loop() {
|
|||
|
|
if _, err := LinearRegression(x, y); err != nil {
|
|||
|
|
b.Fatal(err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// BenchmarkPoissonRegressionInference measures the count fit with its
|
|||
|
|
// Wald inference, the covariance diagonal of the Fisher information
|
|||
|
|
// included.
|
|||
|
|
func BenchmarkPoissonRegressionInference(b *testing.B) {
|
|||
|
|
const n, p = 600, 30
|
|||
|
|
x, vals := regDesign(b, n, p, 0x5eed0003)
|
|||
|
|
src := &randSource{state: 0x5eed0004}
|
|||
|
|
yv := make([]float64, n)
|
|||
|
|
for i := range n {
|
|||
|
|
eta := 0.4
|
|||
|
|
for c := range p {
|
|||
|
|
eta += 0.3 * vals[i*p+c]
|
|||
|
|
}
|
|||
|
|
mu := math.Exp(eta)
|
|||
|
|
yv[i] = math.Round(mu * (0.5 + src.next()))
|
|||
|
|
}
|
|||
|
|
y, err := core.FromFloats(yv, n)
|
|||
|
|
if err != nil {
|
|||
|
|
b.Fatal(err)
|
|||
|
|
}
|
|||
|
|
b.ReportAllocs()
|
|||
|
|
for b.Loop() {
|
|||
|
|
if _, err := PoissonRegression(x, y); err != nil {
|
|||
|
|
b.Fatal(err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// BenchmarkLogisticRegressionInference measures the binary fit with its
|
|||
|
|
// Wald inference, the covariance diagonal of the Fisher information
|
|||
|
|
// included.
|
|||
|
|
func BenchmarkLogisticRegressionInference(b *testing.B) {
|
|||
|
|
const n, p = 1500, 30
|
|||
|
|
x, vals := regDesign(b, n, p, 0x5eed0005)
|
|||
|
|
src := &randSource{state: 0x5eed0006}
|
|||
|
|
yv := make([]float64, n)
|
|||
|
|
for i := range n {
|
|||
|
|
eta := 0.2 + 0.5*vals[i*p]
|
|||
|
|
for c := 1; c < p; c++ {
|
|||
|
|
eta += 0.5 * vals[i*p+c]
|
|||
|
|
}
|
|||
|
|
if src.next() < 1/(1+math.Exp(-eta)) {
|
|||
|
|
yv[i] = 1
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
y, err := core.FromFloats(yv, n)
|
|||
|
|
if err != nil {
|
|||
|
|
b.Fatal(err)
|
|||
|
|
}
|
|||
|
|
b.ReportAllocs()
|
|||
|
|
for b.Loop() {
|
|||
|
|
if _, err := LogisticRegression(x, y); err != nil {
|
|||
|
|
b.Fatal(err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// BenchmarkHuberRegressionInference measures the robust fit with its
|
|||
|
|
// standard errors: the reweighting rounds, their medians, and the
|
|||
|
|
// weighted normal equations read for the covariance diagonal.
|
|||
|
|
func BenchmarkHuberRegressionInference(b *testing.B) {
|
|||
|
|
const n, p = 800, 8
|
|||
|
|
x, vals := regDesign(b, n, p, 0x5eed0007)
|
|||
|
|
src := &randSource{state: 0x5eed0008}
|
|||
|
|
yv := make([]float64, n)
|
|||
|
|
for i := range n {
|
|||
|
|
yv[i] = 1 + 0.5*vals[i*p+1] - 0.3*vals[i*p+2] + 0.2*(src.next()-0.5)
|
|||
|
|
}
|
|||
|
|
yv[11] += 6
|
|||
|
|
y, err := core.FromFloats(yv, n)
|
|||
|
|
if err != nil {
|
|||
|
|
b.Fatal(err)
|
|||
|
|
}
|
|||
|
|
b.ReportAllocs()
|
|||
|
|
for b.Loop() {
|
|||
|
|
if _, err := HuberRegression(x, y); err != nil {
|
|||
|
|
b.Fatal(err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// BenchmarkTheilSenRegression measures the median-of-slopes fit: the
|
|||
|
|
// pairwise walk over the pairs with distinct predictors and the median
|
|||
|
|
// taken over them.
|
|||
|
|
func BenchmarkTheilSenRegression(b *testing.B) {
|
|||
|
|
const n = 512
|
|||
|
|
src := &randSource{state: 0x5eed0009}
|
|||
|
|
xv := make([]float64, n)
|
|||
|
|
yv := make([]float64, n)
|
|||
|
|
for i := range n {
|
|||
|
|
xv[i] = src.next()*10 - 5
|
|||
|
|
yv[i] = 1 + 2*xv[i] + src.next()
|
|||
|
|
}
|
|||
|
|
x, err := core.FromFloats(xv, n)
|
|||
|
|
if err != nil {
|
|||
|
|
b.Fatal(err)
|
|||
|
|
}
|
|||
|
|
y, err := core.FromFloats(yv, n)
|
|||
|
|
if err != nil {
|
|||
|
|
b.Fatal(err)
|
|||
|
|
}
|
|||
|
|
b.ReportAllocs()
|
|||
|
|
for b.Loop() {
|
|||
|
|
if _, _, err := TheilSenRegression(x, y); err != nil {
|
|||
|
|
b.Fatal(err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// BenchmarkLassoPath measures the warm-started regularisation path over
|
|||
|
|
// the documented hundred-lambda grid, on an elastic net mixing that
|
|||
|
|
// exercises the standardised coordinate descent. The design carries no
|
|||
|
|
// intercept column: the penalty standardises every column, and a
|
|||
|
|
// constant one has no slope to report.
|
|||
|
|
func BenchmarkLassoPath(b *testing.B) {
|
|||
|
|
const n, p = 400, 12
|
|||
|
|
src := &randSource{state: 0x5eed000a}
|
|||
|
|
xv := make([]float64, n*p)
|
|||
|
|
yv := make([]float64, n)
|
|||
|
|
for i := range n {
|
|||
|
|
for c := range p {
|
|||
|
|
xv[i*p+c] = src.next() - 0.5
|
|||
|
|
}
|
|||
|
|
yv[i] = 1 + 0.8*xv[i*p+1] - 0.5*xv[i*p+2] + 0.1*(src.next()-0.5)
|
|||
|
|
}
|
|||
|
|
x, err := core.FromFloats(xv, n, p)
|
|||
|
|
if err != nil {
|
|||
|
|
b.Fatal(err)
|
|||
|
|
}
|
|||
|
|
y, err := core.FromFloats(yv, n)
|
|||
|
|
if err != nil {
|
|||
|
|
b.Fatal(err)
|
|||
|
|
}
|
|||
|
|
b.ReportAllocs()
|
|||
|
|
for b.Loop() {
|
|||
|
|
if _, err := LassoPath(x, y, 1); err != nil {
|
|||
|
|
b.Fatal(err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// BenchmarkKendallTauScaling reports the merge-sort pair count against
|
|||
|
|
// the sample size: the linear-ish growth the count replaced the
|
|||
|
|
// quadratic walk with.
|
|||
|
|
func BenchmarkKendallTauScaling(b *testing.B) {
|
|||
|
|
for _, n := range []int{512, 2048, 8192} {
|
|||
|
|
src := &randSource{state: 0x51ed + uint64(n)}
|
|||
|
|
xv := make([]float64, n)
|
|||
|
|
yv := make([]float64, n)
|
|||
|
|
for i := range n {
|
|||
|
|
xv[i] = math.Floor(src.next()*512) / 512
|
|||
|
|
yv[i] = math.Floor(src.next()*512) / 512
|
|||
|
|
}
|
|||
|
|
x, err := core.FromFloats(xv, n)
|
|||
|
|
if err != nil {
|
|||
|
|
b.Fatal(err)
|
|||
|
|
}
|
|||
|
|
y, err := core.FromFloats(yv, n)
|
|||
|
|
if err != nil {
|
|||
|
|
b.Fatal(err)
|
|||
|
|
}
|
|||
|
|
b.Run(fmt.Sprintf("n=%d", n), func(b *testing.B) {
|
|||
|
|
b.ReportAllocs()
|
|||
|
|
for b.Loop() {
|
|||
|
|
if _, err := KendallTau(x, y); err != nil {
|
|||
|
|
b.Fatal(err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// BenchmarkTheilSenRegressionCap runs the fit at the observation cap,
|
|||
|
|
// where the pairwise slope list holds millions of entries and the middle
|
|||
|
|
// order statistic is taken by selection rather than by a full sort.
|
|||
|
|
func BenchmarkTheilSenRegressionCap(b *testing.B) {
|
|||
|
|
const n = TheilSenMaxObservations
|
|||
|
|
src := &randSource{state: 0x5eed1234}
|
|||
|
|
xv := make([]float64, n)
|
|||
|
|
yv := make([]float64, n)
|
|||
|
|
for i := range n {
|
|||
|
|
xv[i] = src.next()*10 - 5
|
|||
|
|
yv[i] = 1 + 2*xv[i] + src.next()
|
|||
|
|
}
|
|||
|
|
x, err := core.FromFloats(xv, n)
|
|||
|
|
if err != nil {
|
|||
|
|
b.Fatal(err)
|
|||
|
|
}
|
|||
|
|
y, err := core.FromFloats(yv, n)
|
|||
|
|
if err != nil {
|
|||
|
|
b.Fatal(err)
|
|||
|
|
}
|
|||
|
|
b.ReportAllocs()
|
|||
|
|
for b.Loop() {
|
|||
|
|
if _, _, err := TheilSenRegression(x, y); err != nil {
|
|||
|
|
b.Fatal(err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|