go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/changepoints/bayesian/sequence_likelihood_test.go (about)

     1  // Copyright 2023 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package bayesian
    16  
    17  import (
    18  	"math"
    19  	"testing"
    20  
    21  	. "github.com/smartystreets/goconvey/convey"
    22  )
    23  
    24  func TestSequenceLikelihood(t *testing.T) {
    25  	Convey("Uniform prior", t, func() {
    26  		prior := BetaDistribution{
    27  			Alpha: 1.0,
    28  			Beta:  1.0,
    29  		}
    30  		sl := NewSequenceLikelihood(prior)
    31  
    32  		Convey("Empty sequence", func() {
    33  			// The probability of observing the empty sequence
    34  			// knowing the sequence length is 0 is 1.0.
    35  			So(math.Exp(sl.LogLikelihood(0, 0)), ShouldAlmostEqual, 1.0)
    36  		})
    37  		Convey("Sequence of length one", func() {
    38  			// If the sequence is of length one, and the prior
    39  			// is not biased one way or the other, the probability
    40  			// of observing a pass or a fail should be the same
    41  			// and add up to 1.0.
    42  			So(math.Exp(sl.LogLikelihood(0, 1)), ShouldAlmostEqual, 0.5)
    43  			So(math.Exp(sl.LogLikelihood(1, 1)), ShouldAlmostEqual, 0.5)
    44  		})
    45  		Convey("Sequence of length two", func() {
    46  			// The following identities are harder to explain as
    47  			// the sequence likelihoods are obtained by integrating
    48  			// over all possible test failure rates.
    49  			// However, we know the probability of observing all
    50  			// sequences must add up to one.
    51  
    52  			So(math.Exp(sl.LogLikelihood(0, 2)), ShouldAlmostEqual, 0.3333333333333333)
    53  			// There are two sequences that have one pass and one failure.
    54  			So(2*math.Exp(sl.LogLikelihood(1, 2)), ShouldAlmostEqual, 0.3333333333333333)
    55  			So(math.Exp(sl.LogLikelihood(2, 2)), ShouldAlmostEqual, 0.3333333333333333)
    56  		})
    57  		Convey("Sequence of length three", func() {
    58  			// Coefficients (1, 3, 3, 1) from Pascal's triangle.
    59  			So(math.Exp(sl.LogLikelihood(0, 3)), ShouldAlmostEqual, 0.25)
    60  			So(3*math.Exp(sl.LogLikelihood(1, 3)), ShouldAlmostEqual, 0.25)
    61  			So(3*math.Exp(sl.LogLikelihood(2, 3)), ShouldAlmostEqual, 0.25)
    62  			So(math.Exp(sl.LogLikelihood(3, 3)), ShouldAlmostEqual, 0.25)
    63  		})
    64  		Convey("Sequence of length four", func() {
    65  			// Coefficients (1, 4, 6, 4, 1) from Pascal's triangle.
    66  			So(math.Exp(sl.LogLikelihood(0, 4)), ShouldAlmostEqual, 0.20)
    67  			So(4*math.Exp(sl.LogLikelihood(1, 4)), ShouldAlmostEqual, 0.20)
    68  			So(6*math.Exp(sl.LogLikelihood(2, 4)), ShouldAlmostEqual, 0.20)
    69  			So(4*math.Exp(sl.LogLikelihood(3, 4)), ShouldAlmostEqual, 0.20)
    70  			So(math.Exp(sl.LogLikelihood(4, 4)), ShouldAlmostEqual, 0.20)
    71  		})
    72  	})
    73  	Convey("Non-uniform prior", t, func() {
    74  		// This prior is biased towards the test either
    75  		// passing or failing consistently, with the
    76  		// passing consistently case more likely.
    77  		prior := BetaDistribution{
    78  			Alpha: 0.3,
    79  			Beta:  0.5,
    80  		}
    81  		sl := NewSequenceLikelihood(prior)
    82  
    83  		Convey("Empty sequence", func() {
    84  			// The probability of observing the empty sequence
    85  			// knowing the sequence length is 0 is 1.0.
    86  			So(math.Exp(sl.LogLikelihood(0, 0)), ShouldAlmostEqual, 1.0)
    87  		})
    88  		Convey("Sequence of length one", func() {
    89  			// Verify sequences with fewer failures are more likely
    90  			// and the probabilities add up to 1.0.
    91  			So(math.Exp(sl.LogLikelihood(0, 1)), ShouldAlmostEqual, 0.625)
    92  			So(math.Exp(sl.LogLikelihood(1, 1)), ShouldAlmostEqual, 0.375)
    93  		})
    94  		Convey("Sequence of length two", func() {
    95  			// The following results were not verified with respect to ground
    96  			// truth, but we did verify the probabilities added up to 1.0
    97  			// and shape is expected.
    98  
    99  			So(math.Exp(sl.LogLikelihood(0, 2)), ShouldAlmostEqual, 0.520833333333333)
   100  			// There are two sequences that have one pass
   101  			// and one failure.
   102  			So(2*math.Exp(sl.LogLikelihood(1, 2)), ShouldAlmostEqual, 0.208333333333333)
   103  			So(math.Exp(sl.LogLikelihood(2, 2)), ShouldAlmostEqual, 0.2708333333333333)
   104  		})
   105  		Convey("Sequence of length three", func() {
   106  			// The following results were not verified with respect to ground
   107  			// truth, but we did verify the probabilities added up to 1.0
   108  			// and shape is expected.
   109  
   110  			// Coefficients (1, 3, 3, 1) from Pascal's triangle.
   111  			So(math.Exp(sl.LogLikelihood(0, 3)), ShouldAlmostEqual, 0.465029761904762)
   112  			So(3*math.Exp(sl.LogLikelihood(1, 3)), ShouldAlmostEqual, 0.16741071428571436)
   113  			So(3*math.Exp(sl.LogLikelihood(2, 3)), ShouldAlmostEqual, 0.14508928571428578)
   114  			So(math.Exp(sl.LogLikelihood(3, 3)), ShouldAlmostEqual, 0.2224702380952381)
   115  		})
   116  		Convey("Sequence of length four", func() {
   117  			// The following results were not verified with respect to ground
   118  			// truth, but we did verify the probabilities added up to 1.0
   119  			// and shape is expected.
   120  
   121  			// Coefficients (1, 4, 6, 4, 1) from Pascal's triangle.
   122  			So(math.Exp(sl.LogLikelihood(0, 4)), ShouldAlmostEqual, 0.4283168859649124)
   123  			So(4*math.Exp(sl.LogLikelihood(1, 4)), ShouldAlmostEqual, 0.14685150375939848)
   124  			So(6*math.Exp(sl.LogLikelihood(2, 4)), ShouldAlmostEqual, 0.11454417293233085)
   125  			So(4*math.Exp(sl.LogLikelihood(3, 4)), ShouldAlmostEqual, 0.11708959899749374)
   126  			So(math.Exp(sl.LogLikelihood(4, 4)), ShouldAlmostEqual, 0.19319783834586465)
   127  		})
   128  	})
   129  }
   130  
   131  func TestAddLogLikelihood(t *testing.T) {
   132  	Convey("AddLogLikelihood", t, func() {
   133  		Convey("One element", func() {
   134  			So(AddLogLikelihoods([]float64{math.Log(0.1)}), ShouldEqual, math.Log(0.1))
   135  		})
   136  
   137  		Convey("Many elements", func() {
   138  			So(AddLogLikelihoods([]float64{math.Log(0.1), math.Log(0.2), math.Log(0.3)}), ShouldAlmostEqual, math.Log(0.6))
   139  		})
   140  
   141  		Convey("Many small elements", func() {
   142  			var eles = make([]float64, 10)
   143  			for i := 0; i < 10; i++ {
   144  				eles[i] = math.Log(0.0000001)
   145  			}
   146  			So(AddLogLikelihoods(eles), ShouldAlmostEqual, math.Log(0.000001))
   147  		})
   148  
   149  	})
   150  }