Apache Commons Statistics 1.3 常用 API 与统计工具详解

系统详解 Apache Commons Statistics 1.3 descriptive 与 ranking 两个模块的 API 设计,覆盖单一统计类、Median/Quantile 的默认不拷贝行为、聚合器、combine 分片合并与平局/NaN 策略实测对比。

Apache Commons StatisticsApache Commons Statistics2026/10/15

Apache Commons Statistics 1.3 常用 API 与统计工具详解

目标:系统掌握 Apache Commons Statistics 1.3 中 descriptive(描述统计)与 ranking(排名)两个模块的 API 设计——单一统计类、聚合器、分位数估计、分片合并与平局/NaN 处理策略,所有输出均来自可运行示例的实测回显。

Commons Statistics 1.3 的 API 有一个统一的设计哲学:每个统计量都是一个不可变值对象,通过 create() 创建空累积器、accept() 逐值累积、getAsDouble() 取结果、combine() 合并两个累积器。本文用一个完整可运行的演示工程贯穿讲解 descriptive 模块的五类核心能力与 ranking 模块的三类策略,先给工程与主类全文,再逐节拆解输出。假设检验与置信区间的内容见《Apache Commons Statistics 1.3 统计计算示例与最佳实践详解》。

一、环境信息

  • 操作系统:Ubuntu 24.04.5 LTS(x86_64)
  • JDK:OpenJDK 17.0.20.1,Eclipse Temurin(Temurin-17.0.20.1+1)
  • 构建工具:Apache Maven 3.9.16
  • 组件版本:commons-statistics-descriptive 1.3、commons-statistics-ranking 1.3(官方当前最新稳定版)
  • Maven 坐标:org.apache.commons:commons-statistics-descriptive:1.3、org.apache.commons:commons-statistics-ranking:1.3
  • 工作目录:/work/stat-demo

环境确认(实时回显):

java -version 2>&1 && mvn -version | head -4
openjdk version "17.0.20.1" 2026-08-18
OpenJDK Runtime Environment Temurin-17.0.20.1+1 (build 17.0.20.1+1)
OpenJDK 64-Bit Server VM Temurin-17.0.20.1+1 (build 17.0.20.1+1, mixed mode, sharing)
Apache Maven 3.9.16 (2bdd9fddda4b155ebf8000e807eb73fd829a51d5)
Maven home: /usr/share/maven
Java version: 17.0.20.1, vendor: Eclipse Adoptium, runtime: /opt/java/openjdk
Default locale: en_US, platform encoding: UTF-8

💡 技术拓展:descriptive 模块的统计类分三个整型特化族——Double 前缀省略的 double 版(Min/Max/Sum/Mean 等)、IntStatistics/LongStatistics 的整型聚合器,以及通用的 DoubleStatistics 聚合器。选型原则见 4.4 节对比表。 本文所用:double 系单一统计类 + 三个聚合器 + ranking 的 NaturalRanking。

二、API 总览

本节给出两个模块的类速查表,后续章节逐类实测。

descriptive 模块(包 org.apache.commons.statistics.descriptive)核心类:

类职责关键方法详解章节
Min / Max最小/最大值create、accept、getAsDouble、combine4.1
Sum / SumOfLogs / SumOfSquares和/对数和/平方和create、accept、getAsDouble、combine4.1
Mean算术均值of、create、accept、getAsDouble、combine4.1
Variance / StandardDeviation样本或总体方差/标准差setBiased、of、accept、getAsDouble4.1
Skewness / Kurtosis偏度/峰度setBiased、accept、getAsDouble4.1
GeometricMean几何均值accept、getAsDouble4.1
Median / Quantile中位数/分位数withDefaults、withCopy、evaluate4.2
DoubleStatistics / IntStatistics / LongStatistics多统计量聚合器of、accept、getAsDouble、combine、getCount4.4
Statistic(枚举)聚合器可开启的统计量常量12 个常量4.4

ranking 模块(包 org.apache.commons.statistics.ranking)核心类:

类职责关键方法详解章节
NaturalRanking自然排名实现apply、getTiesStrategy、getNanStrategy5.1
TiesStrategy(枚举)平局处理策略SEQUENTIAL/MINIMUM/MAXIMUM/AVERAGE/RANDOM5.1
NaNStrategy(枚举)NaN 处理策略MINIMAL/MAXIMAL/REMOVED/FIXED/FAILED5.2

三、前置条件:演示工程与主类

本节给出贯穿全文的演示工程。pom.xml 关键依赖(完整工程 /work/stat-demo,编译入口 mvn package,依赖树上另有 inference 与 interval 模块供姊妹篇使用):

  <dependencies>
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-statistics-descriptive</artifactId>
      <version>1.3</version>
    </dependency>
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-statistics-ranking</artifactId>
      <version>1.3</version>
    </dependency>
  </dependencies>

编译与运行方式(胖 jar 由 maven-shade-plugin 产出,主类为 QuickStart,本文按类名直跑):

cd /work/stat-demo && mvn -q -B package
java -Dfile.encoding=UTF-8 -cp target/statistics-demo.jar DescriptiveDemo
java -Dfile.encoding=UTF-8 -cp target/statistics-demo.jar RankingDemo

演示用固定数据集(便于复现,共 20 个观测值):

static final double[] DATA = {9, 2, 5, 4, 12, 7, 8, 11, 9, 3, 7, 4, 12, 5, 4, 10, 9, 6, 9, 4};

DescriptiveDemo.java 主类全文(文件路径 /work/stat-demo/src/main/java/DescriptiveDemo.java):

import org.apache.commons.statistics.descriptive.DoubleStatistics;
import org.apache.commons.statistics.descriptive.GeometricMean;
import org.apache.commons.statistics.descriptive.IntStatistics;
import org.apache.commons.statistics.descriptive.Kurtosis;
import org.apache.commons.statistics.descriptive.LongStatistics;
import org.apache.commons.statistics.descriptive.Max;
import org.apache.commons.statistics.descriptive.Mean;
import org.apache.commons.statistics.descriptive.Median;
import org.apache.commons.statistics.descriptive.Min;
import org.apache.commons.statistics.descriptive.Quantile;
import org.apache.commons.statistics.descriptive.Skewness;
import org.apache.commons.statistics.descriptive.StandardDeviation;
import org.apache.commons.statistics.descriptive.Statistic;
import org.apache.commons.statistics.descriptive.Sum;
import org.apache.commons.statistics.descriptive.SumOfLogs;
import org.apache.commons.statistics.descriptive.SumOfSquares;
import org.apache.commons.statistics.descriptive.Variance;

import java.util.Arrays;
import java.util.EnumSet;

public class DescriptiveDemo {

    static final double[] DATA = {9, 2, 5, 4, 12, 7, 8, 11, 9, 3, 7, 4, 12, 5, 4, 10, 9, 6, 9, 4};

    public static void main(String[] args) {
        singleStatistics();
        medianQuantile();
        harmonicByReciprocal();
        aggregators();
        storelessVsCombine();
    }

    static void singleStatistics() {
        System.out.println("== [1] 单一统计类:流式累积(storeless) ==");
        System.out.println("data.length = " + DATA.length);
        Min min = Min.create();
        Max max = Max.create();
        Sum sum = Sum.create();
        Mean mean = Mean.create();
        Variance var = Variance.create();
        StandardDeviation sd = StandardDeviation.create();
        Skewness skew = Skewness.create();
        Kurtosis kurt = Kurtosis.create();
        GeometricMean gm = GeometricMean.create();
        SumOfLogs sl = SumOfLogs.create();
        SumOfSquares ss = SumOfSquares.create();
        for (double v : DATA) {
            min.accept(v);
            max.accept(v);
            sum.accept(v);
            mean.accept(v);
            var.accept(v);
            sd.accept(v);
            skew.accept(v);
            kurt.accept(v);
            gm.accept(v);
            sl.accept(v);
            ss.accept(v);
        }
        System.out.println("Min                 = " + min.getAsDouble());
        System.out.println("Max                 = " + max.getAsDouble());
        System.out.println("Sum                 = " + sum.getAsDouble());
        System.out.println("Mean                = " + mean.getAsDouble());
        System.out.println("Variance(样本/无偏)  = " + var.getAsDouble());
        System.out.println("StandardDeviation   = " + sd.getAsDouble());
        System.out.println("Skewness(无偏)       = " + skew.getAsDouble());
        System.out.println("Kurtosis(无偏)       = " + kurt.getAsDouble());
        System.out.println("GeometricMean       = " + gm.getAsDouble());
        System.out.println("SumOfLogs           = " + sl.getAsDouble());
        System.out.println("SumOfSquares        = " + ss.getAsDouble());

        Variance biased = Variance.create().setBiased(true);
        for (double v : DATA) {
            biased.accept(v);
        }
        System.out.println("Variance(总体/有偏)  = " + biased.getAsDouble());
    }

    static void medianQuantile() {
        System.out.println("== [2] Median 与 Quantile ==");
        double[] data = DATA.clone();
        Median median = Median.withDefaults();
        System.out.println("Median = " + median.evaluate(data));
        System.out.println("evaluate 后原数组 = " + Arrays.toString(data));
        System.out.println("(withDefaults 默认不拷贝:输入被部分重排)");
        double[] keep = DATA.clone();
        double m = Median.withDefaults().withCopy(true).evaluate(keep);
        System.out.println("withCopy(true).evaluate = " + m + ",输入首元素仍为 " + keep[0]);
        Quantile q = Quantile.withDefaults();
        double[] probs = q.evaluate(DATA.clone(), 0.25, 0.5, 0.75, 0.9);
        System.out.println("Quantile P25/P50/P75/P90 = [" + probs[0] + ", " + probs[1] + ", "
                + probs[2] + ", " + probs[3] + "]");
        System.out.println("Quantile(HF6).evaluate(P50) = "
                + Quantile.withDefaults().with(Quantile.EstimationMethod.HF6).evaluate(DATA.clone(), 0.5));
    }

    static void harmonicByReciprocal() {
        System.out.println("== [3] 调和均值(1.3 无 HarmonicMean 类,用 Sum 累积倒数) ==");
        Sum reciprocal = Sum.create();
        for (double v : DATA) {
            reciprocal.accept(1.0 / v);
        }
        System.out.println("调和均值 = n / sum(1/x) = " + DATA.length / reciprocal.getAsDouble());
    }

    static void aggregators() {
        System.out.println("== [4] 聚合器 DoubleStatistics / IntStatistics / LongStatistics ==");
        DoubleStatistics ds = DoubleStatistics.of(
                Statistic.MIN, Statistic.MAX, Statistic.MEAN, Statistic.SUM,
                Statistic.VARIANCE, Statistic.STANDARD_DEVIATION,
                Statistic.SKEWNESS, Statistic.KURTOSIS, Statistic.GEOMETRIC_MEAN);
        for (double v : DATA) {
            ds.accept(v);
        }
        System.out.println("DoubleStatistics.getCount() = " + ds.getCount());
        System.out.println("  MIN                 = " + ds.getAsDouble(Statistic.MIN));
        System.out.println("  MAX                 = " + ds.getAsDouble(Statistic.MAX));
        System.out.println("  MEAN                = " + ds.getAsDouble(Statistic.MEAN));
        System.out.println("  SUM                 = " + ds.getAsDouble(Statistic.SUM));
        System.out.println("  VARIANCE            = " + ds.getAsDouble(Statistic.VARIANCE));
        System.out.println("  STANDARD_DEVIATION  = " + ds.getAsDouble(Statistic.STANDARD_DEVIATION));
        System.out.println("  SKEWNESS            = " + ds.getAsDouble(Statistic.SKEWNESS));
        System.out.println("  KURTOSIS            = " + ds.getAsDouble(Statistic.KURTOSIS));
        System.out.println("  GEOMETRIC_MEAN      = " + ds.getAsDouble(Statistic.GEOMETRIC_MEAN));

        int[] ints = {5, 3, 8, 3, 9, 1, 7, 3};
        IntStatistics is = IntStatistics.of(EnumSet.of(
                Statistic.MIN, Statistic.MAX, Statistic.MEAN,
                Statistic.SUM, Statistic.VARIANCE), ints);
        System.out.println("IntStatistics.of({MIN,MAX,MEAN,SUM,VARIANCE}, ints) getCount = " + is.getCount());
        System.out.println("  getAsInt(MIN)   = " + is.getAsInt(Statistic.MIN));
        System.out.println("  getAsInt(MAX)   = " + is.getAsInt(Statistic.MAX));
        System.out.println("  getAsLong(SUM)  = " + is.getAsLong(Statistic.SUM));
        System.out.println("  getAsDouble(MEAN) = " + is.getAsDouble(Statistic.MEAN));
        System.out.println("  getAsDouble(VARIANCE) = " + is.getAsDouble(Statistic.VARIANCE));

        long[] longs = {9_000_000_000L, 8_000_000_000L, 7_000_000_000L, 6_000_000_000L};
        LongStatistics ls = LongStatistics.of(EnumSet.of(
                Statistic.MIN, Statistic.MAX, Statistic.SUM, Statistic.MEAN), longs);
        System.out.println("LongStatistics.of({MIN,MAX,SUM,MEAN}, 4 个 long) getCount = " + ls.getCount());
        System.out.println("  getAsLong(SUM)  = " + ls.getAsLong(Statistic.SUM));
        System.out.println("  getAsLong(MIN)  = " + ls.getAsLong(Statistic.MIN));
        System.out.println("  getAsDouble(MEAN) = " + ls.getAsDouble(Statistic.MEAN));
    }

    static void storelessVsCombine() {
        System.out.println("== [5] 全量工厂 vs 流式累积 vs 分片合并(combine) ==");
        double[] part1 = {9, 2, 5, 4, 12, 7, 8, 11, 9, 3};
        double[] part2 = {7, 4, 12, 5, 4, 10, 9, 6, 9, 4};
        Mean whole = Mean.of(DATA);
        Mean m1 = Mean.of(part1);
        Mean m2 = Mean.of(part2);
        m1.combine(m2);
        System.out.println("Mean.of(全量20)          = " + whole.getAsDouble());
        System.out.println("m1(前10).combine(m2(后10)) = " + m1.getAsDouble());

        Variance v1 = Variance.of(part1);
        Variance v2 = Variance.of(part2);
        v1.combine(v2);
        System.out.println("Variance 分片合并         = " + v1.getAsDouble());

        DoubleStatistics dAll = DoubleStatistics.of(EnumSet.allOf(Statistic.class), DATA);
        DoubleStatistics d1 = DoubleStatistics.of(EnumSet.allOf(Statistic.class), part1);
        DoubleStatistics d2 = DoubleStatistics.of(EnumSet.allOf(Statistic.class), part2);
        d1.combine(d2);
        System.out.println("DoubleStatistics 全量    = " + dAll.getAsDouble(Statistic.MEAN)
                + " / " + dAll.getAsDouble(Statistic.VARIANCE));
        System.out.println("DoubleStatistics 合并    = " + d1.getAsDouble(Statistic.MEAN)
                + " / " + d1.getAsDouble(Statistic.VARIANCE));
    }
}

RankingDemo.java 主类全文(文件路径 /work/stat-demo/src/main/java/RankingDemo.java):

import org.apache.commons.statistics.ranking.NaNStrategy;
import org.apache.commons.statistics.ranking.NaturalRanking;
import org.apache.commons.statistics.ranking.TiesStrategy;

import java.util.Arrays;

public class RankingDemo {

    static final double[] DATA = {1, 3, 2, 3, 4, 2};

    public static void main(String[] args) {
        System.out.println("data = " + Arrays.toString(DATA));
        defaultStrategy();
        tiesStrategies();
        customTieOperator();
        nanStrategies();
    }

    static void print(String name, double[] ranks) {
        System.out.println(name + " = " + Arrays.toString(ranks));
    }

    static void defaultStrategy() {
        System.out.println("== [1] 默认构造的策略组合 ==");
        NaturalRanking ranking = new NaturalRanking();
        System.out.println("getTiesStrategy() = " + ranking.getTiesStrategy());
        System.out.println("getNanStrategy()  = " + ranking.getNanStrategy());
        print("默认(TiesStrategy.AVERAGE)", ranking.apply(DATA.clone()));
    }

    static void tiesStrategies() {
        System.out.println("== [2] 四种确定性平局策略对比 ==");
        print("AVERAGE   ", new NaturalRanking(TiesStrategy.AVERAGE).apply(DATA.clone()));
        print("MINIMUM   ", new NaturalRanking(TiesStrategy.MINIMUM).apply(DATA.clone()));
        print("MAXIMUM   ", new NaturalRanking(TiesStrategy.MAXIMUM).apply(DATA.clone()));
        print("SEQUENTIAL", new NaturalRanking(TiesStrategy.SEQUENTIAL).apply(DATA.clone()));
    }

    static void customTieOperator() {
        System.out.println("== [3] RANDOM 策略:自定义 IntUnaryOperator 固定随机序列 ==");
        NaturalRanking ranking = new NaturalRanking(i -> new java.util.Random(42).nextInt(i));
        print("RANDOM(固定算子)", ranking.apply(DATA.clone()));
        print("RANDOM(固定算子)再次执行", ranking.apply(DATA.clone()));
    }

    static void nanStrategies() {
        System.out.println("== [4] NaN 处理策略对比 ==");
        double[] withNan = {2, Double.NaN, 5, 1, Double.NaN, 3};
        System.out.println("data = " + Arrays.toString(withNan));
        print("REMOVED", new NaturalRanking(NaNStrategy.REMOVED).apply(withNan.clone()));
        print("MINIMAL", new NaturalRanking(NaNStrategy.MINIMAL).apply(withNan.clone()));
        print("MAXIMAL", new NaturalRanking(NaNStrategy.MAXIMAL).apply(withNan.clone()));
    }
}

完整运行输出(实测,五个小节逐一拆解见第四、五章):

== [1] 单一统计类:流式累积(storeless) ==
data.length = 20
Min                 = 2.0
Max                 = 12.0
Sum                 = 140.0
Mean                = 7.0
Variance(样本/无偏)  = 9.368421052631579
StandardDeviation   = 3.0607876523260447
Skewness(无偏)       = 0.12236452761476846
Kurtosis(无偏)       = -1.1605177104297095
GeometricMean       = 6.289624060470062
SumOfLogs           = 36.77802602230763
SumOfSquares        = 1158.0
Variance(总体/有偏)  = 8.9
== [2] Median 与 Quantile ==
Median = 7.0
evaluate 后原数组 = [3.0, 2.0, 4.0, 4.0, 4.0, 4.0, 5.0, 5.0, 6.0, 7.0, 7.0, 8.0, 9.0, 9.0, 9.0, 9.0, 10.0, 11.0, 12.0, 12.0]
(withDefaults 默认不拷贝:输入被部分重排)
withCopy(true).evaluate = 7.0,输入首元素仍为 9.0
Quantile P25/P50/P75/P90 = [4.0, 7.0, 9.0, 11.633333333333333]
Quantile(HF6).evaluate(P50) = 7.0
== [3] 调和均值(1.3 无 HarmonicMean 类,用 Sum 累积倒数) ==
调和均值 = n / sum(1/x) = 5.535972839382895
== [4] 聚合器 DoubleStatistics / IntStatistics / LongStatistics ==
DoubleStatistics.getCount() = 20
  MIN                 = 2.0
  MAX                 = 12.0
  MEAN                = 7.0
  SUM                 = 140.0
  VARIANCE            = 9.368421052631579
  STANDARD_DEVIATION  = 3.0607876523260447
  SKEWNESS            = 0.12236452761476846
  KURTOSIS            = -1.1605177104297095
  GEOMETRIC_MEAN      = 6.289624060470062
IntStatistics.of({MIN,MAX,MEAN,SUM,VARIANCE}, ints) getCount = 8
  getAsInt(MIN)   = 1
  getAsInt(MAX)   = 9
  getAsLong(SUM)  = 39
  getAsDouble(MEAN) = 4.875
  getAsDouble(VARIANCE) = 8.125
LongStatistics.of({MIN,MAX,SUM,MEAN}, 4 个 long) getCount = 4
  getAsLong(SUM)  = 30000000000
  getAsLong(MIN)  = 6000000000
  getAsDouble(MEAN) = 7.5E9
== [5] 全量工厂 vs 流式累积 vs 分片合并(combine) ==
Mean.of(全量20)          = 7.0
m1(前10).combine(m2(后10)) = 7.0
Variance 分片合并         = 9.368421052631579
DoubleStatistics 全量    = 7.0 / 9.368421052631579
DoubleStatistics 合并    = 7.0 / 9.368421052631579

四、描述统计 API 详解

4.1 单一统计类与流式累积

每个统计量类都遵循同一契约:create() 得到空累积器,accept(double) 逐值喂入(storeless 无存储累积,内存占用 O(1)),getAsDouble() 输出结果,combine() 合并另一个同类累积器。全量工厂 of(double...) 是「创建 + 循环 accept」的语法糖。实测输出(对应主类 [1] 节)显示 11 个统计量的真实计算结果,手算核对三个关键值:Sum = 140(20 数直接相加)、Mean = 7.0(140/20)、样本方差 = 9.368421052631579(偏差平方和 178/19,其中 178 = 1158 − 20 × 7.0²,1158 即 SumOfSquares 输出)。

方差默认无偏(除以 n−1),setBiased(true) 切换总体口径(除以 n),实测两者为 9.368421052631579 与 8.9(= 178/20),StandardDeviation 3.0607876523260447 是无偏方差的平方根。

💡 技术拓展:无偏方差与总体方差的口径选择——

口径计算适用
无偏/样本方差(默认)Σ(x−x̄)²/(n−1)由样本推断总体,统计检验的标准输入
有偏/总体方差(setBiased(true))Σ(x−x̄)²/n数据即全集(如全体用户)或与既有系统对齐口径

本文所用:默认无偏口径为主,[1] 节同时输出两种口径便于对比。Skewness/Kurtosis 同样提供 setBiased,注意先 setBiased 再 accept。

要点提示:Mean.of(data) 内部就是 create() + 逐个 accept(等价验证见 [5] 节,两者输出一致),因此不存在「工厂方法更不准」的问题;选择只取决于数据是一次性拿到还是流式到达。

4.2 Median 与 Quantile:默认不拷贝的输入重排坑

Median/Quantile 与其他统计类不同:它们不是增量累积器,而是「配置对象 + evaluate(数组)」的一次性计算模式。实测发现一个必须注意的行为(对应主类 [2] 节输出):Median.withDefaults().evaluate(data) 执行后,输入数组被原地部分重排——原数组 {9, 2, 5, 4, 12, 7, 8, 11, 9, 3, 7, 4, 12, 5, 4, 10, 9, 6, 9, 4} 变成了 [3.0, 2.0, 4.0, 4.0, 4.0, 4.0, 5.0, 5.0, 6.0, 7.0, 7.0, 8.0, 9.0, 9.0, 9.0, 9.0, 10.0, 11.0, 12.0, 12.0],首元素从 9.0 变为 3.0。这是默认配置(等价 withCopy(false))为省一次数组拷贝而做的 quickselect 式部分排序,且结果不是完全有序(位置 0/1 为 3.0、2.0)。

需要保护输入数组时,用 withCopy(true)(实测:求值结果同为 7.0,输入首元素保持 9.0 不变)。Quantile 的行为与 Median 一致(实测两者 withDefaults 均重排、withCopy(true) 均不动输入,见主类 [2] 节代码)。分位数结果实测:P25/P50/P75/P90 = 4.0/7.0/9.0/11.633333333333333,其中 P50 与 Median 一致(7.0);换用 HF6 估计方法求 P50 同为 7.0(分位方法在 n=20 的偶数中位数上结论一致,极端分位下差异更明显)。

💡 技术拓展:Quantile 的分位数估计方法(HF1 至 HF9,对应文献中 Hyndman-Fan 分类)——

方法特点
HF1/HF2逆经验分布类,简单直观
HF3/HF4/HF5线性插值变体,P25/P75 口径随方法漂移
HF6(默认之一)Minitab/SPSS 风格
HF7Excel PERCENTILE 风格
HF8/HF9无偏/近似无偏众数型,适合大样本推断

本文所用:withDefaults() 默认配置求 P25/P50/P75/P90,并实测 HF6 对照。跨系统对数时务必显式声明估计方法。

要点提示:把共享数组传给 Median/Quantile.withDefaults() 是本项目实测中唯一会「改输入」的 API;其余统计类(Min/Sum/Mean 等)均为只读遍历。防御性写法是统一传副本或显式 withCopy(true)。

4.3 调和均值:1.3 无专用类的替代实现

实测枚举 descriptive 1.3 的全部公开类(jar tf + javap 核实)后确认:1.3 没有 HarmonicMean 类(有 GeometricMean 而无 HarmonicMean)。调和均值 H = n / Σ(1/xᵢ) 可用 Sum 累积倒数实现(对应主类 [3] 节),实测输出 5.535972839382895。手算核对:Σ(1/x) = 3.6127344877344876,20 / 3.6127344877344876 = 5.536,一致。

要点提示:升级版本后若官方新增 HarmonicMean 类,应改回专用实现(内部可用高精度补偿算法,长尾数据下比朴素倒数求和更稳);当前替代写法的数据量在百万级以内精度足够。

4.4 聚合器:一次遍历同时算多个统计量

单一统计类各算各的会对数据遍历多遍;聚合器一次遍历同时维护多个统计量。DoubleStatistics.of(Statistic...) 传入要开启的 Statistic 枚举(共 12 个常量:MIN、MAX、MEAN、STANDARD_DEVIATION、VARIANCE、SKEWNESS、KURTOSIS、PRODUCT、SUM、SUM_OF_LOGS、SUM_OF_SQUARES、GEOMETRIC_MEAN),随后 accept 喂数据,getAsDouble(Statistic) 按需取值,getCount() 返回已累积个数。实测输出(对应主类 [4] 节)显示 9 个统计量与 4.1 节单一类逐一相等——两条路径殊途同归。

整型场景用 IntStatistics/LongStatistics:构造时传 EnumSet.of(...) 与原生数组,取值方法更多(getAsInt、getAsLong、getAsBigInteger、getAsDouble)。实测 int 数据 {5, 3, 8, 3, 9, 1, 7, 3} 得 MIN=1、MAX=9、SUM=39、MEAN=4.875、VARIANCE=8.125;4 个超出 int 范围的 long 实测 SUM 精确等于 30000000000(long 精确整数累加,无浮点误差),MEAN 为 7.5E9。

💡 技术拓展:单一统计类与聚合器的选型——

维度单一统计类(Mean 等)聚合器(DoubleStatistics 等)
内存最小,只存自身状态随开启的 Statistic 数量增长
遍历次数每个统计量一遍一次遍历全部
取值 APIgetAsDouble()getAsDouble(Statistic)
适用单一指标高频流一次扫描产出多指标(如监控大盘)

本文所用:4.1 节用单一类验证口径,4.4 节用聚合器验证一致性,两者数值完全相等。

要点提示:IntStatistics.of 直接传 int 数组时用 EnumSet 版本;Statistic 枚举并非对所有聚合器都可用,调用 isSupported(Statistic) 可先探测。

4.5 分片合并 combine:分布式统计的钥匙

combine 把两个同类累积器合而为一,且数学上与全量计算严格等价(Mean/Variance 的合并算法基于矩可加性)。实测(对应主类 [5] 节):20 个数拆成前后各 10 个,分别计算后 combine,Mean 得 7.0、Variance 得 9.368421052631579,与 Mean.of(DATA)/全量 DoubleStatistics 完全一致(浮点末位可能有 ULP 级差异,本数据集下逐位相同)。

要点提示:combine 使得「分片计算 + 汇总合并」的并行/分布式架构成为可能——每个分片只需传输累积器状态(而非原始数据),跨进程场景可将状态序列化后传输。这是 Commons Statistics 相对于「存全量数组再算」的核心优势之一。

五、排名 API 详解

5.1 NaturalRanking 与平局策略

NaturalRanking.apply(double[]) 对数组按升序赋名次(最小值排名 1),返回与输入等长的 double 数组。实测默认构造(对应主类 [1] 节输出):getTiesStrategy() 为 AVERAGE、getNanStrategy() 为 FAILED,即默认对平局取平均名次、遇 NaN 直接抛异常。对固定数据 {1, 3, 2, 3, 4, 2}(两个 2、两个 3 平局),默认输出 [1.0, 4.5, 2.5, 4.5, 6.0, 2.5]:两个 2 占据第 2、3 名,平均得 2.5;两个 3 占据第 4、5 名,平均得 4.5。

四种确定性平局策略实测对比(对应主类 [2] 节输出):

策略输出平局语义
AVERAGE(默认)[1.0, 4.5, 2.5, 4.5, 6.0, 2.5]平局取平均名次
MINIMUM[1.0, 4.0, 2.0, 4.0, 6.0, 2.0]平局全取最小名次(竞赛排名)
MAXIMUM[1.0, 5.0, 3.0, 5.0, 6.0, 3.0]平局全取最大名次
SEQUENTIAL[1.0, 4.0, 2.0, 5.0, 6.0, 3.0]平局按出现顺序依次编号

💡 技术拓展:平局策略的选择直接影响下游显著性检验(如 Mann-Whitney U 检验的平局校正)与榜单并列名次的展示——

场景建议策略
统计检验输入AVERAGE(与多数检验的平局校正一致)
比赛名次(并列取靠前)MINIMUM
严格不重名次(如发券序号)SEQUENTIAL
随机打散平局(Monte Carlo)RANDOM(配合自定义随机源,见 5.3)

本文所用:AVERAGE 为默认并实测四种确定性策略;RANDOM 用固定算子实测见 5.3 节。

5.2 NaN 处理策略

NaNStrategy 有五个枚举值:MINIMAL、MAXIMAL、REMOVED、FIXED、FAILED(默认)。实测三种常用策略(对应主类 [4] 节,数据 {2, NaN, 5, 1, NaN, 3}):

策略输出语义
REMOVED[2.0, 4.0, 1.0, 3.0]先剔除 NaN 再排名,输出长度变为 4
MINIMAL[4.0, 1.5, 6.0, 3.0, 1.5, 5.0]NaN 视为最小值,两个 NaN 平局取平均 1.5
MAXIMAL[2.0, 5.5, 4.0, 1.0, 5.5, 3.0]NaN 视为最大值,平局取平均 5.5

要点提示:REMOVED 策略的输出数组比输入短(实测 6 进 4 出),且 NaN 的原始位置信息完全丢失——下游若按索引对齐会错位,务必知晓这一行为;默认 FAILED 在数据可能含 NaN 时最安全(快速失败优于静默错位)。

5.3 自定义随机算子固定 RANDOM 策略

TiesStrategy.RANDOM 内部使用随机数源,直接用会导致输出不可复现。NaturalRanking 提供接受 IntUnaryOperator 的构造器,可注入自定义随机源。实测传入固定种子算子 i -> new java.util.Random(42).nextInt(i)(对应主类 [3] 节):两次执行输出完全一致,均为 [1.0, 4.0, 2.0, 5.0, 6.0, 3.0]——平局元素(值 3 的两个、值 2 的两个)按固定随机序列编号,结果可复现。

要点提示:固定算子只保证「同一算子 + 同一数据」可复现;生产环境做 Monte Carlo 实验时,把算子绑定到实验种子上即可让整个排名流程可回归。

六、注意事项

  1. Median/Quantile.withDefaults() 默认不拷贝输入数组,evaluate 后输入被部分重排(4.2 节实测);共享数组务必 withCopy(true) 或传副本。
  2. setBiased 必须在累积数据前调用(4.1 节),数据累积后改口径的调用会被拒绝。
  3. 聚合器要先声明要算什么(Statistic 枚举)再喂数据,事后追加统计量需重建累积器。
  4. ranking 的 REMOVED 策略会缩短输出数组(5.2 节实测 6 进 4 出),按索引对齐的场景禁用。
  5. 数值输出是 double 全精度(如 11.633333333333333),展示层自行格式化,不要在统计层四舍五入。
  6. Commons Statistics 1.3 无 HarmonicMean 类(4.3 节实测核实),需要时用 Sum 累积倒数替代。

七、常见问题(FAQ)

Q1:Mean.of(data) 与 create() + accept 结果会不一致吗? 不会。of 内部就是创建后逐个 accept,[5] 节实测两者均值同为 7.0;差异只在入口形态,选择取决于数据到达方式。

Q2:为什么求完中位数,我的原数组变了? 4.2 节实测:Median.withDefaults() 默认等价 withCopy(false),evaluate 会 quickselect 式部分重排输入。修复:Median.withDefaults().withCopy(true).evaluate(data) 或传入克隆数组。

Q3:DoubleStatistics 能一次算 12 个 Statistic 吗? 可以。用 DoubleStatistics.of(EnumSet.allOf(Statistic.class), data) 一次开满([5] 节实测用法);但每个开启的统计量都有状态成本,按需开启更省内存。

Q4:int 数据能不能用 DoubleStatistics? 可以但会先转 double;整型精确累加需求(如金额分、大整数和)用 IntStatistics/LongStatistics,4.4 节实测 LongStatistics 对 4 个 long 的 SUM 精确输出 30000000000,无浮点误差。

Q5:排名结果里出现 4.5 这种半名次正常吗? 正常,是 AVERAGE 平局策略的平均名次(5.1 节实测);要整数名次换 MINIMUM/MAXIMUM/SEQUENTIAL。

Q6:两台机器对同一数据算出的方差末位不同? 浮点求和顺序会影响末位 ULP;同一版本、同一数据、同一调用顺序下 Commons Statistics 结果确定。跨机器比对时保留 12 位以上有效数字再比较,或对输出做容差断言。

八、参考链接

  1. Commons Statistics descriptive 模块用户指南:https://commons.apache.org/proper/commons-statistics/
  2. Commons Statistics Javadoc(descriptive/ranking):https://commons.apache.org/proper/commons-statistics/
  3. Maven 中央仓库 descriptive 1.3:https://repo1.maven.org/maven2/org/apache/commons/commons-statistics-descriptive/
  4. Maven 中央仓库 ranking 1.3:https://repo1.maven.org/maven2/org/apache/commons/commons-statistics-ranking/
  5. 假设检验与置信区间姊妹篇:《Apache Commons Statistics 1.3 统计计算示例与最佳实践详解》