stat/distuv: add ScoreInput implementations for Uniform and Triangle

Also fixes a bug in Laplace ScoreInput, and tests existing ScoreInput implementations.
This commit is contained in:
Roman Werpachowski
2020-07-01 09:50:39 +01:00
committed by GitHub
parent 48db94ddb5
commit c8de933feb
7 changed files with 130 additions and 14 deletions
+40 -12
View File
@@ -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)
}
}
}
+2 -2
View File
@@ -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
+31
View File
@@ -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")
}
}
+17
View File
@@ -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)
+12
View File
@@ -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)
}
}
}
+11
View File
@@ -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
+17
View File
@@ -5,6 +5,7 @@
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)
}
}
}