Apache Commons Statistics 1.3 常用 API 与统计工具详解
系统详解 Apache Commons Statistics 1.3 descriptive 与 ranking 两个模块的 API 设计,覆盖单一统计类、Median/Quantile 的默认不拷贝行为、聚合器、combine 分片合并与平局/NaN 策略实测对比。
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、combine | 4.1 |
| Sum / SumOfLogs / SumOfSquares | 和/对数和/平方和 | create、accept、getAsDouble、combine | 4.1 |
| Mean | 算术均值 | of、create、accept、getAsDouble、combine | 4.1 |
| Variance / StandardDeviation | 样本或总体方差/标准差 | setBiased、of、accept、getAsDouble | 4.1 |
| Skewness / Kurtosis | 偏度/峰度 | setBiased、accept、getAsDouble | 4.1 |
| GeometricMean | 几何均值 | accept、getAsDouble | 4.1 |
| Median / Quantile | 中位数/分位数 | withDefaults、withCopy、evaluate | 4.2 |
| DoubleStatistics / IntStatistics / LongStatistics | 多统计量聚合器 | of、accept、getAsDouble、combine、getCount | 4.4 |
| Statistic(枚举) | 聚合器可开启的统计量常量 | 12 个常量 | 4.4 |
ranking 模块(包 org.apache.commons.statistics.ranking)核心类:
| 类 | 职责 | 关键方法 | 详解章节 |
|---|---|---|---|
| NaturalRanking | 自然排名实现 | apply、getTiesStrategy、getNanStrategy | 5.1 |
| TiesStrategy(枚举) | 平局处理策略 | SEQUENTIAL/MINIMUM/MAXIMUM/AVERAGE/RANDOM | 5.1 |
| NaNStrategy(枚举) | NaN 处理策略 | MINIMAL/MAXIMAL/REMOVED/FIXED/FAILED | 5.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 风格 HF7 Excel 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 数量增长 遍历次数 每个统计量一遍 一次遍历全部 取值 API getAsDouble() 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 实验时,把算子绑定到实验种子上即可让整个排名流程可回归。
六、注意事项
Median/Quantile.withDefaults()默认不拷贝输入数组,evaluate 后输入被部分重排(4.2 节实测);共享数组务必withCopy(true)或传副本。setBiased必须在累积数据前调用(4.1 节),数据累积后改口径的调用会被拒绝。- 聚合器要先声明要算什么(
Statistic枚举)再喂数据,事后追加统计量需重建累积器。 - ranking 的 REMOVED 策略会缩短输出数组(5.2 节实测 6 进 4 出),按索引对齐的场景禁用。
- 数值输出是 double 全精度(如 11.633333333333333),展示层自行格式化,不要在统计层四舍五入。
- 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 位以上有效数字再比较,或对输出做容差断言。
八、参考链接
- Commons Statistics descriptive 模块用户指南:https://commons.apache.org/proper/commons-statistics/
- Commons Statistics Javadoc(descriptive/ranking):https://commons.apache.org/proper/commons-statistics/
- Maven 中央仓库 descriptive 1.3:https://repo1.maven.org/maven2/org/apache/commons/commons-statistics-descriptive/
- Maven 中央仓库 ranking 1.3:https://repo1.maven.org/maven2/org/apache/commons/commons-statistics-ranking/
- 假设检验与置信区间姊妹篇:《Apache Commons Statistics 1.3 统计计算示例与最佳实践详解》