aherbert commented on code in PR #52:
URL: https://github.com/apache/commons-statistics/pull/52#discussion_r1316339953
##########
commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfSquaredDeviations.java:
##########
@@ -36,31 +36,25 @@
*
* <p><strong>Note that this implementation is not synchronized.</strong> If
* multiple threads access an instance of this class concurrently, and at least
- * one of the threads invokes the <code>accept()</code> or
- * <code>combine()</code> method, it must be synchronized externally.
+ * one of the threads invokes the {@link
java.util.function.DoubleConsumer#accept(double) accept} or
+ * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method,
it must be synchronized externally.
*
- * <p>However, it is safe to use <code>accept()</code> and
<code>combine()</code>
+ * <p>However, it is safe to use {@link
java.util.function.DoubleConsumer#accept(double) accept} and {@link
DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
Review Comment:
Wrap this to 100 or 120 character width
##########
commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/TestData.java:
##########
@@ -0,0 +1,114 @@
+/*
+ * 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.descriptive;
+
+import java.util.stream.Stream;
+import org.junit.jupiter.params.provider.Arguments;
+
+final class TestData {
+
+ /** Class contains only static methods. */
+ private TestData() {}
+
+ static Stream<double[]> testValues() {
+ return Stream.of(
+ new double[] {0.0},
+ new double[] {10, 8, 13, 9, 11, 14, 6, 4, 12, 7, 5},
+ new double[] {8.04, 6.95, 7.58, 8.81, 8.33, 9.96, 7.24, 4.26,
10.84, 4.82, 5.68},
+ new double[] {9.14, 8.14, 8.74, 8.77, 9.26, 8.10, 6.13, 3.10,
9.13, 7.26, 4.74, 7.46, 6.77, 12.74, 7.11, 7.81, 8.84, 6.08, 5.39, 8.15, 6.42,
5.73},
+ new double[] {8, 8, 8, 8, 8, 8, 8, 19, 8, 8, 8},
+ new double[] {6.58, 5.76, 7.71, 8.84, 8.47, 7.04, 5.25, 12.50,
5.56, 7.91, 6.89},
+ new double[] {0, 0, 0.0},
+ new double[] {1, -7, 6},
+ new double[] {1, 7, -15, 3},
+ new double[] {2, 2, 2, 2},
+ new double[] {2.3},
+ new double[] {3.14, 2.718, 1.414},
+ new double[] {12.5, 12.0, 11.8, 14.2, 14.9, 14.5, 21.0, 8.2, 10.3,
11.3, 14.1, 9.9, 12.2, 12.0, 12.1, 11.0, 19.8, 11.0, 10.0, 8.8, 9.0, 12.3},
+ new double[] {-0.0, +0.0},
+ new double[] {0.0, -0.0},
+ new double[] {0.0, +0.0},
+ new double[] {0.001, 0.0002, 0.00003, 10000.11, 0.000004},
+ new double[] {10E-50, 5E-100, 25E-200, 35.345E-50},
+ // Overflow of the sum
+ new double[] {Double.MAX_VALUE, Double.MAX_VALUE},
+ new double[] {-Double.MAX_VALUE, -Double.MAX_VALUE},
+ new double[] {Double.MAX_VALUE, 1},
+ new double[] {-Double.MAX_VALUE, 1, 1},
+ new double[] {-Double.MAX_VALUE, -1, 1},
+ new double[] {Double.MAX_VALUE, -1},
+ new double[] {Double.MAX_VALUE, -Double.MAX_VALUE},
+ new double[] {1, -Double.MAX_VALUE},
+ new double[] {1, 1, 1, -Double.MAX_VALUE},
+ new double[] {Double.MAX_VALUE, Double.MAX_VALUE / 2},
+ new double[] {Double.MAX_VALUE, Double.MAX_VALUE,
-Double.MAX_VALUE},
+ new double[] {Double.MAX_VALUE, -Double.MAX_VALUE}
+ );
+ }
+
+ static Stream<Arguments> testValuesNonFinite() {
Review Comment:
The expected value should not be in this method. You are always using NaN so
this is not helpful. Each statistic should be able to compute the expected
value for the data that has non-finite values. For example the min/max/mean may
return an infinity but the variance would be NaN.
##########
commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Variance.java:
##########
@@ -114,11 +114,11 @@ public static Variance of(double... values) {
double accum2 = 0.0;
double squaredDevSum;
for (final double value : values) {
- dev = value - mean;
+ dev = value * 0.5 - mean * 0.5;
Review Comment:
Why protect from overflow here when immediately you square the result?
I removed this and the failing test case is: {MAX, MAX, -MAX}.
This is similar to {2, 2, -2} which has a mean of 2/3 and a variance of
3.5555. So rescaled by 2^1023 squared the variance would be 3.5555 *
(2^1023)^2. This is a large overflow. Your scaling down is protecting computing
the deviation from the mean, but the deviation squared is infinite and the rest
of the computation cannot be saved.
I think your overflow detection can be done on the `accum` value and not
`accum2`. We can document that if the sum of squared deviations from the mean
overflows then the variance is returned as infinity. So we use the formula as
defined (comments removed for brevity):
```java
for (final double value : values) {
dev = value - mean;
accum += dev * dev;
accum2 += dev;
}
final double accum2Squared = accum2 * accum2;
final long n = values.length;
if (accum == Double.POSITIVE_INFINITY) {
squaredDevSum = Double.POSITIVE_INFINITY;
} else {
squaredDevSum = accum - (accum2Squared / n);
}
```
##########
commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/VarianceTest.java:
##########
@@ -31,38 +31,55 @@
* Test for {@link Variance}.
*/
final class VarianceTest {
+ private static final int ULP_ARRAY = 5;
Review Comment:
I wondered why you have a ULP for ARRAY_OF_ARRAYS. I see that combine uses
the accept method but array of arrays uses the of method to construct each.
Perhaps we should rename these to:
```
ULP_COMBINE_ACCEPT
ULP_COMBINE_OF
```
Note: I have updated the `TestHelper` to output the actual ULP error in the
message. This makes setting these limits a bit easier. I will push the updated
TestHelper so you can rebase and collect the change.
##########
commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/VarianceTest.java:
##########
@@ -0,0 +1,259 @@
+/*
+ * 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.descriptive;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Test for {@link Variance}.
+ */
+final class VarianceTest {
+ private static final int ULP_ARRAY = 5;
+
+ private static final int ULP_STREAM = 7;
+
+ private static final int ULP_COMBINE = 7;
+
+ private static final int ULP_ARRAY_OF_ARRAYS = 2;
+
+ @Test
+ void testEmpty() {
+ Variance var = Variance.create();
+ Assertions.assertEquals(Double.NaN, var.getAsDouble());
+ }
+
+ @Test
+ void testNaN() {
+ Variance variance = Variance.create();
+ double[] testArray = {Double.NaN, +0.0d, -0.0d,
Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY};
+ for (double value : testArray) {
+ variance.accept(value);
+ }
+ Assertions.assertEquals(Double.NaN, variance.getAsDouble());
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
"org.apache.commons.statistics.descriptive.TestData#testValues")
+ void testVariance(double[] values) {
+ final double expected = computeExpectedVariance(values);
+ Variance var = Variance.create();
+ for (double value : values) {
+ var.accept(value);
+ }
+ TestHelper.assertEquals(expected, var.getAsDouble(), ULP_STREAM, () ->
"variance");
+ TestHelper.assertEquals(expected, Variance.of(values).getAsDouble(),
ULP_ARRAY, () -> "of (values)");
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
"org.apache.commons.statistics.descriptive.TestData#testValues")
+ void testParallelStream(double[] values) {
+ final double expected = computeExpectedVariance(values);
+ final double actual = Arrays.stream(values)
+ .parallel()
+ .collect(Variance::create, Variance::accept, Variance::combine)
+ .getAsDouble();
+ TestHelper.assertEquals(expected, actual, ULP_COMBINE, () -> "parallel
stream");
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
"org.apache.commons.statistics.descriptive.TestData#testValues")
+ void testVarianceRandomOrder(double[] values) {
+ UniformRandomProvider rng = TestHelper.createRNG();
+ for (int i = 1; i <= 10; i++) {
+ testVariance(TestHelper.shuffle(rng, values));
+ testParallelStream(TestHelper.shuffle(rng, values));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
{"org.apache.commons.statistics.descriptive.TestData#testValuesNonFinite",
"testVarianceNonFinite"})
+ void testVarianceNonFinite(double[] values, double expected) {
+ Variance var = Variance.create();
+ for (double value : values) {
+ var.accept(value);
+ }
+ Assertions.assertEquals(expected, var.getAsDouble(), "variance
non-finite");
+ Assertions.assertEquals(expected, Variance.of(values).getAsDouble(),
"of (values) non-finite");
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
{"org.apache.commons.statistics.descriptive.TestData#testValuesNonFinite",
"testVarianceNonFinite"})
+ void testParallelStreamNonFinite(double[] values, double expected) {
+ final double ans = Arrays.stream(values)
+ .parallel()
+ .collect(Variance::create, Variance::accept, Variance::combine)
+ .getAsDouble();
+ Assertions.assertEquals(expected, ans, "parallel stream non-finite");
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
{"org.apache.commons.statistics.descriptive.TestData#testValuesNonFinite",
"testVarianceNonFinite"})
+ void testVarianceRandomOrderNonFinite(double[] values, double expected) {
+ UniformRandomProvider rng = TestHelper.createRNG();
+ for (int i = 1; i <= 10; i++) {
+ testVarianceNonFinite(TestHelper.shuffle(rng, values), expected);
+ testParallelStreamNonFinite(TestHelper.shuffle(rng, values),
expected);
+ }
+ }
+
+ static Stream<Arguments> testVarianceNonFinite() {
+ return Stream.of(
+ Arguments.of(new double[]{Double.POSITIVE_INFINITY,
Double.POSITIVE_INFINITY}, Double.NaN),
+ Arguments.of(new double[]{Double.NEGATIVE_INFINITY,
Double.NEGATIVE_INFINITY}, Double.NaN),
+ Arguments.of(new double[]{Double.POSITIVE_INFINITY,
Double.MAX_VALUE}, Double.NaN),
+ Arguments.of(new double[]{Double.NEGATIVE_INFINITY,
-Double.MIN_VALUE}, Double.NaN),
+ Arguments.of(new double[]{Double.NEGATIVE_INFINITY,
Double.MAX_VALUE}, Double.NaN),
+ Arguments.of(new double[]{Double.POSITIVE_INFINITY,
Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY},
Double.NaN),
+ Arguments.of(new double[]{-Double.MAX_VALUE,
Double.POSITIVE_INFINITY}, Double.NaN)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
"org.apache.commons.statistics.descriptive.TestData#testCombine")
+ void testCombine(double[] array1, double[] array2) {
+ final double[] combinedArray = TestHelper.concatenate(array1, array2);
+ final double expected = computeExpectedVariance(combinedArray);
+ Variance var1 = Variance.create();
+ Variance var2 = Variance.create();
+ Arrays.stream(array1).forEach(var1);
+ Arrays.stream(array2).forEach(var2);
+ final double var1BeforeCombine = var1.getAsDouble();
+ final double var2BeforeCombine = var2.getAsDouble();
+ var1.combine(var2);
+ TestHelper.assertEquals(expected, var1.getAsDouble(), ULP_COMBINE, ()
-> "combine");
+ Assertions.assertEquals(var2BeforeCombine, var2.getAsDouble());
+ // Combine in reverse order
+ Variance var1b = Variance.create();
+ Arrays.stream(array1).forEach(var1b);
+ var2.combine(var1b);
+ TestHelper.assertEquals(expected, var2.getAsDouble(), ULP_COMBINE, ()
-> "combine");
+ Assertions.assertEquals(var1BeforeCombine, var1b.getAsDouble());
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
"org.apache.commons.statistics.descriptive.TestData#testCombine")
+ void testCombineRandomOrder(double[] array1, double[] array2) {
+ UniformRandomProvider rng = TestHelper.createRNG();
+ double[] data = TestHelper.concatenate(array1, array2);
+ final int n = array1.length;
+ for (int i = 1; i <= 10; i++) {
+ for (int j = 1; j <= 10; j++) {
+ TestHelper.shuffle(rng, array1);
+ TestHelper.shuffle(rng, array2);
+ testCombine(array1, array2);
+ }
+ TestHelper.shuffle(rng, data);
+ System.arraycopy(data, 0, array1, 0, n);
+ System.arraycopy(data, n, array2, 0, array2.length);
+ testCombine(array1, array2);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
"org.apache.commons.statistics.descriptive.TestData#testCombine")
+ void testArrayOfArrays(double[] array1, double[] array2) {
+ final double[] combinedArray = TestHelper.concatenate(array1, array2);
+ final double expected = computeExpectedVariance(combinedArray);
+ final double[][] values = {array1, array2};
+ final double actual = Arrays.stream(values)
+ .map(Variance::of)
+ .reduce(Variance::combine)
+ .map(Variance::getAsDouble)
+ .orElseThrow(RuntimeException::new);
+ TestHelper.assertEquals(expected, actual, ULP_ARRAY_OF_ARRAYS, () ->
"array of arrays combined variance");
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
{"org.apache.commons.statistics.descriptive.TestData#testCombineNonFinite",
"testCombineVarianceNonFinite"})
+ void testCombineNonFinite(double[][] values, double expected) {
+ Variance var1 = Variance.create();
+ Variance var2 = Variance.create();
+ Arrays.stream(values[0]).forEach(var1);
+ Arrays.stream(values[1]).forEach(var2);
+ final double mean2BeforeCombine = var2.getAsDouble();
+ var1.combine(var2);
+ Assertions.assertEquals(expected, var1.getAsDouble(), "combine
non-finite");
+ Assertions.assertEquals(mean2BeforeCombine, var2.getAsDouble());
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
{"org.apache.commons.statistics.descriptive.TestData#testCombineNonFinite",
"testCombineVarianceNonFinite"})
+ void testCombineRandomOrderNonFinite(double[][] values, double expected) {
+ UniformRandomProvider rng = TestHelper.createRNG();
+ final double[] data = TestHelper.concatenate(values[0], values[1]);
+ final int n = values[0].length;
+ for (int i = 1; i <= 10; i++) {
+ for (int j = 1; j <= 10; j++) {
+ TestHelper.shuffle(rng, values[0]);
+ TestHelper.shuffle(rng, values[1]);
+ testCombineNonFinite(values, expected);
+ }
+ TestHelper.shuffle(rng, data);
+ System.arraycopy(data, 0, values[0], 0, n);
+ System.arraycopy(data, n, values[1], 0, values[1].length);
+ testCombineNonFinite(values, expected);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
{"org.apache.commons.statistics.descriptive.TestData#testCombineNonFinite",
"testCombineVarianceNonFinite"})
+ void testArrayOfArraysNonFinite(double[][] values, double expected) {
+ final double actual = Arrays.stream(values)
+ .map(Variance::of)
+ .reduce(Variance::combine)
+ .map(Variance::getAsDouble)
+ .orElseThrow(RuntimeException::new);
+ Assertions.assertEquals(expected, actual, "array of arrays combined
variance non-finite");
+ }
+
+ static Stream<Arguments> testCombineVarianceNonFinite() {
Review Comment:
Again, these are not the same as the MeanTest. So we are missing cases in
one or the other. Please consolidate them to TestData.
##########
commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfSquaredDeviations.java:
##########
@@ -0,0 +1,121 @@
+/*
+ * 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.descriptive;
+
+/**
+ * Computes the sum of squared deviations from the sample mean. This
+ * statistic is related to the second moment.
+ *
+ * <p>
+ * The following recursive updating formula is used:
+ * <p>
+ * Let <ul>
+ * <li> dev = (current obs - previous mean) </li>
+ * <li> n = number of observations (including current obs) </li>
+ * </ul>
+ * Then
+ * <p>
+ * new value = old value + dev^2 * (n - 1) / n.
+ * <p>
+ *
+ * Returns the sum of squared deviations of all values seen so far.
+ *
+ * <p><strong>Note that this implementation is not synchronized.</strong> If
+ * multiple threads access an instance of this class concurrently, and at least
+ * one of the threads invokes the {@link
java.util.function.DoubleConsumer#accept(double) accept} or
+ * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method,
it must be synchronized externally.
+ *
+ * <p>However, it is safe to use {@link
java.util.function.DoubleConsumer#accept(double) accept} and {@link
DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ * as <code>accumulator</code> and <code>combiner</code> functions of
+ * {@link java.util.stream.Collector Collector} on a parallel stream,
+ * because the parallel implementation of {@link
java.util.stream.Stream#collect Stream.collect()}
+ * provides the necessary partitioning, isolation, and merging of results for
+ * safe and efficient parallel execution.
+ */
+class SumOfSquaredDeviations extends FirstMoment implements DoubleStatistic {
Review Comment:
I think we should drop the implementation of DoubleStatistic. You could just
implement DoubleConsumer in FirstMoment and drop the implementation of
DoubleStaticticAccumilator<FirstMoment>. You then do not have to overload
getAsDouble through the hierarchy. You can have typed methods for combine and
specific methods for the stat:
```
FirstMoment implements DoubleConsumer:
void accept(double)
FirstMoment combine(FirstMoment);
double getFirstMoment()
SumOfSquaredDeviations extends FirstMoment:
SumOfSquaredDeviations combine(SumOfSquareDeviations);
double getSumOfSquaredDeviations();
```
This allows the SumOfSquareDeviations to call `super.combine(other)` and
`getFirstMoment()` directly without creating copy instances.
##########
commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/TestData.java:
##########
@@ -0,0 +1,114 @@
+/*
+ * 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.descriptive;
+
+import java.util.stream.Stream;
+import org.junit.jupiter.params.provider.Arguments;
+
+final class TestData {
Review Comment:
Class and methods should have some comments.
##########
commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/MeanTest.java:
##########
@@ -152,37 +117,21 @@ void testMeanRandomOrderNonFinite(double[] values, double
expected) {
static Stream<Arguments> testMeanNonFinite() {
return Stream.of(
- Arguments.of(new double[] {}, Double.NaN),
- Arguments.of(new double[] {Double.POSITIVE_INFINITY,
Double.NEGATIVE_INFINITY},
- Double.NaN),
- Arguments.of(new double[] {Double.POSITIVE_INFINITY,
Double.POSITIVE_INFINITY},
- Double.POSITIVE_INFINITY),
- Arguments.of(new double[] {Double.NEGATIVE_INFINITY,
Double.NEGATIVE_INFINITY},
- Double.NEGATIVE_INFINITY),
- Arguments.of(new double[] {Double.POSITIVE_INFINITY,
Double.MAX_VALUE},
- Double.POSITIVE_INFINITY),
- Arguments.of(new double[] {Double.NEGATIVE_INFINITY,
-Double.MIN_VALUE},
- Double.NEGATIVE_INFINITY),
- Arguments.of(new double[] {Double.NaN, 34.56, 89.74}, Double.NaN),
- Arguments.of(new double[] {34.56, Double.NaN, 89.74}, Double.NaN),
- Arguments.of(new double[] {34.56, 89.74, Double.NaN}, Double.NaN),
- Arguments.of(new double[] {Double.NaN, 3.14, Double.NaN,
Double.NaN},
- Double.NaN),
- Arguments.of(new double[] {Double.NaN, Double.NaN, Double.NaN},
Double.NaN),
- Arguments.of(new double[] {Double.NEGATIVE_INFINITY,
Double.MAX_VALUE},
- Double.NEGATIVE_INFINITY),
- Arguments.of(new double[] {Double.POSITIVE_INFINITY,
Double.POSITIVE_INFINITY,
- Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY},
Double.POSITIVE_INFINITY),
- Arguments.of(new double[] {-Double.MAX_VALUE,
Double.POSITIVE_INFINITY},
- Double.POSITIVE_INFINITY)
+ Arguments.of(new double[]{Double.POSITIVE_INFINITY,
Double.POSITIVE_INFINITY}, Double.POSITIVE_INFINITY),
Review Comment:
Can these apply to the variance too, i.e. move them to TestData
##########
commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/VarianceTest.java:
##########
@@ -116,75 +100,65 @@ void testVarianceNonFinite(double[] values, double
expected) {
}
@ParameterizedTest
- @MethodSource(value = "testVarianceNonFinite")
+ @MethodSource(value =
{"org.apache.commons.statistics.descriptive.TestData#testValuesNonFinite",
"testVarianceNonFinite"})
void testParallelStreamNonFinite(double[] values, double expected) {
- double ans = Arrays.stream(values)
+ final double ans = Arrays.stream(values)
.parallel()
.collect(Variance::create, Variance::accept, Variance::combine)
.getAsDouble();
Assertions.assertEquals(expected, ans, "parallel stream non-finite");
}
@ParameterizedTest
- @MethodSource(value = "testVarianceNonFinite")
+ @MethodSource(value =
{"org.apache.commons.statistics.descriptive.TestData#testValuesNonFinite",
"testVarianceNonFinite"})
void testVarianceRandomOrderNonFinite(double[] values, double expected) {
UniformRandomProvider rng = TestHelper.createRNG();
for (int i = 1; i <= 10; i++) {
testVarianceNonFinite(TestHelper.shuffle(rng, values), expected);
testParallelStreamNonFinite(TestHelper.shuffle(rng, values),
expected);
}
}
+
static Stream<Arguments> testVarianceNonFinite() {
return Stream.of(
- Arguments.of(new double[] {}, Double.NaN),
- Arguments.of(new double[] {Double.POSITIVE_INFINITY,
Double.NEGATIVE_INFINITY},
- Double.NaN),
- Arguments.of(new double[] {Double.POSITIVE_INFINITY,
Double.POSITIVE_INFINITY},
- Double.NaN),
- Arguments.of(new double[] {Double.NEGATIVE_INFINITY,
Double.NEGATIVE_INFINITY},
- Double.NaN),
- Arguments.of(new double[] {Double.POSITIVE_INFINITY,
Double.MAX_VALUE},
- Double.NaN),
- Arguments.of(new double[] {Double.NEGATIVE_INFINITY,
-Double.MIN_VALUE},
- Double.NaN),
- Arguments.of(new double[] {Double.NaN, +0.0d, -0.0d,
Double.POSITIVE_INFINITY,
- Double.NEGATIVE_INFINITY}, Double.NaN),
- Arguments.of(new double[] {Double.NaN, 34.56, 89.74}, Double.NaN),
- Arguments.of(new double[] {34.56, Double.NaN, 89.74}, Double.NaN),
- Arguments.of(new double[] {34.56, 89.74, Double.NaN}, Double.NaN),
- Arguments.of(new double[] {Double.NaN, 3.14, Double.NaN,
Double.NaN},
- Double.NaN),
- Arguments.of(new double[] {Double.NaN, Double.NaN, Double.NaN},
Double.NaN),
- Arguments.of(new double[] {Double.NEGATIVE_INFINITY,
Double.MAX_VALUE},
- Double.NaN),
- Arguments.of(new double[] {Double.POSITIVE_INFINITY,
Double.POSITIVE_INFINITY,
- Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY},
Double.NaN),
- Arguments.of(new double[] {-Double.MAX_VALUE,
Double.POSITIVE_INFINITY},
- Double.NaN)
+ Arguments.of(new double[]{Double.POSITIVE_INFINITY,
Double.POSITIVE_INFINITY}, Double.NaN),
Review Comment:
These are not the same as the MeanTest. So we are missing cases in one or
the other. I would put them all in TestData and then have a method to compute
the expected value when non-finite values are encountered. IIUC when the mean
is infinite the variance will be NaN so computing the expected value is a
simple return of NaN.
##########
commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/MeanTest.java:
##########
@@ -298,17 +215,17 @@ void testCombineRandomOrderNonFinite(double[][] values,
double expected) {
}
@ParameterizedTest
- @MethodSource(value = "testCombineNonFinite")
+ @MethodSource(value =
{"org.apache.commons.statistics.descriptive.TestData#testCombineNonFinite",
"testCombineMeanNonFinite"})
void testArrayOfArraysNonFinite(double[][] values, double expected) {
- double actual = Arrays.stream(values)
+ final double actual = Arrays.stream(values)
.map(Mean::of)
.reduce(Mean::combine)
.map(Mean::getAsDouble)
.orElseThrow(RuntimeException::new);
Assertions.assertEquals(expected, actual, "array of arrays combined
mean non-finite");
}
- static Stream<Arguments> testCombineNonFinite() {
+ static Stream<Arguments> testCombineMeanNonFinite() {
Review Comment:
I do not think we should have expected value in this stream method. Why are
these cases needed for mean and not for any other stat. Can these cases be
moved to the TestData class?
You can just have a second method to compute the expected mean for
non-finite cases. IIUC it should just be the mean of the stream of values:
```java
Arrays.stream(values).flatMapToDouble(Arrays::stream).average().orElse(Double.NaN)
```
##########
commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfSquaredDeviations.java:
##########
@@ -0,0 +1,121 @@
+/*
+ * 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.descriptive;
+
+/**
+ * Computes the sum of squared deviations from the sample mean. This
+ * statistic is related to the second moment.
+ *
+ * <p>
+ * The following recursive updating formula is used:
+ * <p>
+ * Let <ul>
+ * <li> dev = (current obs - previous mean) </li>
+ * <li> n = number of observations (including current obs) </li>
+ * </ul>
+ * Then
+ * <p>
+ * new value = old value + dev^2 * (n - 1) / n.
+ * <p>
+ *
+ * Returns the sum of squared deviations of all values seen so far.
+ *
+ * <p><strong>Note that this implementation is not synchronized.</strong> If
+ * multiple threads access an instance of this class concurrently, and at least
+ * one of the threads invokes the {@link
java.util.function.DoubleConsumer#accept(double) accept} or
+ * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method,
it must be synchronized externally.
+ *
+ * <p>However, it is safe to use {@link
java.util.function.DoubleConsumer#accept(double) accept} and {@link
DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ * as <code>accumulator</code> and <code>combiner</code> functions of
+ * {@link java.util.stream.Collector Collector} on a parallel stream,
+ * because the parallel implementation of {@link
java.util.stream.Stream#collect Stream.collect()}
+ * provides the necessary partitioning, isolation, and merging of results for
+ * safe and efficient parallel execution.
+ */
+class SumOfSquaredDeviations extends FirstMoment implements DoubleStatistic {
+ /** Sum of squared deviations of the values that have been added. */
+ private double squaredDevSum;
+
+ /**
+ * Create a SumOfSquaredDeviations instance.
+ */
+ SumOfSquaredDeviations() {
+ // No-op
+ }
+
+ /**
+ * Create a SumOfSquaredDeviations instance with the given sum of
+ * squared deviations and a FirstMoment instance.
+ *
+ * @param squaredDevSum Sum of squared deviations.
+ * @param mean Mean of values.
+ * @param n Number of values.
+ * @param nonFiniteValue Sum of values.
+ */
+ SumOfSquaredDeviations(double squaredDevSum, double mean, long n, double
nonFiniteValue) {
+ super(mean, n, nonFiniteValue);
+ this.squaredDevSum = squaredDevSum;
+ }
+
+ /**
+ * Updates the state of the statistic to reflect the addition of {@code
value}.
+ * @param value Value.
+ */
+ @Override
+ public void accept(double value) {
+ super.accept(value);
+ squaredDevSum += (n - 1) * dev * nDev;
+ }
+
+ /**
+ * Gets the sum of squared deviations of all input values.
+ *
+ * @return {@code SumOfSquaredDeviations} of all values seen so far.
+ */
+ @Override
+ public double getAsDouble() {
+ return Double.isFinite(super.getAsDouble()) ? squaredDevSum :
Double.NaN;
+ }
+
+ /**
+ * Gets a new FirstMoment instance with all of its parameters copied from
the current instance.
+ * @return The FirstMoment instance.
+ */
+ FirstMoment getFirstMoment() {
+ return new FirstMoment(m1, n, super.getNonFiniteValue(), dev, nDev);
+ }
+
+ /**
+ * Combines the state of another {@code SumOfSquaredDeviations} into this
one.
+ *
+ * @param other Another {@code SumOfSquaredDeviations} to be combined.
+ * @return {@code this} instance after combining {@code other}.
+ */
+ public SumOfSquaredDeviations combine(SumOfSquaredDeviations other) {
+ final long oldN = n;
+ final long otherN = other.n;
+ if (oldN == 0) {
+ squaredDevSum = other.squaredDevSum;
+ } else if (otherN != 0) {
+ final double diffOfMean = other.getFirstMoment().getAsDouble() -
m1;
+ final double sqDiffOfMean = diffOfMean * diffOfMean;
+ squaredDevSum += other.squaredDevSum + sqDiffOfMean * ((double)
(oldN * otherN) / (oldN + otherN));
+ }
+ super.combine(other.getFirstMoment());
Review Comment:
No need to create a copy instance here. Just use: `super.combine(other)`
##########
commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfSquaredDeviations.java:
##########
@@ -0,0 +1,121 @@
+/*
+ * 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.descriptive;
+
+/**
+ * Computes the sum of squared deviations from the sample mean. This
+ * statistic is related to the second moment.
+ *
+ * <p>
+ * The following recursive updating formula is used:
+ * <p>
+ * Let <ul>
+ * <li> dev = (current obs - previous mean) </li>
+ * <li> n = number of observations (including current obs) </li>
+ * </ul>
+ * Then
+ * <p>
+ * new value = old value + dev^2 * (n - 1) / n.
+ * <p>
+ *
+ * Returns the sum of squared deviations of all values seen so far.
+ *
+ * <p><strong>Note that this implementation is not synchronized.</strong> If
+ * multiple threads access an instance of this class concurrently, and at least
+ * one of the threads invokes the {@link
java.util.function.DoubleConsumer#accept(double) accept} or
+ * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method,
it must be synchronized externally.
+ *
+ * <p>However, it is safe to use {@link
java.util.function.DoubleConsumer#accept(double) accept} and {@link
DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ * as <code>accumulator</code> and <code>combiner</code> functions of
+ * {@link java.util.stream.Collector Collector} on a parallel stream,
+ * because the parallel implementation of {@link
java.util.stream.Stream#collect Stream.collect()}
+ * provides the necessary partitioning, isolation, and merging of results for
+ * safe and efficient parallel execution.
+ */
+class SumOfSquaredDeviations extends FirstMoment implements DoubleStatistic {
+ /** Sum of squared deviations of the values that have been added. */
+ private double squaredDevSum;
+
+ /**
+ * Create a SumOfSquaredDeviations instance.
+ */
+ SumOfSquaredDeviations() {
+ // No-op
+ }
+
+ /**
+ * Create a SumOfSquaredDeviations instance with the given sum of
+ * squared deviations and a FirstMoment instance.
+ *
+ * @param squaredDevSum Sum of squared deviations.
+ * @param mean Mean of values.
+ * @param n Number of values.
+ * @param nonFiniteValue Sum of values.
+ */
+ SumOfSquaredDeviations(double squaredDevSum, double mean, long n, double
nonFiniteValue) {
+ super(mean, n, nonFiniteValue);
+ this.squaredDevSum = squaredDevSum;
+ }
+
+ /**
+ * Updates the state of the statistic to reflect the addition of {@code
value}.
+ * @param value Value.
+ */
+ @Override
+ public void accept(double value) {
+ super.accept(value);
+ squaredDevSum += (n - 1) * dev * nDev;
+ }
+
+ /**
+ * Gets the sum of squared deviations of all input values.
+ *
+ * @return {@code SumOfSquaredDeviations} of all values seen so far.
+ */
+ @Override
+ public double getAsDouble() {
+ return Double.isFinite(super.getAsDouble()) ? squaredDevSum :
Double.NaN;
+ }
+
+ /**
+ * Gets a new FirstMoment instance with all of its parameters copied from
the current instance.
+ * @return The FirstMoment instance.
+ */
+ FirstMoment getFirstMoment() {
+ return new FirstMoment(m1, n, super.getNonFiniteValue(), dev, nDev);
+ }
+
+ /**
+ * Combines the state of another {@code SumOfSquaredDeviations} into this
one.
+ *
+ * @param other Another {@code SumOfSquaredDeviations} to be combined.
+ * @return {@code this} instance after combining {@code other}.
+ */
+ public SumOfSquaredDeviations combine(SumOfSquaredDeviations other) {
+ final long oldN = n;
+ final long otherN = other.n;
+ if (oldN == 0) {
+ squaredDevSum = other.squaredDevSum;
+ } else if (otherN != 0) {
+ final double diffOfMean = other.getFirstMoment().getAsDouble() -
m1;
Review Comment:
Avoid creating a copy instance of the FirstMoment. Instead drop the
implementation of the statistic interfaces (these are internal classes so do
not require them):
```java
// In FirstMoment:
double getFirstMoment() {
if (Double.isFinite(m1)) {
return n == 0 ? Double.NaN : m1;
}
// A non-finite value must have been encountered, return
nonFiniteValue which represents m1.
return nonFiniteValue;
}
// In SumOfSquaredDeviations:
final double diffOfMean = other.getFirstMoment() - m1
```
##########
commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Variance.java:
##########
@@ -0,0 +1,234 @@
+/*
+ * 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.descriptive;
+
+/**
+ * Computes the variance of a set of values. By default, the
+ * "sample variance" is computed. The definitional formula for sample
+ * variance is:
+ * <p>
+ * sum((x_i - mean)^2) / (n - 1)
+ * <p>This formula does not have good numerical properties, so this
+ * implementation does not use it to compute the statistic.
+ * <ul>
+ * <li> The {@link #accept(double)} method computes the variance using
+ * updating formulae based on West's algorithm, as described in
+ * <a href="http://doi.acm.org/10.1145/359146.359152"> Chan, T. F. and
+ * J. G. Lewis 1979, <i>Communications of the ACM</i>,
+ * vol. 22 no. 9, pp. 526-531.</a></li>
+ *
+ * <li> The {@link #of(double...)} method leverages the fact that it has the
+ * full array of values in memory to execute a two-pass algorithm.
+ * Specifically, this method uses the "corrected two-pass algorithm" from
+ * Chan, Golub, Levesque, <i>Algorithms for Computing the Sample Variance</i>,
+ * American Statistician, vol. 37, no. 3 (1983) pp. 242-247.</li></ul>
+ *
+ * Note that adding values using {@code accept} and then executing {@code
getAsDouble} will
+ * sometimes give a different, less accurate, result than executing
+ * {@code of} with the full array of values. The former approach
+ * should only be used when the full array of values is not available.
+ *
+ * <p>
+ * Returns <code>Double.NaN</code> if no data values have been added and
+ * returns <code>0</code> if there is just one finite value in the data set.
+ * Note that <code>Double.NaN</code> may also be returned if the input includes
+ * <code>Double.NaN</code> and / or infinite values.
+ *
+ * <p>This class is designed to work with (though does not require)
+ * {@linkplain java.util.stream streams}.
+ *
+ * <p><strong>Note that this implementation is not synchronized.</strong> If
+ * multiple threads access an instance of this class concurrently, and at least
+ * one of the threads invokes the {@link
java.util.function.DoubleConsumer#accept(double) accept} or
+ * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method,
it must be synchronized externally.
+ *
+ * <p>However, it is safe to use <code>accept()</code> and
<code>combine()</code>
+ * as <code>accumulator</code> and <code>combiner</code> functions of
+ * {@link java.util.stream.Collector Collector} on a parallel stream,
+ * because the parallel implementation of {@link
java.util.stream.Stream#collect Stream.collect()}
+ * provides the necessary partitioning, isolation, and merging of results for
+ * safe and efficient parallel execution.
+ *
+ * @since 1.1
+ */
+public abstract class Variance implements DoubleStatistic,
DoubleStatisticAccumulator<Variance> {
+
+ /**
+ * Create a Variance instance.
+ */
+ Variance() {
+ // No-op
+ }
+
+ /**
+ * Creates a {@code Variance} implementation which does not store the
input value(s) it consumes.
+ *
+ * <p>The result is <code>NaN</code> if:
+ * <ul>
+ * <li>no values have been added,</li>
+ * <li>any of the values is <code>NaN</code>, or</li>
+ * <li>an infinite value of either sign is encountered</li>
+ * </ul>
+ *
+ * @return {@code Variance} implementation.
+ */
+ public static Variance create() {
+ return new StorelessSampleVariance();
+ }
+
+ /**
+ * Returns a {@code Variance} instance that has the variance of all input
values, or <code>NaN</code>
+ * if:
+ * <ul>
+ * <li>the input array is empty,</li>
+ * <li>any of the values is <code>NaN</code>, or</li>
+ * <li>an infinite value of either sign is encountered</li>
+ * </ul>
+ *
+ * <p>Note: {@code Variance} computed using {@link Variance#accept
Variance.accept()} may be different
+ * from this variance.
+ *
+ * <p>See {@link Variance} for details on the computing algorithm.
+ *
+ * @param values Values.
+ * @return {@code Variance} instance.
+ */
+ public static Variance of(double... values) {
+ final double mean = Mean.of(values).getAsDouble();
Review Comment:
You can short-circuit here if the mean is non-finite. I think this is
correct. We either return a infinity if the mean is +/-infinity or propagate
the nan for the sum of squared deviations.
```java
if (!Double.isFinite(mean)) {
return StorelessSampleVariance.create(Math.abs(mean), mean,
values.length, Math.abs(mean));
}
```
##########
commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/VarianceTest.java:
##########
@@ -0,0 +1,259 @@
+/*
+ * 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.descriptive;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Test for {@link Variance}.
+ */
+final class VarianceTest {
+ private static final int ULP_ARRAY = 5;
+
+ private static final int ULP_STREAM = 7;
+
+ private static final int ULP_COMBINE = 7;
+
+ private static final int ULP_ARRAY_OF_ARRAYS = 2;
+
+ @Test
+ void testEmpty() {
+ Variance var = Variance.create();
+ Assertions.assertEquals(Double.NaN, var.getAsDouble());
+ }
+
+ @Test
+ void testNaN() {
+ Variance variance = Variance.create();
+ double[] testArray = {Double.NaN, +0.0d, -0.0d,
Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY};
+ for (double value : testArray) {
+ variance.accept(value);
+ }
+ Assertions.assertEquals(Double.NaN, variance.getAsDouble());
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
"org.apache.commons.statistics.descriptive.TestData#testValues")
+ void testVariance(double[] values) {
+ final double expected = computeExpectedVariance(values);
+ Variance var = Variance.create();
+ for (double value : values) {
+ var.accept(value);
+ }
+ TestHelper.assertEquals(expected, var.getAsDouble(), ULP_STREAM, () ->
"variance");
+ TestHelper.assertEquals(expected, Variance.of(values).getAsDouble(),
ULP_ARRAY, () -> "of (values)");
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
"org.apache.commons.statistics.descriptive.TestData#testValues")
+ void testParallelStream(double[] values) {
+ final double expected = computeExpectedVariance(values);
+ final double actual = Arrays.stream(values)
+ .parallel()
+ .collect(Variance::create, Variance::accept, Variance::combine)
+ .getAsDouble();
+ TestHelper.assertEquals(expected, actual, ULP_COMBINE, () -> "parallel
stream");
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
"org.apache.commons.statistics.descriptive.TestData#testValues")
+ void testVarianceRandomOrder(double[] values) {
+ UniformRandomProvider rng = TestHelper.createRNG();
+ for (int i = 1; i <= 10; i++) {
+ testVariance(TestHelper.shuffle(rng, values));
+ testParallelStream(TestHelper.shuffle(rng, values));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
{"org.apache.commons.statistics.descriptive.TestData#testValuesNonFinite",
"testVarianceNonFinite"})
+ void testVarianceNonFinite(double[] values, double expected) {
+ Variance var = Variance.create();
+ for (double value : values) {
+ var.accept(value);
+ }
+ Assertions.assertEquals(expected, var.getAsDouble(), "variance
non-finite");
+ Assertions.assertEquals(expected, Variance.of(values).getAsDouble(),
"of (values) non-finite");
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
{"org.apache.commons.statistics.descriptive.TestData#testValuesNonFinite",
"testVarianceNonFinite"})
+ void testParallelStreamNonFinite(double[] values, double expected) {
+ final double ans = Arrays.stream(values)
+ .parallel()
+ .collect(Variance::create, Variance::accept, Variance::combine)
+ .getAsDouble();
+ Assertions.assertEquals(expected, ans, "parallel stream non-finite");
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
{"org.apache.commons.statistics.descriptive.TestData#testValuesNonFinite",
"testVarianceNonFinite"})
+ void testVarianceRandomOrderNonFinite(double[] values, double expected) {
+ UniformRandomProvider rng = TestHelper.createRNG();
+ for (int i = 1; i <= 10; i++) {
+ testVarianceNonFinite(TestHelper.shuffle(rng, values), expected);
+ testParallelStreamNonFinite(TestHelper.shuffle(rng, values),
expected);
+ }
+ }
+
+ static Stream<Arguments> testVarianceNonFinite() {
+ return Stream.of(
+ Arguments.of(new double[]{Double.POSITIVE_INFINITY,
Double.POSITIVE_INFINITY}, Double.NaN),
+ Arguments.of(new double[]{Double.NEGATIVE_INFINITY,
Double.NEGATIVE_INFINITY}, Double.NaN),
+ Arguments.of(new double[]{Double.POSITIVE_INFINITY,
Double.MAX_VALUE}, Double.NaN),
+ Arguments.of(new double[]{Double.NEGATIVE_INFINITY,
-Double.MIN_VALUE}, Double.NaN),
+ Arguments.of(new double[]{Double.NEGATIVE_INFINITY,
Double.MAX_VALUE}, Double.NaN),
+ Arguments.of(new double[]{Double.POSITIVE_INFINITY,
Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY},
Double.NaN),
+ Arguments.of(new double[]{-Double.MAX_VALUE,
Double.POSITIVE_INFINITY}, Double.NaN)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
"org.apache.commons.statistics.descriptive.TestData#testCombine")
+ void testCombine(double[] array1, double[] array2) {
+ final double[] combinedArray = TestHelper.concatenate(array1, array2);
+ final double expected = computeExpectedVariance(combinedArray);
+ Variance var1 = Variance.create();
+ Variance var2 = Variance.create();
+ Arrays.stream(array1).forEach(var1);
+ Arrays.stream(array2).forEach(var2);
+ final double var1BeforeCombine = var1.getAsDouble();
+ final double var2BeforeCombine = var2.getAsDouble();
+ var1.combine(var2);
+ TestHelper.assertEquals(expected, var1.getAsDouble(), ULP_COMBINE, ()
-> "combine");
+ Assertions.assertEquals(var2BeforeCombine, var2.getAsDouble());
+ // Combine in reverse order
+ Variance var1b = Variance.create();
+ Arrays.stream(array1).forEach(var1b);
+ var2.combine(var1b);
+ TestHelper.assertEquals(expected, var2.getAsDouble(), ULP_COMBINE, ()
-> "combine");
+ Assertions.assertEquals(var1BeforeCombine, var1b.getAsDouble());
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
"org.apache.commons.statistics.descriptive.TestData#testCombine")
+ void testCombineRandomOrder(double[] array1, double[] array2) {
+ UniformRandomProvider rng = TestHelper.createRNG();
+ double[] data = TestHelper.concatenate(array1, array2);
+ final int n = array1.length;
+ for (int i = 1; i <= 10; i++) {
+ for (int j = 1; j <= 10; j++) {
+ TestHelper.shuffle(rng, array1);
+ TestHelper.shuffle(rng, array2);
+ testCombine(array1, array2);
+ }
+ TestHelper.shuffle(rng, data);
+ System.arraycopy(data, 0, array1, 0, n);
+ System.arraycopy(data, n, array2, 0, array2.length);
+ testCombine(array1, array2);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
"org.apache.commons.statistics.descriptive.TestData#testCombine")
+ void testArrayOfArrays(double[] array1, double[] array2) {
+ final double[] combinedArray = TestHelper.concatenate(array1, array2);
+ final double expected = computeExpectedVariance(combinedArray);
+ final double[][] values = {array1, array2};
+ final double actual = Arrays.stream(values)
+ .map(Variance::of)
+ .reduce(Variance::combine)
+ .map(Variance::getAsDouble)
+ .orElseThrow(RuntimeException::new);
+ TestHelper.assertEquals(expected, actual, ULP_ARRAY_OF_ARRAYS, () ->
"array of arrays combined variance");
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
{"org.apache.commons.statistics.descriptive.TestData#testCombineNonFinite",
"testCombineVarianceNonFinite"})
+ void testCombineNonFinite(double[][] values, double expected) {
+ Variance var1 = Variance.create();
+ Variance var2 = Variance.create();
+ Arrays.stream(values[0]).forEach(var1);
+ Arrays.stream(values[1]).forEach(var2);
+ final double mean2BeforeCombine = var2.getAsDouble();
+ var1.combine(var2);
+ Assertions.assertEquals(expected, var1.getAsDouble(), "combine
non-finite");
+ Assertions.assertEquals(mean2BeforeCombine, var2.getAsDouble());
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
{"org.apache.commons.statistics.descriptive.TestData#testCombineNonFinite",
"testCombineVarianceNonFinite"})
+ void testCombineRandomOrderNonFinite(double[][] values, double expected) {
+ UniformRandomProvider rng = TestHelper.createRNG();
+ final double[] data = TestHelper.concatenate(values[0], values[1]);
+ final int n = values[0].length;
+ for (int i = 1; i <= 10; i++) {
+ for (int j = 1; j <= 10; j++) {
+ TestHelper.shuffle(rng, values[0]);
+ TestHelper.shuffle(rng, values[1]);
+ testCombineNonFinite(values, expected);
+ }
+ TestHelper.shuffle(rng, data);
+ System.arraycopy(data, 0, values[0], 0, n);
+ System.arraycopy(data, n, values[1], 0, values[1].length);
+ testCombineNonFinite(values, expected);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource(value =
{"org.apache.commons.statistics.descriptive.TestData#testCombineNonFinite",
"testCombineVarianceNonFinite"})
+ void testArrayOfArraysNonFinite(double[][] values, double expected) {
+ final double actual = Arrays.stream(values)
+ .map(Variance::of)
+ .reduce(Variance::combine)
+ .map(Variance::getAsDouble)
+ .orElseThrow(RuntimeException::new);
+ Assertions.assertEquals(expected, actual, "array of arrays combined
variance non-finite");
+ }
+
+ static Stream<Arguments> testCombineVarianceNonFinite() {
+ return Stream.of(
+ Arguments.of(new double[][] {{Double.POSITIVE_INFINITY},
{Double.POSITIVE_INFINITY}}, Double.NaN),
+ Arguments.of(new double[][] {{Double.NEGATIVE_INFINITY},
{Double.NEGATIVE_INFINITY}}, Double.NaN),
+ Arguments.of(new double[][] {{Double.POSITIVE_INFINITY},
{Double.MAX_VALUE}}, Double.NaN),
+ Arguments.of(new double[][] {{-Double.MAX_VALUE},
{Double.POSITIVE_INFINITY}}, Double.NaN),
+ Arguments.of(new double[][] {{Double.NEGATIVE_INFINITY},
{-Double.MIN_VALUE}}, Double.NaN),
+ Arguments.of(new double[][] {{Double.NEGATIVE_INFINITY,
-Double.MAX_VALUE, -Double.MIN_VALUE}, {Double.MAX_VALUE, Double.MIN_VALUE}},
Double.NaN)
+ );
+ }
+
+ // Helper function to compute the expected value of Variance using
BigDecimal.
+ static double computeExpectedVariance(double[] values) {
+ long n = values.length;
+ if (n == 1) {
+ return BigDecimal.ZERO.doubleValue();
Review Comment:
Just return zero here
##########
commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/TestData.java:
##########
@@ -0,0 +1,114 @@
+/*
+ * 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.descriptive;
+
+import java.util.stream.Stream;
+import org.junit.jupiter.params.provider.Arguments;
+
+final class TestData {
+
+ /** Class contains only static methods. */
+ private TestData() {}
+
+ static Stream<double[]> testValues() {
+ return Stream.of(
+ new double[] {0.0},
+ new double[] {10, 8, 13, 9, 11, 14, 6, 4, 12, 7, 5},
+ new double[] {8.04, 6.95, 7.58, 8.81, 8.33, 9.96, 7.24, 4.26,
10.84, 4.82, 5.68},
+ new double[] {9.14, 8.14, 8.74, 8.77, 9.26, 8.10, 6.13, 3.10,
9.13, 7.26, 4.74, 7.46, 6.77, 12.74, 7.11, 7.81, 8.84, 6.08, 5.39, 8.15, 6.42,
5.73},
+ new double[] {8, 8, 8, 8, 8, 8, 8, 19, 8, 8, 8},
+ new double[] {6.58, 5.76, 7.71, 8.84, 8.47, 7.04, 5.25, 12.50,
5.56, 7.91, 6.89},
+ new double[] {0, 0, 0.0},
+ new double[] {1, -7, 6},
+ new double[] {1, 7, -15, 3},
+ new double[] {2, 2, 2, 2},
+ new double[] {2.3},
+ new double[] {3.14, 2.718, 1.414},
+ new double[] {12.5, 12.0, 11.8, 14.2, 14.9, 14.5, 21.0, 8.2, 10.3,
11.3, 14.1, 9.9, 12.2, 12.0, 12.1, 11.0, 19.8, 11.0, 10.0, 8.8, 9.0, 12.3},
+ new double[] {-0.0, +0.0},
+ new double[] {0.0, -0.0},
+ new double[] {0.0, +0.0},
+ new double[] {0.001, 0.0002, 0.00003, 10000.11, 0.000004},
+ new double[] {10E-50, 5E-100, 25E-200, 35.345E-50},
+ // Overflow of the sum
+ new double[] {Double.MAX_VALUE, Double.MAX_VALUE},
+ new double[] {-Double.MAX_VALUE, -Double.MAX_VALUE},
+ new double[] {Double.MAX_VALUE, 1},
+ new double[] {-Double.MAX_VALUE, 1, 1},
+ new double[] {-Double.MAX_VALUE, -1, 1},
+ new double[] {Double.MAX_VALUE, -1},
+ new double[] {Double.MAX_VALUE, -Double.MAX_VALUE},
+ new double[] {1, -Double.MAX_VALUE},
+ new double[] {1, 1, 1, -Double.MAX_VALUE},
+ new double[] {Double.MAX_VALUE, Double.MAX_VALUE / 2},
+ new double[] {Double.MAX_VALUE, Double.MAX_VALUE,
-Double.MAX_VALUE},
+ new double[] {Double.MAX_VALUE, -Double.MAX_VALUE}
+ );
+ }
+
+ static Stream<Arguments> testValuesNonFinite() {
+ return Stream.of(
+ Arguments.of(new double[]{}, Double.NaN),
+ Arguments.of(new double[]{Double.POSITIVE_INFINITY,
Double.NEGATIVE_INFINITY}, Double.NaN),
+ Arguments.of(new double[]{Double.NaN, 34.56, 89.74}, Double.NaN),
+ Arguments.of(new double[]{34.56, Double.NaN, 89.74}, Double.NaN),
+ Arguments.of(new double[]{34.56, 89.74, Double.NaN}, Double.NaN),
+ Arguments.of(new double[]{Double.NaN, 3.14, Double.NaN,
Double.NaN}, Double.NaN),
+ Arguments.of(new double[]{Double.NaN, Double.NaN, Double.NaN},
Double.NaN)
+ );
+ }
+
+ static Stream<Arguments> testCombine() {
+ return Stream.of(
+ Arguments.of(new double[] {}, new double[] {1}),
+ Arguments.of(new double[] {1}, new double[] {}),
+ Arguments.of(new double[] {}, new double[] {1, 7, -15, 3}),
+ Arguments.of(new double[] {0}, new double[] {0, 0.0}),
+ Arguments.of(new double[] {4, 8, -6, 3, 18}, new double[] {1, -7,
6}),
+ Arguments.of(new double[] {10, 8, 13, 9, 11, 14, 6, 4, 12, 7, 5},
new double[] {8, 8, 8, 8, 8, 8, 8, 19, 8, 8, 8}),
+ Arguments.of(new double[] {10, 8, 13, 9, 11, 14, 6, 4, 12, 7, 5},
new double[] {7.46, 6.77, 12.74, 7.11, 7.81, 8.84, 6.08, 5.39, 8.15, 6.42,
5.73}),
+ Arguments.of(new double[] {6.0, -1.32, -5.78, 8.967, 13.32, -9.67,
0.14, 7.321, 11.456, -3.111}, new double[] {2, 2, 2, 2}),
+ Arguments.of(new double[] {2.3}, new double[] {-42, 10, -88, 5,
-17}),
+ Arguments.of(new double[] {-20, 34.983, -12.745, 28.12, -8.34, 42,
-4, 16}, new double[] {3.14, 2.718, 1.414}),
+ Arguments.of(new double[] {12.5, 12.0, 11.8, 14.2, 14.9, 14.5,
21.0, 8.2, 10.3, 11.3, 14.1, 9.9}, new double[] {12.2, 12.0, 12.1, 11.0, 19.8,
11.0, 10.0, 8.8, 9.0, 12.3}),
+ Arguments.of(new double[] {-0.0}, new double[] {+0.0}),
+ Arguments.of(new double[] {0.0}, new double[] {-0.0}),
+ Arguments.of(new double[] {0.0}, new double[] {+0.0}),
+ Arguments.of(new double[] {10E-50, 5E-100}, new double[] {25E-200,
35.345E-50}),
+ Arguments.of(new double[] {Double.MAX_VALUE}, new double[]
{Double.MAX_VALUE}),
+ Arguments.of(new double[] {-Double.MAX_VALUE}, new double[]
{-Double.MAX_VALUE}),
+ Arguments.of(new double[] {-Double.MAX_VALUE, 1}, new double[]
{1}),
+ Arguments.of(new double[] {Double.MAX_VALUE, 3.1415E153}, new
double[] {}),
+ Arguments.of(new double[] {Double.MAX_VALUE}, new double[]
{-Double.MAX_VALUE}),
+ Arguments.of(new double[] {1}, new double[] {-Double.MAX_VALUE}),
+ Arguments.of(new double[] {1, 1, 1}, new double[]
{-Double.MAX_VALUE}),
+ Arguments.of(new double[] {Double.MAX_VALUE}, new double[] {1,
1E300})
+ );
+ }
+
+ static Stream<Arguments> testCombineNonFinite() {
Review Comment:
Again, the expected should not be here. It is always NaN so not helpful to
any consumers of the data.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]