1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 package org.apache.commons.statistics.descriptive; 18 19 import java.math.BigInteger; 20 import java.util.Objects; 21 import java.util.Set; 22 import java.util.function.DoubleConsumer; 23 import java.util.function.Function; 24 import java.util.function.LongConsumer; 25 26 /** 27 * Statistics for {@code long} values. 28 * 29 * <p>This class provides combinations of individual statistic implementations in the 30 * {@code org.apache.commons.statistics.descriptive} package. 31 * 32 * <p>Supports up to 2<sup>63</sup> (exclusive) observations. 33 * This implementation does not check for overflow of the count. 34 * 35 * @since 1.1 36 */ 37 public final class LongStatistics implements LongConsumer { 38 /** Error message for non configured statistics. */ 39 private static final String NO_CONFIGURED_STATISTICS = "No configured statistics"; 40 /** Error message for an unsupported statistic. */ 41 private static final String UNSUPPORTED_STATISTIC = "Unsupported statistic: "; 42 43 /** Count of values recorded. */ 44 private long count; 45 /** The consumer of values. */ 46 private final LongConsumer consumer; 47 /** The {@link LongMin} implementation. */ 48 private final LongMin min; 49 /** The {@link LongMax} implementation. */ 50 private final LongMax max; 51 /** The moment implementation. May be any instance of {@link FirstMoment}. 52 * This implementation uses only the third and fourth moments. */ 53 private final FirstMoment moment; 54 /** The {@link LongSum} implementation. */ 55 private final LongSum sum; 56 /** The {@link Product} implementation. */ 57 private final Product product; 58 /** The {@link LongSumOfSquares} implementation. */ 59 private final LongSumOfSquares sumOfSquares; 60 /** The {@link SumOfLogs} implementation. */ 61 private final SumOfLogs sumOfLogs; 62 /** Configuration options for computation of statistics. */ 63 private StatisticsConfiguration config; 64 65 /** 66 * A builder for {@link LongStatistics}. 67 */ 68 public static final class Builder { 69 /** An empty double array. */ 70 private static final long[] NO_VALUES = {}; 71 72 /** The {@link LongMin} constructor. */ 73 private Function<long[], LongMin> min; 74 /** The {@link LongMax} constructor. */ 75 private Function<long[], LongMax> max; 76 /** The moment constructor. May return any instance of {@link FirstMoment}. */ 77 private Function<long[], FirstMoment> moment; 78 /** The {@link LongSum} constructor. */ 79 private Function<long[], LongSum> sum; 80 /** The {@link Product} constructor. */ 81 private Function<long[], Product> product; 82 /** The {@link LongSumOfSquares} constructor. */ 83 private Function<long[], LongSumOfSquares> sumOfSquares; 84 /** The {@link SumOfLogs} constructor. */ 85 private Function<long[], SumOfLogs> sumOfLogs; 86 /** The order of the moment. It corresponds to the power computed by the {@link FirstMoment} 87 * instance constructed by {@link #moment}. This should only be increased from the default 88 * of zero (corresponding to no moment computation). */ 89 private int momentOrder; 90 /** Configuration options for computation of statistics. */ 91 private StatisticsConfiguration config = StatisticsConfiguration.withDefaults(); 92 93 /** 94 * Create an instance. 95 */ 96 Builder() { 97 // Do nothing 98 } 99 100 /** 101 * Add the statistic to the statistics to compute. 102 * 103 * @param statistic Statistic to compute. 104 * @return {@code this} instance 105 */ 106 Builder add(Statistic statistic) { 107 switch (statistic) { 108 case GEOMETRIC_MEAN: 109 case SUM_OF_LOGS: 110 sumOfLogs = SumOfLogs::of; 111 break; 112 case KURTOSIS: 113 createMoment(4); 114 break; 115 case MAX: 116 max = LongMax::of; 117 break; 118 case MIN: 119 min = LongMin::of; 120 break; 121 case PRODUCT: 122 product = Product::of; 123 break; 124 case SKEWNESS: 125 createMoment(3); 126 break; 127 case STANDARD_DEVIATION: 128 case VARIANCE: 129 sum = LongSum::of; 130 sumOfSquares = LongSumOfSquares::of; 131 break; 132 case MEAN: 133 case SUM: 134 sum = LongSum::of; 135 break; 136 case SUM_OF_SQUARES: 137 sumOfSquares = LongSumOfSquares::of; 138 break; 139 default: 140 throw new IllegalArgumentException(UNSUPPORTED_STATISTIC + statistic); 141 } 142 return this; 143 } 144 145 /** 146 * Creates the moment constructor for the specified {@code order}, 147 * e.g. order=3 is sum of cubed deviations. 148 * 149 * @param order Order. 150 */ 151 private void createMoment(int order) { 152 if (order > momentOrder) { 153 momentOrder = order; 154 if (order == 4) { 155 moment = SumOfFourthDeviations::of; 156 } else { 157 // Assume order == 3 158 moment = SumOfCubedDeviations::of; 159 } 160 } 161 } 162 163 /** 164 * Sets the statistics configuration options for computation of statistics. 165 * 166 * @param v Value. 167 * @return the builder 168 * @throws NullPointerException if the value is null 169 */ 170 public Builder setConfiguration(StatisticsConfiguration v) { 171 config = Objects.requireNonNull(v); 172 return this; 173 } 174 175 /** 176 * Builds a {@code LongStatistics} instance. 177 * 178 * @return {@code LongStatistics} instance. 179 */ 180 public LongStatistics build() { 181 return build(NO_VALUES); 182 } 183 184 /** 185 * Builds a {@code LongStatistics} instance using the input {@code values}. 186 * 187 * <p>Note: {@code LongStatistics} computed using 188 * {@link LongStatistics#accept(long) accept} may be 189 * different from this instance. 190 * 191 * @param values Values. 192 * @return {@code LongStatistics} instance. 193 */ 194 public LongStatistics build(long... values) { 195 Objects.requireNonNull(values, "values"); 196 return new LongStatistics( 197 values.length, 198 create(min, values), 199 create(max, values), 200 create(moment, values), 201 create(sum, values), 202 create(product, values), 203 create(sumOfSquares, values), 204 create(sumOfLogs, values), 205 config); 206 } 207 208 /** 209 * Creates the object from the {@code values}. 210 * 211 * @param <T> object type 212 * @param constructor Constructor. 213 * @param values Values 214 * @return the instance 215 */ 216 private static <T> T create(Function<long[], T> constructor, long[] values) { 217 if (constructor != null) { 218 return constructor.apply(values); 219 } 220 return null; 221 } 222 } 223 224 /** 225 * Create an instance. 226 * 227 * @param count Count of values. 228 * @param min LongMin implementation. 229 * @param max LongMax implementation. 230 * @param moment Moment implementation. 231 * @param sum LongSum implementation. 232 * @param product Product implementation. 233 * @param sumOfSquares Sum of squares implementation. 234 * @param sumOfLogs Sum of logs implementation. 235 * @param config Statistics configuration. 236 */ 237 LongStatistics(long count, LongMin min, LongMax max, FirstMoment moment, LongSum sum, 238 Product product, LongSumOfSquares sumOfSquares, SumOfLogs sumOfLogs, 239 StatisticsConfiguration config) { 240 this.count = count; 241 this.min = min; 242 this.max = max; 243 this.moment = moment; 244 this.sum = sum; 245 this.product = product; 246 this.sumOfSquares = sumOfSquares; 247 this.sumOfLogs = sumOfLogs; 248 this.config = config; 249 // The final consumer should never be null as the builder is created 250 // with at least one statistic. 251 consumer = Statistics.compose(min, max, sum, sumOfSquares, 252 composeAsLong(moment, product, sumOfLogs)); 253 } 254 255 /** 256 * Chain the {@code consumers} into a single composite {@code LongConsumer}. 257 * Ignore any {@code null} consumer. 258 * 259 * @param consumers Consumers. 260 * @return a composed consumer (or null) 261 */ 262 private static LongConsumer composeAsLong(DoubleConsumer... consumers) { 263 final DoubleConsumer c = Statistics.compose(consumers); 264 if (c != null) { 265 return c::accept; 266 } 267 return null; 268 } 269 270 /** 271 * Returns a new instance configured to compute the specified {@code statistics}. 272 * 273 * <p>The statistics will be empty and so will return the default values for each 274 * computed statistic. 275 * 276 * @param statistics Statistics to compute. 277 * @return the instance 278 * @throws IllegalArgumentException if there are no {@code statistics} to compute. 279 */ 280 public static LongStatistics of(Statistic... statistics) { 281 return builder(statistics).build(); 282 } 283 284 /** 285 * Returns a new instance configured to compute the specified {@code statistics} 286 * populated using the input {@code values}. 287 * 288 * <p>Use this method to create an instance populated with a (variable) array of 289 * {@code long[]} data: 290 * 291 * <pre> 292 * LongStatistics stats = LongStatistics.of( 293 * EnumSet.of(Statistic.MIN, Statistic.MAX), 294 * 1, 1, 2, 3, 5, 8, 13); 295 * </pre> 296 * 297 * @param statistics Statistics to compute. 298 * @param values Values. 299 * @return the instance 300 * @throws IllegalArgumentException if there are no {@code statistics} to compute. 301 */ 302 public static LongStatistics of(Set<Statistic> statistics, long... values) { 303 if (statistics.isEmpty()) { 304 throw new IllegalArgumentException(NO_CONFIGURED_STATISTICS); 305 } 306 final Builder b = new Builder(); 307 statistics.forEach(b::add); 308 return b.build(values); 309 } 310 311 /** 312 * Returns a new builder configured to create instances to compute the specified 313 * {@code statistics}. 314 * 315 * <p>Use this method to create an instance populated with an array of {@code long[]} 316 * data using the {@link Builder#build(long...)} method: 317 * 318 * <pre> 319 * long[] data = ... 320 * LongStatistics stats = LongStatistics.builder( 321 * Statistic.MIN, Statistic.MAX, Statistic.VARIANCE) 322 * .build(data); 323 * </pre> 324 * 325 * <p>The builder can be used to create multiple instances of {@link LongStatistics} 326 * to be used in parallel, or on separate arrays of {@code long[]} data. These may 327 * be {@link #combine(LongStatistics) combined}. For example: 328 * 329 * <pre> 330 * long[][] data = ... 331 * LongStatistics.Builder builder = LongStatistics.builder( 332 * Statistic.MIN, Statistic.MAX, Statistic.VARIANCE); 333 * LongStatistics stats = Arrays.stream(data) 334 * .parallel() 335 * .map(builder::build) 336 * .reduce(LongStatistics::combine) 337 * .get(); 338 * </pre> 339 * 340 * <p>The builder can be used to create a {@link java.util.stream.Collector} for repeat 341 * use on multiple data: 342 * 343 * <pre>{@code 344 * LongStatistics.Builder builder = LongStatistics.builder( 345 * Statistic.MIN, Statistic.MAX, Statistic.VARIANCE); 346 * Collector<long[], LongStatistics, LongStatistics> collector = 347 * Collector.of(builder::build, 348 * (s, d) -> s.combine(builder.build(d)), 349 * LongStatistics::combine); 350 * 351 * // Repeated 352 * long[][] data = ... 353 * LongStatistics stats = Arrays.stream(data).collect(collector); 354 * }</pre> 355 * 356 * @param statistics Statistics to compute. 357 * @return the builder 358 * @throws IllegalArgumentException if there are no {@code statistics} to compute. 359 */ 360 public static Builder builder(Statistic... statistics) { 361 if (statistics.length == 0) { 362 throw new IllegalArgumentException(NO_CONFIGURED_STATISTICS); 363 } 364 final Builder b = new Builder(); 365 for (final Statistic s : statistics) { 366 b.add(s); 367 } 368 return b; 369 } 370 371 /** 372 * Updates the state of the statistics to reflect the addition of {@code value}. 373 * 374 * @param value Value. 375 */ 376 @Override 377 public void accept(long value) { 378 count++; 379 consumer.accept(value); 380 } 381 382 /** 383 * Return the count of values recorded. 384 * 385 * @return the count of values 386 */ 387 public long getCount() { 388 return count; 389 } 390 391 /** 392 * Check if the specified {@code statistic} is supported. 393 * 394 * <p>Note: This method will not return {@code false} if the argument is {@code null}. 395 * 396 * @param statistic Statistic. 397 * @return {@code true} if supported 398 * @throws NullPointerException if the {@code statistic} is {@code null} 399 * @see #getResult(Statistic) 400 */ 401 public boolean isSupported(Statistic statistic) { 402 // Check for the appropriate underlying implementation 403 switch (statistic) { 404 case GEOMETRIC_MEAN: 405 case SUM_OF_LOGS: 406 return sumOfLogs != null; 407 case KURTOSIS: 408 return moment instanceof SumOfFourthDeviations; 409 case MAX: 410 return max != null; 411 case MIN: 412 return min != null; 413 case PRODUCT: 414 return product != null; 415 case SKEWNESS: 416 return moment instanceof SumOfCubedDeviations; 417 case STANDARD_DEVIATION: 418 case VARIANCE: 419 return sum != null && sumOfSquares != null; 420 case MEAN: 421 case SUM: 422 return sum != null; 423 case SUM_OF_SQUARES: 424 return sumOfSquares != null; 425 default: 426 return false; 427 } 428 } 429 430 /** 431 * Gets the value of the specified {@code statistic} as a {@code double}. 432 * 433 * @param statistic Statistic. 434 * @return the value 435 * @throws IllegalArgumentException if the {@code statistic} is not supported 436 * @see #isSupported(Statistic) 437 * @see #getResult(Statistic) 438 */ 439 public double getAsDouble(Statistic statistic) { 440 return getResult(statistic).getAsDouble(); 441 } 442 443 /** 444 * Gets the value of the specified {@code statistic} as a {@code long}. 445 * 446 * <p>Use this method to access the {@code long} result for exact integer statistics, 447 * for example {@link Statistic#MIN}. 448 * 449 * <p>Note: This method may throw an {@link ArithmeticException} if the result 450 * overflows an {@code long}. 451 * 452 * @param statistic Statistic. 453 * @return the value 454 * @throws IllegalArgumentException if the {@code statistic} is not supported 455 * @throws ArithmeticException if the {@code result} overflows an {@code long} or is not 456 * finite 457 * @see #isSupported(Statistic) 458 * @see #getResult(Statistic) 459 */ 460 public long getAsLong(Statistic statistic) { 461 return getResult(statistic).getAsLong(); 462 } 463 464 /** 465 * Gets the value of the specified {@code statistic} as a {@code BigInteger}. 466 * 467 * <p>Use this method to access the {@code BigInteger} result for exact integer statistics, 468 * for example {@link Statistic#SUM_OF_SQUARES}. 469 * 470 * <p>Note: This method may throw an {@link ArithmeticException} if the result 471 * is not finite. 472 * 473 * @param statistic Statistic. 474 * @return the value 475 * @throws IllegalArgumentException if the {@code statistic} is not supported 476 * @throws ArithmeticException if the {@code result} is not finite 477 * @see #isSupported(Statistic) 478 * @see #getResult(Statistic) 479 */ 480 public BigInteger getAsBigInteger(Statistic statistic) { 481 return getResult(statistic).getAsBigInteger(); 482 } 483 484 /** 485 * Gets a supplier for the value of the specified {@code statistic}. 486 * 487 * <p>The returned function will supply the correct result after 488 * calls to {@link #accept(long) accept} or 489 * {@link #combine(LongStatistics) combine} further values into 490 * {@code this} instance. 491 * 492 * <p>This method can be used to perform a one-time look-up of the statistic 493 * function to compute statistics as values are dynamically added. 494 * 495 * @param statistic Statistic. 496 * @return the supplier 497 * @throws IllegalArgumentException if the {@code statistic} is not supported 498 * @see #isSupported(Statistic) 499 * @see #getAsDouble(Statistic) 500 */ 501 public StatisticResult getResult(Statistic statistic) { 502 // Locate the implementation. 503 // Statistics that wrap an underlying implementation are created in methods. 504 // The return argument should be an interface reference and not an instance 505 // of LongStatistic. This ensures the statistic implementation cannot 506 // be updated with new values by casting the result and calling accept(long). 507 StatisticResult stat = null; 508 switch (statistic) { 509 case GEOMETRIC_MEAN: 510 stat = getGeometricMean(); 511 break; 512 case KURTOSIS: 513 stat = getKurtosis(); 514 break; 515 case MAX: 516 stat = Statistics.getResultAsLongOrNull(max); 517 break; 518 case MEAN: 519 stat = getMean(); 520 break; 521 case MIN: 522 stat = Statistics.getResultAsLongOrNull(min); 523 break; 524 case PRODUCT: 525 stat = Statistics.getResultAsDoubleOrNull(product); 526 break; 527 case SKEWNESS: 528 stat = getSkewness(); 529 break; 530 case STANDARD_DEVIATION: 531 stat = getStandardDeviation(); 532 break; 533 case SUM: 534 stat = Statistics.getResultAsBigIntegerOrNull(sum); 535 break; 536 case SUM_OF_LOGS: 537 stat = Statistics.getResultAsDoubleOrNull(sumOfLogs); 538 break; 539 case SUM_OF_SQUARES: 540 stat = Statistics.getResultAsBigIntegerOrNull(sumOfSquares); 541 break; 542 case VARIANCE: 543 stat = getVariance(); 544 break; 545 default: 546 break; 547 } 548 if (stat != null) { 549 return stat; 550 } 551 throw new IllegalArgumentException(UNSUPPORTED_STATISTIC + statistic); 552 } 553 554 /** 555 * Gets the geometric mean. 556 * 557 * @return a geometric mean supplier (or null if unsupported) 558 */ 559 private StatisticResult getGeometricMean() { 560 if (sumOfLogs != null) { 561 // Return a function that has access to the count and sumOfLogs 562 return () -> GeometricMean.computeGeometricMean(count, sumOfLogs); 563 } 564 return null; 565 } 566 567 /** 568 * Gets the kurtosis. 569 * 570 * @return a kurtosis supplier (or null if unsupported) 571 */ 572 private StatisticResult getKurtosis() { 573 if (moment instanceof SumOfFourthDeviations) { 574 return new Kurtosis((SumOfFourthDeviations) moment) 575 .setBiased(config.isBiased())::getAsDouble; 576 } 577 return null; 578 } 579 580 /** 581 * Gets the mean. 582 * 583 * @return a mean supplier (or null if unsupported) 584 */ 585 private StatisticResult getMean() { 586 if (sum != null) { 587 // Return a function that has access to the count and sum 588 final Int128 s = sum.getSum(); 589 return () -> LongMean.computeMean(s, count); 590 } 591 return null; 592 } 593 594 /** 595 * Gets the skewness. 596 * 597 * @return a skewness supplier (or null if unsupported) 598 */ 599 private StatisticResult getSkewness() { 600 if (moment instanceof SumOfCubedDeviations) { 601 return new Skewness((SumOfCubedDeviations) moment) 602 .setBiased(config.isBiased())::getAsDouble; 603 } 604 return null; 605 } 606 607 /** 608 * Gets the standard deviation. 609 * 610 * @return a standard deviation supplier (or null if unsupported) 611 */ 612 private StatisticResult getStandardDeviation() { 613 return getVarianceOrStd(true); 614 } 615 616 /** 617 * Gets the variance. 618 * 619 * @return a variance supplier (or null if unsupported) 620 */ 621 private StatisticResult getVariance() { 622 return getVarianceOrStd(false); 623 } 624 625 /** 626 * Gets the variance or standard deviation. 627 * 628 * @param std Flag to control if the statistic is the standard deviation. 629 * @return a variance/standard deviation supplier (or null if unsupported) 630 */ 631 private StatisticResult getVarianceOrStd(boolean std) { 632 if (sum != null && sumOfSquares != null) { 633 // Return a function that has access to the count, sum and sum of squares 634 final Int128 s = sum.getSum(); 635 final UInt192 ss = sumOfSquares.getSumOfSquares(); 636 final boolean biased = config.isBiased(); 637 return () -> LongVariance.computeVarianceOrStd(ss, s, count, biased, std); 638 } 639 return null; 640 } 641 642 /** 643 * Combines the state of the {@code other} statistics into this one. 644 * Only {@code this} instance is modified by the {@code combine} operation. 645 * 646 * <p>The {@code other} instance must be <em>compatible</em>. This is {@code true} if the 647 * {@code other} instance returns {@code true} for {@link #isSupported(Statistic)} for 648 * all values of the {@link Statistic} enum which are supported by {@code this} 649 * instance. 650 * 651 * <p>Note that this operation is <em>not symmetric</em>. It may be possible to perform 652 * {@code a.combine(b)} but not {@code b.combine(a)}. In the event that the {@code other} 653 * instance is not compatible then an exception is raised before any state is modified. 654 * 655 * @param other Another set of statistics to be combined. 656 * @return {@code this} instance after combining {@code other}. 657 * @throws IllegalArgumentException if the {@code other} is not compatible 658 */ 659 public LongStatistics combine(LongStatistics other) { 660 // Check compatibility 661 Statistics.checkCombineCompatible(min, other.min); 662 Statistics.checkCombineCompatible(max, other.max); 663 Statistics.checkCombineCompatible(sum, other.sum); 664 Statistics.checkCombineCompatible(product, other.product); 665 Statistics.checkCombineCompatible(sumOfSquares, other.sumOfSquares); 666 Statistics.checkCombineCompatible(sumOfLogs, other.sumOfLogs); 667 Statistics.checkCombineAssignable(moment, other.moment); 668 // Combine 669 count += other.count; 670 Statistics.combine(min, other.min); 671 Statistics.combine(max, other.max); 672 Statistics.combine(sum, other.sum); 673 Statistics.combine(product, other.product); 674 Statistics.combine(sumOfSquares, other.sumOfSquares); 675 Statistics.combine(sumOfLogs, other.sumOfLogs); 676 Statistics.combineMoment(moment, other.moment); 677 return this; 678 } 679 680 /** 681 * Sets the statistics configuration. 682 * 683 * <p>These options only control the final computation of statistics. The configuration 684 * will not affect compatibility between instances during a 685 * {@link #combine(LongStatistics) combine} operation. 686 * 687 * <p>Note: These options will affect any future computation of statistics. Supplier functions 688 * that have been previously created will not be updated with the new configuration. 689 * 690 * @param v Value. 691 * @return {@code this} instance 692 * @throws NullPointerException if the value is null 693 * @see #getResult(Statistic) 694 */ 695 public LongStatistics setConfiguration(StatisticsConfiguration v) { 696 config = Objects.requireNonNull(v); 697 return this; 698 } 699 }