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