gonum.org/v1/gonum@v0.14.0/stat/distmv/studentst.go (about) 1 // Copyright ©2016 The Gonum Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package distmv 6 7 import ( 8 "math" 9 "sort" 10 11 "golang.org/x/exp/rand" 12 "golang.org/x/tools/container/intsets" 13 14 "gonum.org/v1/gonum/floats" 15 "gonum.org/v1/gonum/mat" 16 "gonum.org/v1/gonum/stat" 17 "gonum.org/v1/gonum/stat/distuv" 18 ) 19 20 // StudentsT is a multivariate Student's T distribution. It is a distribution over 21 // ℝ^n with the probability density 22 // 23 // p(y) = (Γ((ν+n)/2) / Γ(ν/2)) * (νπ)^(-n/2) * |Ʃ|^(-1/2) * 24 // (1 + 1/ν * (y-μ)ᵀ * Ʃ^-1 * (y-μ))^(-(ν+n)/2) 25 // 26 // where ν is a scalar greater than 2, μ is a vector in ℝ^n, and Ʃ is an n×n 27 // symmetric positive definite matrix. 28 // 29 // In this distribution, ν sets the spread of the distribution, similar to 30 // the degrees of freedom in a univariate Student's T distribution. As ν → ∞, 31 // the distribution approaches a multi-variate normal distribution. 32 // μ is the mean of the distribution, and the covariance is ν/(ν-2)*Ʃ. 33 // 34 // See https://en.wikipedia.org/wiki/Student%27s_t-distribution and 35 // http://users.isy.liu.se/en/rt/roth/student.pdf for more information. 36 type StudentsT struct { 37 nu float64 38 mu []float64 39 // If src is altered, rnd must be updated. 40 src rand.Source 41 rnd *rand.Rand 42 43 sigma mat.SymDense // only stored if needed 44 45 chol mat.Cholesky 46 lower mat.TriDense 47 logSqrtDet float64 48 dim int 49 } 50 51 // NewStudentsT creates a new StudentsT with the given nu, mu, and sigma 52 // parameters. 53 // 54 // NewStudentsT panics if len(mu) == 0, or if len(mu) != sigma.SymmetricDim(). If 55 // the covariance matrix is not positive-definite, nil is returned and ok is false. 56 func NewStudentsT(mu []float64, sigma mat.Symmetric, nu float64, src rand.Source) (dist *StudentsT, ok bool) { 57 if len(mu) == 0 { 58 panic(badZeroDimension) 59 } 60 dim := sigma.SymmetricDim() 61 if dim != len(mu) { 62 panic(badSizeMismatch) 63 } 64 65 s := &StudentsT{ 66 nu: nu, 67 mu: make([]float64, dim), 68 dim: dim, 69 src: src, 70 } 71 if src != nil { 72 s.rnd = rand.New(src) 73 } 74 copy(s.mu, mu) 75 76 ok = s.chol.Factorize(sigma) 77 if !ok { 78 return nil, false 79 } 80 s.sigma = *mat.NewSymDense(dim, nil) 81 s.sigma.CopySym(sigma) 82 s.chol.LTo(&s.lower) 83 s.logSqrtDet = 0.5 * s.chol.LogDet() 84 return s, true 85 } 86 87 // ConditionStudentsT returns the Student's T distribution that is the receiver 88 // conditioned on the input evidence, and the success of the operation. 89 // The returned Student's T has dimension 90 // n - len(observed), where n is the dimension of the original receiver. 91 // The dimension order is preserved during conditioning, so if the value 92 // of dimension 1 is observed, the returned normal represents dimensions {0, 2, ...} 93 // of the original Student's T distribution. 94 // 95 // ok indicates whether there was a failure during the update. If ok is false 96 // the operation failed and dist is not usable. 97 // Mathematically this is impossible, but can occur with finite precision arithmetic. 98 func (s *StudentsT) ConditionStudentsT(observed []int, values []float64, src rand.Source) (dist *StudentsT, ok bool) { 99 if len(observed) == 0 { 100 panic("studentst: no observed value") 101 } 102 if len(observed) != len(values) { 103 panic(badInputLength) 104 } 105 106 for _, v := range observed { 107 if v < 0 || v >= s.dim { 108 panic("studentst: observed value out of bounds") 109 } 110 } 111 112 newNu, newMean, newSigma := studentsTConditional(observed, values, s.nu, s.mu, &s.sigma) 113 if newMean == nil { 114 return nil, false 115 } 116 117 return NewStudentsT(newMean, newSigma, newNu, src) 118 119 } 120 121 // studentsTConditional updates a Student's T distribution based on the observed samples 122 // (see documentation for the public function). The Gaussian conditional update 123 // is treated as a special case when nu == math.Inf(1). 124 func studentsTConditional(observed []int, values []float64, nu float64, mu []float64, sigma mat.Symmetric) (newNu float64, newMean []float64, newSigma *mat.SymDense) { 125 dim := len(mu) 126 ob := len(observed) 127 128 unobserved := findUnob(observed, dim) 129 130 unob := len(unobserved) 131 if unob == 0 { 132 panic("stat: all dimensions observed") 133 } 134 135 mu1 := make([]float64, unob) 136 for i, v := range unobserved { 137 mu1[i] = mu[v] 138 } 139 mu2 := make([]float64, ob) // really v - mu2 140 for i, v := range observed { 141 mu2[i] = values[i] - mu[v] 142 } 143 144 var sigma11, sigma22 mat.SymDense 145 sigma11.SubsetSym(sigma, unobserved) 146 sigma22.SubsetSym(sigma, observed) 147 148 sigma21 := mat.NewDense(ob, unob, nil) 149 for i, r := range observed { 150 for j, c := range unobserved { 151 v := sigma.At(r, c) 152 sigma21.Set(i, j, v) 153 } 154 } 155 156 var chol mat.Cholesky 157 ok := chol.Factorize(&sigma22) 158 if !ok { 159 return math.NaN(), nil, nil 160 } 161 162 // Compute mu_1 + sigma_{2,1}ᵀ * sigma_{2,2}^-1 (v - mu_2). 163 v := mat.NewVecDense(ob, mu2) 164 var tmp, tmp2 mat.VecDense 165 err := chol.SolveVecTo(&tmp, v) 166 if err != nil { 167 return math.NaN(), nil, nil 168 } 169 tmp2.MulVec(sigma21.T(), &tmp) 170 171 for i := range mu1 { 172 mu1[i] += tmp2.At(i, 0) 173 } 174 175 // Compute tmp4 = sigma_{2,1}ᵀ * sigma_{2,2}^-1 * sigma_{2,1}. 176 // TODO(btracey): Should this be a method of SymDense? 177 var tmp3, tmp4 mat.Dense 178 err = chol.SolveTo(&tmp3, sigma21) 179 if err != nil { 180 return math.NaN(), nil, nil 181 } 182 tmp4.Mul(sigma21.T(), &tmp3) 183 184 // Compute sigma_{1,1} - tmp4 185 // TODO(btracey): If tmp4 can constructed with a method, then this can be 186 // replaced with SubSym. 187 for i := 0; i < len(unobserved); i++ { 188 for j := i; j < len(unobserved); j++ { 189 v := sigma11.At(i, j) 190 sigma11.SetSym(i, j, v-tmp4.At(i, j)) 191 } 192 } 193 194 // The computed variables are accurate for a Normal. 195 if math.IsInf(nu, 1) { 196 return nu, mu1, &sigma11 197 } 198 199 // Compute beta = (v - mu_2)ᵀ * sigma_{2,2}^-1 * (v - mu_2)ᵀ 200 beta := mat.Dot(v, &tmp) 201 202 // Scale the covariance matrix 203 sigma11.ScaleSym((nu+beta)/(nu+float64(ob)), &sigma11) 204 205 return nu + float64(ob), mu1, &sigma11 206 } 207 208 // findUnob returns the unobserved variables (the complementary set to observed). 209 // findUnob panics if any value repeated in observed. 210 func findUnob(observed []int, dim int) (unobserved []int) { 211 var setOb intsets.Sparse 212 for _, v := range observed { 213 setOb.Insert(v) 214 } 215 var setAll intsets.Sparse 216 for i := 0; i < dim; i++ { 217 setAll.Insert(i) 218 } 219 var setUnob intsets.Sparse 220 setUnob.Difference(&setAll, &setOb) 221 unobserved = setUnob.AppendTo(nil) 222 sort.Ints(unobserved) 223 return unobserved 224 } 225 226 // CovarianceMatrix calculates the covariance matrix of the distribution, 227 // storing the result in dst. Upon return, the value at element {i, j} of the 228 // covariance matrix is equal to the covariance of the i^th and j^th variables. 229 // 230 // covariance(i, j) = E[(x_i - E[x_i])(x_j - E[x_j])] 231 // 232 // If the dst matrix is empty it will be resized to the correct dimensions, 233 // otherwise dst must match the dimension of the receiver or CovarianceMatrix 234 // will panic. 235 func (st *StudentsT) CovarianceMatrix(dst *mat.SymDense) { 236 if dst.IsEmpty() { 237 *dst = *(dst.GrowSym(st.dim).(*mat.SymDense)) 238 } else if dst.SymmetricDim() != st.dim { 239 panic("studentst: input matrix size mismatch") 240 } 241 dst.CopySym(&st.sigma) 242 dst.ScaleSym(st.nu/(st.nu-2), dst) 243 } 244 245 // Dim returns the dimension of the distribution. 246 func (s *StudentsT) Dim() int { 247 return s.dim 248 } 249 250 // LogProb computes the log of the pdf of the point x. 251 func (s *StudentsT) LogProb(y []float64) float64 { 252 if len(y) != s.dim { 253 panic(badInputLength) 254 } 255 256 nu := s.nu 257 n := float64(s.dim) 258 lg1, _ := math.Lgamma((nu + n) / 2) 259 lg2, _ := math.Lgamma(nu / 2) 260 261 t1 := lg1 - lg2 - n/2*math.Log(nu*math.Pi) - s.logSqrtDet 262 263 mahal := stat.Mahalanobis(mat.NewVecDense(len(y), y), mat.NewVecDense(len(s.mu), s.mu), &s.chol) 264 mahal *= mahal 265 return t1 - ((nu+n)/2)*math.Log(1+mahal/nu) 266 } 267 268 // MarginalStudentsT returns the marginal distribution of the given input variables, 269 // and the success of the operation. 270 // That is, MarginalStudentsT returns 271 // 272 // p(x_i) = \int_{x_o} p(x_i | x_o) p(x_o) dx_o 273 // 274 // where x_i are the dimensions in the input, and x_o are the remaining dimensions. 275 // See https://en.wikipedia.org/wiki/Marginal_distribution for more information. 276 // 277 // The input src is passed to the created StudentsT. 278 // 279 // ok indicates whether there was a failure during the marginalization. If ok is false 280 // the operation failed and dist is not usable. 281 // Mathematically this is impossible, but can occur with finite precision arithmetic. 282 func (s *StudentsT) MarginalStudentsT(vars []int, src rand.Source) (dist *StudentsT, ok bool) { 283 newMean := make([]float64, len(vars)) 284 for i, v := range vars { 285 newMean[i] = s.mu[v] 286 } 287 var newSigma mat.SymDense 288 newSigma.SubsetSym(&s.sigma, vars) 289 return NewStudentsT(newMean, &newSigma, s.nu, src) 290 } 291 292 // MarginalStudentsTSingle returns the marginal distribution of the given input variable. 293 // That is, MarginalStudentsTSingle returns 294 // 295 // p(x_i) = \int_{x_o} p(x_i | x_o) p(x_o) dx_o 296 // 297 // where i is the input index, and x_o are the remaining dimensions. 298 // See https://en.wikipedia.org/wiki/Marginal_distribution for more information. 299 // 300 // The input src is passed to the call to NewStudentsT. 301 func (s *StudentsT) MarginalStudentsTSingle(i int, src rand.Source) distuv.StudentsT { 302 return distuv.StudentsT{ 303 Mu: s.mu[i], 304 Sigma: math.Sqrt(s.sigma.At(i, i)), 305 Nu: s.nu, 306 Src: src, 307 } 308 } 309 310 // TODO(btracey): Implement marginal single. Need to modify univariate StudentsT 311 // to be three-parameter. 312 313 // Mean returns the mean of the probability distribution at x. If the 314 // input argument is nil, a new slice will be allocated, otherwise the result 315 // will be put in-place into the receiver. 316 func (s *StudentsT) Mean(x []float64) []float64 { 317 x = reuseAs(x, s.dim) 318 copy(x, s.mu) 319 return x 320 } 321 322 // Nu returns the degrees of freedom parameter of the distribution. 323 func (s *StudentsT) Nu() float64 { 324 return s.nu 325 } 326 327 // Prob computes the value of the probability density function at x. 328 func (s *StudentsT) Prob(y []float64) float64 { 329 return math.Exp(s.LogProb(y)) 330 } 331 332 // Rand generates a random number according to the distributon. 333 // If the input slice is nil, new memory is allocated, otherwise the result is stored 334 // in place. 335 func (s *StudentsT) Rand(x []float64) []float64 { 336 // If Y is distributed according to N(0,Sigma), and U is chi^2 with 337 // parameter ν, then 338 // X = mu + Y * sqrt(nu / U) 339 // X is distributed according to this distribution. 340 341 // Generate Y. 342 x = reuseAs(x, s.dim) 343 tmp := make([]float64, s.dim) 344 if s.rnd == nil { 345 for i := range x { 346 tmp[i] = rand.NormFloat64() 347 } 348 } else { 349 for i := range x { 350 tmp[i] = s.rnd.NormFloat64() 351 } 352 } 353 xVec := mat.NewVecDense(s.dim, x) 354 tmpVec := mat.NewVecDense(s.dim, tmp) 355 xVec.MulVec(&s.lower, tmpVec) 356 357 u := distuv.ChiSquared{K: s.nu, Src: s.src}.Rand() 358 floats.Scale(math.Sqrt(s.nu/u), x) 359 360 floats.Add(x, s.mu) 361 return x 362 }