// // Math.NET Numerics, part of the Math.NET Project // http://numerics.mathdotnet.com // http://github.com/mathnet/mathnet-numerics // // Copyright (c) 2009-2020 Math.NET // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation // files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, // copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following // conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // using System; using System.Collections.Generic; using IStation.Numerics.Random; namespace IStation.Numerics.Distributions { /// /// Continuous Univariate Skewed Generalized T-distribution. /// Implements the univariate Skewed Generalized t-distribution. For details about this /// distribution, see /// /// Wikipedia - Skewed generalized t-distribution. /// The skewed generalized t-distribution contains many different distributions within it /// as special cases based on the parameterization chosen. /// /// This implementation is based on the R package dsgt and corresponding viginette, see /// https://cran.r-project.org/web/packages/sgt/vignettes/sgt.pdf. Compared to that /// implementation, the options for mean adjustment and variance adjustment are always true. /// The location (μ) is the mean of the distribution. /// The scale (σ) squared is the variance of the distribution. /// /// The distribution will use the by /// default. Users can get/set the random number generator by using the /// property. /// The statistics classes will check all the incoming parameters /// whether they are in the allowed range. public class SkewedGeneralizedT : IContinuousDistribution { System.Random _random; // If the given parameterization is one of the recognized special cases, then // this variable is non-null and the special case is used for all functions. // Else this value is null and the full formulation of the generalized distribution is used. IContinuousDistribution _d; readonly double _skewness; /// /// Initializes a new instance of the SkewedGeneralizedT class. This is a skewed generalized t-distribution /// with location=0.0, scale=1.0, skew=0.0, p=2.0 and q=Inf (a standard normal distribution). /// public SkewedGeneralizedT() { _random = SystemRandomSource.Default; Location = 0.0; Scale = 1.0; Skew = 0.0; P = 2.0; Q = double.PositiveInfinity; _d = new Normal(Location, Scale, _random); } /// /// Initializes a new instance of the SkewedGeneralizedT class with a particular location, scale, skew /// and kurtosis parameters. Different parameterizations result in different distributions. /// /// The location (μ) of the distribution. /// The scale (σ) of the distribution. Range: σ > 0. /// The skew, 1 > λ > -1 /// First parameter that controls kurtosis. Range: p > 0 /// Second parameter that controls kurtosis. Range: q > 0 public SkewedGeneralizedT(double location, double scale, double skew, double p, double q) { if (!IsValidParameterSet(location, scale, skew, p, q)) { throw new ArgumentException("Invalid parametrization for the distribution."); } _random = SystemRandomSource.Default; Location = location; Scale = scale; Skew = skew; P = p; Q = q; _d = FindSpecializedDistribution(location, scale, skew, p, q); if (_d == null) { _skewness = CalculateSkewness(); } } /// /// Given a parameter set, returns the distribution that matches this parameterization. /// /// The location (μ) of the distribution. /// The scale (σ) of the distribution. Range: σ > 0. /// The skew, 1 > λ > -1 /// First parameter that controls kurtosis. Range: p > 0 /// Second parameter that controls kurtosis. Range: q > 0 /// Null if no known distribution matches the parameterization, else the distribution. public static IContinuousDistribution FindSpecializedDistribution(double location, double scale, double skew, double p, double q) { if (p == double.PositiveInfinity) { scale *= Math.Sqrt(3.0); return new ContinuousUniform(location - scale, location + scale); } if (q == double.PositiveInfinity) return new SkewedGeneralizedError(location, scale, skew, p); return null; } /// /// Gets or sets the random number generator which is used to draw random samples. /// public System.Random RandomSource { get => _random; set => _random = value ?? SystemRandomSource.Default; } /// /// A string representation of the distribution. /// /// a string representation of the distribution. public override string ToString() { return $"SkewedGeneralizedT(μ = {Location}, σ = {Scale}, λ = {Skew}, p = {P}, q = {Q})"; } /// /// Tests whether the provided values are valid parameters for this distribution. /// /// The location (μ) of the distribution. /// The scale (σ) of the distribution. Range: σ > 0. /// The skew, 1 > λ > -1 /// First parameter that controls kurtosis. Range: p > 0 /// Second parameter that controls kurtosis. Range: q > 0 public static bool IsValidParameterSet(double location, double scale, double skew, double p, double q) { return scale > 0.0 && skew > -1.0 && skew < 1.0 && p > 0.0 && q > 0.0 && p*q> 2.0 && !double.IsNaN(location); } /// /// Gets the location (μ) of the Skewed Generalized t-distribution. /// public double Location { get; } /// /// Gets the scale (σ) of the Skewed Generalized t-distribution. Range: σ > 0. /// public double Scale { get; } /// /// Gets the skew (λ) of the Skewed Generalized t-distribution. Range: 1 > λ > -1. /// public double Skew { get; } /// /// Gets the first parameter that controls the kurtosis of the distribution. Range: p > 0. /// public double P { get; } /// /// Gets the second parameter that controls the kurtosis of the distribution. Range: q > 0. /// public double Q { get; } // No skew implies Median=Mode=Mean public double Mode => _d?.Mode ?? (Skew == 0 ? Mean : Mean - AdjustAddend(AdjustScale(Scale, Skew, P, Q), Skew, P, Q)); public double Minimum => _d?.Minimum ?? double.NegativeInfinity; public double Maximum => _d?.Maximum ?? double.PositiveInfinity; // Mean=Location due to our adjustments made public double Mean => _d?.Mean ?? Location; // Variance=Scale*Scale due to our adjustments made public double Variance => _d?.Variance ?? Scale * Scale; public double StdDev => _d?.StdDev ?? Scale; public double Entropy => _d?.Entropy ?? throw new NotImplementedException(); public double Skewness => _d?.Skewness ?? _skewness; // No skew implies Median=Mode=Mean // Else find it via the point where CDF gives 0.5 public double Median => _d?.Median ?? (Skew == 0 ? Mean : InverseCumulativeDistribution(0.5)); double CalculateSkewness() { if (P * Q <= 3 || Skew == 0) { return 0.0; } var scale = AdjustScale(Scale, Skew, P, Q); var b1 = SpecialFunctions.Beta(1.0 / P, Q); var b2 = SpecialFunctions.Beta(2.0 / P, Q - 1.0 / P); var b3 = SpecialFunctions.Beta(3.0 / P, Q - 2.0 / P); var b4 = SpecialFunctions.Beta(4.0 / P, Q - 3.0 / P); var t1 = (2.0 * Math.Pow(Q, 3.0 / P) * Skew * Math.Pow(scale, 3.0)) / Math.Pow(b1, 3.0); var t2 = 8.0 * Skew * Skew * Math.Pow(b2, 3.0); var t3 = 3.0 * (1.0 + 3.0 * Skew * Skew) * b1; var t4 = b2 * b3; var t5 = 2.0 * (1.0 + Skew * Skew) * Math.Pow(b1, 2.0) * b4; return t1 * (t2 - t3 * t4 + t5); } static double AdjustScale(double scale, double skew, double p, double q) { var b1 = SpecialFunctions.Beta(3.0 / p, q - 2.0 / p); var b2 = SpecialFunctions.Beta(1.0 / p, q); var b3 = SpecialFunctions.Beta(2.0 / p, q - 1.0 / p); var b4 = SpecialFunctions.Beta(1.0 / p, q); return scale / (Math.Pow(q, 1.0 / p) * Math.Sqrt((3.0 * skew * skew + 1.0) * b1 / b2 - 4.0 * skew * skew * ((b3 / b4) * (b3 / b4)))); } // Note: Scale is assumed to be adjusted already when calling this function. static double AdjustX(double x, double scale, double skew, double p, double q) { return x + AdjustAddend(scale, skew, p, q); } // Note: Scale is assumed to be adjusted already when calling this function. static double AdjustAddend(double scale, double skew, double p, double q) { var b1 = SpecialFunctions.Beta(2.0 / p, q - 1.0 / p); var b2 = SpecialFunctions.Beta(1.0 / p, q); return (2.0 * scale * skew * Math.Pow(q, 1.0 / p) * b1) / b2; } /// /// Computes the probability density of the distribution (PDF) at x, i.e. ∂P(X ≤ x)/∂x. /// /// The location (μ) of the distribution. /// The scale (σ) of the distribution. Range: σ > 0. /// The skew, 1 > λ > -1 /// First parameter that controls kurtosis. Range: p > 0 /// Second parameter that controls kurtosis. Range: q > 0 /// The location at which to compute the density. /// the density at . /// public static double PDF(double location, double scale, double skew, double p, double q, double x) { if (!IsValidParameterSet(location, scale, skew, p, q)) { throw new ArgumentException("Invalid parametrization for the distribution."); } var fn = PDFunc(location, scale, skew, p, q, false); return fn(x); } /// /// Computes the log probability density of the distribution (lnPDF) at x, i.e. ln(∂P(X ≤ x)/∂x). /// /// The location (μ) of the distribution. /// The scale (σ) of the distribution. Range: σ > 0. /// The skew, 1 > λ > -1 /// First parameter that controls kurtosis. Range: p > 0 /// Second parameter that controls kurtosis. Range: q > 0 /// The location at which to compute the density. /// the density at . /// public static double PDFLn(double location, double scale, double skew, double p, double q, double x) { if (!IsValidParameterSet(location, scale, skew, p, q)) { throw new ArgumentException("Invalid parametrization for the distribution."); } var fn = PDFunc(location, scale, skew, p, q, true); return fn(x); } static double PDFull(double location, double scale, double skew, double p, double q, double x) { scale = AdjustScale(scale, skew, p, q); x = AdjustX(x, scale, skew, p, q); var b = SpecialFunctions.Beta(1.0 / p, q); var skewSign = Math.Sign(x - location); var d1 = Math.Pow(Math.Abs(x - location), p); var d2 = q * Math.Pow(scale, p) * Math.Pow(skew * skewSign + 1.0, p); var denominator = 2.0 * scale * Math.Pow(q, 1.0 / p) * b * Math.Pow(d1 / d2 + 1.0, 1.0 / p + q); return p / denominator; } static double PDFullLn(double location, double scale, double skew, double p, double q, double x) { scale = AdjustScale(scale, skew, p, q); x = AdjustX(x, scale, skew, p, q); var bLn = SpecialFunctions.BetaLn(1.0 / p, q); return Math.Log(p) - Math.Log(2.0) - Math.Log(scale) - Math.Log(q) / p - bLn - (1.0 / p + q) * Math.Log(1.0 + Math.Pow(Math.Abs(x - location), p) / (q * Math.Pow(scale, p) * Math.Pow(1.0 + skew * Math.Sign(x - location), p))); } // For known parameterizations we just use the existing distributions as visualized // by Hansen, McDonald and Newey (2010). // Note that, for all cases where skew is required to be 0, if skew is non-zero, this // simply gives the corresponding skewed version of the distribution. static Func PDFunc(double location, double scale, double skew, double p, double q, bool ln) { if (p == double.PositiveInfinity) { scale *= Math.Sqrt(3.0); return x => ln ? ContinuousUniform.PDFLn(location - scale, location + scale, x) : ContinuousUniform.PDF(-1.0 * (Math.Sqrt(3.0) * scale + location), Math.Sqrt(3.0) * scale + location, x); } if (q == double.PositiveInfinity) return x => ln ? SkewedGeneralizedError.PDFLn(location, scale, skew, p, x) : SkewedGeneralizedError.PDF(location, scale, skew, p, x); return x => ln ? PDFullLn(location, scale, skew, p, q, x) : PDFull(location, scale, skew, p, q, x); } /// /// Computes the cumulative distribution (CDF) of the distribution at x, i.e. P(X ≤ x). /// /// The location at which to compute the cumulative distribution function. /// The location (μ) of the distribution. /// The scale (σ) of the distribution. Range: σ > 0. /// The skew, 1 > λ > -1 /// First parameter that controls kurtosis. Range: p > 0 /// Second parameter that controls kurtosis. Range: q > 0 /// the cumulative distribution at location . /// public static double CDF(double location, double scale, double skew, double p, double q, double x) { if (!IsValidParameterSet(location, scale, skew, p, q)) { throw new ArgumentException("Invalid parametrization for the distribution."); } // Note: Adapted from the R package, // based on a transformation of the cumulative probability density function that uses the // incomplete beta function or incomplete gamma function. scale = AdjustScale(scale, skew, p, q); x = AdjustX(x, scale, skew, p, q) - location; var flip = x > 0; if (flip) { skew = -skew; x = -x; } var res = (1.0 - skew) / 2.0 + (skew - 1.0) / 2.0 * Beta.CDF(1.0 / p, q, 1.0 / (1.0 + q * Math.Pow(scale * (1.0 - skew) / -x, p))); return flip ? 1.0 - res : res; } /// /// Computes the inverse of the cumulative distribution function (InvCDF) for the distribution /// at the given probability. This is also known as the quantile or percent point function. /// /// The location at which to compute the inverse cumulative density. /// The location (μ) of the distribution. /// The scale (σ) of the distribution. Range: σ > 0. /// The skew, 1 > λ > -1 /// First parameter that controls kurtosis. Range: p > 0 /// Second parameter that controls kurtosis. Range: q > 0 /// the inverse cumulative density at . /// public static double InvCDF(double location, double scale, double skew, double p, double q, double pr) { if (!IsValidParameterSet(location, scale, skew, p, q)) { throw new ArgumentException("Invalid parametrization for the distribution."); } // If parameters represent a specialized distribution, then we use that distribution to avoid // problems with infinite p or q parameters. var d = FindSpecializedDistribution(location, scale, skew, p, q); // InverseCumulativeDistribution is not a part of the interface, so resort to type-checking. if (d != null) { switch (d) { case SkewedGeneralizedError sge: return sge.InverseCumulativeDistribution(pr); case ContinuousUniform u: return u.InverseCumulativeDistribution(pr); } } // Note: Adapted from the R package, // solving for the inverse of the CDF that uses the inverse of the incomplete beta function or // incomplete gamma function scale = AdjustScale(scale, skew, p, q); var flip = pr > (1.0 - skew) / 2.0; var lambda = skew; if (flip) { pr = 1.0 - pr; lambda = -lambda; } var res = scale * (lambda - 1.0) * Math.Pow(1.0 / (q * Beta.InvCDF(1.0 / p, q, 1.0 - 2.0 * pr / (1.0 - lambda))) - 1.0 / q, -1.0 / p); if (flip) res = -res; res += location; return res - AdjustAddend(scale, skew, p, q); } public double CumulativeDistribution(double x) { return _d?.CumulativeDistribution(x) ?? CDF(Location, Scale, Skew, P, Q, x); } /// /// Computes the inverse of the cumulative distribution function (InvCDF) for the distribution /// at the given probability. This is also known as the quantile or percent point function. /// /// The location at which to compute the inverse cumulative density. /// the inverse cumulative density at . /// public double InverseCumulativeDistribution(double p) { // InverseCumulativeDistribution is not a part of the interface, so resort to type-checking. if (_d != null) { switch (_d) { case SkewedGeneralizedError sge: return sge.InverseCumulativeDistribution(p); case ContinuousUniform u: return u.InverseCumulativeDistribution(p); } } return InvCDF(Location, Scale, Skew, P, Q, p); } public double Density(double x) { return _d?.Density(x) ?? PDF(Location, Scale, Skew, P, Q, x); } public double DensityLn(double x) { return _d?.DensityLn(x) ?? PDFLn(Location, Scale, Skew, P, Q, x); } public double Sample() { return SampleUnchecked(_random, Location, Scale, Skew, P, Q); } public void Samples(double[] values) { SamplesUnchecked(_random, values, Location, Scale, Skew, P, Q); } public IEnumerable Samples() { return SamplesUnchecked(_random, Location, Scale, Skew, P, Q); } static double SampleUnchecked(System.Random rnd, double location, double scale, double skew, double p, double q) { var u = ContinuousUniform.Sample(rnd, 0, 1); return InvCDF(location, scale, skew, p, q, u); } static void SamplesUnchecked(System.Random rnd, double[] values, double location, double scale, double skew, double p, double q) { for (int i = 0; i < values.Length; i++) { values[i] = SampleUnchecked(rnd, location, scale, skew, p, q); } } static IEnumerable SamplesUnchecked(System.Random rnd, double location, double scale, double skew, double p, double q) { while (true) { yield return SampleUnchecked(rnd, location, scale, skew, p, q); } } /// /// Generates a sample from the Skew Generalized t-distribution. /// /// The random number generator to use. /// The location (μ) of the distribution. /// The scale (σ) of the distribution. Range: σ > 0. /// The skew, 1 > λ > -1 /// First parameter that controls kurtosis. Range: p > 0 /// Second parameter that controls kurtosis. Range: q > 0 /// a sample from the distribution. public static double Sample(System.Random rnd, double location, double scale, double skew, double p, double q) { if (!IsValidParameterSet(location, scale, skew, p, q)) { throw new ArgumentException("Invalid parametrization for the distribution."); } return SampleUnchecked(rnd, location, scale, skew, p, q); } /// /// Generates a sequence of samples from the Skew Generalized t-distribution using inverse transform. /// /// The random number generator to use. /// The location (μ) of the distribution. /// The scale (σ) of the distribution. Range: σ > 0. /// The skew, 1 > λ > -1 /// First parameter that controls kurtosis. Range: p > 0 /// Second parameter that controls kurtosis. Range: q > 0 /// a sequence of samples from the distribution. public static IEnumerable Samples(System.Random rnd, double location, double scale, double skew, double p, double q) { if (!IsValidParameterSet(location, scale, skew, p, q)) { throw new ArgumentException("Invalid parametrization for the distribution."); } return SamplesUnchecked(rnd, location, scale, skew, p, q); } /// /// Fills an array with samples from the Skew Generalized t-distribution using inverse transform. /// /// The random number generator to use. /// The array to fill with the samples. /// The location (μ) of the distribution. /// The scale (σ) of the distribution. Range: σ > 0. /// The skew, 1 > λ > -1 /// First parameter that controls kurtosis. Range: p > 0 /// Second parameter that controls kurtosis. Range: q > 0 /// a sequence of samples from the distribution. public static void Samples(System.Random rnd, double[] values, double location, double scale, double skew, double p, double q) { if (!IsValidParameterSet(location, scale, skew, p, q)) { throw new ArgumentException("Invalid parametrization for the distribution."); } SamplesUnchecked(rnd, values, location, scale, skew, p, q); } /// /// Generates a sample from the Skew Generalized t-distribution. /// /// The location (μ) of the distribution. /// The scale (σ) of the distribution. Range: σ > 0. /// The skew, 1 > λ > -1 /// First parameter that controls kurtosis. Range: p > 0 /// Second parameter that controls kurtosis. Range: q > 0 /// a sample from the distribution. public static double Sample(double location, double scale, double skew, double p, double q) { if (!IsValidParameterSet(location, scale, skew, p, q)) { throw new ArgumentException("Invalid parametrization for the distribution."); } return SampleUnchecked(SystemRandomSource.Default, location, scale, skew, p, q); } /// /// Generates a sequence of samples from the Skew Generalized t-distribution using inverse transform. /// /// The location (μ) of the distribution. /// The scale (σ) of the distribution. Range: σ > 0. /// The skew, 1 > λ > -1 /// First parameter that controls kurtosis. Range: p > 0 /// Second parameter that controls kurtosis. Range: q > 0 /// a sequence of samples from the distribution. public static IEnumerable Samples(double location, double scale, double skew, double p, double q) { if (!IsValidParameterSet(location, scale, skew, p, q)) { throw new ArgumentException("Invalid parametrization for the distribution."); } return SamplesUnchecked(SystemRandomSource.Default, location, scale, skew, p, q); } /// /// Fills an array with samples from the Skew Generalized t-distribution using inverse transform. /// /// The array to fill with the samples. /// The location (μ) of the distribution. /// The scale (σ) of the distribution. Range: σ > 0. /// The skew, 1 > λ > -1 /// First parameter that controls kurtosis. Range: p > 0 /// Second parameter that controls kurtosis. Range: q > 0 /// a sequence of samples from the distribution. public static void Samples(double[] values, double location, double scale, double skew, double p, double q) { if (!IsValidParameterSet(location, scale, skew, p, q)) { throw new ArgumentException("Invalid parametrization for the distribution."); } SamplesUnchecked(SystemRandomSource.Default, values, location, scale, skew, p, q); } } }