View Javadoc
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    *      https://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  
18  /*
19   * This is not the original file distributed by the Apache Software Foundation
20   * It has been modified by the Hipparchus project
21   */
22  package org.hipparchus.stat.inference;
23  
24  import org.hipparchus.distribution.discrete.BinomialDistribution;
25  import org.hipparchus.exception.LocalizedCoreFormats;
26  import org.hipparchus.exception.MathIllegalArgumentException;
27  import org.hipparchus.exception.MathRuntimeException;
28  import org.hipparchus.util.MathUtils;
29  
30  /**
31   * Implements binomial test statistics.
32   * <p>
33   * Exact test for the statistical significance of deviations from a
34   * theoretically expected distribution of observations into two categories.
35   *
36   * @see <a href="http://en.wikipedia.org/wiki/Binomial_test">Binomial test (Wikipedia)</a>
37   */
38  public class BinomialTest { // NOPMD - this is not a Junit test class, PMD false positive here
39  
40      /** Empty constructor.
41       * <p>
42       * This constructor is not strictly necessary, but it prevents spurious
43       * javadoc warnings with JDK 18 and later.
44       * </p>
45       * @since 3.0
46       */
47      public BinomialTest() { // NOPMD - unnecessary constructor added intentionally to make javadoc happy
48          // nothing to do
49      }
50  
51      /**
52       * Returns whether the null hypothesis can be rejected with the given confidence level.
53       * <p>
54       * <strong>Preconditions</strong>:
55       * <ul>
56       * <li>Number of trials must be &ge; 0.</li>
57       * <li>Number of successes must be &ge; 0.</li>
58       * <li>Number of successes must be &le; number of trials.</li>
59       * <li>Probability must be &ge; 0 and &le; 1.</li>
60       * </ul>
61       *
62       * @param numberOfTrials number of trials performed
63       * @param numberOfSuccesses number of successes observed
64       * @param probability assumed probability of a single trial under the null hypothesis
65       * @param alternativeHypothesis type of hypothesis being evaluated (one- or two-sided)
66       * @param alpha significance level of the test
67       * @return true if the null hypothesis can be rejected with confidence {@code 1 - alpha}
68       * @throws MathIllegalArgumentException if {@code numberOfTrials} or {@code numberOfSuccesses} is negative
69       * @throws MathIllegalArgumentException if {@code probability} is not between 0 and 1
70       * @throws MathIllegalArgumentException if {@code numberOfTrials} &lt; {@code numberOfSuccesses} or
71       * if {@code alternateHypothesis} is null.
72       * @see AlternativeHypothesis
73       */
74      public boolean binomialTest(int numberOfTrials, int numberOfSuccesses, double probability,
75                                  AlternativeHypothesis alternativeHypothesis, double alpha) {
76          double pValue = binomialTest(numberOfTrials, numberOfSuccesses, probability, alternativeHypothesis);
77          return pValue < alpha;
78      }
79  
80      /**
81       * Returns the <i>observed significance level</i>, or
82       * <a href="http://www.cas.lancs.ac.uk/glossary_v1.1/hyptest.html#pvalue">p-value</a>,
83       * associated with a <a href="http://en.wikipedia.org/wiki/Binomial_test"> Binomial test</a>.
84       * <p>
85       * The number returned is the smallest significance level at which one can reject the null hypothesis.
86       * The form of the hypothesis depends on {@code alternativeHypothesis}.</p>
87       * <p>
88       * The p-Value represents the likelihood of getting a result at least as extreme as the sample,
89       * given the provided {@code probability} of success on a single trial. For single-sided tests,
90       * this value can be directly derived from the Binomial distribution. For the two-sided test,
91       * the implementation works as follows: we start by looking at the most extreme cases
92       * (0 success and n success where n is the number of trials from the sample) and determine their likelihood.
93       * The lower value is added to the p-Value (if both values are equal, both are added). Then we continue with
94       * the next extreme value, until we added the value for the actual observed sample.</p>
95       * <p>* <strong>Preconditions</strong>:</p>
96       * <ul>
97       * <li>Number of trials must be &ge; 0.</li>
98       * <li>Number of successes must be &ge; 0.</li>
99       * <li>Number of successes must be &le; number of trials.</li>
100      * <li>Probability must be &ge; 0 and &le; 1.</li>
101      * </ul>
102      *
103      * @param numberOfTrials number of trials performed
104      * @param numberOfSuccesses number of successes observed
105      * @param probability assumed probability of a single trial under the null hypothesis
106      * @param alternativeHypothesis type of hypothesis being evaluated (one- or two-sided)
107      * @return p-value
108      * @throws MathIllegalArgumentException if {@code numberOfTrials} or {@code numberOfSuccesses} is negative
109      * @throws MathIllegalArgumentException if {@code probability} is not between 0 and 1
110      * @throws MathIllegalArgumentException if {@code numberOfTrials} &lt; {@code numberOfSuccesses} or
111      * if {@code alternateHypothesis} is null.
112      * @see AlternativeHypothesis
113      */
114     public double binomialTest(int numberOfTrials, int numberOfSuccesses, double probability,
115                                AlternativeHypothesis alternativeHypothesis) {
116         if (numberOfTrials < 0) {
117             throw new MathIllegalArgumentException(LocalizedCoreFormats.NUMBER_TOO_SMALL, numberOfTrials, 0);
118         }
119         if (numberOfSuccesses < 0) {
120             throw new MathIllegalArgumentException(LocalizedCoreFormats.NUMBER_TOO_SMALL, numberOfSuccesses, 0);
121         }
122         MathUtils.checkRangeInclusive(probability, 0, 1);
123         if (numberOfTrials < numberOfSuccesses) {
124             throw new MathIllegalArgumentException(
125                 LocalizedCoreFormats.BINOMIAL_INVALID_PARAMETERS_ORDER,
126                 numberOfTrials, numberOfSuccesses);
127         }
128         MathUtils.checkNotNull(alternativeHypothesis);
129 
130         final BinomialDistribution distribution = new BinomialDistribution(numberOfTrials, probability);
131         switch (alternativeHypothesis) {
132         case GREATER_THAN:
133             return 1 - distribution.cumulativeProbability(numberOfSuccesses - 1);
134         case LESS_THAN:
135             return distribution.cumulativeProbability(numberOfSuccesses);
136         case TWO_SIDED:
137             int criticalValueLow = 0;
138             int criticalValueHigh = numberOfTrials;
139             double pTotal = 0;
140 
141             while (true) {
142                 final double pLow = distribution.probability(criticalValueLow);
143                 final double pHigh = distribution.probability(criticalValueHigh);
144 
145                 if (pLow == pHigh) {
146                     if (criticalValueLow == criticalValueHigh) { // One side can't move
147                         pTotal += pLow;
148                     } else {
149                         pTotal += 2 * pLow;
150                     }
151                     criticalValueLow++;
152                     criticalValueHigh--;
153                 } else if (pLow < pHigh) {
154                     pTotal += pLow;
155                     criticalValueLow++;
156                 } else {
157                     pTotal += pHigh;
158                     criticalValueHigh--;
159                 }
160 
161                 if (criticalValueLow > numberOfSuccesses || criticalValueHigh < numberOfSuccesses) {
162                     break;
163                 }
164             }
165             return pTotal;
166         default:
167             // this should never happen
168             throw MathRuntimeException.createInternalError();
169         }
170     }
171 }