stat/distuv: add ScoreInput implementations for Uniform and Triangle
Also fixes a bug in Laplace ScoreInput, and tests existing ScoreInput implementations.
MODIFY
stat/distuv/general_test.go
stat/distuv/general_test.go
@@ -193,6 +193,7 @@ func parametersEqual(p1, p2 []Parameter, tol float64) bool {
type derivParamTester interface {
LogProb(x float64) float64
Score(deriv []float64, x float64) []float64
+ ScoreInput(x float64) float64
Quantile(p float64) float64
NumParameters() int
parameters([]Parameter) []Parameter
@@ -206,14 +207,33 @@ func testDerivParam(t *testing.T, d derivParamTester) {
quantiles := make([]float64, nTest)
floats.Span(quantiles, 0.1, 0.9)
- deriv := make([]float64, d.NumParameters())
- fdDeriv := make([]float64, d.NumParameters())
+ scoreInPlace := make([]float64, d.NumParameters())
+ fdDerivParam := make([]float64, d.NumParameters())
if !panics(func() { d.Score(make([]float64, d.NumParameters()+1), 0) }) {
t.Errorf("Expected panic for wrong derivative slice length")
}
+ if !panics(func() { d.parameters(make([]Parameter, d.NumParameters()+1)) }) {
+ t.Errorf("Expected panic for wrong parameter slice length")
+ }
initParams := d.parameters(nil)
+ tooLongParams := make([]Parameter, len(initParams)+1)
+ copy(tooLongParams, initParams)
+ if !panics(func() { d.setParameters(tooLongParams) }) {
+ t.Errorf("Expected panic for wrong parameter slice length")
+ }
+ badNameParams := make([]Parameter, len(initParams))
+ copy(badNameParams, initParams)
+ const badName = "__badName__"
+ for i := 0; i < len(initParams); i++ {
+ badNameParams[i].Name = badName
+ if !panics(func() { d.setParameters(badNameParams) }) {
+ t.Errorf("Expected panic for wrong %d-th parameter name", i)
+ }
+ badNameParams[i].Name = initParams[i].Name
+ }
+
init := make([]float64, d.NumParameters())
for i, v := range initParams {
init[i] = v.Value
@@ -221,11 +241,11 @@ func testDerivParam(t *testing.T, d derivParamTester) {
for _, v := range quantiles {
d.setParameters(initParams)
x := d.Quantile(v)
- gotDeriv := d.Score(deriv, x)
- if &gotDeriv[0] != &deriv[0] {
- t.Errorf("Returned a different derivative slice than passed in. Got %v, want %v", gotDeriv, deriv)
+ score := d.Score(scoreInPlace, x)
+ if &score[0] != &scoreInPlace[0] {
+ t.Errorf("Returned a different derivative slice than passed in. Got %v, want %v", score, scoreInPlace)
}
- f := func(p []float64) float64 {
+ logProbParams := func(p []float64) float64 {
params := d.parameters(nil)
for i, v := range p {
params[i].Value = v
@@ -233,14 +253,22 @@ func testDerivParam(t *testing.T, d derivParamTester) {
d.setParameters(params)
return d.LogProb(x)
}
- fd.Gradient(fdDeriv, f, init, nil)
- if !floats.EqualApprox(deriv, fdDeriv, 1e-6) {
- t.Errorf("Derivative mismatch at x = %g. Want %v, got %v", x, fdDeriv, deriv)
+ fd.Gradient(fdDerivParam, logProbParams, init, nil)
+ if !floats.EqualApprox(scoreInPlace, fdDerivParam, 1e-6) {
+ t.Errorf("Score mismatch at x = %g. Want %v, got %v", x, fdDerivParam, scoreInPlace)
}
d.setParameters(initParams)
- d2 := d.Score(nil, x)
- if !floats.EqualApprox(d2, deriv, 1e-14) {
- t.Errorf("Derivative mismatch when input nil Want %v, got %v", d2, deriv)
+ score2 := d.Score(nil, x)
+ if !floats.EqualApprox(score2, scoreInPlace, 1e-14) {
+ t.Errorf("Score mismatch when input nil Want %v, got %v", score2, scoreInPlace)
+ }
+ logProbInput := func(x2 float64) float64 {
+ return d.LogProb(x2)
+ }
+ scoreInput := d.ScoreInput(x)
+ fdDerivInput := fd.Derivative(logProbInput, x, nil)
+ if !absEqTol(scoreInput, fdDerivInput, 1e-6) {
+ t.Errorf("ScoreInput mismatch at x = %g. Want %v, got %v", x, fdDerivInput, scoreInput)
}
}
}

MODIFY
stat/distuv/laplace.go
stat/distuv/laplace.go
@@ -210,11 +210,11 @@ func (l Laplace) Score(deriv []float64, x float64) []float64 {
// derivative of the log-likelihood
// (d/dx) log(p(x)) .
// Special cases:
-// ScoreInput(l.Mu) = 0
+// ScoreInput(l.Mu) = NaN
func (l Laplace) ScoreInput(x float64) float64 {
diff := x - l.Mu
if diff == 0 {
- return 0
+ return math.NaN()
}
if diff > 0 {
return -1 / l.Scale

MODIFY
stat/distuv/laplace_test.go
stat/distuv/laplace_test.go
@@ -105,6 +105,10 @@ func testLaplace(t *testing.T, dist Laplace, i int) {
if score[1] != -1/dist.Scale {
t.Errorf("Mismatch in score over Scale value for x == Mu, got: %v, want: %g", score[1], -1/dist.Scale)
}
+ scoreInput := dist.ScoreInput(dist.Mu)
+ if !math.IsNaN(scoreInput) {
+ t.Errorf("Expected NaN input score for x == Mu, got %v", scoreInput)
+ }
}
func TestLaplaceFit(t *testing.T) {
@@ -133,6 +137,18 @@ func TestLaplaceFit(t *testing.T) {
wantMu: 1,
wantScale: 0,
},
+ {
+ samples: []float64{1, 1, 10},
+ weights: []float64{1, 1, 0},
+ wantMu: 1,
+ wantScale: 0,
+ },
+ {
+ samples: []float64{10},
+ weights: nil,
+ wantMu: 10,
+ wantScale: 0,
+ },
}
for i, test := range cases {
d := Laplace{}
@@ -169,3 +185,18 @@ func TestLaplaceFitRandomSamples(t *testing.T) {
t.Errorf("unexpected scale result for random test got:%f, want:%f", le.Scale, l.Scale)
}
}
+
+func TestLaplaceFitPanic(t *testing.T) {
+ t.Parallel()
+ l := Laplace{
+ Mu: 3,
+ Scale: 5,
+ Src: nil,
+ }
+ if !panics(func() { l.Fit([]float64{1, 1, 1}, []float64{0.4, 0.4}) }) {
+ t.Errorf("Expected panic in Fit for len(sample) != len(weights)")
+ }
+ if !panics(func() { l.Fit([]float64{}, nil) }) {
+ t.Errorf("Expected panic in Fit for len(sample) == 0")
+ }
+}

MODIFY
stat/distuv/triangle.go
stat/distuv/triangle.go
@@ -194,6 +194,23 @@ func (t Triangle) Score(deriv []float64, x float64) []float64 {
return deriv
}
+// ScoreInput returns the score function with respect to the input of the
+// distribution at the input location specified by x. The score function is the
+// derivative of the log-likelihood
+// (d/dx) log(p(x)) .
+// Special cases (c is the mode of the distribution):
+// ScoreInput(c) = NaN
+// ScoreInput(x) = NaN for x not in (a, b)
+func (t Triangle) ScoreInput(x float64) float64 {
+ if (x <= t.a) || (x >= t.b) || (x == t.c) {
+ return math.NaN()
+ }
+ if x < t.c {
+ return 1 / (x - t.a)
+ }
+ return 1 / (x - t.b)
+}
+
// Skewness returns the skewness of the distribution.
func (t Triangle) Skewness() float64 {
n := math.Sqrt2 * (t.a + t.b - 2*t.c) * (2*t.a - t.b - t.c) * (t.a - 2*t.b + t.c)

MODIFY
stat/distuv/triangle_test.go
stat/distuv/triangle_test.go
@@ -165,3 +165,15 @@ func logProbDerivative(t Triangle, x float64, i int, h float64) float64 {
t.setParameters(origParams)
return (lpUp - lpDown) / (2 * h)
}
+
+func TestTriangleScoreInput(t *testing.T) {
+ t.Parallel()
+ f := Triangle{a: -0.5, b: 0.7, c: 0.1}
+ xs := []float64{f.a, f.b, f.c, f.a - 0.0001, f.b + 0.0001}
+ for _, x := range xs {
+ scoreInput := f.ScoreInput(x)
+ if !math.IsNaN(scoreInput) {
+ t.Errorf("Expected NaN input score for x == %g, got %v", x, scoreInput)
+ }
+ }
+}

MODIFY
stat/distuv/uniform.go
stat/distuv/uniform.go
@@ -154,6 +154,17 @@ func (u Uniform) Score(deriv []float64, x float64) []float64 {
return deriv
}
+// ScoreInput returns the score function with respect to the input of the
+// distribution at the input location specified by x. The score function is the
+// derivative of the log-likelihood
+// (d/dx) log(p(x)) .
+func (u Uniform) ScoreInput(x float64) float64 {
+ if (x <= u.Min) || (x >= u.Max) {
+ return math.NaN()
+ }
+ return 0
+}
+
// Skewness returns the skewness of the distribution.
func (Uniform) Skewness() float64 {
return 0

MODIFY
stat/distuv/uniform_test.go
stat/distuv/uniform_test.go
package distuv
import (
+ "math"
"sort"
"testing"
@@ -78,3 +79,19 @@ func testUniform(t *testing.T, u Uniform, i int) {
checkQuantileCDFSurvival(t, i, x, u, 1e-2)
testDerivParam(t, &u)
}
+
+func TestUniformScoreInput(t *testing.T) {
+ t.Parallel()
+ u := Uniform{0, 1, nil}
+ scoreInput := u.ScoreInput(0.5)
+ if scoreInput != 0 {
+ t.Errorf("Mismatch in input score for U(0, 1) at x == 0.5: got %v, want 0", scoreInput)
+ }
+ xs := []float64{-0.0001, 0, 1, 1.0001}
+ for _, x := range xs {
+ scoreInput = u.ScoreInput(x)
+ if !math.IsNaN(scoreInput) {
+ t.Errorf("Expected NaN score input for U(0, 1) at x == %g, got %v", x, scoreInput)
+ }
+ }
+}

stat/distuv: add ScoreInput implementations for Uniform and Triangle

Also fixes a bug in Laplace ScoreInput, and tests existing ScoreInput implementations.