feat: initial release
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,638 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user