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

     1  // Copyright ©2017 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  
    13  // Triangle represents a triangle distribution (https://en.wikipedia.org/wiki/Triangular_distribution).
    14  type Triangle struct {
    15  	a, b, c float64
    16  	src     rand.Source
    17  }
    18  
    19  // NewTriangle constructs a new triangle distribution with lower limit a, upper limit b, and mode c.
    20  // Constraints are a < b and a ≤ c ≤ b.
    21  // This distribution is uncommon in nature, but may be useful for simulation.
    22  func NewTriangle(a, b, c float64, src rand.Source) Triangle {
    23  	checkTriangleParameters(a, b, c)
    24  	return Triangle{a: a, b: b, c: c, src: src}
    25  }
    26  
    27  func checkTriangleParameters(a, b, c float64) {
    28  	if a >= b {
    29  		panic("triangle: constraint of a < b violated")
    30  	}
    31  	if a > c {
    32  		panic("triangle: constraint of a <= c violated")
    33  	}
    34  	if c > b {
    35  		panic("triangle: constraint of c <= b violated")
    36  	}
    37  }
    38  
    39  // CDF computes the value of the cumulative density function at x.
    40  func (t Triangle) CDF(x float64) float64 {
    41  	switch {
    42  	case x <= t.a:
    43  		return 0
    44  	case x <= t.c:
    45  		d := x - t.a
    46  		return (d * d) / ((t.b - t.a) * (t.c - t.a))
    47  	case x < t.b:
    48  		d := t.b - x
    49  		return 1 - (d*d)/((t.b-t.a)*(t.b-t.c))
    50  	default:
    51  		return 1
    52  	}
    53  }
    54  
    55  // Entropy returns the entropy of the distribution.
    56  func (t Triangle) Entropy() float64 {
    57  	return 0.5 + math.Log(t.b-t.a) - math.Ln2
    58  }
    59  
    60  // ExKurtosis returns the excess kurtosis of the distribution.
    61  func (Triangle) ExKurtosis() float64 {
    62  	return -3.0 / 5.0
    63  }
    64  
    65  // Fit is not appropriate for Triangle, because the distribution is generally used when there is little data.
    66  
    67  // LogProb computes the natural logarithm of the value of the probability density function at x.
    68  func (t Triangle) LogProb(x float64) float64 {
    69  	return math.Log(t.Prob(x))
    70  }
    71  
    72  // Mean returns the mean of the probability distribution.
    73  func (t Triangle) Mean() float64 {
    74  	return (t.a + t.b + t.c) / 3
    75  }
    76  
    77  // Median returns the median of the probability distribution.
    78  func (t Triangle) Median() float64 {
    79  	if t.c >= (t.a+t.b)/2 {
    80  		return t.a + math.Sqrt((t.b-t.a)*(t.c-t.a)/2)
    81  	}
    82  	return t.b - math.Sqrt((t.b-t.a)*(t.b-t.c)/2)
    83  }
    84  
    85  // Mode returns the mode of the probability distribution.
    86  func (t Triangle) Mode() float64 {
    87  	return t.c
    88  }
    89  
    90  // NumParameters returns the number of parameters in the distribution.
    91  func (Triangle) NumParameters() int {
    92  	return 3
    93  }
    94  
    95  // Prob computes the value of the probability density function at x.
    96  func (t Triangle) Prob(x float64) float64 {
    97  	switch {
    98  	case x < t.a:
    99  		return 0
   100  	case x < t.c:
   101  		return 2 * (x - t.a) / ((t.b - t.a) * (t.c - t.a))
   102  	case x == t.c:
   103  		return 2 / (t.b - t.a)
   104  	case x <= t.b:
   105  		return 2 * (t.b - x) / ((t.b - t.a) * (t.b - t.c))
   106  	default:
   107  		return 0
   108  	}
   109  }
   110  
   111  // Quantile returns the inverse of the cumulative probability distribution.
   112  func (t Triangle) Quantile(p float64) float64 {
   113  	if p < 0 || p > 1 {
   114  		panic(badPercentile)
   115  	}
   116  
   117  	f := (t.c - t.a) / (t.b - t.a)
   118  
   119  	if p < f {
   120  		return t.a + math.Sqrt(p*(t.b-t.a)*(t.c-t.a))
   121  	}
   122  	return t.b - math.Sqrt((1-p)*(t.b-t.a)*(t.b-t.c))
   123  }
   124  
   125  // Rand returns a random sample drawn from the distribution.
   126  func (t Triangle) Rand() float64 {
   127  	var rnd float64
   128  	if t.src == nil {
   129  		rnd = rand.Float64()
   130  	} else {
   131  		rnd = rand.New(t.src).Float64()
   132  	}
   133  
   134  	return t.Quantile(rnd)
   135  }
   136  
   137  // Score returns the score function with respect to the parameters of the
   138  // distribution at the input location x. The score function is the derivative
   139  // of the log-likelihood at x with respect to the parameters
   140  //
   141  //	(∂/∂θ) log(p(x;θ))
   142  //
   143  // If deriv is non-nil, len(deriv) must equal the number of parameters otherwise
   144  // Score will panic, and the derivative is stored in-place into deriv. If deriv
   145  // is nil a new slice will be allocated and returned.
   146  //
   147  // The order is [∂LogProb / ∂Mu, ∂LogProb / ∂Sigma].
   148  //
   149  // For more information, see https://en.wikipedia.org/wiki/Score_%28statistics%29.
   150  func (t Triangle) Score(deriv []float64, x float64) []float64 {
   151  	if deriv == nil {
   152  		deriv = make([]float64, t.NumParameters())
   153  	}
   154  	if len(deriv) != t.NumParameters() {
   155  		panic(badLength)
   156  	}
   157  	if (x < t.a) || (x > t.b) {
   158  		deriv[0] = math.NaN()
   159  		deriv[1] = math.NaN()
   160  		deriv[2] = math.NaN()
   161  	} else {
   162  		invBA := 1 / (t.b - t.a)
   163  		invCA := 1 / (t.c - t.a)
   164  		invBC := 1 / (t.b - t.c)
   165  		switch {
   166  		case x < t.c:
   167  			deriv[0] = -1/(x-t.a) + invBA + invCA
   168  			deriv[1] = -invBA
   169  			deriv[2] = -invCA
   170  		case x > t.c:
   171  			deriv[0] = invBA
   172  			deriv[1] = 1/(t.b-x) - invBA - invBC
   173  			deriv[2] = invBC
   174  		default:
   175  			deriv[0] = invBA
   176  			deriv[1] = -invBA
   177  			deriv[2] = 0
   178  		}
   179  		switch {
   180  		case x == t.a:
   181  			deriv[0] = math.NaN()
   182  		case x == t.b:
   183  			deriv[1] = math.NaN()
   184  		case x == t.c:
   185  			deriv[2] = math.NaN()
   186  		}
   187  		switch {
   188  		case t.a == t.c:
   189  			deriv[0] = math.NaN()
   190  			deriv[2] = math.NaN()
   191  		case t.b == t.c:
   192  			deriv[1] = math.NaN()
   193  			deriv[2] = math.NaN()
   194  		}
   195  	}
   196  	return deriv
   197  }
   198  
   199  // ScoreInput returns the score function with respect to the input of the
   200  // distribution at the input location specified by x. The score function is the
   201  // derivative of the log-likelihood
   202  //
   203  //	(d/dx) log(p(x)) .
   204  //
   205  // Special cases (c is the mode of the distribution):
   206  //
   207  //	ScoreInput(c) = NaN
   208  //	ScoreInput(x) = NaN for x not in (a, b)
   209  func (t Triangle) ScoreInput(x float64) float64 {
   210  	if (x <= t.a) || (x >= t.b) || (x == t.c) {
   211  		return math.NaN()
   212  	}
   213  	if x < t.c {
   214  		return 1 / (x - t.a)
   215  	}
   216  	return 1 / (x - t.b)
   217  }
   218  
   219  // Skewness returns the skewness of the distribution.
   220  func (t Triangle) Skewness() float64 {
   221  	n := math.Sqrt2 * (t.a + t.b - 2*t.c) * (2*t.a - t.b - t.c) * (t.a - 2*t.b + t.c)
   222  	d := 5 * math.Pow(t.a*t.a+t.b*t.b+t.c*t.c-t.a*t.b-t.a*t.c-t.b*t.c, 3.0/2.0)
   223  
   224  	return n / d
   225  }
   226  
   227  // StdDev returns the standard deviation of the probability distribution.
   228  func (t Triangle) StdDev() float64 {
   229  	return math.Sqrt(t.Variance())
   230  }
   231  
   232  // Survival returns the survival function (complementary CDF) at x.
   233  func (t Triangle) Survival(x float64) float64 {
   234  	return 1 - t.CDF(x)
   235  }
   236  
   237  // parameters returns the parameters of the distribution.
   238  func (t Triangle) parameters(p []Parameter) []Parameter {
   239  	nParam := t.NumParameters()
   240  	if p == nil {
   241  		p = make([]Parameter, nParam)
   242  	} else if len(p) != nParam {
   243  		panic("triangle: improper parameter length")
   244  	}
   245  	p[0].Name = "A"
   246  	p[0].Value = t.a
   247  	p[1].Name = "B"
   248  	p[1].Value = t.b
   249  	p[2].Name = "C"
   250  	p[2].Value = t.c
   251  	return p
   252  }
   253  
   254  // setParameters modifies the parameters of the distribution.
   255  func (t *Triangle) setParameters(p []Parameter) {
   256  	if len(p) != t.NumParameters() {
   257  		panic("triangle: incorrect number of parameters to set")
   258  	}
   259  	if p[0].Name != "A" {
   260  		panic("triangle: " + panicNameMismatch)
   261  	}
   262  	if p[1].Name != "B" {
   263  		panic("triangle: " + panicNameMismatch)
   264  	}
   265  	if p[2].Name != "C" {
   266  		panic("triangle: " + panicNameMismatch)
   267  	}
   268  
   269  	checkTriangleParameters(p[0].Value, p[1].Value, p[2].Value)
   270  
   271  	t.a = p[0].Value
   272  	t.b = p[1].Value
   273  	t.c = p[2].Value
   274  }
   275  
   276  // Variance returns the variance of the probability distribution.
   277  func (t Triangle) Variance() float64 {
   278  	return (t.a*t.a + t.b*t.b + t.c*t.c - t.a*t.b - t.a*t.c - t.b*t.c) / 18
   279  }