LongStatistics.java
/*
* 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.BigInteger;
import java.util.Objects;
import java.util.Set;
import java.util.function.DoubleConsumer;
import java.util.function.Function;
import java.util.function.LongConsumer;
/**
* Statistics for {@code long} values.
*
* <p>This class provides combinations of individual statistic implementations in the
* {@code org.apache.commons.statistics.descriptive} package.
*
* <p>Supports up to 2<sup>63</sup> (exclusive) observations.
* This implementation does not check for overflow of the count.
*
* @since 1.1
*/
public final class LongStatistics implements LongConsumer {
/** Error message for non configured statistics. */
private static final String NO_CONFIGURED_STATISTICS = "No configured statistics";
/** Error message for an unsupported statistic. */
private static final String UNSUPPORTED_STATISTIC = "Unsupported statistic: ";
/** Count of values recorded. */
private long count;
/** The consumer of values. */
private final LongConsumer consumer;
/** The {@link LongMin} implementation. */
private final LongMin min;
/** The {@link LongMax} implementation. */
private final LongMax max;
/** The moment implementation. May be any instance of {@link FirstMoment}.
* This implementation uses only the third and fourth moments. */
private final FirstMoment moment;
/** The {@link LongSum} implementation. */
private final LongSum sum;
/** The {@link Product} implementation. */
private final Product product;
/** The {@link LongSumOfSquares} implementation. */
private final LongSumOfSquares sumOfSquares;
/** The {@link SumOfLogs} implementation. */
private final SumOfLogs sumOfLogs;
/** Configuration options for computation of statistics. */
private StatisticsConfiguration config;
/**
* A builder for {@link LongStatistics}.
*/
public static final class Builder {
/** An empty double array. */
private static final long[] NO_VALUES = {};
/** The {@link LongMin} constructor. */
private Function<long[], LongMin> min;
/** The {@link LongMax} constructor. */
private Function<long[], LongMax> max;
/** The moment constructor. May return any instance of {@link FirstMoment}. */
private Function<long[], FirstMoment> moment;
/** The {@link LongSum} constructor. */
private Function<long[], LongSum> sum;
/** The {@link Product} constructor. */
private Function<long[], Product> product;
/** The {@link LongSumOfSquares} constructor. */
private Function<long[], LongSumOfSquares> sumOfSquares;
/** The {@link SumOfLogs} constructor. */
private Function<long[], SumOfLogs> sumOfLogs;
/** The order of the moment. It corresponds to the power computed by the {@link FirstMoment}
* instance constructed by {@link #moment}. This should only be increased from the default
* of zero (corresponding to no moment computation). */
private int momentOrder;
/** Configuration options for computation of statistics. */
private StatisticsConfiguration config = StatisticsConfiguration.withDefaults();
/**
* Create an instance.
*/
Builder() {
// Do nothing
}
/**
* Add the statistic to the statistics to compute.
*
* @param statistic Statistic to compute.
* @return {@code this} instance
*/
Builder add(Statistic statistic) {
switch (statistic) {
case GEOMETRIC_MEAN:
case SUM_OF_LOGS:
sumOfLogs = SumOfLogs::of;
break;
case KURTOSIS:
createMoment(4);
break;
case MAX:
max = LongMax::of;
break;
case MIN:
min = LongMin::of;
break;
case PRODUCT:
product = Product::of;
break;
case SKEWNESS:
createMoment(3);
break;
case STANDARD_DEVIATION:
case VARIANCE:
sum = LongSum::of;
sumOfSquares = LongSumOfSquares::of;
break;
case MEAN:
case SUM:
sum = LongSum::of;
break;
case SUM_OF_SQUARES:
sumOfSquares = LongSumOfSquares::of;
break;
default:
throw new IllegalArgumentException(UNSUPPORTED_STATISTIC + statistic);
}
return this;
}
/**
* Creates the moment constructor for the specified {@code order},
* e.g. order=3 is sum of cubed deviations.
*
* @param order Order.
*/
private void createMoment(int order) {
if (order > momentOrder) {
momentOrder = order;
if (order == 4) {
moment = SumOfFourthDeviations::of;
} else {
// Assume order == 3
moment = SumOfCubedDeviations::of;
}
}
}
/**
* Sets the statistics configuration options for computation of statistics.
*
* @param v Value.
* @return the builder
* @throws NullPointerException if the value is null
*/
public Builder setConfiguration(StatisticsConfiguration v) {
config = Objects.requireNonNull(v);
return this;
}
/**
* Builds a {@code LongStatistics} instance.
*
* @return {@code LongStatistics} instance.
*/
public LongStatistics build() {
return build(NO_VALUES);
}
/**
* Builds a {@code LongStatistics} instance using the input {@code values}.
*
* <p>Note: {@code LongStatistics} computed using
* {@link LongStatistics#accept(long) accept} may be
* different from this instance.
*
* @param values Values.
* @return {@code LongStatistics} instance.
*/
public LongStatistics build(long... values) {
Objects.requireNonNull(values, "values");
return new LongStatistics(
values.length,
create(min, values),
create(max, values),
create(moment, values),
create(sum, values),
create(product, values),
create(sumOfSquares, values),
create(sumOfLogs, values),
config);
}
/**
* Creates the object from the {@code values}.
*
* @param <T> object type
* @param constructor Constructor.
* @param values Values
* @return the instance
*/
private static <T> T create(Function<long[], T> constructor, long[] values) {
if (constructor != null) {
return constructor.apply(values);
}
return null;
}
}
/**
* Create an instance.
*
* @param count Count of values.
* @param min LongMin implementation.
* @param max LongMax implementation.
* @param moment Moment implementation.
* @param sum LongSum implementation.
* @param product Product implementation.
* @param sumOfSquares Sum of squares implementation.
* @param sumOfLogs Sum of logs implementation.
* @param config Statistics configuration.
*/
LongStatistics(long count, LongMin min, LongMax max, FirstMoment moment, LongSum sum,
Product product, LongSumOfSquares sumOfSquares, SumOfLogs sumOfLogs,
StatisticsConfiguration config) {
this.count = count;
this.min = min;
this.max = max;
this.moment = moment;
this.sum = sum;
this.product = product;
this.sumOfSquares = sumOfSquares;
this.sumOfLogs = sumOfLogs;
this.config = config;
// The final consumer should never be null as the builder is created
// with at least one statistic.
consumer = Statistics.compose(min, max, sum, sumOfSquares,
composeAsLong(moment, product, sumOfLogs));
}
/**
* Chain the {@code consumers} into a single composite {@code LongConsumer}.
* Ignore any {@code null} consumer.
*
* @param consumers Consumers.
* @return a composed consumer (or null)
*/
private static LongConsumer composeAsLong(DoubleConsumer... consumers) {
final DoubleConsumer c = Statistics.compose(consumers);
if (c != null) {
return c::accept;
}
return null;
}
/**
* Returns a new instance configured to compute the specified {@code statistics}.
*
* <p>The statistics will be empty and so will return the default values for each
* computed statistic.
*
* @param statistics Statistics to compute.
* @return the instance
* @throws IllegalArgumentException if there are no {@code statistics} to compute.
*/
public static LongStatistics of(Statistic... statistics) {
return builder(statistics).build();
}
/**
* Returns a new instance configured to compute the specified {@code statistics}
* populated using the input {@code values}.
*
* <p>Use this method to create an instance populated with a (variable) array of
* {@code long[]} data:
*
* <pre>
* LongStatistics stats = LongStatistics.of(
* EnumSet.of(Statistic.MIN, Statistic.MAX),
* 1, 1, 2, 3, 5, 8, 13);
* </pre>
*
* @param statistics Statistics to compute.
* @param values Values.
* @return the instance
* @throws IllegalArgumentException if there are no {@code statistics} to compute.
*/
public static LongStatistics of(Set<Statistic> statistics, long... values) {
if (statistics.isEmpty()) {
throw new IllegalArgumentException(NO_CONFIGURED_STATISTICS);
}
final Builder b = new Builder();
statistics.forEach(b::add);
return b.build(values);
}
/**
* Returns a new builder configured to create instances to compute the specified
* {@code statistics}.
*
* <p>Use this method to create an instance populated with an array of {@code long[]}
* data using the {@link Builder#build(long...)} method:
*
* <pre>
* long[] data = ...
* LongStatistics stats = LongStatistics.builder(
* Statistic.MIN, Statistic.MAX, Statistic.VARIANCE)
* .build(data);
* </pre>
*
* <p>The builder can be used to create multiple instances of {@link LongStatistics}
* to be used in parallel, or on separate arrays of {@code long[]} data. These may
* be {@link #combine(LongStatistics) combined}. For example:
*
* <pre>
* long[][] data = ...
* LongStatistics.Builder builder = LongStatistics.builder(
* Statistic.MIN, Statistic.MAX, Statistic.VARIANCE);
* LongStatistics stats = Arrays.stream(data)
* .parallel()
* .map(builder::build)
* .reduce(LongStatistics::combine)
* .get();
* </pre>
*
* <p>The builder can be used to create a {@link java.util.stream.Collector} for repeat
* use on multiple data:
*
* <pre>{@code
* LongStatistics.Builder builder = LongStatistics.builder(
* Statistic.MIN, Statistic.MAX, Statistic.VARIANCE);
* Collector<long[], LongStatistics, LongStatistics> collector =
* Collector.of(builder::build,
* (s, d) -> s.combine(builder.build(d)),
* LongStatistics::combine);
*
* // Repeated
* long[][] data = ...
* LongStatistics stats = Arrays.stream(data).collect(collector);
* }</pre>
*
* @param statistics Statistics to compute.
* @return the builder
* @throws IllegalArgumentException if there are no {@code statistics} to compute.
*/
public static Builder builder(Statistic... statistics) {
if (statistics.length == 0) {
throw new IllegalArgumentException(NO_CONFIGURED_STATISTICS);
}
final Builder b = new Builder();
for (final Statistic s : statistics) {
b.add(s);
}
return b;
}
/**
* Updates the state of the statistics to reflect the addition of {@code value}.
*
* @param value Value.
*/
@Override
public void accept(long value) {
count++;
consumer.accept(value);
}
/**
* Return the count of values recorded.
*
* @return the count of values
*/
public long getCount() {
return count;
}
/**
* Check if the specified {@code statistic} is supported.
*
* <p>Note: This method will not return {@code false} if the argument is {@code null}.
*
* @param statistic Statistic.
* @return {@code true} if supported
* @throws NullPointerException if the {@code statistic} is {@code null}
* @see #getResult(Statistic)
*/
public boolean isSupported(Statistic statistic) {
// Check for the appropriate underlying implementation
switch (statistic) {
case GEOMETRIC_MEAN:
case SUM_OF_LOGS:
return sumOfLogs != null;
case KURTOSIS:
return moment instanceof SumOfFourthDeviations;
case MAX:
return max != null;
case MIN:
return min != null;
case PRODUCT:
return product != null;
case SKEWNESS:
return moment instanceof SumOfCubedDeviations;
case STANDARD_DEVIATION:
case VARIANCE:
return sum != null && sumOfSquares != null;
case MEAN:
case SUM:
return sum != null;
case SUM_OF_SQUARES:
return sumOfSquares != null;
default:
return false;
}
}
/**
* Gets the value of the specified {@code statistic} as a {@code double}.
*
* @param statistic Statistic.
* @return the value
* @throws IllegalArgumentException if the {@code statistic} is not supported
* @see #isSupported(Statistic)
* @see #getResult(Statistic)
*/
public double getAsDouble(Statistic statistic) {
return getResult(statistic).getAsDouble();
}
/**
* Gets the value of the specified {@code statistic} as a {@code long}.
*
* <p>Use this method to access the {@code long} result for exact integer statistics,
* for example {@link Statistic#MIN}.
*
* <p>Note: This method may throw an {@link ArithmeticException} if the result
* overflows an {@code long}.
*
* @param statistic Statistic.
* @return the value
* @throws IllegalArgumentException if the {@code statistic} is not supported
* @throws ArithmeticException if the {@code result} overflows an {@code long} or is not
* finite
* @see #isSupported(Statistic)
* @see #getResult(Statistic)
*/
public long getAsLong(Statistic statistic) {
return getResult(statistic).getAsLong();
}
/**
* Gets the value of the specified {@code statistic} as a {@code BigInteger}.
*
* <p>Use this method to access the {@code BigInteger} result for exact integer statistics,
* for example {@link Statistic#SUM_OF_SQUARES}.
*
* <p>Note: This method may throw an {@link ArithmeticException} if the result
* is not finite.
*
* @param statistic Statistic.
* @return the value
* @throws IllegalArgumentException if the {@code statistic} is not supported
* @throws ArithmeticException if the {@code result} is not finite
* @see #isSupported(Statistic)
* @see #getResult(Statistic)
*/
public BigInteger getAsBigInteger(Statistic statistic) {
return getResult(statistic).getAsBigInteger();
}
/**
* Gets a supplier for the value of the specified {@code statistic}.
*
* <p>The returned function will supply the correct result after
* calls to {@link #accept(long) accept} or
* {@link #combine(LongStatistics) combine} further values into
* {@code this} instance.
*
* <p>This method can be used to perform a one-time look-up of the statistic
* function to compute statistics as values are dynamically added.
*
* @param statistic Statistic.
* @return the supplier
* @throws IllegalArgumentException if the {@code statistic} is not supported
* @see #isSupported(Statistic)
* @see #getAsDouble(Statistic)
*/
public StatisticResult getResult(Statistic statistic) {
// Locate the implementation.
// Statistics that wrap an underlying implementation are created in methods.
// The return argument should be an interface reference and not an instance
// of LongStatistic. This ensures the statistic implementation cannot
// be updated with new values by casting the result and calling accept(long).
StatisticResult stat = null;
switch (statistic) {
case GEOMETRIC_MEAN:
stat = getGeometricMean();
break;
case KURTOSIS:
stat = getKurtosis();
break;
case MAX:
stat = Statistics.getResultAsLongOrNull(max);
break;
case MEAN:
stat = getMean();
break;
case MIN:
stat = Statistics.getResultAsLongOrNull(min);
break;
case PRODUCT:
stat = Statistics.getResultAsDoubleOrNull(product);
break;
case SKEWNESS:
stat = getSkewness();
break;
case STANDARD_DEVIATION:
stat = getStandardDeviation();
break;
case SUM:
stat = Statistics.getResultAsBigIntegerOrNull(sum);
break;
case SUM_OF_LOGS:
stat = Statistics.getResultAsDoubleOrNull(sumOfLogs);
break;
case SUM_OF_SQUARES:
stat = Statistics.getResultAsBigIntegerOrNull(sumOfSquares);
break;
case VARIANCE:
stat = getVariance();
break;
default:
break;
}
if (stat != null) {
return stat;
}
throw new IllegalArgumentException(UNSUPPORTED_STATISTIC + statistic);
}
/**
* Gets the geometric mean.
*
* @return a geometric mean supplier (or null if unsupported)
*/
private StatisticResult getGeometricMean() {
if (sumOfLogs != null) {
// Return a function that has access to the count and sumOfLogs
return () -> GeometricMean.computeGeometricMean(count, sumOfLogs);
}
return null;
}
/**
* Gets the kurtosis.
*
* @return a kurtosis supplier (or null if unsupported)
*/
private StatisticResult getKurtosis() {
if (moment instanceof SumOfFourthDeviations) {
return new Kurtosis((SumOfFourthDeviations) moment)
.setBiased(config.isBiased())::getAsDouble;
}
return null;
}
/**
* Gets the mean.
*
* @return a mean supplier (or null if unsupported)
*/
private StatisticResult getMean() {
if (sum != null) {
// Return a function that has access to the count and sum
final Int128 s = sum.getSum();
return () -> LongMean.computeMean(s, count);
}
return null;
}
/**
* Gets the skewness.
*
* @return a skewness supplier (or null if unsupported)
*/
private StatisticResult getSkewness() {
if (moment instanceof SumOfCubedDeviations) {
return new Skewness((SumOfCubedDeviations) moment)
.setBiased(config.isBiased())::getAsDouble;
}
return null;
}
/**
* Gets the standard deviation.
*
* @return a standard deviation supplier (or null if unsupported)
*/
private StatisticResult getStandardDeviation() {
return getVarianceOrStd(true);
}
/**
* Gets the variance.
*
* @return a variance supplier (or null if unsupported)
*/
private StatisticResult getVariance() {
return getVarianceOrStd(false);
}
/**
* Gets the variance or standard deviation.
*
* @param std Flag to control if the statistic is the standard deviation.
* @return a variance/standard deviation supplier (or null if unsupported)
*/
private StatisticResult getVarianceOrStd(boolean std) {
if (sum != null && sumOfSquares != null) {
// Return a function that has access to the count, sum and sum of squares
final Int128 s = sum.getSum();
final UInt192 ss = sumOfSquares.getSumOfSquares();
final boolean biased = config.isBiased();
return () -> LongVariance.computeVarianceOrStd(ss, s, count, biased, std);
}
return null;
}
/**
* Combines the state of the {@code other} statistics into this one.
* Only {@code this} instance is modified by the {@code combine} operation.
*
* <p>The {@code other} instance must be <em>compatible</em>. This is {@code true} if the
* {@code other} instance returns {@code true} for {@link #isSupported(Statistic)} for
* all values of the {@link Statistic} enum which are supported by {@code this}
* instance.
*
* <p>Note that this operation is <em>not symmetric</em>. It may be possible to perform
* {@code a.combine(b)} but not {@code b.combine(a)}. In the event that the {@code other}
* instance is not compatible then an exception is raised before any state is modified.
*
* @param other Another set of statistics to be combined.
* @return {@code this} instance after combining {@code other}.
* @throws IllegalArgumentException if the {@code other} is not compatible
*/
public LongStatistics combine(LongStatistics other) {
// Check compatibility
Statistics.checkCombineCompatible(min, other.min);
Statistics.checkCombineCompatible(max, other.max);
Statistics.checkCombineCompatible(sum, other.sum);
Statistics.checkCombineCompatible(product, other.product);
Statistics.checkCombineCompatible(sumOfSquares, other.sumOfSquares);
Statistics.checkCombineCompatible(sumOfLogs, other.sumOfLogs);
Statistics.checkCombineAssignable(moment, other.moment);
// Combine
count += other.count;
Statistics.combine(min, other.min);
Statistics.combine(max, other.max);
Statistics.combine(sum, other.sum);
Statistics.combine(product, other.product);
Statistics.combine(sumOfSquares, other.sumOfSquares);
Statistics.combine(sumOfLogs, other.sumOfLogs);
Statistics.combineMoment(moment, other.moment);
return this;
}
/**
* Sets the statistics configuration.
*
* <p>These options only control the final computation of statistics. The configuration
* will not affect compatibility between instances during a
* {@link #combine(LongStatistics) combine} operation.
*
* <p>Note: These options will affect any future computation of statistics. Supplier functions
* that have been previously created will not be updated with the new configuration.
*
* @param v Value.
* @return {@code this} instance
* @throws NullPointerException if the value is null
* @see #getResult(Statistic)
*/
public LongStatistics setConfiguration(StatisticsConfiguration v) {
config = Objects.requireNonNull(v);
return this;
}
}