mirror of
https://github.com/gonum/gonum.git
synced 2025-09-27 03:26:04 +08:00
2150 lines
54 KiB
Go
2150 lines
54 KiB
Go
// Copyright ©2014 The Gonum Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package stat
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"math/rand/v2"
|
|
"reflect"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"gonum.org/v1/gonum/floats"
|
|
"gonum.org/v1/gonum/floats/scalar"
|
|
"gonum.org/v1/gonum/mat"
|
|
)
|
|
|
|
func ExampleCircularMean() {
|
|
x := []float64{0, 0.25 * math.Pi, 0.75 * math.Pi}
|
|
weights := []float64{1, 2, 2.5}
|
|
cmean := CircularMean(x, weights)
|
|
|
|
fmt.Printf("The circular mean is %.5f.\n", cmean)
|
|
// Output:
|
|
// The circular mean is 1.37037.
|
|
}
|
|
|
|
func TestCircularMean(t *testing.T) {
|
|
for i, test := range []struct {
|
|
x []float64
|
|
wts []float64
|
|
ans float64
|
|
}{
|
|
// Values compared against scipy.
|
|
{
|
|
x: []float64{0, 2 * math.Pi},
|
|
ans: 0,
|
|
},
|
|
{
|
|
x: []float64{0, 0.5 * math.Pi},
|
|
ans: 0.78539816339744,
|
|
},
|
|
{
|
|
x: []float64{-1.5 * math.Pi, 0.5 * math.Pi, 2.5 * math.Pi},
|
|
wts: []float64{1, 2, 3},
|
|
ans: 0.5 * math.Pi,
|
|
},
|
|
{
|
|
x: []float64{0, 0.5 * math.Pi},
|
|
wts: []float64{1, 2},
|
|
ans: 1.10714871779409,
|
|
},
|
|
} {
|
|
c := CircularMean(test.x, test.wts)
|
|
if math.Abs(c-test.ans) > 1e-14 {
|
|
t.Errorf("Circular mean mismatch case %d: Expected %v, Found %v", i, test.ans, c)
|
|
}
|
|
}
|
|
if !panics(func() { CircularMean(make([]float64, 3), make([]float64, 2)) }) {
|
|
t.Errorf("CircularMean did not panic with x, wts length mismatch")
|
|
}
|
|
}
|
|
|
|
func ExampleCorrelation() {
|
|
x := []float64{8, -3, 7, 8, -4}
|
|
y := []float64{10, 5, 6, 3, -1}
|
|
w := []float64{2, 1.5, 3, 3, 2}
|
|
|
|
fmt.Println("Correlation computes the degree to which two datasets move together")
|
|
fmt.Println("about their mean. For example, x and y above move similarly.")
|
|
|
|
c := Correlation(x, y, w)
|
|
fmt.Printf("Correlation is %.5f\n", c)
|
|
|
|
// Output:
|
|
// Correlation computes the degree to which two datasets move together
|
|
// about their mean. For example, x and y above move similarly.
|
|
// Correlation is 0.59915
|
|
}
|
|
|
|
func TestCorrelation(t *testing.T) {
|
|
for i, test := range []struct {
|
|
x []float64
|
|
y []float64
|
|
w []float64
|
|
ans float64
|
|
}{
|
|
{
|
|
x: []float64{8, -3, 7, 8, -4},
|
|
y: []float64{8, -3, 7, 8, -4},
|
|
w: nil,
|
|
ans: 1,
|
|
},
|
|
{
|
|
x: []float64{8, -3, 7, 8, -4},
|
|
y: []float64{8, -3, 7, 8, -4},
|
|
w: []float64{1, 1, 1, 1, 1},
|
|
ans: 1,
|
|
},
|
|
{
|
|
x: []float64{8, -3, 7, 8, -4},
|
|
y: []float64{8, -3, 7, 8, -4},
|
|
w: []float64{1, 6, 7, 0.8, 2.1},
|
|
ans: 1,
|
|
},
|
|
{
|
|
x: []float64{8, -3, 7, 8, -4},
|
|
y: []float64{10, 15, 4, 5, -1},
|
|
w: nil,
|
|
ans: 0.0093334660769059,
|
|
},
|
|
{
|
|
x: []float64{8, -3, 7, 8, -4},
|
|
y: []float64{10, 15, 4, 5, -1},
|
|
w: nil,
|
|
ans: 0.0093334660769059,
|
|
},
|
|
{
|
|
x: []float64{8, -3, 7, 8, -4},
|
|
y: []float64{10, 15, 4, 5, -1},
|
|
w: []float64{1, 3, 1, 2, 2},
|
|
ans: -0.13966633352689,
|
|
},
|
|
} {
|
|
c := Correlation(test.x, test.y, test.w)
|
|
if math.Abs(test.ans-c) > 1e-14 {
|
|
t.Errorf("Correlation mismatch case %d. Expected %v, Found %v", i, test.ans, c)
|
|
}
|
|
}
|
|
if !panics(func() { Correlation(make([]float64, 2), make([]float64, 3), make([]float64, 3)) }) {
|
|
t.Errorf("Correlation did not panic with length mismatch")
|
|
}
|
|
if !panics(func() { Correlation(make([]float64, 2), make([]float64, 3), nil) }) {
|
|
t.Errorf("Correlation did not panic with length mismatch")
|
|
}
|
|
if !panics(func() { Correlation(make([]float64, 3), make([]float64, 3), make([]float64, 2)) }) {
|
|
t.Errorf("Correlation did not panic with weights length mismatch")
|
|
}
|
|
}
|
|
|
|
func ExampleKendall() {
|
|
x := []float64{8, -3, 7, 8, -4}
|
|
y := []float64{10, 5, 6, 3, -1}
|
|
w := []float64{2, 1.5, 3, 3, 2}
|
|
|
|
fmt.Println("Kendall correlation computes the number of ordered pairs")
|
|
fmt.Println("between two datasets.")
|
|
|
|
c := Kendall(x, y, w)
|
|
fmt.Printf("Kendall correlation is %.5f\n", c)
|
|
|
|
// Output:
|
|
// Kendall correlation computes the number of ordered pairs
|
|
// between two datasets.
|
|
// Kendall correlation is 0.25000
|
|
}
|
|
|
|
func TestKendall(t *testing.T) {
|
|
for i, test := range []struct {
|
|
x []float64
|
|
y []float64
|
|
weights []float64
|
|
ans float64
|
|
}{
|
|
{
|
|
x: []float64{0, 1, 2, 3},
|
|
y: []float64{0, 1, 2, 3},
|
|
weights: nil,
|
|
ans: 1,
|
|
},
|
|
{
|
|
x: []float64{0, 1},
|
|
y: []float64{1, 0},
|
|
weights: nil,
|
|
ans: -1,
|
|
},
|
|
{
|
|
x: []float64{8, -3, 7, 8, -4},
|
|
y: []float64{10, 15, 4, 5, -1},
|
|
weights: nil,
|
|
ans: 0.2,
|
|
},
|
|
{
|
|
x: []float64{8, -3, 7, 8, -4},
|
|
y: []float64{10, 5, 6, 3, -1},
|
|
weights: nil,
|
|
ans: 0.4,
|
|
},
|
|
{
|
|
x: []float64{1, 2, 3, 4, 5},
|
|
y: []float64{2, 3, 4, 5, 6},
|
|
weights: []float64{1, 1, 1, 1, 1},
|
|
ans: 1,
|
|
},
|
|
{
|
|
x: []float64{1, 2, 3, 2, 1},
|
|
y: []float64{2, 3, 2, 1, 0},
|
|
weights: []float64{1, 1, 0, 0, 0},
|
|
ans: 1,
|
|
},
|
|
} {
|
|
c := Kendall(test.x, test.y, test.weights)
|
|
if math.Abs(test.ans-c) > 1e-14 {
|
|
t.Errorf("Correlation mismatch case %d. Expected %v, Found %v", i, test.ans, c)
|
|
}
|
|
}
|
|
if !panics(func() { Kendall(make([]float64, 2), make([]float64, 3), make([]float64, 3)) }) {
|
|
t.Errorf("Kendall did not panic with length mismatch")
|
|
}
|
|
if !panics(func() { Kendall(make([]float64, 2), make([]float64, 3), nil) }) {
|
|
t.Errorf("Kendall did not panic with length mismatch")
|
|
}
|
|
if !panics(func() { Kendall(make([]float64, 3), make([]float64, 3), make([]float64, 2)) }) {
|
|
t.Errorf("Kendall did not panic with weights length mismatch")
|
|
}
|
|
}
|
|
|
|
func ExampleCovariance() {
|
|
fmt.Println("Covariance computes the degree to which datasets move together")
|
|
fmt.Println("about their mean.")
|
|
x := []float64{8, -3, 7, 8, -4}
|
|
y := []float64{10, 2, 2, 4, 1}
|
|
cov := Covariance(x, y, nil)
|
|
fmt.Printf("Cov = %.4f\n", cov)
|
|
fmt.Println("If datasets move perfectly together, the variance equals the covariance")
|
|
y2 := []float64{12, 1, 11, 12, 0}
|
|
cov2 := Covariance(x, y2, nil)
|
|
varX := Variance(x, nil)
|
|
fmt.Printf("Cov2 is %.4f, VarX is %.4f", cov2, varX)
|
|
// Output:
|
|
// Covariance computes the degree to which datasets move together
|
|
// about their mean.
|
|
// Cov = 13.8000
|
|
// If datasets move perfectly together, the variance equals the covariance
|
|
// Cov2 is 37.7000, VarX is 37.7000
|
|
}
|
|
|
|
func TestCovariance(t *testing.T) {
|
|
for i, test := range []struct {
|
|
p []float64
|
|
q []float64
|
|
weights []float64
|
|
ans float64
|
|
}{
|
|
{
|
|
p: []float64{0.75, 0.1, 0.05},
|
|
q: []float64{0.5, 0.25, 0.25},
|
|
ans: 0.05625,
|
|
},
|
|
{
|
|
p: []float64{1, 2, 3},
|
|
q: []float64{2, 4, 6},
|
|
ans: 2,
|
|
},
|
|
{
|
|
p: []float64{1, 2, 3},
|
|
q: []float64{1, 4, 9},
|
|
ans: 4,
|
|
},
|
|
{
|
|
p: []float64{1, 2, 3},
|
|
q: []float64{1, 4, 9},
|
|
weights: []float64{1, 1.5, 1},
|
|
ans: 3.2,
|
|
},
|
|
{
|
|
p: []float64{1, 4, 9},
|
|
q: []float64{1, 4, 9},
|
|
weights: []float64{1, 1.5, 1},
|
|
ans: 13.142857142857146,
|
|
},
|
|
} {
|
|
c := Covariance(test.p, test.q, test.weights)
|
|
if math.Abs(c-test.ans) > 1e-14 {
|
|
t.Errorf("Covariance mismatch case %d: Expected %v, Found %v", i, test.ans, c)
|
|
}
|
|
}
|
|
|
|
// test the panic states
|
|
if !panics(func() { Covariance(make([]float64, 2), make([]float64, 3), nil) }) {
|
|
t.Errorf("Covariance did not panic with x, y length mismatch")
|
|
}
|
|
if !panics(func() { Covariance(make([]float64, 3), make([]float64, 3), make([]float64, 2)) }) {
|
|
t.Errorf("Covariance did not panic with x, weights length mismatch")
|
|
}
|
|
|
|
}
|
|
|
|
func TestCrossEntropy(t *testing.T) {
|
|
for i, test := range []struct {
|
|
p []float64
|
|
q []float64
|
|
ans float64
|
|
}{
|
|
{
|
|
p: []float64{0.75, 0.1, 0.05},
|
|
q: []float64{0.5, 0.25, 0.25},
|
|
ans: 0.7278045395879426,
|
|
},
|
|
{
|
|
p: []float64{0.75, 0.1, 0.05, 0, 0, 0},
|
|
q: []float64{0.5, 0.25, 0.25, 0, 0, 0},
|
|
ans: 0.7278045395879426,
|
|
},
|
|
{
|
|
p: []float64{0.75, 0.1, 0.05, 0, 0, 0.1},
|
|
q: []float64{0.5, 0.25, 0.25, 0, 0, 0},
|
|
ans: math.Inf(1),
|
|
},
|
|
{
|
|
p: nil,
|
|
q: nil,
|
|
ans: 0,
|
|
},
|
|
} {
|
|
c := CrossEntropy(test.p, test.q)
|
|
if math.Abs(c-test.ans) > 1e-14 {
|
|
t.Errorf("Cross entropy mismatch case %d: Expected %v, Found %v", i, test.ans, c)
|
|
}
|
|
}
|
|
if !panics(func() { CrossEntropy(make([]float64, 3), make([]float64, 2)) }) {
|
|
t.Errorf("CrossEntropy did not panic with p, q length mismatch")
|
|
}
|
|
}
|
|
|
|
func ExampleEntropy() {
|
|
|
|
p := []float64{0.05, 0.1, 0.9, 0.05}
|
|
entP := Entropy(p)
|
|
|
|
q := []float64{0.2, 0.4, 0.25, 0.15}
|
|
entQ := Entropy(q)
|
|
|
|
r := []float64{0.2, 0, 0, 0.5, 0, 0.2, 0.1, 0, 0, 0}
|
|
entR := Entropy(r)
|
|
|
|
s := []float64{0, 0, 1, 0}
|
|
entS := Entropy(s)
|
|
|
|
fmt.Println("Entropy is a measure of the amount of uncertainty in a distribution")
|
|
fmt.Printf("The second bin of p is very likely to occur. It's entropy is %.4f\n", entP)
|
|
fmt.Printf("The distribution of q is more spread out. It's entropy is %.4f\n", entQ)
|
|
fmt.Println("Adding buckets with zero probability does not change the entropy.")
|
|
fmt.Printf("The entropy of r is: %.4f\n", entR)
|
|
fmt.Printf("A distribution with no uncertainty has entropy %.4f\n", entS)
|
|
// Output:
|
|
// Entropy is a measure of the amount of uncertainty in a distribution
|
|
// The second bin of p is very likely to occur. It's entropy is 0.6247
|
|
// The distribution of q is more spread out. It's entropy is 1.3195
|
|
// Adding buckets with zero probability does not change the entropy.
|
|
// The entropy of r is: 1.2206
|
|
// A distribution with no uncertainty has entropy 0.0000
|
|
}
|
|
|
|
func ExampleExKurtosis() {
|
|
fmt.Println(`Kurtosis is a measure of the 'peakedness' of a distribution, and the
|
|
excess kurtosis is the kurtosis above or below that of the standard normal
|
|
distribution`)
|
|
x := []float64{5, 4, -3, -2}
|
|
kurt := ExKurtosis(x, nil)
|
|
fmt.Printf("ExKurtosis = %.5f\n", kurt)
|
|
weights := []float64{1, 2, 3, 5}
|
|
wKurt := ExKurtosis(x, weights)
|
|
fmt.Printf("Weighted ExKurtosis is %.4f", wKurt)
|
|
// Output:
|
|
// Kurtosis is a measure of the 'peakedness' of a distribution, and the
|
|
// excess kurtosis is the kurtosis above or below that of the standard normal
|
|
// distribution
|
|
// ExKurtosis = -5.41200
|
|
// Weighted ExKurtosis is -0.6779
|
|
}
|
|
|
|
func TestExKurtosis(t *testing.T) {
|
|
// the example does a good job, this just has to cover the panic
|
|
if !panics(func() { ExKurtosis(make([]float64, 3), make([]float64, 2)) }) {
|
|
t.Errorf("ExKurtosis did not panic with x, weights length mismatch")
|
|
}
|
|
}
|
|
|
|
func ExampleGeometricMean() {
|
|
x := []float64{8, 2, 9, 15, 4}
|
|
weights := []float64{2, 2, 6, 7, 1}
|
|
mean := Mean(x, weights)
|
|
gmean := GeometricMean(x, weights)
|
|
|
|
logx := make([]float64, len(x))
|
|
for i, v := range x {
|
|
logx[i] = math.Log(v)
|
|
}
|
|
expMeanLog := math.Exp(Mean(logx, weights))
|
|
fmt.Printf("The arithmetic mean is %.4f, but the geometric mean is %.4f.\n", mean, gmean)
|
|
fmt.Printf("The exponential of the mean of the logs is %.4f\n", expMeanLog)
|
|
// Output:
|
|
// The arithmetic mean is 10.1667, but the geometric mean is 8.7637.
|
|
// The exponential of the mean of the logs is 8.7637
|
|
}
|
|
|
|
func TestGeometricMean(t *testing.T) {
|
|
for i, test := range []struct {
|
|
x []float64
|
|
wts []float64
|
|
ans float64
|
|
}{
|
|
{
|
|
x: []float64{2, 8},
|
|
ans: 4,
|
|
},
|
|
{
|
|
x: []float64{3, 81},
|
|
wts: []float64{2, 1},
|
|
ans: 9,
|
|
},
|
|
} {
|
|
c := GeometricMean(test.x, test.wts)
|
|
if math.Abs(c-test.ans) > 1e-14 {
|
|
t.Errorf("Geometric mean mismatch case %d: Expected %v, Found %v", i, test.ans, c)
|
|
}
|
|
}
|
|
if !panics(func() { GeometricMean(make([]float64, 3), make([]float64, 2)) }) {
|
|
t.Errorf("GeometricMean did not panic with x, wts length mismatch")
|
|
}
|
|
}
|
|
|
|
func ExampleHarmonicMean() {
|
|
x := []float64{8, 2, 9, 15, 4}
|
|
weights := []float64{2, 2, 6, 7, 1}
|
|
mean := Mean(x, weights)
|
|
hmean := HarmonicMean(x, weights)
|
|
|
|
fmt.Printf("The arithmetic mean is %.5f, but the harmonic mean is %.4f.\n", mean, hmean)
|
|
// Output:
|
|
// The arithmetic mean is 10.16667, but the harmonic mean is 6.8354.
|
|
}
|
|
|
|
func TestHarmonicMean(t *testing.T) {
|
|
for i, test := range []struct {
|
|
x []float64
|
|
wts []float64
|
|
ans float64
|
|
}{
|
|
{
|
|
x: []float64{.5, .125},
|
|
ans: .2,
|
|
},
|
|
{
|
|
x: []float64{.5, .125},
|
|
wts: []float64{2, 1},
|
|
ans: .25,
|
|
},
|
|
} {
|
|
c := HarmonicMean(test.x, test.wts)
|
|
if math.Abs(c-test.ans) > 1e-14 {
|
|
t.Errorf("Harmonic mean mismatch case %d: Expected %v, Found %v", i, test.ans, c)
|
|
}
|
|
}
|
|
if !panics(func() { HarmonicMean(make([]float64, 3), make([]float64, 2)) }) {
|
|
t.Errorf("HarmonicMean did not panic with x, wts length mismatch")
|
|
}
|
|
}
|
|
|
|
func TestHistogram(t *testing.T) {
|
|
for i, test := range []struct {
|
|
x []float64
|
|
weights []float64
|
|
dividers []float64
|
|
ans []float64
|
|
}{
|
|
{
|
|
x: []float64{1, 3, 5, 6, 7, 8},
|
|
dividers: []float64{0, 2, 4, 6, 7, 9},
|
|
ans: []float64{1, 1, 1, 1, 2},
|
|
},
|
|
{
|
|
x: []float64{1, 3, 5, 6, 7, 8},
|
|
dividers: []float64{1, 2, 4, 6, 7, 9},
|
|
weights: []float64{1, 2, 1, 1, 1, 2},
|
|
ans: []float64{1, 2, 1, 1, 3},
|
|
},
|
|
{
|
|
x: []float64{1, 8},
|
|
dividers: []float64{0, 2, 4, 6, 7, 9},
|
|
weights: []float64{1, 2},
|
|
ans: []float64{1, 0, 0, 0, 2},
|
|
},
|
|
{
|
|
x: []float64{1, 8},
|
|
dividers: []float64{0, 2, 4, 6, 7, 9},
|
|
ans: []float64{1, 0, 0, 0, 1},
|
|
},
|
|
{
|
|
x: []float64{},
|
|
dividers: []float64{1, 3},
|
|
ans: []float64{0},
|
|
},
|
|
} {
|
|
hist := Histogram(nil, test.dividers, test.x, test.weights)
|
|
if !floats.Equal(hist, test.ans) {
|
|
t.Errorf("Hist mismatch case %d. Expected %v, Found %v", i, test.ans, hist)
|
|
}
|
|
// Test with non-zero values
|
|
Histogram(hist, test.dividers, test.x, test.weights)
|
|
if !floats.Equal(hist, test.ans) {
|
|
t.Errorf("Hist mismatch case %d. Expected %v, Found %v", i, test.ans, hist)
|
|
}
|
|
}
|
|
// panic cases
|
|
for _, test := range []struct {
|
|
name string
|
|
x []float64
|
|
weights []float64
|
|
dividers []float64
|
|
count []float64
|
|
}{
|
|
{
|
|
name: "len(x) != len(weights)",
|
|
x: []float64{1, 3, 5, 6, 7, 8},
|
|
weights: []float64{1, 1, 1, 1},
|
|
},
|
|
{
|
|
name: "len(count) != len(dividers) - 1",
|
|
x: []float64{1, 3, 5, 6, 7, 8},
|
|
dividers: []float64{1, 4, 9},
|
|
count: make([]float64, 6),
|
|
},
|
|
{
|
|
name: "dividers not sorted",
|
|
x: []float64{1, 3, 5, 6, 7, 8},
|
|
dividers: []float64{0, -1, 0},
|
|
},
|
|
{
|
|
name: "x not sorted",
|
|
x: []float64{1, 5, 2, 9, 7, 8},
|
|
dividers: []float64{1, 4, 9},
|
|
},
|
|
{
|
|
name: "fewer than 2 dividers",
|
|
x: []float64{1, 2, 3},
|
|
dividers: []float64{5},
|
|
},
|
|
{
|
|
name: "x too large",
|
|
x: []float64{1, 2, 3},
|
|
dividers: []float64{1, 3},
|
|
},
|
|
{
|
|
name: "x too small",
|
|
x: []float64{1, 2, 3},
|
|
dividers: []float64{2, 3},
|
|
},
|
|
} {
|
|
if !panics(func() { Histogram(test.count, test.dividers, test.x, test.weights) }) {
|
|
t.Errorf("Histogram did not panic when %s", test.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func ExampleHistogram() {
|
|
x := make([]float64, 101)
|
|
for i := range x {
|
|
x[i] = 1.1 * float64(i) // x data ranges from 0 to 110
|
|
}
|
|
dividers := []float64{0, 7, 20, 100, 1000}
|
|
fmt.Println(`Histogram counts the amount of data in the bins specified by
|
|
the dividers. In this data set, there are 7 data points less than 7 (between dividers[0]
|
|
and dividers[1]), 12 data points between 7 and 20 (dividers[1] and dividers[2]),
|
|
and 0 data points above 1000. Since dividers has length 5, there will be 4 bins.`)
|
|
hist := Histogram(nil, dividers, x, nil)
|
|
fmt.Printf("Hist = %v\n", hist)
|
|
|
|
fmt.Println()
|
|
fmt.Println("For ease, the floats Span function can be used to set the dividers")
|
|
nBins := 10
|
|
dividers = make([]float64, nBins+1)
|
|
min := floats.Min(x)
|
|
max := floats.Max(x)
|
|
// Increase the maximum divider so that the maximum value of x is contained
|
|
// within the last bucket.
|
|
max++
|
|
floats.Span(dividers, min, max)
|
|
// Span includes the min and the max. Trim the dividers to create 10 buckets
|
|
hist = Histogram(nil, dividers, x, nil)
|
|
fmt.Printf("Hist = %v\n", hist)
|
|
fmt.Println()
|
|
fmt.Println(`Histogram also works with weighted data, and allows reusing of
|
|
the count field in order to avoid extra garbage`)
|
|
weights := make([]float64, len(x))
|
|
for i := range weights {
|
|
weights[i] = float64(i + 1)
|
|
}
|
|
Histogram(hist, dividers, x, weights)
|
|
fmt.Printf("Weighted Hist = %v\n", hist)
|
|
|
|
// Output:
|
|
// Histogram counts the amount of data in the bins specified by
|
|
// the dividers. In this data set, there are 7 data points less than 7 (between dividers[0]
|
|
// and dividers[1]), 12 data points between 7 and 20 (dividers[1] and dividers[2]),
|
|
// and 0 data points above 1000. Since dividers has length 5, there will be 4 bins.
|
|
// Hist = [7 12 72 10]
|
|
//
|
|
// For ease, the floats Span function can be used to set the dividers
|
|
// Hist = [11 10 10 10 10 10 10 10 10 10]
|
|
//
|
|
// Histogram also works with weighted data, and allows reusing of
|
|
// the count field in order to avoid extra garbage
|
|
// Weighted Hist = [66 165 265 365 465 565 665 765 865 965]
|
|
}
|
|
|
|
func TestJensenShannon(t *testing.T) {
|
|
for i, test := range []struct {
|
|
p []float64
|
|
q []float64
|
|
}{
|
|
{
|
|
p: []float64{0.5, 0.1, 0.3, 0.1},
|
|
q: []float64{0.1, 0.4, 0.25, 0.25},
|
|
},
|
|
{
|
|
p: []float64{0.4, 0.6, 0.0},
|
|
q: []float64{0.2, 0.2, 0.6},
|
|
},
|
|
{
|
|
p: []float64{0.1, 0.1, 0.0, 0.8},
|
|
q: []float64{0.6, 0.3, 0.0, 0.1},
|
|
},
|
|
{
|
|
p: []float64{0.5, 0.1, 0.3, 0.1},
|
|
q: []float64{0.5, 0, 0.25, 0.25},
|
|
},
|
|
{
|
|
p: []float64{0.5, 0.1, 0, 0.4},
|
|
q: []float64{0.1, 0.4, 0.25, 0.25},
|
|
},
|
|
} {
|
|
|
|
m := make([]float64, len(test.p))
|
|
p := test.p
|
|
q := test.q
|
|
floats.Add(m, p)
|
|
floats.Add(m, q)
|
|
floats.Scale(0.5, m)
|
|
|
|
js1 := 0.5*KullbackLeibler(p, m) + 0.5*KullbackLeibler(q, m)
|
|
js2 := JensenShannon(p, q)
|
|
|
|
if math.IsNaN(js2) {
|
|
t.Errorf("In case %v, JS distance is NaN", i)
|
|
}
|
|
|
|
if math.Abs(js1-js2) > 1e-14 {
|
|
t.Errorf("JS mismatch case %v. Expected %v, found %v.", i, js1, js2)
|
|
}
|
|
}
|
|
if !panics(func() { JensenShannon(make([]float64, 3), make([]float64, 2)) }) {
|
|
t.Errorf("JensenShannon did not panic with p, q length mismatch")
|
|
}
|
|
}
|
|
|
|
func TestKolmogorovSmirnov(t *testing.T) {
|
|
for i, test := range []struct {
|
|
x []float64
|
|
xWeights []float64
|
|
y []float64
|
|
yWeights []float64
|
|
dist float64
|
|
}{
|
|
|
|
{
|
|
dist: 0,
|
|
},
|
|
{
|
|
x: []float64{1},
|
|
dist: 1,
|
|
},
|
|
{
|
|
y: []float64{1},
|
|
dist: 1,
|
|
},
|
|
{
|
|
x: []float64{1},
|
|
xWeights: []float64{8},
|
|
dist: 1,
|
|
},
|
|
{
|
|
y: []float64{1},
|
|
yWeights: []float64{8},
|
|
dist: 1,
|
|
},
|
|
{
|
|
x: []float64{1},
|
|
xWeights: []float64{8},
|
|
y: []float64{1},
|
|
yWeights: []float64{8},
|
|
dist: 0,
|
|
},
|
|
{
|
|
x: []float64{1, 1, 1},
|
|
xWeights: []float64{2, 3, 7},
|
|
y: []float64{1},
|
|
yWeights: []float64{8},
|
|
dist: 0,
|
|
},
|
|
{
|
|
x: []float64{1, 1, 1, 1, 1},
|
|
y: []float64{1, 1, 1},
|
|
yWeights: []float64{2, 5, 2},
|
|
dist: 0,
|
|
},
|
|
|
|
{
|
|
x: []float64{1, 2, 3},
|
|
y: []float64{1, 2, 3},
|
|
dist: 0,
|
|
},
|
|
{
|
|
x: []float64{1, 2, 3},
|
|
y: []float64{1, 2, 3},
|
|
yWeights: []float64{1, 1, 1},
|
|
dist: 0,
|
|
},
|
|
|
|
{
|
|
x: []float64{1, 2, 3},
|
|
xWeights: []float64{1, 1, 1},
|
|
y: []float64{1, 2, 3},
|
|
yWeights: []float64{1, 1, 1},
|
|
dist: 0,
|
|
},
|
|
{
|
|
x: []float64{1, 2},
|
|
xWeights: []float64{2, 5},
|
|
y: []float64{1, 1, 2, 2, 2, 2, 2},
|
|
dist: 0,
|
|
},
|
|
{
|
|
x: []float64{1, 1, 2, 2, 2, 2, 2},
|
|
y: []float64{1, 2},
|
|
yWeights: []float64{2, 5},
|
|
dist: 0,
|
|
},
|
|
{
|
|
x: []float64{1, 1, 2, 2, 2},
|
|
xWeights: []float64{0.5, 1.5, 1, 2, 2},
|
|
y: []float64{1, 2},
|
|
yWeights: []float64{2, 5},
|
|
dist: 0,
|
|
},
|
|
{
|
|
x: []float64{1, 2, 3, 4},
|
|
y: []float64{5, 6},
|
|
dist: 1,
|
|
},
|
|
{
|
|
x: []float64{5, 6},
|
|
y: []float64{1, 2, 3, 4},
|
|
dist: 1,
|
|
},
|
|
{
|
|
x: []float64{5, 6},
|
|
xWeights: []float64{8, 7},
|
|
y: []float64{1, 2, 3, 4},
|
|
dist: 1,
|
|
},
|
|
{
|
|
x: []float64{5, 6},
|
|
xWeights: []float64{8, 7},
|
|
y: []float64{1, 2, 3, 4},
|
|
yWeights: []float64{9, 2, 1, 6},
|
|
dist: 1,
|
|
},
|
|
{
|
|
x: []float64{-4, 5, 6},
|
|
xWeights: []float64{0, 8, 7},
|
|
y: []float64{1, 2, 3, 4},
|
|
yWeights: []float64{9, 2, 1, 6},
|
|
dist: 1,
|
|
},
|
|
{
|
|
x: []float64{-4, -2, -2, 5, 6},
|
|
xWeights: []float64{0, 0, 0, 8, 7},
|
|
y: []float64{1, 2, 3, 4},
|
|
yWeights: []float64{9, 2, 1, 6},
|
|
dist: 1,
|
|
},
|
|
{
|
|
x: []float64{1, 2, 3},
|
|
y: []float64{1, 1, 3},
|
|
dist: 1.0 / 3.0,
|
|
},
|
|
{
|
|
x: []float64{1, 2, 3},
|
|
y: []float64{1, 3},
|
|
yWeights: []float64{2, 1},
|
|
dist: 1.0 / 3.0,
|
|
},
|
|
{
|
|
x: []float64{1, 2, 3},
|
|
xWeights: []float64{2, 2, 2},
|
|
y: []float64{1, 3},
|
|
yWeights: []float64{2, 1},
|
|
dist: 1.0 / 3.0,
|
|
},
|
|
{
|
|
x: []float64{2, 3, 4},
|
|
y: []float64{1, 5},
|
|
dist: 1.0 / 2.0,
|
|
},
|
|
{
|
|
x: []float64{1, 2, math.NaN()},
|
|
y: []float64{1, 1, 3},
|
|
dist: math.NaN(),
|
|
},
|
|
{
|
|
x: []float64{1, 2, 3},
|
|
y: []float64{1, 1, math.NaN()},
|
|
dist: math.NaN(),
|
|
},
|
|
} {
|
|
dist := KolmogorovSmirnov(test.x, test.xWeights, test.y, test.yWeights)
|
|
if math.Abs(dist-test.dist) > 1e-14 && !(math.IsNaN(test.dist) && math.IsNaN(dist)) {
|
|
t.Errorf("Distance mismatch case %v: Expected: %v, Found: %v", i, test.dist, dist)
|
|
}
|
|
}
|
|
// panic cases
|
|
for _, test := range []struct {
|
|
name string
|
|
x []float64
|
|
xWeights []float64
|
|
y []float64
|
|
yWeights []float64
|
|
}{
|
|
{
|
|
name: "len(x) != len(xWeights)",
|
|
x: []float64{1, 3, 5, 6, 7, 8},
|
|
xWeights: []float64{1, 1, 1, 1},
|
|
},
|
|
{
|
|
name: "len(y) != len(yWeights)",
|
|
x: []float64{1, 3, 5, 6, 7, 8},
|
|
y: []float64{1, 3, 5, 6, 7, 8},
|
|
yWeights: []float64{1, 1, 1, 1},
|
|
},
|
|
{
|
|
name: "x not sorted",
|
|
x: []float64{10, 3, 5, 6, 7, 8},
|
|
y: []float64{1, 3, 5, 6, 7, 8},
|
|
},
|
|
{
|
|
name: "y not sorted",
|
|
x: []float64{1, 3, 5, 6, 7, 8},
|
|
y: []float64{10, 3, 5, 6, 7, 8},
|
|
},
|
|
} {
|
|
if !panics(func() { KolmogorovSmirnov(test.x, test.xWeights, test.y, test.yWeights) }) {
|
|
t.Errorf("KolmogorovSmirnov did not panic when %s", test.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func ExampleKullbackLeibler() {
|
|
|
|
p := []float64{0.05, 0.1, 0.9, 0.05}
|
|
q := []float64{0.2, 0.4, 0.25, 0.15}
|
|
s := []float64{0, 0, 1, 0}
|
|
|
|
klPQ := KullbackLeibler(p, q)
|
|
klPS := KullbackLeibler(p, s)
|
|
klPP := KullbackLeibler(p, p)
|
|
|
|
fmt.Println("Kullback-Leibler is one measure of the difference between two distributions")
|
|
fmt.Printf("The K-L distance between p and q is %.4f\n", klPQ)
|
|
fmt.Println("It is impossible for s and p to be the same distribution, because")
|
|
fmt.Println("the first bucket has zero probability in s and non-zero in p. Thus,")
|
|
fmt.Printf("the K-L distance between them is %.4f\n", klPS)
|
|
fmt.Printf("The K-L distance between identical distributions is %.4f\n", klPP)
|
|
|
|
// Kullback-Leibler is one measure of the difference between two distributions
|
|
// The K-L distance between p and q is 0.8900
|
|
// It is impossible for s and p to be the same distribution, because
|
|
// the first bucket has zero probability in s and non-zero in p. Thus,
|
|
// the K-L distance between them is +Inf
|
|
// The K-L distance between identical distributions is 0.0000
|
|
}
|
|
|
|
func TestKullbackLeibler(t *testing.T) {
|
|
if !panics(func() { KullbackLeibler(make([]float64, 3), make([]float64, 2)) }) {
|
|
t.Errorf("KullbackLeibler did not panic with p, q length mismatch")
|
|
}
|
|
}
|
|
|
|
var linearRegressionTests = []struct {
|
|
name string
|
|
|
|
x, y []float64
|
|
weights []float64
|
|
origin bool
|
|
|
|
alpha float64
|
|
beta float64
|
|
r float64
|
|
|
|
tol float64
|
|
}{
|
|
{
|
|
name: "faithful",
|
|
|
|
x: faithful.waiting,
|
|
y: faithful.eruptions,
|
|
|
|
// Values calculated by R using lm(eruptions ~ waiting, data=faithful).
|
|
alpha: -1.87402,
|
|
beta: 0.07563,
|
|
r: 0.8114608,
|
|
|
|
tol: 1e-5,
|
|
},
|
|
{
|
|
name: "faithful through origin",
|
|
|
|
x: faithful.waiting,
|
|
y: faithful.eruptions,
|
|
origin: true,
|
|
|
|
// Values calculated by R using lm(eruptions ~ waiting - 1, data=faithful).
|
|
alpha: 0,
|
|
beta: 0.05013,
|
|
r: 0.9726036,
|
|
|
|
tol: 1e-5,
|
|
},
|
|
{
|
|
name: "faithful explicit weights",
|
|
|
|
x: faithful.waiting,
|
|
y: faithful.eruptions,
|
|
weights: func() []float64 {
|
|
w := make([]float64, len(faithful.eruptions))
|
|
for i := range w {
|
|
w[i] = 1
|
|
}
|
|
return w
|
|
}(),
|
|
|
|
// Values calculated by R using lm(eruptions ~ waiting, data=faithful).
|
|
alpha: -1.87402,
|
|
beta: 0.07563,
|
|
r: 0.8114608,
|
|
|
|
tol: 1e-5,
|
|
},
|
|
{
|
|
name: "faithful non-uniform weights",
|
|
|
|
x: faithful.waiting,
|
|
y: faithful.eruptions,
|
|
weights: faithful.waiting, // Just an arbitrary set of non-uniform weights.
|
|
|
|
// Values calculated by R using lm(eruptions ~ waiting, data=faithful, weights=faithful$waiting).
|
|
alpha: -1.79268,
|
|
beta: 0.07452,
|
|
r: 0.7840372,
|
|
|
|
tol: 1e-5,
|
|
},
|
|
}
|
|
|
|
func TestLinearRegression(t *testing.T) {
|
|
for _, test := range linearRegressionTests {
|
|
alpha, beta := LinearRegression(test.x, test.y, test.weights, test.origin)
|
|
var r float64
|
|
if test.origin {
|
|
r = RNoughtSquared(test.x, test.y, test.weights, beta)
|
|
} else {
|
|
r = RSquared(test.x, test.y, test.weights, alpha, beta)
|
|
ests := make([]float64, len(test.y))
|
|
for i, x := range test.x {
|
|
ests[i] = alpha + beta*x
|
|
}
|
|
rvals := RSquaredFrom(ests, test.y, test.weights)
|
|
if r != rvals {
|
|
t.Errorf("%s: RSquared and RSquaredFrom mismatch: %v != %v", test.name, r, rvals)
|
|
}
|
|
}
|
|
if !scalar.EqualWithinAbsOrRel(alpha, test.alpha, test.tol, test.tol) {
|
|
t.Errorf("%s: unexpected alpha estimate: want:%v got:%v", test.name, test.alpha, alpha)
|
|
}
|
|
if !scalar.EqualWithinAbsOrRel(beta, test.beta, test.tol, test.tol) {
|
|
t.Errorf("%s: unexpected beta estimate: want:%v got:%v", test.name, test.beta, beta)
|
|
}
|
|
if !scalar.EqualWithinAbsOrRel(r, test.r, test.tol, test.tol) {
|
|
t.Errorf("%s: unexpected r estimate: want:%v got:%v", test.name, test.r, r)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkLinearRegression(b *testing.B) {
|
|
rnd := rand.New(rand.NewPCG(1, 1))
|
|
slope, offset := 2.0, 3.0
|
|
|
|
maxn := 10000
|
|
xs := make([]float64, maxn)
|
|
ys := make([]float64, maxn)
|
|
weights := make([]float64, maxn)
|
|
for i := range xs {
|
|
x := rnd.Float64()
|
|
xs[i] = x
|
|
ys[i] = slope*x + offset
|
|
weights[i] = rnd.Float64()
|
|
}
|
|
|
|
for _, n := range []int{10, 100, 1000, maxn} {
|
|
for _, weighted := range []bool{true, false} {
|
|
for _, origin := range []bool{true, false} {
|
|
name := "n" + strconv.Itoa(n)
|
|
if weighted {
|
|
name += "wt"
|
|
} else {
|
|
name += "wf"
|
|
}
|
|
if origin {
|
|
name += "ot"
|
|
} else {
|
|
name += "of"
|
|
}
|
|
b.Run(name, func(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
var ws []float64
|
|
if weighted {
|
|
ws = weights[:n]
|
|
}
|
|
LinearRegression(xs[:n], ys[:n], ws, origin)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestChiSquare(t *testing.T) {
|
|
for i, test := range []struct {
|
|
p []float64
|
|
q []float64
|
|
res float64
|
|
}{
|
|
{
|
|
p: []float64{16, 18, 16, 14, 12, 12},
|
|
q: []float64{16, 16, 16, 16, 16, 8},
|
|
res: 3.5,
|
|
},
|
|
{
|
|
p: []float64{16, 18, 16, 14, 12, 12},
|
|
q: []float64{8, 20, 20, 16, 12, 12},
|
|
res: 9.25,
|
|
},
|
|
{
|
|
p: []float64{40, 60, 30, 45},
|
|
q: []float64{50, 50, 50, 50},
|
|
res: 12.5,
|
|
},
|
|
{
|
|
p: []float64{40, 60, 30, 45, 0, 0},
|
|
q: []float64{50, 50, 50, 50, 0, 0},
|
|
res: 12.5,
|
|
},
|
|
} {
|
|
resultpq := ChiSquare(test.p, test.q)
|
|
|
|
if math.Abs(resultpq-test.res) > 1e-10 {
|
|
t.Errorf("ChiSquare distance mismatch in case %d. Expected %v, Found %v", i, test.res, resultpq)
|
|
}
|
|
}
|
|
if !panics(func() { ChiSquare(make([]float64, 2), make([]float64, 3)) }) {
|
|
t.Errorf("ChiSquare did not panic with length mismatch")
|
|
}
|
|
}
|
|
|
|
// panics returns true if the called function panics during evaluation.
|
|
func panics(fun func()) (b bool) {
|
|
defer func() {
|
|
err := recover()
|
|
if err != nil {
|
|
b = true
|
|
}
|
|
}()
|
|
fun()
|
|
return
|
|
}
|
|
|
|
func TestBhattacharyya(t *testing.T) {
|
|
for i, test := range []struct {
|
|
p []float64
|
|
q []float64
|
|
res float64
|
|
}{
|
|
{
|
|
p: []float64{0.5, 0.1, 0.3, 0.1},
|
|
q: []float64{0.1, 0.4, 0.25, 0.25},
|
|
res: 0.15597338718671386,
|
|
},
|
|
{
|
|
p: []float64{0.4, 0.6, 0.0},
|
|
q: []float64{0.2, 0.2, 0.6},
|
|
res: 0.46322207765351153,
|
|
},
|
|
{
|
|
p: []float64{0.1, 0.1, 0.0, 0.8},
|
|
q: []float64{0.6, 0.3, 0.0, 0.1},
|
|
res: 0.3552520032137785,
|
|
},
|
|
} {
|
|
resultpq := Bhattacharyya(test.p, test.q)
|
|
resultqp := Bhattacharyya(test.q, test.p)
|
|
|
|
if math.Abs(resultpq-test.res) > 1e-10 {
|
|
t.Errorf("Bhattacharyya distance mismatch in case %d. Expected %v, Found %v", i, test.res, resultpq)
|
|
}
|
|
if math.Abs(resultpq-resultqp) > 1e-10 {
|
|
t.Errorf("Bhattacharyya distance is asymmetric in case %d.", i)
|
|
}
|
|
}
|
|
// Bhattacharyya should panic if the inputs have different length
|
|
if !panics(func() { Bhattacharyya(make([]float64, 2), make([]float64, 3)) }) {
|
|
t.Errorf("Bhattacharyya did not panic with length mismatch")
|
|
}
|
|
}
|
|
|
|
func TestHellinger(t *testing.T) {
|
|
for i, test := range []struct {
|
|
p []float64
|
|
q []float64
|
|
res float64
|
|
}{
|
|
{
|
|
p: []float64{0.5, 0.1, 0.3, 0.1},
|
|
q: []float64{0.1, 0.4, 0.25, 0.25},
|
|
res: 0.3800237367441919,
|
|
},
|
|
{
|
|
p: []float64{0.4, 0.6, 0.0},
|
|
q: []float64{0.2, 0.2, 0.6},
|
|
res: 0.6088900771170487,
|
|
},
|
|
{
|
|
p: []float64{0.1, 0.1, 0.0, 0.8},
|
|
q: []float64{0.6, 0.3, 0.0, 0.1},
|
|
res: 0.5468118803484205,
|
|
},
|
|
} {
|
|
resultpq := Hellinger(test.p, test.q)
|
|
resultqp := Hellinger(test.q, test.p)
|
|
|
|
if math.Abs(resultpq-test.res) > 1e-10 {
|
|
t.Errorf("Hellinger distance mismatch in case %d. Expected %v, Found %v", i, test.res, resultpq)
|
|
}
|
|
if math.Abs(resultpq-resultqp) > 1e-10 {
|
|
t.Errorf("Hellinger distance is asymmetric in case %d.", i)
|
|
}
|
|
}
|
|
if !panics(func() { Hellinger(make([]float64, 2), make([]float64, 3)) }) {
|
|
t.Errorf("Hellinger did not panic with length mismatch")
|
|
}
|
|
}
|
|
|
|
func ExampleMean() {
|
|
x := []float64{8.2, -6, 5, 7}
|
|
mean := Mean(x, nil)
|
|
fmt.Printf("The mean of the samples is %.4f\n", mean)
|
|
w := []float64{2, 6, 3, 5}
|
|
weightedMean := Mean(x, w)
|
|
fmt.Printf("The weighted mean of the samples is %.4f\n", weightedMean)
|
|
x2 := []float64{8.2, 8.2, -6, -6, -6, -6, -6, -6, 5, 5, 5, 7, 7, 7, 7, 7}
|
|
mean2 := Mean(x2, nil)
|
|
fmt.Printf("The mean of x2 is %.4f\n", mean2)
|
|
fmt.Println("The weights act as if there were more samples of that number")
|
|
// Output:
|
|
// The mean of the samples is 3.5500
|
|
// The weighted mean of the samples is 1.9000
|
|
// The mean of x2 is 1.9000
|
|
// The weights act as if there were more samples of that number
|
|
}
|
|
func TestMean(t *testing.T) {
|
|
if !panics(func() { Mean(make([]float64, 3), make([]float64, 2)) }) {
|
|
t.Errorf("Mean did not panic with x, weights length mismatch")
|
|
}
|
|
}
|
|
|
|
func TestMode(t *testing.T) {
|
|
for i, test := range []struct {
|
|
x []float64
|
|
weights []float64
|
|
ans float64
|
|
count float64
|
|
}{
|
|
{},
|
|
{
|
|
x: []float64{1, 6, 1, 9, -2},
|
|
ans: 1,
|
|
count: 2,
|
|
},
|
|
{
|
|
x: []float64{1, 6, 1, 9, -2},
|
|
weights: []float64{1, 7, 3, 5, 0},
|
|
ans: 6,
|
|
count: 7,
|
|
},
|
|
} {
|
|
m, count := Mode(test.x, test.weights)
|
|
if test.ans != m {
|
|
t.Errorf("Mode mismatch case %d. Expected %v, found %v", i, test.ans, m)
|
|
}
|
|
if test.count != count {
|
|
t.Errorf("Mode count mismatch case %d. Expected %v, found %v", i, test.count, count)
|
|
}
|
|
}
|
|
if !panics(func() { Mode(make([]float64, 3), make([]float64, 2)) }) {
|
|
t.Errorf("Mode did not panic with x, weights length mismatch")
|
|
}
|
|
}
|
|
|
|
func TestMixedMoment(t *testing.T) {
|
|
for i, test := range []struct {
|
|
x, y, weights []float64
|
|
r, s float64
|
|
ans float64
|
|
}{
|
|
{
|
|
x: []float64{10, 2, 1, 8, 5},
|
|
y: []float64{8, 15, 1, 6, 3},
|
|
r: 1,
|
|
s: 1,
|
|
ans: 0.48,
|
|
},
|
|
{
|
|
x: []float64{10, 2, 1, 8, 5},
|
|
y: []float64{8, 15, 1, 6, 3},
|
|
weights: []float64{1, 1, 1, 1, 1},
|
|
r: 1,
|
|
s: 1,
|
|
ans: 0.48,
|
|
},
|
|
{
|
|
x: []float64{10, 2, 1, 8, 5},
|
|
y: []float64{8, 15, 1, 6, 3},
|
|
weights: []float64{2, 3, 0.2, 8, 4},
|
|
r: 1,
|
|
s: 1,
|
|
ans: -4.786371011357490,
|
|
},
|
|
{
|
|
x: []float64{10, 2, 1, 8, 5},
|
|
y: []float64{8, 15, 1, 6, 3},
|
|
weights: []float64{2, 3, 0.2, 8, 4},
|
|
r: 2,
|
|
s: 3,
|
|
ans: 1.598600579313326e+03,
|
|
},
|
|
} {
|
|
m := BivariateMoment(test.r, test.s, test.x, test.y, test.weights)
|
|
if math.Abs(test.ans-m) > 1e-14 {
|
|
t.Errorf("Moment mismatch case %d. Expected %v, found %v", i, test.ans, m)
|
|
}
|
|
}
|
|
if !panics(func() { BivariateMoment(1, 1, make([]float64, 3), make([]float64, 2), nil) }) {
|
|
t.Errorf("Moment did not panic with x, y length mismatch")
|
|
}
|
|
if !panics(func() { BivariateMoment(1, 1, make([]float64, 2), make([]float64, 3), nil) }) {
|
|
t.Errorf("Moment did not panic with x, y length mismatch")
|
|
}
|
|
if !panics(func() { BivariateMoment(1, 1, make([]float64, 2), make([]float64, 2), make([]float64, 3)) }) {
|
|
t.Errorf("Moment did not panic with x, weights length mismatch")
|
|
}
|
|
if !panics(func() { BivariateMoment(1, 1, make([]float64, 2), make([]float64, 2), make([]float64, 1)) }) {
|
|
t.Errorf("Moment did not panic with x, weights length mismatch")
|
|
}
|
|
}
|
|
|
|
func TestMoment(t *testing.T) {
|
|
for i, test := range []struct {
|
|
x []float64
|
|
weights []float64
|
|
moment float64
|
|
ans float64
|
|
}{
|
|
{
|
|
x: []float64{6, 2, 4, 8, 10},
|
|
moment: 5,
|
|
ans: 0,
|
|
},
|
|
{
|
|
x: []float64{6, 2, 4, 8, 10},
|
|
weights: []float64{1, 2, 2, 2, 1},
|
|
moment: 5,
|
|
ans: 121.875,
|
|
},
|
|
} {
|
|
m := Moment(test.moment, test.x, test.weights)
|
|
if math.Abs(test.ans-m) > 1e-14 {
|
|
t.Errorf("Moment mismatch case %d. Expected %v, found %v", i, test.ans, m)
|
|
}
|
|
}
|
|
if !panics(func() { Moment(1, make([]float64, 3), make([]float64, 2)) }) {
|
|
t.Errorf("Moment did not panic with x, weights length mismatch")
|
|
}
|
|
if !panics(func() { Moment(1, make([]float64, 2), make([]float64, 3)) }) {
|
|
t.Errorf("Moment did not panic with x, weights length mismatch")
|
|
}
|
|
}
|
|
|
|
func TestMomentAbout(t *testing.T) {
|
|
for i, test := range []struct {
|
|
x []float64
|
|
weights []float64
|
|
moment float64
|
|
mean float64
|
|
ans float64
|
|
}{
|
|
{
|
|
x: []float64{6, 2, 4, 8, 9},
|
|
mean: 3,
|
|
moment: 5,
|
|
ans: 2.2288e3,
|
|
},
|
|
{
|
|
x: []float64{6, 2, 4, 8, 9},
|
|
weights: []float64{1, 2, 2, 2, 1},
|
|
mean: 3,
|
|
moment: 5,
|
|
ans: 1.783625e3,
|
|
},
|
|
} {
|
|
m := MomentAbout(test.moment, test.x, test.mean, test.weights)
|
|
if math.Abs(test.ans-m) > 1e-14 {
|
|
t.Errorf("MomentAbout mismatch case %d. Expected %v, found %v", i, test.ans, m)
|
|
}
|
|
}
|
|
if !panics(func() { MomentAbout(1, make([]float64, 3), 0, make([]float64, 2)) }) {
|
|
t.Errorf("MomentAbout did not panic with x, weights length mismatch")
|
|
}
|
|
}
|
|
|
|
func TestCDF(t *testing.T) {
|
|
cumulantKinds := []CumulantKind{Empirical}
|
|
for i, test := range []struct {
|
|
q []float64
|
|
x []float64
|
|
weights []float64
|
|
ans [][]float64
|
|
}{
|
|
{},
|
|
{
|
|
q: []float64{0, 0.9, 1, 1.1, 2.9, 3, 3.1, 4.9, 5, 5.1},
|
|
x: []float64{1, 2, 3, 4, 5},
|
|
ans: [][]float64{{0, 0, 0.2, 0.2, 0.4, 0.6, 0.6, 0.8, 1, 1}},
|
|
},
|
|
{
|
|
q: []float64{0, 0.9, 1, 1.1, 2.9, 3, 3.1, 4.9, 5, 5.1},
|
|
x: []float64{1, 2, 3, 4, 5},
|
|
weights: []float64{1, 1, 1, 1, 1},
|
|
ans: [][]float64{{0, 0, 0.2, 0.2, 0.4, 0.6, 0.6, 0.8, 1, 1}},
|
|
},
|
|
{
|
|
q: []float64{0, 0.9, 1},
|
|
x: []float64{math.NaN()},
|
|
ans: [][]float64{{math.NaN(), math.NaN(), math.NaN()}},
|
|
},
|
|
} {
|
|
copyX := make([]float64, len(test.x))
|
|
copy(copyX, test.x)
|
|
var copyW []float64
|
|
if test.weights != nil {
|
|
copyW = make([]float64, len(test.weights))
|
|
copy(copyW, test.weights)
|
|
}
|
|
for j, q := range test.q {
|
|
for k, kind := range cumulantKinds {
|
|
v := CDF(q, kind, test.x, test.weights)
|
|
if !floats.Equal(copyX, test.x) && !math.IsNaN(v) {
|
|
t.Errorf("x changed for case %d kind %d percentile %v", i, k, q)
|
|
}
|
|
if !floats.Equal(copyW, test.weights) {
|
|
t.Errorf("x changed for case %d kind %d percentile %v", i, k, q)
|
|
}
|
|
if v != test.ans[k][j] && !(math.IsNaN(v) && math.IsNaN(test.ans[k][j])) {
|
|
t.Errorf("mismatch case %d kind %d percentile %v. Expected: %v, found: %v", i, k, q, test.ans[k][j], v)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// these test cases should all result in a panic
|
|
for i, test := range []struct {
|
|
name string
|
|
q float64
|
|
kind CumulantKind
|
|
x []float64
|
|
weights []float64
|
|
}{
|
|
{
|
|
name: "x == nil",
|
|
kind: Empirical,
|
|
x: nil,
|
|
},
|
|
{
|
|
name: "len(x) == 0",
|
|
kind: Empirical,
|
|
x: []float64{},
|
|
},
|
|
{
|
|
name: "len(x) != len(weights)",
|
|
q: 1.5,
|
|
kind: Empirical,
|
|
x: []float64{1, 2, 3, 4, 5},
|
|
weights: []float64{1, 2, 3},
|
|
},
|
|
{
|
|
name: "unsorted x",
|
|
q: 1.5,
|
|
kind: Empirical,
|
|
x: []float64{3, 2, 1},
|
|
},
|
|
{
|
|
name: "unknown CumulantKind",
|
|
q: 1.5,
|
|
kind: CumulantKind(1000), // bogus
|
|
x: []float64{1, 2, 3},
|
|
},
|
|
} {
|
|
if !panics(func() { CDF(test.q, test.kind, test.x, test.weights) }) {
|
|
t.Errorf("did not panic as expected with %s for case %d kind %d percentile %v x %v weights %v", test.name, i, test.kind, test.q, test.x, test.weights)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func TestQuantile(t *testing.T) {
|
|
cumulantKinds := []CumulantKind{
|
|
Empirical,
|
|
LinInterp,
|
|
}
|
|
for i, test := range []struct {
|
|
p []float64
|
|
x []float64
|
|
w []float64
|
|
ans [][]float64
|
|
panics bool
|
|
}{
|
|
{
|
|
p: []float64{0, 0.05, 0.1, 0.15, 0.45, 0.5, 0.55, 0.85, 0.9, 0.95, 1},
|
|
x: nil,
|
|
w: nil,
|
|
panics: true,
|
|
},
|
|
{
|
|
p: []float64{0, 0.05, 0.1, 0.15, 0.45, 0.5, 0.55, 0.85, 0.9, 0.95, 1},
|
|
x: []float64{},
|
|
w: nil,
|
|
panics: true,
|
|
},
|
|
{
|
|
p: []float64{0, 0.05, 0.1, 0.15, 0.45, 0.5, 0.55, 0.85, 0.9, 0.95, 1},
|
|
x: []float64{1},
|
|
w: nil,
|
|
ans: [][]float64{
|
|
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
|
|
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
|
|
},
|
|
},
|
|
{
|
|
p: []float64{0, 0.05, 0.1, 0.15, 0.45, 0.5, 0.55, 0.85, 0.9, 0.95, 1},
|
|
x: []float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
|
|
w: nil,
|
|
ans: [][]float64{
|
|
{1, 1, 1, 2, 5, 5, 6, 9, 9, 10, 10},
|
|
{1, 1, 1, 1.5, 4.5, 5, 5.5, 8.5, 9, 9.5, 10},
|
|
},
|
|
},
|
|
{
|
|
p: []float64{0, 0.05, 0.1, 0.15, 0.45, 0.5, 0.55, 0.85, 0.9, 0.95, 1},
|
|
x: []float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
|
|
w: []float64{3, 3, 3, 3, 3, 3, 3, 3, 3, 3},
|
|
ans: [][]float64{
|
|
{1, 1, 1, 2, 5, 5, 6, 9, 9, 10, 10},
|
|
{1, 1, 1, 1.5, 4.5, 5, 5.5, 8.5, 9, 9.5, 10},
|
|
},
|
|
},
|
|
{
|
|
p: []float64{0, 0.05, 0.1, 0.15, 0.45, 0.5, 0.55, 0.85, 0.9, 0.95, 1},
|
|
x: []float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
|
|
w: []float64{0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0},
|
|
ans: [][]float64{
|
|
{1, 2, 3, 4, 7, 7, 8, 10, 10, 10, 10},
|
|
{1, 1.875, 2.833333333333333, 3.5625, 6.535714285714286, 6.928571428571429, 7.281250000000001, 9.175, 9.45, 9.725, 10},
|
|
},
|
|
},
|
|
{
|
|
p: []float64{0.5},
|
|
x: []float64{1, 2, 3, 4, 5, 6, 7, 8, math.NaN(), 10},
|
|
ans: [][]float64{
|
|
{math.NaN()},
|
|
{math.NaN()},
|
|
},
|
|
},
|
|
} {
|
|
copyX := make([]float64, len(test.x))
|
|
copy(copyX, test.x)
|
|
var copyW []float64
|
|
if test.w != nil {
|
|
copyW = make([]float64, len(test.w))
|
|
copy(copyW, test.w)
|
|
}
|
|
for j, p := range test.p {
|
|
for k, kind := range cumulantKinds {
|
|
var v float64
|
|
if test.panics != panics(func() { v = Quantile(p, kind, test.x, test.w) }) {
|
|
t.Errorf("Quantile did not panic when expected: test %d", j)
|
|
}
|
|
if !floats.Same(copyX, test.x) {
|
|
t.Errorf("x changed for case %d kind %d percentile %v", i, k, p)
|
|
}
|
|
if !floats.Same(copyW, test.w) {
|
|
t.Errorf("x changed for case %d kind %d percentile %v", i, k, p)
|
|
}
|
|
if test.panics {
|
|
continue
|
|
}
|
|
if v != test.ans[k][j] && !(math.IsNaN(v) && math.IsNaN(test.ans[k][j])) {
|
|
t.Errorf("mismatch case %d kind %d percentile %v. Expected: %v, found: %v", i, k, p, test.ans[k][j], v)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestQuantileInvalidInput(t *testing.T) {
|
|
cumulantKinds := []CumulantKind{
|
|
Empirical,
|
|
LinInterp,
|
|
}
|
|
for _, test := range []struct {
|
|
name string
|
|
p float64
|
|
x []float64
|
|
w []float64
|
|
}{
|
|
{
|
|
name: "p < 0",
|
|
p: -1,
|
|
},
|
|
{
|
|
name: "p > 1",
|
|
p: 2,
|
|
},
|
|
{
|
|
name: "p is NaN",
|
|
p: math.NaN(),
|
|
},
|
|
{
|
|
name: "len(x) != len(weights)",
|
|
p: .5,
|
|
x: make([]float64, 4),
|
|
w: make([]float64, 2),
|
|
},
|
|
{
|
|
name: "x not sorted",
|
|
p: .5,
|
|
x: []float64{3, 2, 1},
|
|
},
|
|
} {
|
|
for _, kind := range cumulantKinds {
|
|
if !panics(func() { Quantile(test.p, kind, test.x, test.w) }) {
|
|
t.Errorf("Quantile did not panic when %s", test.name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestQuantileInvalidCumulantKind(t *testing.T) {
|
|
if !panics(func() { Quantile(0.5, CumulantKind(1000), []float64{1, 2, 3}, nil) }) {
|
|
t.Errorf("Quantile did not panic when CumulantKind is unknown")
|
|
}
|
|
}
|
|
|
|
func ExampleStdDev() {
|
|
x := []float64{8, 2, -9, 15, 4}
|
|
stdev := StdDev(x, nil)
|
|
fmt.Printf("The standard deviation of the samples is %.4f\n", stdev)
|
|
|
|
weights := []float64{2, 2, 6, 7, 1}
|
|
weightedStdev := StdDev(x, weights)
|
|
fmt.Printf("The weighted standard deviation of the samples is %.4f\n", weightedStdev)
|
|
// Output:
|
|
// The standard deviation of the samples is 8.8034
|
|
// The weighted standard deviation of the samples is 10.5733
|
|
}
|
|
|
|
func ExamplePopStdDev() {
|
|
x := []float64{8, 2, -9, 15, 4}
|
|
stdev := PopStdDev(x, nil)
|
|
fmt.Printf("The standard deviation of the population is %.4f\n", stdev)
|
|
|
|
weights := []float64{2, 2, 6, 7, 1}
|
|
weightedStdev := PopStdDev(x, weights)
|
|
fmt.Printf("The weighted standard deviation of the population is %.4f\n", weightedStdev)
|
|
// Output:
|
|
// The standard deviation of the population is 7.8740
|
|
// The weighted standard deviation of the population is 10.2754
|
|
}
|
|
|
|
func ExampleStdErr() {
|
|
x := []float64{8, 2, -9, 15, 4}
|
|
weights := []float64{2, 2, 6, 7, 1}
|
|
mean := Mean(x, weights)
|
|
stdev := StdDev(x, weights)
|
|
nSamples := floats.Sum(weights)
|
|
stdErr := StdErr(stdev, nSamples)
|
|
fmt.Printf("The standard deviation is %.4f and there are %g samples, so the mean\nis likely %.4f ± %.4f.", stdev, nSamples, mean, stdErr)
|
|
// Output:
|
|
// The standard deviation is 10.5733 and there are 18 samples, so the mean
|
|
// is likely 4.1667 ± 2.4921.
|
|
}
|
|
|
|
func TestSkew(t *testing.T) {
|
|
for i, test := range []struct {
|
|
x []float64
|
|
weights []float64
|
|
ans float64
|
|
}{
|
|
{
|
|
x: []float64{8, 3, 7, 8, 4},
|
|
weights: nil,
|
|
ans: -0.581456499151665,
|
|
},
|
|
{
|
|
x: []float64{8, 3, 7, 8, 4},
|
|
weights: []float64{1, 1, 1, 1, 1},
|
|
ans: -0.581456499151665,
|
|
},
|
|
{
|
|
x: []float64{8, 3, 7, 8, 4},
|
|
weights: []float64{2, 1, 2, 1, 1},
|
|
ans: -1.12066646837198,
|
|
},
|
|
} {
|
|
skew := Skew(test.x, test.weights)
|
|
if math.Abs(skew-test.ans) > 1e-14 {
|
|
t.Errorf("Skew mismatch case %d. Expected %v, Found %v", i, test.ans, skew)
|
|
}
|
|
}
|
|
if !panics(func() { Skew(make([]float64, 3), make([]float64, 2)) }) {
|
|
t.Errorf("Skew did not panic with x, weights length mismatch")
|
|
}
|
|
}
|
|
|
|
func TestSortWeighted(t *testing.T) {
|
|
for i, test := range []struct {
|
|
x []float64
|
|
w []float64
|
|
ansx []float64
|
|
answ []float64
|
|
}{
|
|
{
|
|
x: []float64{8, 3, 7, 8, 4},
|
|
ansx: []float64{3, 4, 7, 8, 8},
|
|
},
|
|
{
|
|
x: []float64{8, 3, 7, 8, 4},
|
|
w: []float64{.5, 1, 1, .5, 1},
|
|
ansx: []float64{3, 4, 7, 8, 8},
|
|
answ: []float64{1, 1, 1, .5, .5},
|
|
},
|
|
} {
|
|
SortWeighted(test.x, test.w)
|
|
if !floats.Same(test.x, test.ansx) {
|
|
t.Errorf("SortWeighted mismatch case %d. Expected x %v, Found x %v", i, test.ansx, test.x)
|
|
}
|
|
if !(test.w == nil) && !floats.Same(test.w, test.answ) {
|
|
t.Errorf("SortWeighted mismatch case %d. Expected w %v, Found w %v", i, test.answ, test.w)
|
|
}
|
|
}
|
|
if !panics(func() { SortWeighted(make([]float64, 3), make([]float64, 2)) }) {
|
|
t.Errorf("SortWeighted did not panic with x, weights length mismatch")
|
|
}
|
|
}
|
|
|
|
func TestSortWeightedLabeled(t *testing.T) {
|
|
for i, test := range []struct {
|
|
x []float64
|
|
l []bool
|
|
w []float64
|
|
ansx []float64
|
|
ansl []bool
|
|
answ []float64
|
|
}{
|
|
{
|
|
x: []float64{8, 3, 7, 8, 4},
|
|
ansx: []float64{3, 4, 7, 8, 8},
|
|
},
|
|
{
|
|
x: []float64{8, 3, 7, 8, 4},
|
|
w: []float64{.5, 1, 1, .5, 1},
|
|
ansx: []float64{3, 4, 7, 8, 8},
|
|
answ: []float64{1, 1, 1, .5, .5},
|
|
},
|
|
{
|
|
x: []float64{8, 3, 7, 8, 4},
|
|
l: []bool{false, false, true, false, true},
|
|
ansx: []float64{3, 4, 7, 8, 8},
|
|
ansl: []bool{false, true, true, false, false},
|
|
},
|
|
{
|
|
x: []float64{8, 3, 7, 8, 4},
|
|
l: []bool{false, false, true, false, true},
|
|
w: []float64{.5, 1, 1, .5, 1},
|
|
ansx: []float64{3, 4, 7, 8, 8},
|
|
ansl: []bool{false, true, true, false, false},
|
|
answ: []float64{1, 1, 1, .5, .5},
|
|
},
|
|
} {
|
|
SortWeightedLabeled(test.x, test.l, test.w)
|
|
if !floats.Same(test.x, test.ansx) {
|
|
t.Errorf("SortWeightedLabelled mismatch case %d. Expected x %v, Found x %v", i, test.ansx, test.x)
|
|
}
|
|
if (test.l != nil) && !reflect.DeepEqual(test.l, test.ansl) {
|
|
t.Errorf("SortWeightedLabelled mismatch case %d. Expected l %v, Found l %v", i, test.ansl, test.l)
|
|
}
|
|
if (test.w != nil) && !floats.Same(test.w, test.answ) {
|
|
t.Errorf("SortWeightedLabelled mismatch case %d. Expected w %v, Found w %v", i, test.answ, test.w)
|
|
}
|
|
}
|
|
if !panics(func() { SortWeightedLabeled(make([]float64, 3), make([]bool, 2), make([]float64, 3)) }) {
|
|
t.Errorf("SortWeighted did not panic with x, labels length mismatch")
|
|
}
|
|
if !panics(func() { SortWeightedLabeled(make([]float64, 3), make([]bool, 2), nil) }) {
|
|
t.Errorf("SortWeighted did not panic with x, labels length mismatch")
|
|
}
|
|
if !panics(func() { SortWeightedLabeled(make([]float64, 3), make([]bool, 3), make([]float64, 2)) }) {
|
|
t.Errorf("SortWeighted did not panic with x, weights length mismatch")
|
|
}
|
|
if !panics(func() { SortWeightedLabeled(make([]float64, 3), nil, make([]float64, 2)) }) {
|
|
t.Errorf("SortWeighted did not panic with x, weights length mismatch")
|
|
}
|
|
}
|
|
|
|
func TestVariance(t *testing.T) {
|
|
for i, test := range []struct {
|
|
x []float64
|
|
weights []float64
|
|
ans float64
|
|
}{
|
|
{
|
|
x: []float64{8, -3, 7, 8, -4},
|
|
weights: nil,
|
|
ans: 37.7,
|
|
},
|
|
{
|
|
x: []float64{8, -3, 7, 8, -4},
|
|
weights: []float64{1, 1, 1, 1, 1},
|
|
ans: 37.7,
|
|
},
|
|
{
|
|
x: []float64{8, 3, 7, 8, 4},
|
|
weights: []float64{2, 1, 2, 1, 1},
|
|
ans: 4.2857142857142865,
|
|
},
|
|
{
|
|
x: []float64{1, 4, 9},
|
|
weights: []float64{1, 1.5, 1},
|
|
ans: 13.142857142857146,
|
|
},
|
|
{
|
|
x: []float64{1, 2, 3},
|
|
weights: []float64{1, 1.5, 1},
|
|
ans: .8,
|
|
},
|
|
} {
|
|
variance := Variance(test.x, test.weights)
|
|
if math.Abs(variance-test.ans) > 1e-14 {
|
|
t.Errorf("Variance mismatch case %d. Expected %v, Found %v", i, test.ans, variance)
|
|
}
|
|
}
|
|
if !panics(func() { Variance(make([]float64, 3), make([]float64, 2)) }) {
|
|
t.Errorf("Variance did not panic with x, weights length mismatch")
|
|
}
|
|
}
|
|
|
|
func TestPopVariance(t *testing.T) {
|
|
for i, test := range []struct {
|
|
x []float64
|
|
weights []float64
|
|
ans float64
|
|
}{
|
|
{
|
|
x: []float64{8, -3, 7, 8, -4},
|
|
weights: nil,
|
|
ans: 30.16,
|
|
},
|
|
{
|
|
x: []float64{8, -3, 7, 8, -4},
|
|
weights: []float64{1, 1, 1, 1, 1},
|
|
ans: 30.16,
|
|
},
|
|
{
|
|
x: []float64{8, 3, 7, 8, 4},
|
|
weights: []float64{2, 1, 2, 1, 1},
|
|
ans: 3.6734693877551026,
|
|
},
|
|
{
|
|
x: []float64{1, 4, 9},
|
|
weights: []float64{1, 1.5, 1},
|
|
ans: 9.387755102040817,
|
|
},
|
|
{
|
|
x: []float64{1, 2, 3},
|
|
weights: []float64{1, 1.5, 1},
|
|
ans: 0.5714285714285714,
|
|
},
|
|
{
|
|
x: []float64{2},
|
|
weights: nil,
|
|
ans: 0,
|
|
},
|
|
{
|
|
x: []float64{2},
|
|
weights: []float64{2},
|
|
ans: 0,
|
|
},
|
|
} {
|
|
variance := PopVariance(test.x, test.weights)
|
|
if math.Abs(variance-test.ans) > 1e-14 {
|
|
t.Errorf("PopVariance mismatch case %d. Expected %v, Found %v", i, test.ans, variance)
|
|
}
|
|
}
|
|
if !panics(func() { PopVariance(make([]float64, 3), make([]float64, 2)) }) {
|
|
t.Errorf("PopVariance did not panic with x, weights length mismatch")
|
|
}
|
|
}
|
|
|
|
func ExampleVariance() {
|
|
x := []float64{8, 2, -9, 15, 4}
|
|
variance := Variance(x, nil)
|
|
fmt.Printf("The variance of the samples is %.4f\n", variance)
|
|
|
|
weights := []float64{2, 2, 6, 7, 1}
|
|
weightedVariance := Variance(x, weights)
|
|
fmt.Printf("The weighted variance of the samples is %.4f\n", weightedVariance)
|
|
// Output:
|
|
// The variance of the samples is 77.5000
|
|
// The weighted variance of the samples is 111.7941
|
|
}
|
|
|
|
func ExamplePopVariance() {
|
|
x := []float64{8, 2, -9, 15, 4}
|
|
variance := PopVariance(x, nil)
|
|
fmt.Printf("The biased variance of the samples is %.4f\n", variance)
|
|
|
|
weights := []float64{2, 2, 6, 7, 1}
|
|
weightedVariance := PopVariance(x, weights)
|
|
fmt.Printf("The weighted biased variance of the samples is %.4f\n", weightedVariance)
|
|
// Output:
|
|
// The biased variance of the samples is 62.0000
|
|
// The weighted biased variance of the samples is 105.5833
|
|
}
|
|
|
|
func TestStdScore(t *testing.T) {
|
|
for i, test := range []struct {
|
|
x float64
|
|
u float64
|
|
s float64
|
|
z float64
|
|
}{
|
|
{
|
|
x: 4,
|
|
u: -6,
|
|
s: 5,
|
|
z: 2,
|
|
},
|
|
{
|
|
x: 1,
|
|
u: 0,
|
|
s: 1,
|
|
z: 1,
|
|
},
|
|
} {
|
|
z := StdScore(test.x, test.u, test.s)
|
|
if math.Abs(z-test.z) > 1e-14 {
|
|
t.Errorf("StdScore mismatch case %d. Expected %v, Found %v", i, test.z, z)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWassersteinDistance(t *testing.T) {
|
|
const tol = 1e-8
|
|
|
|
// sample cases were taken from scipy's documentation
|
|
tests := []struct {
|
|
name string
|
|
p []float64
|
|
q []float64
|
|
pWeights []float64
|
|
qWeights []float64
|
|
want float64
|
|
}{
|
|
{
|
|
name: "Example 1: Basic case with different distributions",
|
|
p: []float64{0, 1, 3},
|
|
q: []float64{5, 6, 8},
|
|
pWeights: nil,
|
|
qWeights: nil,
|
|
want: 5.0,
|
|
},
|
|
{
|
|
name: "Example 2: Same distributions with different weights",
|
|
p: []float64{0, 1},
|
|
q: []float64{0, 1},
|
|
pWeights: []float64{3, 1},
|
|
qWeights: []float64{2, 2},
|
|
want: 0.25,
|
|
},
|
|
{
|
|
name: "Example 3: Complex case with different sizes and weights",
|
|
p: []float64{3.4, 3.9, 7.5, 7.8},
|
|
q: []float64{4.5, 1.4},
|
|
pWeights: []float64{1.4, 0.9, 3.1, 7.2},
|
|
qWeights: []float64{3.2, 3.5},
|
|
want: 4.078133143804786,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
got := WassersteinDistance(test.p, test.q, test.pWeights, test.qWeights)
|
|
if math.Abs(got-test.want) > tol {
|
|
t.Errorf("WassersteinDistance() = %v, want %v", got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWassersteinDistanceEdgeCases tests specific edge cases
|
|
func TestWassersteinDistanceEdgeCases(t *testing.T) {
|
|
// Test empty distribution.
|
|
result := WassersteinDistance([]float64{}, []float64{1.0}, nil, nil)
|
|
|
|
if !math.IsNaN(result) {
|
|
t.Errorf("WassersteinDistance() = %v, want %v", result, math.NaN())
|
|
}
|
|
}
|
|
|
|
// TestWassersteinDistanceWeightNormalization tests that weights are properly normalized
|
|
func TestWassersteinDistanceWeightNormalization(t *testing.T) {
|
|
const tol = 1e-8
|
|
|
|
p := []float64{1.0, 2.0, 3.0}
|
|
q := []float64{2.0, 3.0, 4.0}
|
|
|
|
// Non-normalized weights that sum to different values
|
|
pWeights := []float64{2.0, 3.0, 5.0} // Sum = 10
|
|
qWeights := []float64{1.0, 1.0, 1.0} // Sum = 3
|
|
|
|
// Create normalized copies for reference
|
|
pWeightsNorm := make([]float64, len(pWeights))
|
|
qWeightsNorm := make([]float64, len(qWeights))
|
|
copy(pWeightsNorm, pWeights)
|
|
copy(qWeightsNorm, qWeights)
|
|
floats.Scale(1.0/floats.Sum(pWeightsNorm), pWeightsNorm)
|
|
floats.Scale(1.0/floats.Sum(qWeightsNorm), qWeightsNorm)
|
|
|
|
// Calculate with original and normalized weights
|
|
result1 := WassersteinDistance(p, q, pWeights, qWeights)
|
|
result2 := WassersteinDistance(p, q, pWeightsNorm, qWeightsNorm)
|
|
|
|
// Results should be the same
|
|
if math.Abs(result1-result2) > tol {
|
|
t.Errorf("Weight normalization failed: got %v and %v", result1, result2)
|
|
}
|
|
}
|
|
|
|
func TestWassersteinDistanceND(t *testing.T) {
|
|
const tol = 1e-8
|
|
|
|
tests := []struct {
|
|
name string
|
|
p *mat.Dense
|
|
q *mat.Dense
|
|
pWeights []float64
|
|
qWeights []float64
|
|
want float64
|
|
}{
|
|
{
|
|
name: "Example 1: 2D with uniform weights",
|
|
p: mat.NewDense(3, 2, []float64{
|
|
0, 0,
|
|
1, 1,
|
|
2, 2,
|
|
}),
|
|
q: mat.NewDense(3, 2, []float64{
|
|
0, 1,
|
|
1, 2,
|
|
2, 3,
|
|
}),
|
|
pWeights: []float64{1.0 / 3, 1.0 / 3, 1.0 / 3},
|
|
qWeights: []float64{1.0 / 3, 1.0 / 3, 1.0 / 3},
|
|
want: 1.0,
|
|
},
|
|
{
|
|
name: "Example 2: 2D with different sizes",
|
|
p: mat.NewDense(2, 2, []float64{
|
|
0, 0,
|
|
1, 1,
|
|
}),
|
|
q: mat.NewDense(3, 2, []float64{
|
|
0, 1,
|
|
1, 2,
|
|
2, 3,
|
|
}),
|
|
pWeights: []float64{0.5, 0.5},
|
|
qWeights: []float64{1.0 / 3, 1.0 / 3, 1.0 / 3},
|
|
want: 1.618033988749895,
|
|
},
|
|
{
|
|
name: "Example 3: 2D with custom weights",
|
|
p: mat.NewDense(3, 2, []float64{
|
|
0, 0,
|
|
1, 1,
|
|
2, 2,
|
|
}),
|
|
q: mat.NewDense(3, 2, []float64{
|
|
0, 0,
|
|
1, 1,
|
|
2, 2,
|
|
}),
|
|
pWeights: []float64{0.2, 0.3, 0.5},
|
|
qWeights: []float64{0.5, 0.3, 0.2},
|
|
want: 0.848528137423857,
|
|
},
|
|
{
|
|
name: "Example 4: 3D with uniform weights",
|
|
p: mat.NewDense(3, 3, []float64{
|
|
0, 0, 0,
|
|
1, 1, 1,
|
|
2, 2, 2,
|
|
}),
|
|
q: mat.NewDense(3, 3, []float64{
|
|
0, 1, 0,
|
|
1, 2, 1,
|
|
2, 3, 2,
|
|
}),
|
|
pWeights: []float64{1.0 / 3, 1.0 / 3, 1.0 / 3},
|
|
qWeights: []float64{1.0 / 3, 1.0 / 3, 1.0 / 3},
|
|
want: 1.0,
|
|
},
|
|
{
|
|
name: "Example 5: Single points",
|
|
p: mat.NewDense(1, 2, []float64{
|
|
1, 2,
|
|
}),
|
|
q: mat.NewDense(1, 2, []float64{
|
|
3, 4,
|
|
}),
|
|
pWeights: []float64{1.0},
|
|
qWeights: []float64{1.0},
|
|
want: 2.8284271247461903,
|
|
},
|
|
{
|
|
name: "Example 6: Identical distributions",
|
|
p: mat.NewDense(2, 2, []float64{
|
|
1, 2,
|
|
3, 4,
|
|
}),
|
|
q: mat.NewDense(2, 2, []float64{
|
|
1, 2,
|
|
3, 4,
|
|
}),
|
|
pWeights: nil,
|
|
qWeights: nil,
|
|
want: 0.0,
|
|
},
|
|
{
|
|
name: "Example 7: 3D points from SciPy docs",
|
|
p: mat.NewDense(2, 3, []float64{
|
|
0, 2, 3,
|
|
1, 2, 5,
|
|
}),
|
|
q: mat.NewDense(2, 3, []float64{
|
|
3, 2, 3,
|
|
4, 2, 5,
|
|
}),
|
|
pWeights: nil,
|
|
qWeights: nil,
|
|
want: 3.0,
|
|
},
|
|
{
|
|
name: "Example 8: 2D with custom weights from SciPy docs",
|
|
p: mat.NewDense(3, 2, []float64{
|
|
0, 2.75,
|
|
2, 209.3,
|
|
0, 0,
|
|
}),
|
|
q: mat.NewDense(2, 2, []float64{
|
|
0.2, 0.322,
|
|
4.5, 25.1808,
|
|
}),
|
|
pWeights: []float64{0.4, 5.2, 0.114},
|
|
qWeights: []float64{0.8, 1.5},
|
|
want: 174.15840245217169,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
got, err := WassersteinDistanceND(test.p, test.q, test.pWeights, test.qWeights, tol)
|
|
|
|
if err != nil {
|
|
t.Errorf("WassersteinDistanceND() error: %v", err)
|
|
}
|
|
|
|
if math.Abs(got-test.want) > tol {
|
|
t.Errorf("WassersteinDistanceND() = %v, want %v", got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormalizeWeights(t *testing.T) {
|
|
t.Run("panics on zero weight", func(t *testing.T) {
|
|
weights := []float64{1.0, 0.0, 3.0}
|
|
|
|
defer func() {
|
|
if r := recover(); r == nil {
|
|
t.Errorf("expected panic on zero weight, but got none")
|
|
}
|
|
}()
|
|
|
|
normalizeWeights(weights)
|
|
})
|
|
|
|
t.Run("panics on negative weight", func(t *testing.T) {
|
|
weights := []float64{1.0, -2.0, 3.0}
|
|
|
|
defer func() {
|
|
if r := recover(); r == nil {
|
|
t.Errorf("expected panic on negative weight, but got none")
|
|
}
|
|
}()
|
|
|
|
normalizeWeights(weights)
|
|
})
|
|
}
|