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  }