TTest.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.statistics.inference;

  18. import java.util.EnumSet;
  19. import java.util.Objects;
  20. import org.apache.commons.statistics.descriptive.DoubleStatistics;
  21. import org.apache.commons.statistics.descriptive.Statistic;
  22. import org.apache.commons.statistics.distribution.TDistribution;

  23. /**
  24.  * Implements Student's t-test statistics.
  25.  *
  26.  * <p>Tests can be:
  27.  * <ul>
  28.  * <li>One-sample or two-sample
  29.  * <li>One-sided or two-sided
  30.  * <li>Paired or unpaired (for two-sample tests)
  31.  * <li>Homoscedastic (equal variance assumption) or heteroscedastic (for two sample tests)
  32.  * </ul>
  33.  *
  34.  * <p>Input to tests can be either {@code double[]} arrays or the mean, variance, and size
  35.  * of the sample.
  36.  *
  37.  * @see <a href="https://en.wikipedia.org/wiki/Student%27s_t-test">Student&#39;s t-test (Wikipedia)</a>
  38.  * @since 1.1
  39.  */
  40. public final class TTest {
  41.     /** Default instance. */
  42.     private static final TTest DEFAULT = new TTest(AlternativeHypothesis.TWO_SIDED, false, 0);

  43.     /** Alternative hypothesis. */
  44.     private final AlternativeHypothesis alternative;
  45.     /** Assume the two samples have the same population variance. */
  46.     private final boolean equalVariances;
  47.     /** The true value of the mean (or difference in means for a two sample test). */
  48.     private final double mu;

  49.     /**
  50.      * Result for the t-test.
  51.      *
  52.      * <p>This class is immutable.
  53.      */
  54.     public static final class Result extends BaseSignificanceResult {
  55.         /** Degrees of freedom. */
  56.         private final double degreesOfFreedom;

  57.         /**
  58.          * Create an instance.
  59.          *
  60.          * @param statistic Test statistic.
  61.          * @param degreesOfFreedom Degrees of freedom.
  62.          * @param p Result p-value.
  63.          */
  64.         Result(double statistic, double degreesOfFreedom, double p) {
  65.             super(statistic, p);
  66.             this.degreesOfFreedom = degreesOfFreedom;
  67.         }

  68.         /**
  69.          * Gets the degrees of freedom.
  70.          *
  71.          * @return the degrees of freedom
  72.          */
  73.         public double getDegreesOfFreedom() {
  74.             return degreesOfFreedom;
  75.         }
  76.     }

  77.     /**
  78.      * @param alternative Alternative hypothesis.
  79.      * @param equalVariances Assume the two samples have the same population variance.
  80.      * @param mu true value of the mean (or difference in means for a two sample test).
  81.      */
  82.     private TTest(AlternativeHypothesis alternative, boolean equalVariances, double mu) {
  83.         this.alternative = alternative;
  84.         this.equalVariances = equalVariances;
  85.         this.mu = mu;
  86.     }

  87.     /**
  88.      * Return an instance using the default options.
  89.      *
  90.      * <ul>
  91.      * <li>{@link AlternativeHypothesis#TWO_SIDED}
  92.      * <li>{@link DataDispersion#HETEROSCEDASTIC}
  93.      * <li>{@linkplain #withMu(double) mu = 0}
  94.      * </ul>
  95.      *
  96.      * @return default instance
  97.      */
  98.     public static TTest withDefaults() {
  99.         return DEFAULT;
  100.     }

  101.     /**
  102.      * Return an instance with the configured alternative hypothesis.
  103.      *
  104.      * @param v Value.
  105.      * @return an instance
  106.      */
  107.     public TTest with(AlternativeHypothesis v) {
  108.         return new TTest(Objects.requireNonNull(v), equalVariances, mu);
  109.     }

  110.     /**
  111.      * Return an instance with the configured assumption on the data dispersion.
  112.      *
  113.      * <p>Applies to the two-sample independent t-test.
  114.      * The statistic can compare the means without the assumption of equal
  115.      * sub-population variances (heteroscedastic); otherwise the means are compared
  116.      * under the assumption of equal sub-population variances (homoscedastic).
  117.      *
  118.      * @param v Value.
  119.      * @return an instance
  120.      * @see #test(double[], double[])
  121.      * @see #test(double, double, long, double, double, long)
  122.      */
  123.     public TTest with(DataDispersion v) {
  124.         return new TTest(alternative, Objects.requireNonNull(v) == DataDispersion.HOMOSCEDASTIC, mu);
  125.     }

  126.     /**
  127.      * Return an instance with the configured {@code mu}.
  128.      *
  129.      * <p>For the one-sample test this is the expected mean.
  130.      *
  131.      * <p>For the two-sample test this is the expected difference between the means.
  132.      *
  133.      * @param v Value.
  134.      * @return an instance
  135.      * @throws IllegalArgumentException if the value is not finite
  136.      */
  137.     public TTest withMu(double v) {
  138.         return new TTest(alternative, equalVariances, Arguments.checkFinite(v));
  139.     }

  140.     /**
  141.      * Computes a one-sample t statistic comparing the mean of the dataset to {@code mu}.
  142.      *
  143.      * <p>The returned t-statistic is:
  144.      *
  145.      * <p>\[ t = \frac{m - \mu}{ \sqrt{ \frac{v}{n} } } \]
  146.      *
  147.      * @param m Sample mean.
  148.      * @param v Sample variance.
  149.      * @param n Sample size.
  150.      * @return t statistic
  151.      * @throws IllegalArgumentException if the number of samples is {@code < 2}; or the
  152.      * variance is negative
  153.      * @see #withMu(double)
  154.      */
  155.     public double statistic(double m, double v, long n) {
  156.         Arguments.checkNonNegative(v);
  157.         checkSampleSize(n);
  158.         return computeT(m - mu, v, n);
  159.     }

  160.     /**
  161.      * Computes a one-sample t statistic comparing the mean of the sample to {@code mu}.
  162.      *
  163.      * @param x Sample values.
  164.      * @return t statistic
  165.      * @throws IllegalArgumentException if the number of samples is {@code < 2}
  166.      * @see #statistic(double, double, long)
  167.      * @see #withMu(double)
  168.      */
  169.     public double statistic(double[] x) {
  170.         final long n = checkSampleSize(x.length);
  171.         final DoubleStatistics s = DoubleStatistics.of(
  172.             EnumSet.of(Statistic.MEAN, Statistic.VARIANCE), x);
  173.         final double m = s.getAsDouble(Statistic.MEAN);
  174.         final double v = s.getAsDouble(Statistic.VARIANCE);
  175.         return computeT(m - mu, v, n);
  176.     }

  177.     /**
  178.      * Computes a paired two-sample t-statistic on related samples comparing the mean difference
  179.      * between the samples to {@code mu}.
  180.      *
  181.      * <p>The t-statistic returned is functionally equivalent to what would be returned by computing
  182.      * the one-sample t-statistic {@link #statistic(double[])}, with
  183.      * the sample array consisting of the (signed) differences between corresponding
  184.      * entries in {@code x} and {@code y}.
  185.      *
  186.      * @param x First sample values.
  187.      * @param y Second sample values.
  188.      * @return t statistic
  189.      * @throws IllegalArgumentException if the number of samples is {@code < 2}; or the
  190.      * the size of the samples is not equal
  191.      * @see #withMu(double)
  192.      */
  193.     public double pairedStatistic(double[] x, double[] y) {
  194.         final long n = checkSampleSize(x.length);
  195.         final double m = StatisticUtils.meanDifference(x, y);
  196.         final double v = StatisticUtils.varianceDifference(x, y, m);
  197.         return computeT(m - mu, v, n);
  198.     }

  199.     /**
  200.      * Computes a two-sample t statistic on independent samples comparing the difference in means
  201.      * of the datasets to {@code mu}.
  202.      *
  203.      * <p>Use the {@link DataDispersion} to control the computation of the variance.
  204.      *
  205.      * <p>The heteroscedastic t-statistic is:
  206.      *
  207.      * <p>\[ t = \frac{m1 - m2 - \mu}{ \sqrt{ \frac{v_1}{n_1} + \frac{v_2}{n_2} } } \]
  208.      *
  209.      * <p>The homoscedastic t-statistic is:
  210.      *
  211.      * <p>\[ t = \frac{m1 - m2 - \mu}{ \sqrt{ v (\frac{1}{n_1} + \frac{1}{n_2}) } } \]
  212.      *
  213.      * <p>where \( v \) is the pooled variance estimate:
  214.      *
  215.      * <p>\[ v = \frac{(n_1-1)v_1 + (n_2-1)v_2}{n_1 + n_2 - 2} \]
  216.      *
  217.      * @param m1 First sample mean.
  218.      * @param v1 First sample variance.
  219.      * @param n1 First sample size.
  220.      * @param m2 Second sample mean.
  221.      * @param v2 Second sample variance.
  222.      * @param n2 Second sample size.
  223.      * @return t statistic
  224.      * @throws IllegalArgumentException if the number of samples in either dataset is
  225.      * {@code < 2}; or the variances are negative.
  226.      * @see #withMu(double)
  227.      * @see #with(DataDispersion)
  228.      */
  229.     public double statistic(double m1, double v1, long n1,
  230.                             double m2, double v2, long n2) {
  231.         Arguments.checkNonNegative(v1);
  232.         Arguments.checkNonNegative(v2);
  233.         checkSampleSize(n1);
  234.         checkSampleSize(n2);
  235.         return equalVariances ?
  236.             computeHomoscedasticT(mu, m1, v1, n1, m2, v2, n2) :
  237.             computeT(mu, m1, v1, n1, m2, v2, n2);
  238.     }

  239.     /**
  240.      * Computes a two-sample t statistic on independent samples comparing the difference
  241.      * in means of the samples to {@code mu}.
  242.      *
  243.      * <p>Use the {@link DataDispersion} to control the computation of the variance.
  244.      *
  245.      * @param x First sample values.
  246.      * @param y Second sample values.
  247.      * @return t statistic
  248.      * @throws IllegalArgumentException if the number of samples in either dataset is {@code < 2}
  249.      * @see #withMu(double)
  250.      * @see #with(DataDispersion)
  251.      */
  252.     public double statistic(double[] x, double[] y) {
  253.         final long n1 = checkSampleSize(x.length);
  254.         final long n2 = checkSampleSize(y.length);
  255.         final DoubleStatistics.Builder b = DoubleStatistics.builder(Statistic.MEAN, Statistic.VARIANCE);
  256.         final DoubleStatistics s1 = b.build(x);
  257.         final double m1 = s1.getAsDouble(Statistic.MEAN);
  258.         final double v1 = s1.getAsDouble(Statistic.VARIANCE);
  259.         final DoubleStatistics s2 = b.build(y);
  260.         final double m2 = s2.getAsDouble(Statistic.MEAN);
  261.         final double v2 = s2.getAsDouble(Statistic.VARIANCE);
  262.         return equalVariances ?
  263.             computeHomoscedasticT(mu, m1, v1, n1, m2, v2, n2) :
  264.             computeT(mu, m1, v1, n1, m2, v2, n2);
  265.     }

  266.     /**
  267.      * Perform a one-sample t-test comparing the mean of the dataset to {@code mu}.
  268.      *
  269.      * <p>Degrees of freedom are \( v = n - 1 \).
  270.      *
  271.      * @param m Sample mean.
  272.      * @param v Sample variance.
  273.      * @param n Sample size.
  274.      * @return test result
  275.      * @throws IllegalArgumentException if the number of samples is {@code < 2}; or the
  276.      * variance is negative
  277.      * @see #statistic(double, double, long)
  278.      */
  279.     public Result test(double m, double v, long n) {
  280.         final double t = statistic(m, v, n);
  281.         final double df = n - 1.0;
  282.         final double p = computeP(t, df);
  283.         return new Result(t, df, p);
  284.     }

  285.     /**
  286.      * Performs a one-sample t-test comparing the mean of the sample to {@code mu}.
  287.      *
  288.      * <p>Degrees of freedom are \( v = n - 1 \).
  289.      *
  290.      * @param sample Sample values.
  291.      * @return the test result
  292.      * @throws IllegalArgumentException if the number of samples is {@code < 2}; or the
  293.      * the size of the samples is not equal
  294.      * @see #statistic(double[])
  295.      */
  296.     public Result test(double[] sample) {
  297.         final double t = statistic(sample);
  298.         final double df = sample.length - 1.0;
  299.         final double p = computeP(t, df);
  300.         return new Result(t, df, p);
  301.     }

  302.     /**
  303.      * Performs a paired two-sample t-test on related samples comparing the mean difference between
  304.      * the samples to {@code mu}.
  305.      *
  306.      * <p>The test is functionally equivalent to what would be returned by computing
  307.      * the one-sample t-test {@link #test(double[])}, with
  308.      * the sample array consisting of the (signed) differences between corresponding
  309.      * entries in {@code x} and {@code y}.
  310.      *
  311.      * @param x First sample values.
  312.      * @param y Second sample values.
  313.      * @return the test result
  314.      * @throws IllegalArgumentException if the number of samples is {@code < 2}; or the
  315.      * the size of the samples is not equal
  316.      * @see #pairedStatistic(double[], double[])
  317.      */
  318.     public Result pairedTest(double[] x, double[] y) {
  319.         final double t = pairedStatistic(x, y);
  320.         final double df = x.length - 1.0;
  321.         final double p = computeP(t, df);
  322.         return new Result(t, df, p);
  323.     }

  324.     /**
  325.      * Performs a two-sample t-test on independent samples comparing the difference in means of the
  326.      * datasets to {@code mu}.
  327.      *
  328.      * <p>Use the {@link DataDispersion} to control the computation of the variance.
  329.      *
  330.      * <p>The heteroscedastic degrees of freedom are estimated using the
  331.      * Welch-Satterthwaite approximation:
  332.      *
  333.      * <p>\[ v = \frac{ (\frac{v_1}{n_1} + \frac{v_2}{n_2})^2 }
  334.      *                { \frac{(v_1/n_1)^2}{n_1-1} + \frac{(v_2/n_2)^2}{n_2-1} } \]
  335.      *
  336.      * <p>The homoscedastic degrees of freedom are \( v = n_1 + n_2 - 2 \).
  337.      *
  338.      * @param m1 First sample mean.
  339.      * @param v1 First sample variance.
  340.      * @param n1 First sample size.
  341.      * @param m2 Second sample mean.
  342.      * @param v2 Second sample variance.
  343.      * @param n2 Second sample size.
  344.      * @return test result
  345.      * @throws IllegalArgumentException if the number of samples in either dataset is
  346.      * {@code < 2}; or the variances are negative.
  347.      * @see #statistic(double, double, long, double, double, long)
  348.      */
  349.     public Result test(double m1, double v1, long n1,
  350.                        double m2, double v2, long n2) {
  351.         final double t = statistic(m1, v1, n1, m2, v2, n2);
  352.         final double df = equalVariances ?
  353.                 -2.0 + n1 + n2 :
  354.                 computeDf(v1, n1, v2, n2);
  355.         final double p = computeP(t, df);
  356.         return new Result(t, df, p);
  357.     }

  358.     /**
  359.      * Performs a two-sample t-test on independent samples comparing the difference in means of
  360.      * the samples to {@code mu}.
  361.      *
  362.      * <p>Use the {@link DataDispersion} to control the computation of the variance.
  363.      *
  364.      * @param x First sample values.
  365.      * @param y Second sample values.
  366.      * @return the test result
  367.      * @throws IllegalArgumentException if the number of samples in either dataset
  368.      * is {@code < 2}
  369.      * @see #statistic(double[], double[])
  370.      * @see #test(double, double, long, double, double, long)
  371.      */
  372.     public Result test(double[] x, double[] y) {
  373.         // Here we do not call statistic(double[], double[]) because the degreesOfFreedom
  374.         // requires the variance. So repeat the computation and compute p.
  375.         final long n1 = checkSampleSize(x.length);
  376.         final long n2 = checkSampleSize(y.length);
  377.         final DoubleStatistics.Builder b = DoubleStatistics.builder(Statistic.MEAN, Statistic.VARIANCE);
  378.         final DoubleStatistics s1 = b.build(x);
  379.         final double m1 = s1.getAsDouble(Statistic.MEAN);
  380.         final double v1 = s1.getAsDouble(Statistic.VARIANCE);
  381.         final DoubleStatistics s2 = b.build(y);
  382.         final double m2 = s2.getAsDouble(Statistic.MEAN);
  383.         final double v2 = s2.getAsDouble(Statistic.VARIANCE);
  384.         final double t;
  385.         final double df;
  386.         if (equalVariances) {
  387.             t = computeHomoscedasticT(mu, m1, v1, n1, m2, v2, n2);
  388.             df = -2.0 + n1 + n2;
  389.         } else {
  390.             t = computeT(mu, m1, v1, n1, m2, v2, n2);
  391.             df = computeDf(v1, n1, v2, n2);
  392.         }
  393.         final double p = computeP(t, df);
  394.         return new Result(t, df, p);
  395.     }

  396.     /**
  397.      * Computes t statistic for one-sample t-test.
  398.      *
  399.      * @param m Sample mean.
  400.      * @param v Sample variance.
  401.      * @param n Sample size.
  402.      * @return t test statistic
  403.      */
  404.     private static double computeT(double m, double v, long n) {
  405.         return m / Math.sqrt(v / n);
  406.     }

  407.     /**
  408.      * Computes t statistic for two-sample t-test without the assumption of equal
  409.      * samples sizes or sub-population variances.
  410.      *
  411.      * @param mu Expected difference between means.
  412.      * @param m1 First sample mean.
  413.      * @param v1 First sample variance.
  414.      * @param n1 First sample size.
  415.      * @param m2 Second sample mean.
  416.      * @param v2 Second sample variance.
  417.      * @param n2 Second sample size.
  418.      * @return t test statistic
  419.      */
  420.     private static double computeT(double mu,
  421.                                    double m1, double v1, long n1,
  422.                                    double m2, double v2, long n2)  {
  423.         return (m1 - m2 - mu) / Math.sqrt((v1 / n1) + (v2 / n2));
  424.     }

  425.     /**
  426.      * Computes approximate degrees of freedom for two-sample t-test without the
  427.      * assumption of equal samples sizes or sub-population variances.
  428.      *
  429.      * @param v1 First sample variance.
  430.      * @param n1 First sample size.
  431.      * @param v2 Second sample variance.
  432.      * @param n2 Second sample size.
  433.      * @return approximate degrees of freedom
  434.      */
  435.     private static double computeDf(double v1, long n1,
  436.                                     double v2, long n2) {
  437.         // Sample sizes are specified as a double to avoid integer overflow
  438.         final double d1 = n1;
  439.         final double d2 = n2;
  440.         return (((v1 / d1) + (v2 / d2)) * ((v1 / d1) + (v2 / d2))) /
  441.             ((v1 * v1) / (d1 * d1 * (n1 - 1)) + (v2 * v2) / (d2 * d2 * (n2 - 1)));
  442.     }

  443.     /**
  444.      * Computes t statistic for two-sample t-test under the hypothesis of equal
  445.      * sub-population variances.
  446.      *
  447.      * @param mu Expected difference between means.
  448.      * @param m1 First sample mean.
  449.      * @param v1 First sample variance.
  450.      * @param n1 First sample size.
  451.      * @param m2 Second sample mean.
  452.      * @param v2 Second sample variance.
  453.      * @param n2 Second sample size.
  454.      * @return t test statistic
  455.      */
  456.     private static double computeHomoscedasticT(double mu,
  457.                                                 double m1, double v1, long n1,
  458.                                                 double m2, double v2, long n2)  {
  459.         final double pooledVariance = ((n1 - 1) * v1 + (n2 - 1) * v2) / (-2.0 + n1 + n2);
  460.         return (m1 - m2 - mu) / Math.sqrt(pooledVariance * (1.0 / n1 + 1.0 / n2));
  461.     }

  462.     /**
  463.      * Computes p-value for the specified t statistic.
  464.      *
  465.      * @param t T statistic.
  466.      * @param degreesOfFreedom Degrees of freedom.
  467.      * @return p-value for t-test
  468.      */
  469.     private double computeP(double t, double degreesOfFreedom) {
  470.         if (alternative == AlternativeHypothesis.LESS_THAN) {
  471.             return TDistribution.of(degreesOfFreedom).cumulativeProbability(t);
  472.         }
  473.         if (alternative == AlternativeHypothesis.GREATER_THAN) {
  474.             return TDistribution.of(degreesOfFreedom).survivalProbability(t);
  475.         }
  476.         // two-sided
  477.         return 2.0 * TDistribution.of(degreesOfFreedom).survivalProbability(Math.abs(t));
  478.     }

  479.     /**
  480.      * Check sample data size.
  481.      *
  482.      * @param n Data size.
  483.      * @return the sample size
  484.      * @throws IllegalArgumentException if the number of samples {@code < 2}
  485.      */
  486.     private static long checkSampleSize(long n) {
  487.         if (n <= 1) {
  488.             throw new InferenceException(InferenceException.TWO_VALUES_REQUIRED, n);
  489.         }
  490.         return n;
  491.     }
  492. }