Skip to content

Commit d85d307

Browse files
committed
STATISTICS-97: Check for arguments that invalidate the test statistic
1 parent 0c000e5 commit d85d307

8 files changed

Lines changed: 114 additions & 30 deletions

File tree

‎commons-statistics-inference/src/main/java/org/apache/commons/statistics/inference/Arguments.java‎

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ static int checkNonNegative(int v) {
6363
* Check that the value is {@code >= 0}.
6464
*
6565
* @param v Value to be tested.
66-
* @throws IllegalArgumentException if the value is less than 0.
66+
* @throws IllegalArgumentException if the value is less than 0, or is NaN.
6767
*/
6868
static void checkNonNegative(double v) {
6969
if (v >= 0) {
@@ -118,7 +118,7 @@ static int checkStrictlyPositive(int v) {
118118
*
119119
* @param v Value to be tested.
120120
* @return the value
121-
* @throws IllegalArgumentException if the value is not strictly positive.
121+
* @throws IllegalArgumentException if the value is not strictly positive finite.
122122
*/
123123
static double checkStrictlyPositive(double v) {
124124
if (v > 0) {
@@ -132,7 +132,7 @@ static double checkStrictlyPositive(double v) {
132132
* Check that all values are {@code > 0}.
133133
*
134134
* @param values Values to be tested.
135-
* @throws IllegalArgumentException if any values are not strictly positive.
135+
* @throws IllegalArgumentException if any values are not strictly positive finite.
136136
*/
137137
static void checkStrictlyPositive(double[] values) {
138138
for (final double v : values) {
@@ -152,25 +152,39 @@ static void checkStrictlyPositive(double[] values) {
152152
*/
153153
static double checkFinite(double v) {
154154
if (!Double.isFinite(v)) {
155-
throw new InferenceException("Non-finite input value: " + v);
155+
throw new InferenceException(InferenceException.NOT_FINITE, v);
156156
}
157157
return v;
158158
}
159159

160160
/**
161-
* Check that all values are not {@link Double#NaN}.
161+
* Check that all the values are finite.
162162
*
163163
* @param values Values to be tested.
164-
* @throws IllegalArgumentException if any values are NaN.
164+
* @throws IllegalArgumentException if any values are not finite.
165165
*/
166-
static void checkNonNaN(double[] values) {
166+
static void checkFinite(double[] values) {
167167
for (final double v : values) {
168-
if (Double.isNaN(v)) {
169-
throw new InferenceException("NaN input value");
168+
if (!Double.isFinite(v)) {
169+
throw new InferenceException(InferenceException.NOT_FINITE, v);
170170
}
171171
}
172172
}
173173

174+
/**
175+
* Check that the value is not {@link Double#NaN}.
176+
*
177+
* @param v Value to be tested.
178+
* @return the value
179+
* @throws IllegalArgumentException if the value is NaN.
180+
*/
181+
static double checkNonNaN(double v) {
182+
if (Double.isNaN(v)) {
183+
throw new InferenceException(InferenceException.NAN);
184+
}
185+
return v;
186+
}
187+
174188
/**
175189
* Checks if the input array is rectangular. It is assumed the array is non-null
176190
* and has a non-zero length.

‎commons-statistics-inference/src/main/java/org/apache/commons/statistics/inference/InferenceException.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ class InferenceException extends IllegalArgumentException {
4848
static final String INVALID_SIGNIFICANCE = "Not a significance: %s is out of range (0, 0.5]";
4949
/** Error message for "not strictly positive" condition when "{@code x <= 0}". */
5050
static final String NOT_STRICTLY_POSITIVE = "Number %s is not greater than 0";
51+
/** Error message for "not finite" condition when "{@code x}" is not finite. */
52+
static final String NOT_FINITE = "Number %s is not finite";
5153
/** Error message for "no data" condition. */
5254
static final String NO_DATA = "No data";
5355
/** Error message for "too large" condition when "{@code x > y}". */
@@ -56,6 +58,8 @@ class InferenceException extends IllegalArgumentException {
5658
static final String X_GTE_Y = "%s >= %s";
5759
/** Error message for "too small" condition when "{@code x < y}". */
5860
static final String X_LT_Y = "%s < %s";
61+
/** Error message for a NaN value. */
62+
static final String NAN = "NaN input value";
5963

6064
/** Serializable version identifier. */
6165
private static final long serialVersionUID = 20221203L;

‎commons-statistics-inference/src/main/java/org/apache/commons/statistics/inference/OneWayAnova.java‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,9 @@ public static OneWayAnova withDefaults() {
208208
* @param data Category summary data.
209209
* @return F statistic
210210
* @throws IllegalArgumentException if the number of categories is less than
211-
* two; a contained category does not have at least one value; or all
212-
* categories have only one value (zero degrees of freedom within groups)
211+
* two; a contained category does not have at least one value; all
212+
* categories have only one value (zero degrees of freedom within groups);
213+
* or any category contains non-finite values
213214
*/
214215
public double statistic(Collection<double[]> data) {
215216
final double[] f = new double[1];
@@ -233,8 +234,9 @@ public double statistic(Collection<double[]> data) {
233234
* @param data Category summary data.
234235
* @return test result
235236
* @throws IllegalArgumentException if the number of categories is less than
236-
* two; a contained category does not have at least one value; or all
237-
* categories have only one value (zero degrees of freedom within groups)
237+
* two; a contained category does not have at least one value; all
238+
* categories have only one value (zero degrees of freedom within groups);
239+
* or any category contains non-finite values
238240
*/
239241
public Result test(Collection<double[]> data) {
240242
return aov(data, null);
@@ -252,15 +254,17 @@ public Result test(Collection<double[]> data) {
252254
* @param statistic Result for the F statistic (or null).
253255
* @return test result (or null)
254256
* @throws IllegalArgumentException if the number of categories is less than two; a
255-
* contained category does not have at least one value; or all categories have only
256-
* one value (zero degrees of freedom within groups)
257+
* contained category does not have at least one value; all categories have only
258+
* one value (zero degrees of freedom within groups); or any category contains
259+
* non-finite values
257260
*/
258261
private static Result aov(Collection<double[]> data, double[] statistic) {
259262
Arguments.checkCategoriesRequiredSize(data.size(), 2);
260263
long n = 0;
261264
for (final double[] array : data) {
262265
n += array.length;
263266
Arguments.checkValuesRequiredSize(array.length, 1);
267+
Arguments.checkFinite(array);
264268
}
265269
final long dfwg = n - data.size();
266270
if (dfwg == 0) {

‎commons-statistics-inference/src/main/java/org/apache/commons/statistics/inference/TTest.java‎

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -160,11 +160,12 @@ public TTest withMu(double v) {
160160
* @param v Sample variance.
161161
* @param n Sample size.
162162
* @return t statistic
163-
* @throws IllegalArgumentException if the number of samples is {@code < 2}; or the
164-
* variance is negative
163+
* @throws IllegalArgumentException if the number of samples is {@code < 2}; the
164+
* variance is negative or NaN; or the mean is NaN
165165
* @see #withMu(double)
166166
*/
167167
public double statistic(double m, double v, long n) {
168+
Arguments.checkNonNaN(m);
168169
Arguments.checkNonNegative(v);
169170
checkSampleSize(n);
170171
return computeT(m - mu, v, n);
@@ -175,12 +176,14 @@ public double statistic(double m, double v, long n) {
175176
*
176177
* @param x Sample values.
177178
* @return t statistic
178-
* @throws IllegalArgumentException if the number of samples is {@code < 2}
179+
* @throws IllegalArgumentException if the number of samples is {@code < 2}; or the
180+
* sample contains non-finite values
179181
* @see #statistic(double, double, long)
180182
* @see #withMu(double)
181183
*/
182184
public double statistic(double[] x) {
183185
final long n = checkSampleSize(x.length);
186+
Arguments.checkFinite(x);
184187
final DoubleStatistics s = DoubleStatistics.of(
185188
EnumSet.of(Statistic.MEAN, Statistic.VARIANCE), x);
186189
final double m = s.getAsDouble(Statistic.MEAN);
@@ -200,12 +203,14 @@ public double statistic(double[] x) {
200203
* @param x First sample values.
201204
* @param y Second sample values.
202205
* @return t statistic
203-
* @throws IllegalArgumentException if the number of samples is {@code < 2}; or the
204-
* the size of the samples is not equal
206+
* @throws IllegalArgumentException if the number of samples is {@code < 2}; the
207+
* size of the samples is not equal; or the samples contain non-finite values
205208
* @see #withMu(double)
206209
*/
207210
public double pairedStatistic(double[] x, double[] y) {
208211
final long n = checkSampleSize(x.length);
212+
Arguments.checkFinite(x);
213+
Arguments.checkFinite(y);
209214
final double m = StatisticUtils.meanDifference(x, y);
210215
final double v = StatisticUtils.varianceDifference(x, y, m);
211216
return computeT(m - mu, v, n);
@@ -237,12 +242,14 @@ public double pairedStatistic(double[] x, double[] y) {
237242
* @param n2 Second sample size.
238243
* @return t statistic
239244
* @throws IllegalArgumentException if the number of samples in either dataset is
240-
* {@code < 2}; or the variances are negative.
245+
* {@code < 2}; the variances are negative; or the means are NaN.
241246
* @see #withMu(double)
242247
* @see #with(DataDispersion)
243248
*/
244249
public double statistic(double m1, double v1, long n1,
245250
double m2, double v2, long n2) {
251+
Arguments.checkNonNaN(m1);
252+
Arguments.checkNonNaN(m2);
246253
Arguments.checkNonNegative(v1);
247254
Arguments.checkNonNegative(v2);
248255
checkSampleSize(n1);
@@ -261,13 +268,16 @@ public double statistic(double m1, double v1, long n1,
261268
* @param x First sample values.
262269
* @param y Second sample values.
263270
* @return t statistic
264-
* @throws IllegalArgumentException if the number of samples in either dataset is {@code < 2}
271+
* @throws IllegalArgumentException if the number of samples in either dataset is
272+
* {@code < 2}; or the samples contain non-finite values
265273
* @see #withMu(double)
266274
* @see #with(DataDispersion)
267275
*/
268276
public double statistic(double[] x, double[] y) {
269277
final long n1 = checkSampleSize(x.length);
270278
final long n2 = checkSampleSize(y.length);
279+
Arguments.checkFinite(x);
280+
Arguments.checkFinite(y);
271281
final DoubleStatistics.Builder b = DoubleStatistics.builder(Statistic.MEAN, Statistic.VARIANCE);
272282
final DoubleStatistics s1 = b.build(x);
273283
final double m1 = s1.getAsDouble(Statistic.MEAN);
@@ -386,7 +396,7 @@ public Result test(double m1, double v1, long n1,
386396
* @param y Second sample values.
387397
* @return the test result
388398
* @throws IllegalArgumentException if the number of samples in either dataset
389-
* is {@code < 2}
399+
* is {@code < 2}; or the samples contain non-finite values
390400
* @see #statistic(double[], double[])
391401
* @see #test(double, double, long, double, double, long)
392402
*/
@@ -395,6 +405,8 @@ public Result test(double[] x, double[] y) {
395405
// requires the variance. So repeat the computation and compute p.
396406
final long n1 = checkSampleSize(x.length);
397407
final long n2 = checkSampleSize(y.length);
408+
Arguments.checkFinite(x);
409+
Arguments.checkFinite(y);
398410
final DoubleStatistics.Builder b = DoubleStatistics.builder(Statistic.MEAN, Statistic.VARIANCE);
399411
final DoubleStatistics s1 = b.build(x);
400412
final double m1 = s1.getAsDouble(Statistic.MEAN);

‎commons-statistics-inference/src/test/java/org/apache/commons/statistics/inference/ArgumentsTest.java‎

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,20 +95,28 @@ void testCheckFiniteThrows(double v) {
9595
Assertions.assertTrue(ex.getMessage().contains(Double.toString(v)));
9696
}
9797

98-
@Test
99-
void testCheckNonNanArrayThrows() {
100-
Assertions.assertDoesNotThrow(() -> Arguments.checkNonNaN(new double[0]));
98+
@ParameterizedTest
99+
@ValueSource(doubles = {Double.NEGATIVE_INFINITY, Double.NaN, Double.POSITIVE_INFINITY})
100+
void testCheckFiniteArrayThrows(double v) {
101+
Assertions.assertDoesNotThrow(() -> Arguments.checkFinite(new double[0]));
101102
final double[] a = new double[3];
102-
Assertions.assertDoesNotThrow(() -> Arguments.checkNonNaN(a));
103+
Assertions.assertDoesNotThrow(() -> Arguments.checkFinite(a));
103104
for (int i = 0; i < a.length; i++) {
104-
a[i] = Double.NaN;
105+
a[i] = v;
105106
final IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class,
106-
() -> Arguments.checkNonNaN(a));
107-
Assertions.assertTrue(ex.getMessage().contains("NaN"));
107+
() -> Arguments.checkFinite(a));
108+
Assertions.assertTrue(ex.getMessage().contains(Double.toString(v)));
108109
a[i] = 0;
109110
}
110111
}
111112

113+
@Test
114+
void testCheckNonNanThrows() {
115+
final IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class,
116+
() -> Arguments.checkNonNaN(Double.NaN));
117+
Assertions.assertTrue(ex.getMessage().contains("NaN"));
118+
}
119+
112120
@Test
113121
void testCheckRectangular() {
114122
// Input is assumed to be non-zero length: this test what happens

‎commons-statistics-inference/src/test/java/org/apache/commons/statistics/inference/OneWayAnovaTest.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ private static void assertAnovaTestThrows(Consumer<Collection<double[]>> action)
5454
final List<double[]> allLength1 = Arrays.asList(new double[] {1}, new double[] {2}, new double[] {3});
5555
TestUtils.assertThrowsWithMessage(IllegalArgumentException.class,
5656
() -> action.accept(allLength1), "degrees", "freedom", "within", "group", "zero");
57+
58+
for (final double v : new double[] {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}) {
59+
final List<double[]> nanContents = Arrays.asList(new double[] {1, 2, 3}, new double[] {4, v});
60+
TestUtils.assertThrowsWithMessage(IllegalArgumentException.class,
61+
() -> action.accept(nanContents), "finite", Double.toString(v));
62+
}
5763
}
5864

5965
@ParameterizedTest

‎commons-statistics-inference/src/test/java/org/apache/commons/statistics/inference/TTestTest.java‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ private static void assertOneSampleDatasetThrows(DDLConsumer action) {
6767
() -> action.accept(m, v, 1), "values", "size");
6868
TestUtils.assertThrowsWithMessage(IllegalArgumentException.class,
6969
() -> action.accept(m, -1, n), "negative");
70+
TestUtils.assertThrowsWithMessage(IllegalArgumentException.class,
71+
() -> action.accept(m, Double.NaN, n), "NaN");
72+
TestUtils.assertThrowsWithMessage(IllegalArgumentException.class,
73+
() -> action.accept(Double.NaN, v, n), "NaN");
7074
}
7175

7276
@ParameterizedTest
@@ -120,6 +124,12 @@ void testOneSampleThrows() {
120124
private static void assertOneSampleThrows(Consumer<double[]> action) {
121125
TestUtils.assertThrowsWithMessage(IllegalArgumentException.class,
122126
() -> action.accept(new double[1]), "values", "size");
127+
128+
for (final double v : new double[] {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}) {
129+
final double[] badSample = {1, 2, 3, v};
130+
TestUtils.assertThrowsWithMessage(IllegalArgumentException.class,
131+
() -> action.accept(badSample), "finite", Double.toString(v));
132+
}
123133
}
124134

125135
@ParameterizedTest
@@ -182,6 +192,13 @@ private static void assertPairedSampleThrows(BiConsumer<double[], double[]> acti
182192
() -> action.accept(sample, unequalSize), "values", "size", "mismatch");
183193
TestUtils.assertThrowsWithMessage(IllegalArgumentException.class,
184194
() -> action.accept(unequalSize, sample), "values", "size", "mismatch");
195+
for (final double v : new double[] {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}) {
196+
final double[] badSample = {1, 2, 3, v};
197+
TestUtils.assertThrowsWithMessage(IllegalArgumentException.class,
198+
() -> action.accept(badSample, sample), "finite", Double.toString(v));
199+
TestUtils.assertThrowsWithMessage(IllegalArgumentException.class,
200+
() -> action.accept(sample, badSample), "finite", Double.toString(v));
201+
}
185202
}
186203

187204
@ParameterizedTest
@@ -254,6 +271,14 @@ private static void assertTwoSampleDatasetThrows(DDLDDLConsumer action) {
254271
() -> action.accept(m, -1, n, m, v, n), "negative");
255272
TestUtils.assertThrowsWithMessage(IllegalArgumentException.class,
256273
() -> action.accept(m, v, n, m, -1, n), "negative");
274+
TestUtils.assertThrowsWithMessage(IllegalArgumentException.class,
275+
() -> action.accept(Double.NaN, v, n, m, v, n), "NaN");
276+
TestUtils.assertThrowsWithMessage(IllegalArgumentException.class,
277+
() -> action.accept(m, Double.NaN, n, m, v, n), "NaN");
278+
TestUtils.assertThrowsWithMessage(IllegalArgumentException.class,
279+
() -> action.accept(m, v, n, Double.NaN, v, n), "NaN");
280+
TestUtils.assertThrowsWithMessage(IllegalArgumentException.class,
281+
() -> action.accept(m, v, n, m, Double.NaN, n), "NaN");
257282
}
258283

259284
@ParameterizedTest
@@ -344,6 +369,13 @@ private static void assertTwoSampleThrows(BiConsumer<double[], double[]> action)
344369
() -> action.accept(sample, tooSmall), "values", "size");
345370
TestUtils.assertThrowsWithMessage(IllegalArgumentException.class,
346371
() -> action.accept(tooSmall, sample), "values", "size");
372+
for (final double v : new double[] {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}) {
373+
final double[] badSample = {1, 2, 3, v};
374+
TestUtils.assertThrowsWithMessage(IllegalArgumentException.class,
375+
() -> action.accept(badSample, sample), "finite", Double.toString(v));
376+
TestUtils.assertThrowsWithMessage(IllegalArgumentException.class,
377+
() -> action.accept(sample, badSample), "finite", Double.toString(v));
378+
}
347379
}
348380

349381
@ParameterizedTest

‎src/changes/changes.xml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ If the output is not quite correct, check for invisible trailing spaces!
5353
</properties>
5454
<body>
5555
<release version="1.4" date="TBD" description="Adds new features (requires Java 8).">
56+
<action dev="aherbert" type="fix" due-to="Security scan, Aex Herbert" issue="STATISTICS-97">
57+
"TTest/OneWayAnova": Checks added for arguments that invalidate the test
58+
statistic: non-finite values in input arrays; and NaN mean arguments.
59+
</action>
5660
<action dev="aherbert" type="update" due-to="Security scan, Aex Herbert">
5761
"FisherExactTest/HypergeometricDistribution": Update documentation
5862
on possible long runtime when the hypergeometric distribution

0 commit comments

Comments
 (0)