gonum.org/v1/gonum@v0.14.0/stat/distuv/norm.go (about)

     1  // Copyright ©2014 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 distuv
     6  
     7  import (
     8  	"math"
     9  
    10  	"golang.org/x/exp/rand"
    11  
    12  	"gonum.org/v1/gonum/floats"
    13  	"gonum.org/v1/gonum/mathext"
    14  	"gonum.org/v1/gonum/stat"
    15  )
    16  
    17  // UnitNormal is an instantiation of the normal distribution with Mu = 0 and Sigma = 1.
    18  var UnitNormal = Normal{Mu: 0, Sigma: 1}
    19  
    20  // Normal represents a normal (Gaussian) distribution (https://en.wikipedia.org/wiki/Normal_distribution).
    21  type Normal struct {
    22  	Mu    float64 // Mean of the normal distribution
    23  	Sigma float64 // Standard deviation of the normal distribution
    24  	Src   rand.Source
    25  
    26  	// Needs to be Mu and Sigma and not Mean and StdDev because Normal has functions
    27  	// Mean and StdDev
    28  }
    29  
    30  // CDF computes the value of the cumulative density function at x.
    31  func (n Normal) CDF(x float64) float64 {
    32  	return 0.5 * math.Erfc(-(x-n.Mu)/(n.Sigma*math.Sqrt2))
    33  }
    34  
    35  // ConjugateUpdate updates the parameters of the distribution from the sufficient
    36  // statistics of a set of samples. The sufficient statistics, suffStat, have been
    37  // observed with nSamples observations. The prior values of the distribution are those
    38  // currently in the distribution, and have been observed with priorStrength samples.
    39  //
    40  // For the normal distribution, the sufficient statistics are the mean and
    41  // uncorrected standard deviation of the samples.
    42  // The prior is having seen strength[0] samples with mean Normal.Mu
    43  // and strength[1] samples with standard deviation Normal.Sigma. As a result of
    44  // this function, Normal.Mu and Normal.Sigma are updated based on the weighted
    45  // samples, and strength is modified to include the new number of samples observed.
    46  //
    47  // This function panics if len(suffStat) != n.NumSuffStat() or
    48  // len(priorStrength) != n.NumSuffStat().
    49  func (n *Normal) ConjugateUpdate(suffStat []float64, nSamples float64, priorStrength []float64) {
    50  	// TODO: Support prior strength with math.Inf(1) to allow updating with
    51  	// a known mean/standard deviation
    52  	if len(suffStat) != n.NumSuffStat() {
    53  		panic("norm: incorrect suffStat length")
    54  	}
    55  	if len(priorStrength) != n.NumSuffStat() {
    56  		panic("norm: incorrect priorStrength length")
    57  	}
    58  
    59  	totalMeanSamples := nSamples + priorStrength[0]
    60  	totalSum := suffStat[0]*nSamples + n.Mu*priorStrength[0]
    61  
    62  	totalVarianceSamples := nSamples + priorStrength[1]
    63  	// sample variance
    64  	totalVariance := nSamples * suffStat[1] * suffStat[1]
    65  	// add prior variance
    66  	totalVariance += priorStrength[1] * n.Sigma * n.Sigma
    67  	// add cross variance from the difference of the means
    68  	meanDiff := (suffStat[0] - n.Mu)
    69  	totalVariance += priorStrength[0] * nSamples * meanDiff * meanDiff / totalMeanSamples
    70  
    71  	n.Mu = totalSum / totalMeanSamples
    72  	n.Sigma = math.Sqrt(totalVariance / totalVarianceSamples)
    73  	floats.AddConst(nSamples, priorStrength)
    74  }
    75  
    76  // Entropy returns the differential entropy of the distribution.
    77  func (n Normal) Entropy() float64 {
    78  	return 0.5 * (log2Pi + 1 + 2*math.Log(n.Sigma))
    79  }
    80  
    81  // ExKurtosis returns the excess kurtosis of the distribution.
    82  func (Normal) ExKurtosis() float64 {
    83  	return 0
    84  }
    85  
    86  // Fit sets the parameters of the probability distribution from the
    87  // data samples x with relative weights w. If weights is nil, then all the weights
    88  // are 1. If weights is not nil, then the len(weights) must equal len(samples).
    89  func (n *Normal) Fit(samples, weights []float64) {
    90  	suffStat := make([]float64, n.NumSuffStat())
    91  	nSamples := n.SuffStat(suffStat, samples, weights)
    92  	n.ConjugateUpdate(suffStat, nSamples, make([]float64, n.NumSuffStat()))
    93  }
    94  
    95  // LogProb computes the natural logarithm of the value of the probability density function at x.
    96  func (n Normal) LogProb(x float64) float64 {
    97  	return negLogRoot2Pi - math.Log(n.Sigma) - (x-n.Mu)*(x-n.Mu)/(2*n.Sigma*n.Sigma)
    98  }
    99  
   100  // Mean returns the mean of the probability distribution.
   101  func (n Normal) Mean() float64 {
   102  	return n.Mu
   103  }
   104  
   105  // Median returns the median of the normal distribution.
   106  func (n Normal) Median() float64 {
   107  	return n.Mu
   108  }
   109  
   110  // Mode returns the mode of the normal distribution.
   111  func (n Normal) Mode() float64 {
   112  	return n.Mu
   113  }
   114  
   115  // NumParameters returns the number of parameters in the distribution.
   116  func (Normal) NumParameters() int {
   117  	return 2
   118  }
   119  
   120  // NumSuffStat returns the number of sufficient statistics for the distribution.
   121  func (Normal) NumSuffStat() int {
   122  	return 2
   123  }
   124  
   125  // Prob computes the value of the probability density function at x.
   126  func (n Normal) Prob(x float64) float64 {
   127  	return math.Exp(n.LogProb(x))
   128  }
   129  
   130  // Quantile returns the inverse of the cumulative probability distribution.
   131  func (n Normal) Quantile(p float64) float64 {
   132  	if p < 0 || p > 1 {
   133  		panic(badPercentile)
   134  	}
   135  	return n.Mu + n.Sigma*mathext.NormalQuantile(p)
   136  }
   137  
   138  // Rand returns a random sample drawn from the distribution.
   139  func (n Normal) Rand() float64 {
   140  	var rnd float64
   141  	if n.Src == nil {
   142  		rnd = rand.NormFloat64()
   143  	} else {
   144  		rnd = rand.New(n.Src).NormFloat64()
   145  	}
   146  	return rnd*n.Sigma + n.Mu
   147  }
   148  
   149  // Score returns the score function with respect to the parameters of the
   150  // distribution at the input location x. The score function is the derivative
   151  // of the log-likelihood at x with respect to the parameters
   152  //
   153  //	(∂/∂θ) log(p(x;θ))
   154  //
   155  // If deriv is non-nil, len(deriv) must equal the number of parameters otherwise
   156  // Score will panic, and the derivative is stored in-place into deriv. If deriv
   157  // is nil a new slice will be allocated and returned.
   158  //
   159  // The order is [∂LogProb / ∂Mu, ∂LogProb / ∂Sigma].
   160  //
   161  // For more information, see https://en.wikipedia.org/wiki/Score_%28statistics%29.
   162  func (n Normal) Score(deriv []float64, x float64) []float64 {
   163  	if deriv == nil {
   164  		deriv = make([]float64, n.NumParameters())
   165  	}
   166  	if len(deriv) != n.NumParameters() {
   167  		panic(badLength)
   168  	}
   169  	deriv[0] = (x - n.Mu) / (n.Sigma * n.Sigma)
   170  	deriv[1] = 1 / n.Sigma * (-1 + ((x-n.Mu)/n.Sigma)*((x-n.Mu)/n.Sigma))
   171  	return deriv
   172  }
   173  
   174  // ScoreInput returns the score function with respect to the input of the
   175  // distribution at the input location specified by x. The score function is the
   176  // derivative of the log-likelihood
   177  //
   178  //	(d/dx) log(p(x)) .
   179  func (n Normal) ScoreInput(x float64) float64 {
   180  	return -(1 / (2 * n.Sigma * n.Sigma)) * 2 * (x - n.Mu)
   181  }
   182  
   183  // Skewness returns the skewness of the distribution.
   184  func (Normal) Skewness() float64 {
   185  	return 0
   186  }
   187  
   188  // StdDev returns the standard deviation of the probability distribution.
   189  func (n Normal) StdDev() float64 {
   190  	return n.Sigma
   191  }
   192  
   193  // SuffStat computes the sufficient statistics of a set of samples to update
   194  // the distribution. The sufficient statistics are stored in place, and the
   195  // effective number of samples are returned.
   196  //
   197  // The normal distribution has two sufficient statistics, the mean of the samples
   198  // and the standard deviation of the samples.
   199  //
   200  // If weights is nil, the weights are assumed to be 1, otherwise panics if
   201  // len(samples) != len(weights). Panics if len(suffStat) != NumSuffStat().
   202  func (Normal) SuffStat(suffStat, samples, weights []float64) (nSamples float64) {
   203  	lenSamp := len(samples)
   204  	if len(weights) != 0 && len(samples) != len(weights) {
   205  		panic(badLength)
   206  	}
   207  	if len(suffStat) != (Normal{}).NumSuffStat() {
   208  		panic(badSuffStat)
   209  	}
   210  
   211  	if len(weights) == 0 {
   212  		nSamples = float64(lenSamp)
   213  	} else {
   214  		nSamples = floats.Sum(weights)
   215  	}
   216  
   217  	mean := stat.Mean(samples, weights)
   218  	suffStat[0] = mean
   219  
   220  	// Use Moment and not StdDev because we want it to be uncorrected
   221  	variance := stat.MomentAbout(2, samples, mean, weights)
   222  	suffStat[1] = math.Sqrt(variance)
   223  	return nSamples
   224  }
   225  
   226  // Survival returns the survival function (complementary CDF) at x.
   227  func (n Normal) Survival(x float64) float64 {
   228  	return 0.5 * (1 - math.Erf((x-n.Mu)/(n.Sigma*math.Sqrt2)))
   229  }
   230  
   231  // setParameters modifies the parameters of the distribution.
   232  func (n *Normal) setParameters(p []Parameter) {
   233  	if len(p) != n.NumParameters() {
   234  		panic("normal: incorrect number of parameters to set")
   235  	}
   236  	if p[0].Name != "Mu" {
   237  		panic("normal: " + panicNameMismatch)
   238  	}
   239  	if p[1].Name != "Sigma" {
   240  		panic("normal: " + panicNameMismatch)
   241  	}
   242  	n.Mu = p[0].Value
   243  	n.Sigma = p[1].Value
   244  }
   245  
   246  // Variance returns the variance of the probability distribution.
   247  func (n Normal) Variance() float64 {
   248  	return n.Sigma * n.Sigma
   249  }
   250  
   251  // parameters returns the parameters of the distribution.
   252  func (n Normal) parameters(p []Parameter) []Parameter {
   253  	nParam := n.NumParameters()
   254  	if p == nil {
   255  		p = make([]Parameter, nParam)
   256  	} else if len(p) != nParam {
   257  		panic("normal: improper parameter length")
   258  	}
   259  	p[0].Name = "Mu"
   260  	p[0].Value = n.Mu
   261  	p[1].Name = "Sigma"
   262  	p[1].Value = n.Sigma
   263  	return p
   264  }