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 }