TDistribution.java
- /*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package org.apache.commons.statistics.distribution;
- import org.apache.commons.numbers.gamma.Beta;
- import org.apache.commons.numbers.gamma.LogBeta;
- import org.apache.commons.numbers.gamma.RegularizedBeta;
- import org.apache.commons.rng.UniformRandomProvider;
- import org.apache.commons.rng.sampling.distribution.TSampler;
- /**
- * Implementation of Student's t-distribution.
- *
- * <p>The probability density function of \( X \) is:
- *
- * <p>\[ f(x; v) = \frac{\Gamma(\frac{\nu+1}{2})} {\sqrt{\nu\pi}\,\Gamma(\frac{\nu}{2})} \left(1+\frac{t^2}{\nu} \right)^{\!-\frac{\nu+1}{2}} \]
- *
- * <p>for \( v > 0 \) the degrees of freedom,
- * \( \Gamma \) is the gamma function, and
- * \( x \in (-\infty, \infty) \).
- *
- * @see <a href="https://en.wikipedia.org/wiki/Student%27s_t-distribution">Student's t-distribution (Wikipedia)</a>
- * @see <a href="https://mathworld.wolfram.com/Studentst-Distribution.html">Student's t-distribution (MathWorld)</a>
- */
- public abstract class TDistribution extends AbstractContinuousDistribution {
- /** The degrees of freedom. */
- private final double degreesOfFreedom;
- /**
- * Specialisation of the T-distribution used when there are infinite degrees of freedom.
- * In this case the distribution matches a normal distribution. This is used when the
- * variance is not different from 1.0.
- *
- * <p>This delegates all methods to the standard normal distribution. Instances are
- * allowed to provide access to the degrees of freedom used during construction.
- */
- private static class NormalTDistribution extends TDistribution {
- /** A standard normal distribution used for calculations.
- * This is immutable and thread-safe and can be used across instances. */
- private static final NormalDistribution STANDARD_NORMAL = NormalDistribution.of(0, 1);
- /**
- * @param degreesOfFreedom Degrees of freedom.
- */
- NormalTDistribution(double degreesOfFreedom) {
- super(degreesOfFreedom);
- }
- @Override
- public double density(double x) {
- return STANDARD_NORMAL.density(x);
- }
- @Override
- public double probability(double x0, double x1) {
- return STANDARD_NORMAL.probability(x0, x1);
- }
- @Override
- public double logDensity(double x) {
- return STANDARD_NORMAL.logDensity(x);
- }
- @Override
- public double cumulativeProbability(double x) {
- return STANDARD_NORMAL.cumulativeProbability(x);
- }
- @Override
- public double inverseCumulativeProbability(double p) {
- return STANDARD_NORMAL.inverseCumulativeProbability(p);
- }
- // Survival probability functions inherit the symmetry operations from the TDistribution
- @Override
- public double getMean() {
- return 0;
- }
- @Override
- public double getVariance() {
- return 1.0;
- }
- @Override
- public Sampler createSampler(UniformRandomProvider rng) {
- return STANDARD_NORMAL.createSampler(rng);
- }
- }
- /**
- * Implementation of Student's T-distribution.
- */
- private static class StudentsTDistribution extends TDistribution {
- /** 2. */
- private static final double TWO = 2;
- /** The threshold for the density function where the
- * power function base minus 1 is close to zero. */
- private static final double CLOSE_TO_ZERO = 0.25;
- /** -(v + 1) / 2, where v = degrees of freedom. */
- private final double mvp1Over2;
- /** Density normalisation factor, sqrt(v) * beta(1/2, v/2), where v = degrees of freedom. */
- private final double densityNormalisation;
- /** Log density normalisation term, 0.5 * log(v) + log(beta(1/2, v/2)), where v = degrees of freedom. */
- private final double logDensityNormalisation;
- /** Cached value for inverse probability function. */
- private final double mean;
- /** Cached value for inverse probability function. */
- private final double variance;
- /**
- * @param degreesOfFreedom Degrees of freedom.
- * @param variance Precomputed variance
- */
- StudentsTDistribution(double degreesOfFreedom, double variance) {
- super(degreesOfFreedom);
- mvp1Over2 = -0.5 * (degreesOfFreedom + 1);
- densityNormalisation = Math.sqrt(degreesOfFreedom) * Beta.value(0.5, degreesOfFreedom / 2);
- logDensityNormalisation = 0.5 * Math.log(degreesOfFreedom) + LogBeta.value(0.5, degreesOfFreedom / 2);
- mean = degreesOfFreedom > 1 ? 0 : Double.NaN;
- this.variance = variance;
- }
- /**
- * @param degreesOfFreedom Degrees of freedom.
- * @return the variance
- */
- static double computeVariance(double degreesOfFreedom) {
- if (degreesOfFreedom == Double.POSITIVE_INFINITY) {
- return 1;
- } else if (degreesOfFreedom > TWO) {
- return degreesOfFreedom / (degreesOfFreedom - 2);
- } else if (degreesOfFreedom > 1) {
- return Double.POSITIVE_INFINITY;
- } else {
- return Double.NaN;
- }
- }
- @Override
- public double density(double x) {
- // 1 -(v+1)/2
- // ------------------- * (1 + t^2/v)
- // sqrt(v) B(1/2, v/2)
- final double t2OverV = x * x / getDegreesOfFreedom();
- if (t2OverV < CLOSE_TO_ZERO) {
- // Avoid power function when the base is close to 1
- return Math.exp(Math.log1p(t2OverV) * mvp1Over2) / densityNormalisation;
- }
- return Math.pow(1 + t2OverV, mvp1Over2) / densityNormalisation;
- }
- @Override
- public double logDensity(double x) {
- return Math.log1p(x * x / getDegreesOfFreedom()) * mvp1Over2 - logDensityNormalisation;
- }
- @Override
- public double cumulativeProbability(double x) {
- if (x == 0) {
- return 0.5;
- }
- final double v = getDegreesOfFreedom();
- // cdf(t) = 1 - 0.5 * I_x(t)(v/2, 1/2)
- // where x(t) = v / (v + t^2)
- //
- // When t^2 << v using the regularized beta results
- // in loss of precision. Use the complement instead:
- // I[x](a,b) = 1 - I[1-x](b,a)
- // x = v / (v + t^2)
- // 1-x = t^2 / (v + t^2)
- // Use the threshold documented in the Boost t-distribution:
- // https://www.boost.org/doc/libs/1_78_0/libs/math/doc/html/math_toolkit/dist_ref/dists/students_t_dist.html
- final double t2 = x * x;
- final double z;
- if (v < 2 * t2) {
- z = RegularizedBeta.value(v / (v + t2), v / 2, 0.5) / 2;
- } else {
- z = RegularizedBeta.complement(t2 / (v + t2), 0.5, v / 2) / 2;
- }
- return x > 0 ? 1 - z : z;
- }
- @Override
- public double getMean() {
- return mean;
- }
- @Override
- public double getVariance() {
- return variance;
- }
- @Override
- double getMedian() {
- // Overridden for the probability(double, double) method.
- // This is intentionally not a public method.
- return 0;
- }
- @Override
- public Sampler createSampler(UniformRandomProvider rng) {
- // T distribution sampler.
- return TSampler.of(rng, getDegreesOfFreedom())::sample;
- }
- }
- /**
- * @param degreesOfFreedom Degrees of freedom.
- */
- TDistribution(double degreesOfFreedom) {
- this.degreesOfFreedom = degreesOfFreedom;
- }
- /**
- * Creates a Student's t-distribution.
- *
- * @param degreesOfFreedom Degrees of freedom.
- * @return the distribution
- * @throws IllegalArgumentException if {@code degreesOfFreedom <= 0}
- */
- public static TDistribution of(double degreesOfFreedom) {
- if (degreesOfFreedom <= 0) {
- throw new DistributionException(DistributionException.NOT_STRICTLY_POSITIVE,
- degreesOfFreedom);
- }
- // If the variance converges to 1 use a NormalDistribution.
- // Occurs at 2^55 = 3.60e16
- final double variance = StudentsTDistribution.computeVariance(degreesOfFreedom);
- if (variance == 1) {
- return new NormalTDistribution(degreesOfFreedom);
- }
- return new StudentsTDistribution(degreesOfFreedom, variance);
- }
- /**
- * Gets the degrees of freedom parameter of this distribution.
- *
- * @return the degrees of freedom.
- */
- public double getDegreesOfFreedom() {
- return degreesOfFreedom;
- }
- /** {@inheritDoc} */
- @Override
- public double survivalProbability(double x) {
- // Exploit symmetry
- return cumulativeProbability(-x);
- }
- /** {@inheritDoc} */
- @Override
- public double inverseSurvivalProbability(double p) {
- // Exploit symmetry
- // Subtract from 0 avoids returning -0.0 for p=0.5
- return 0.0 - inverseCumulativeProbability(p);
- }
- /**
- * {@inheritDoc}
- *
- * <p>For degrees of freedom parameter \( v \), the mean is:
- *
- * <p>\[ \mathbb{E}[X] = \begin{cases}
- * 0 & \text{for } v \gt 1 \\
- * \text{undefined} & \text{otherwise}
- * \end{cases} \]
- *
- * @return the mean, or {@link Double#NaN NaN} if it is not defined.
- */
- @Override
- public abstract double getMean();
- /**
- * {@inheritDoc}
- *
- * <p>For degrees of freedom parameter \( v \), the variance is:
- *
- * <p>\[ \operatorname{var}[X] = \begin{cases}
- * \frac{v}{v - 2} & \text{for } v \gt 2 \\
- * \infty & \text{for } 1 \lt v \le 2 \\
- * \text{undefined} & \text{otherwise}
- * \end{cases} \]
- *
- * @return the variance, or {@link Double#NaN NaN} if it is not defined.
- */
- @Override
- public abstract double getVariance();
- /**
- * {@inheritDoc}
- *
- * <p>The lower bound of the support is always negative infinity.
- *
- * @return {@linkplain Double#NEGATIVE_INFINITY negative infinity}.
- */
- @Override
- public double getSupportLowerBound() {
- return Double.NEGATIVE_INFINITY;
- }
- /**
- * {@inheritDoc}
- *
- * <p>The upper bound of the support is always positive infinity.
- *
- * @return {@linkplain Double#POSITIVE_INFINITY positive infinity}.
- */
- @Override
- public double getSupportUpperBound() {
- return Double.POSITIVE_INFINITY;
- }
- }