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)
|
||
}
|
||
}
|
||
}
|