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 ≥ 0.</li>
57 * <li>Number of successes must be ≥ 0.</li>
58 * <li>Number of successes must be ≤ number of trials.</li>
59 * <li>Probability must be ≥ 0 and ≤ 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} < {@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 ≥ 0.</li>
98 * <li>Number of successes must be ≥ 0.</li>
99 * <li>Number of successes must be ≤ number of trials.</li>
100 * <li>Probability must be ≥ 0 and ≤ 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} < {@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 }