项目背景详细介绍

平均数(Average)是统计学与数据分析中最基础、最常用的描述集中趋势的量度之一。不同形式的平均数(算术平均数、几何平均数、调和平均数、加权平均数、中位数、截尾平均、移动平均、指数加权移动平均等)在不同场景中有着明确而重要的意义:

  • 算术平均数用于描述总和分布(例如班级的平均分、传感器读数的均值);

  • 几何平均数用于处理比率或相乘增长的量(例如年化收益率、多期增长率的平均);

  • 调和平均数用于平均速率或密度场景(例如平均速度、单位成本);

  • 加权平均用于给不同样本赋予不同重要性(例如评分系统、推荐引擎);

  • 中位数与截尾平均用于抗异常值(鲁棒统计);

  • 移动平均用于信号平滑与时间序列分析,指数加权平均用于实时滤波与衰减历史影响;

  • 在线平均(running average)与数值稳定版本(Welford 算法)在流数据或内存受限场景中尤为重要。

工程上实现“平均数”看似简单,但却有诸多实际问题需要注意:数值稳定性(大体量和/或大数相加导致溢出)、精度(double 累加误差)、内存(一次性加载全部数据不可行时需要在线算法)、并行化(MapReduce 风格的合并平均)、多类型支持(int/long/double/BigDecimal/BigInteger)、以及 API 需求(是否返回中间步骤、是否支持 Java Stream、是否带权重、是否支持滑动窗口与指数加权等)。此外,在教学上需要一套详尽的、可运行的示例代码,以便说明每种平均的数学原理、实现复杂度与适用场景。


项目需求详细介绍

  1. 功能需求

    • 实现并公开以下平均数计算函数(至少):

      • 算术平均(Arithmetic Mean):数组/列表/流输入,支持 int/long/double/BigDecimal。

      • 几何平均(Geometric Mean):适用于非负数或正数序列,支持 double 与 BigDecimal。

      • 调和平均(Harmonic Mean):适用于速率类数据,注意不能有 0。

      • 加权平均(Weighted Mean):支持权重数组/集合,长度需一致,权重可为 double 或 BigDecimal。

      • 截尾平均(Trimmed Mean):按百分比截尾两端异常值后求平均。

      • 中位数(Median):虽然严格不是“平均”但常与平均一起比较,提供作为对比指标并用于计算中位数平均。

      • 简单移动平均(SMA)和指数加权移动平均(EWMA / EMA):适用于时间序列,提供窗口大小或平滑因子。

      • 在线(流式)平均:含 Welford 的数值稳定实现,用于单次遍历且可返回方差。

      • 并行可合并统计(mergeable aggregator):能将不同分片的统计量合并得到总平均与方差,方便 MapReduce / 并行计算。

    • 提供数值稳定选项(例如 Kahan summation)与高精度 BigDecimal 实现。

    • 提供 Java Stream 示例(DoubleStream、LongStream)以展示声明式 API。

    • 所有方法做参数校验并在非法参数时抛出 IllegalArgumentException(例如数组为空、长度不一致、权重和为0、负数传入几何平均等)。

  2. 代码组织main 包含示例用法:各种平均的典型输入、异常输入演示、数值稳定性对比(例如 Kahan vs 普通累加)与性能耗时(纳秒)。

  3. 教学要求

    • 文档中在“实现思路”部分详细讲解数值误差、Kahan 和 Welford 算法的原理、截尾平均的统计学背景、几何/调和平均的数学适用性。


相关技术详细介绍

  1. 截尾平均(Trimmed Mean)
    去掉最高和最低一定比例的数据后求平均,适合对抗异常值。截尾比例通常在 000 到 0.50.50.5 之间。

  2. 移动平均(SMA 与 EMA)

    • SMA:滑动窗口内算术平均;时间复杂度 O(n)(可用滑动累加到 O(1) 每步)。

    • EMA(指数加权):递归 S_t = α x_t + (1-α) S_{t-1},权重随指数衰减,适合实时滤波。

  3. 在线 / 数值稳定算法

    • Welford 算法用于一次遍历同时求均值和方差,数值稳定且可合并:维护 n, mean, M2,合并两组统计也有公式。

    • Kahan summation 用于减少浮点累加误差。

  4. 并行可合并聚合
    在分布式或并行场景中,每个分片计算 (count, mean, M2),最终聚合能得到全局均值与方差,避免传输原始数据。

  5. 精度与性能权衡

    • BigDecimal 提供高精度但性能慢;double 快但有舍入误差;Welford + Kahan 提供在 double 下很好的稳定性。选择取决于是否需要精确数值还是性能优先。


实现思路详细介绍

  1. API 设计:创建 AverageUtils 工具类,提供静态方法集合,并为不同数据类型(int[], long[], double[], BigDecimal[])分别实现重载。还实现 OnlineStats 类实现 Welford 的在线均值与方差函数,并提供 merge 方法以支持并行合并。

  2. 数值稳定实现

    • 算术平均:默认实现用 Kahan summation(减少舍入误差),同时提供普通实现以便性能对比。

    • 在线与并行场景:实现 Welford 算法:在一次遍历中更新 count, mean, M2,最后方差为 M2/(count - 1)(样本方差)或 M2/count(总体方差)。Welford 算法数值稳定并可合并。

    • 几何平均:使用对数求和 sum(log(x)) 然后 Math.exp(sum / n);对小数或大数用 BigDecimal 或多重精度库。

  3. 高精度 BigDecimal 实现:实现 BigDecimal 版本的算术平均(使用指定 MathContext),以及 BigDecimal 的加权平均、几何平均(若需用 BigDecimalln/exp 实现,Java 标准库未提供;此处给出对数近似说明并提供对 BigDecimal 的简单近似实现或建议使用 BigDecimal 做乘积并开 nth 根,注意性能)。在代码中对 BigDecimal 的 ln/exp 未做复杂实现,而是建议对需要高精度几何平均的情形使用 BigDecimal 做对数近似或外部库。

  4. 截尾平均实现:对数据进行排序(或利用选择算法找到阈值)并丢弃两端 k 个值后求平均;时间复杂度 O(nlog⁡n)O(n \log n)O(nlogn)(排序)或 O(n)O(n)O(n)(选择算法)。

  5. 移动平均实现:SMA 采用滑动窗口与队列或循环数组实现 O(1) 每步更新;EMA 提供递归公式,参数 alpha 在 0~1。

  6. 并行与 Stream:提供 DoubleStream 示例计算平均值(内置 average()),并演示如何使用 collectOnlineStats 来得到在线累加并行安全的聚合器。

  7. 异常与校验:对所有方法检查 null、长度 0、特殊值(例如几何平均出现负值)、权重和为 0 等,抛出 IllegalArgumentException 并指明原因。

  8. 示例与对比main 中提供各种输入数据(含异常与极值),比较普通累加、Kahan、Welford 与 BigDecimal 结果,打印耗时与误差,解释差异与选择理由。


完整实现代码

/* ============================================================
   文件: AverageProject.java
   说明: 平均数(Average)计算工具合集(教学 + 工程)
     - Arithmetic Mean(普通/Kahan/BigDecimal)
     - Geometric Mean(对数法 & BigDecimal 注意事项)
     - Harmonic Mean
     - Weighted Mean(double / BigDecimal)
     - Trimmed Mean(截尾平均)
     - Median(中位数 - 用作对比)
     - Simple Moving Average (SMA) & Exponential Moving Average (EMA)
     - OnlineStats (Welford) 可在线与可合并统计 mean/variance
     - Java Stream 示例(DoubleStream / LongStream)
   编译: javac AverageProject.java
   运行: java AverageProject
   ============================================================ */

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.DoubleStream;
import java.util.stream.LongStream;

public class AverageProject {

    // ============================
    // 文件: AverageUtils.java
    // 各类平均数实现(静态工具方法)
    // ============================
    public static class AverageUtils {

        /* ====================================================
           方法: arithmeticMean(double[] arr)
           作用: 计算 double 数组的算术平均数(使用 Kahan summation 减小累加误差)
           参数: arr - 非空 double 数组
           返回: double 平均值
           时间复杂度: O(n)
           空间复杂度: O(1)
           注意: 若需要更高精度可使用 BigDecimal 版本
         ==================================================== */
        public static double arithmeticMean(double[] arr) {
            if (arr == null) throw new IllegalArgumentException("输入数组不能为 null");
            int n = arr.length;
            if (n == 0) throw new IllegalArgumentException("数组长度必须大于 0");
            // Kahan summation
            double sum = 0.0;
            double c = 0.0; // 补偿项
            for (double v : arr) {
                double y = v - c;
                double t = sum + y;
                c = (t - sum) - y;
                sum = t;
            }
            return sum / n;
        }

        /* ====================================================
           方法: arithmeticMeanPlain(double[] arr)
           作用: 直接使用普通累加计算平均(快速但误差较大)
           参数: arr - 非空 double 数组
           返回: double
           时间复杂度: O(n)
           注意: 仅用于对比或在数值范围较小时使用
         ==================================================== */
        public static double arithmeticMeanPlain(double[] arr) {
            if (arr == null) throw new IllegalArgumentException("输入数组不能为 null");
            int n = arr.length;
            if (n == 0) throw new IllegalArgumentException("数组长度必须大于 0");
            double sum = 0.0;
            for (double v : arr) sum += v;
            return sum / n;
        }

        /* ====================================================
           方法: arithmeticMeanLong(long[] arr)
           作用: 计算 long 数组的算术平均,使用双精度累加避免溢出并返回 double
           参数: arr - 非空 long 数组
           返回: double
           复杂度: O(n)
           注意: 若需精确整数平均(分子/分母),请使用 BigDecimal 或 分数表示
         ==================================================== */
        public static double arithmeticMeanLong(long[] arr) {
            if (arr == null) throw new IllegalArgumentException("输入数组不能为 null");
            int n = arr.length;
            if (n == 0) throw new IllegalArgumentException("数组长度必须大于 0");
            double sum = 0.0;
            double c = 0.0;
            for (long v : arr) {
                double y = v - c;
                double t = sum + y;
                c = (t - sum) - y;
                sum = t;
            }
            return sum / n;
        }

        /* ====================================================
           方法: arithmeticMeanBigDecimal(BigDecimal[] arr, MathContext mc)
           作用: 使用 BigDecimal 在指定 MathContext 下计算算术平均,精度可控
           参数: arr - 非空 BigDecimal 数组,mc - MathContext 精度与舍入模式
           返回: BigDecimal 平均值
           复杂度: O(n * cost(BigDecimal ops))
           注意: BigDecimal 运算昂贵,仅在需要高精度时使用
         ==================================================== */
        public static BigDecimal arithmeticMeanBigDecimal(BigDecimal[] arr, MathContext mc) {
            if (arr == null) throw new IllegalArgumentException("输入数组不能为 null");
            if (mc == null) throw new IllegalArgumentException("MathContext 不能为 null");
            int n = arr.length;
            if (n == 0) throw new IllegalArgumentException("数组长度必须大于 0");
            BigDecimal sum = BigDecimal.ZERO;
            for (BigDecimal v : arr) {
                if (v == null) throw new IllegalArgumentException("数组元素不能为 null");
                sum = sum.add(v, mc);
            }
            return sum.divide(BigDecimal.valueOf(n), mc);
        }

        /* ====================================================
           方法: geometricMean(double[] arr)
           作用: 计算几何平均:exp( (1/n) * sum(log(x_i)) )(要求所有 x_i > 0)
           参数: arr - 非空 double 数组,元素必须 > 0
           返回: double
           复杂度: O(n)
           注意: 对可能少于或等于 0 的元素抛出异常;使用 log/exp 可避免直接乘积溢出
         ==================================================== */
        public static double geometricMean(double[] arr) {
            if (arr == null) throw new IllegalArgumentException("输入数组不能为 null");
            int n = arr.length;
            if (n == 0) throw new IllegalArgumentException("数组长度必须大于 0");
            double sumLog = 0.0;
            for (double v : arr) {
                if (v <= 0.0) throw new IllegalArgumentException("几何平均要求所有元素 > 0");
                sumLog += Math.log(v);
            }
            return Math.exp(sumLog / n);
        }

        /* ====================================================
           方法: harmonicMean(double[] arr)
           作用: 计算调和平均:n / sum(1/x_i)(所有 x_i != 0)
           参数: arr - 非空 double 数组,元素不能为 0
           返回: double
           复杂度: O(n)
           注意: 若存在 0 元素,调和平均未定义(或视为 0),抛出异常
         ==================================================== */
        public static double harmonicMean(double[] arr) {
            if (arr == null) throw new IllegalArgumentException("输入数组不能为 null");
            int n = arr.length;
            if (n == 0) throw new IllegalArgumentException("数组长度必须大于 0");
            double sumRecip = 0.0;
            for (double v : arr) {
                if (v == 0.0) throw new IllegalArgumentException("调和平均要求所有元素非 0");
                sumRecip += 1.0 / v;
            }
            return n / sumRecip;
        }

        /* ====================================================
           方法: weightedMean(double[] values, double[] weights)
           作用: 计算加权平均:sum(w_i * x_i) / sum(w_i)
           参数: values, weights - 非空数组且长度一致,权重和不能为 0
           返回: double
           复杂度: O(n)
           注意: 权重可为负但需谨慎解释
         ==================================================== */
        public static double weightedMean(double[] values, double[] weights) {
            if (values == null || weights == null) throw new IllegalArgumentException("输入不能为 null");
            int n = values.length;
            if (n == 0 || weights.length != n) throw new IllegalArgumentException("长度必须一致且大于 0");
            double sumW = 0.0;
            double sumWX = 0.0;
            double cW = 0.0, cWX = 0.0; // Kahan 补偿分别用于权重和与加权和
            for (int i = 0; i < n; i++) {
                double w = weights[i];
                double x = values[i];
                // sumW += w (Kahan)
                double yW = w - cW;
                double tW = sumW + yW;
                cW = (tW - sumW) - yW;
                sumW = tW;
                // sumWX += w * x (Kahan)
                double wx = w * x;
                double yWX = wx - cWX;
                double tWX = sumWX + yWX;
                cWX = (tWX - sumWX) - yWX;
                sumWX = tWX;
            }
            if (Math.abs(sumW) < 1e-300) throw new IllegalArgumentException("权重和接近零 (可能导致除以零)");
            return sumWX / sumW;
        }

        /* ====================================================
           方法: weightedMeanBigDecimal(BigDecimal[] values, BigDecimal[] weights, MathContext mc)
           作用: BigDecimal 版本的加权平均,精度由 MathContext 控制
           参数: 非空数组、长度一致、mc 非空
           返回: BigDecimal
           复杂度: O(n * cost(BigDecimal ops))
         ==================================================== */
        public static BigDecimal weightedMeanBigDecimal(BigDecimal[] values, BigDecimal[] weights, MathContext mc) {
            if (values == null || weights == null) throw new IllegalArgumentException("输入不能为 null");
            if (mc == null) throw new IllegalArgumentException("MathContext 不能为 null");
            int n = values.length;
            if (n == 0 || weights.length != n) throw new IllegalArgumentException("长度必须一致且大于 0");
            BigDecimal sumW = BigDecimal.ZERO;
            BigDecimal sumWX = BigDecimal.ZERO;
            for (int i = 0; i < n; i++) {
                if (values[i] == null || weights[i] == null) throw new IllegalArgumentException("数组元素不能为 null");
                sumW = sumW.add(weights[i], mc);
                sumWX = sumWX.add(weights[i].multiply(values[i], mc), mc);
            }
            if (sumW.compareTo(BigDecimal.ZERO) == 0) throw new IllegalArgumentException("权重和不能为 0");
            return sumWX.divide(sumW, mc);
        }

        /* ====================================================
           方法: trimmedMean(double[] arr, double trimFraction)
           作用: 计算截尾平均(去掉两端 trimFraction 的数据后求平均)
           参数: arr 非空数组,trimFraction in [0, 0.5)
           返回: double
           复杂度: O(n log n)(排序)或可用选择算法降至 O(n)
           注意: trimFraction 为比例,例如 0.1 表示去掉两端各 10%
         ==================================================== */
        public static double trimmedMean(double[] arr, double trimFraction) {
            if (arr == null) throw new IllegalArgumentException("输入数组不能为 null");
            int n = arr.length;
            if (n == 0) throw new IllegalArgumentException("数组长度必须大于 0");
            if (trimFraction < 0.0 || trimFraction >= 0.5) throw new IllegalArgumentException("trimFraction 必须在 [0, 0.5)");
            double[] copy = Arrays.copyOf(arr, n);
            Arrays.sort(copy);
            int k = (int) Math.floor(trimFraction * n);
            int start = k;
            int end = n - k; // exclusive
            if (start >= end) throw new IllegalArgumentException("trimFraction 太大导致无数据");
            double sum = 0.0;
            for (int i = start; i < end; i++) sum += copy[i];
            return sum / (end - start);
        }

        /* ====================================================
           方法: median(double[] arr)
           作用: 计算中位数(用于对比)
           参数: arr 非空数组
           返回: double 中位数
           复杂度: O(n log n)(排序)或 O(n) 使用选择算法
         ==================================================== */
        public static double median(double[] arr) {
            if (arr == null) throw new IllegalArgumentException("输入数组不能为 null");
            int n = arr.length;
            if (n == 0) throw new IllegalArgumentException("数组长度必须大于 0");
            double[] copy = Arrays.copyOf(arr, n);
            Arrays.sort(copy);
            if ((n & 1) == 1) return copy[n / 2];
            else return (copy[n / 2 - 1] + copy[n / 2]) / 2.0;
        }

        /* ====================================================
           方法: simpleMovingAverage(double[] arr, int window)
           作用: 计算简单移动平均(SMA),返回与输入相同长度的数组(前 window-1 个位置用 Double.NaN 表示)
           参数: arr 非空数组,window >= 1
           返回: double[] SMA
           复杂度: O(n)
         ==================================================== */
        public static double[] simpleMovingAverage(double[] arr, int window) {
            if (arr == null) throw new IllegalArgumentException("输入数组不能为 null");
            int n = arr.length;
            if (window <= 0) throw new IllegalArgumentException("window 必须 >= 1");
            if (n == 0) return new double[0];
            double[] res = new double[n];
            double sum = 0.0;
            for (int i = 0; i < n; i++) {
                sum += arr[i];
                if (i >= window) sum -= arr[i - window];
                if (i >= window - 1) res[i] = sum / window;
                else res[i] = Double.NaN;
            }
            return res;
        }

        /* ====================================================
           方法: exponentialMovingAverage(double[] arr, double alpha)
           作用: 计算指数加权移动平均(EMA),S_t = alpha*x_t + (1-alpha)*S_{t-1}
           参数: arr 非空数组,alpha in (0,1]
           返回: double[] EMA,与输入长度相同(初始 S_0 取 arr[0])
           复杂度: O(n)
         ==================================================== */
        public static double[] exponentialMovingAverage(double[] arr, double alpha) {
            if (arr == null) throw new IllegalArgumentException("输入数组不能为 null");
            if (alpha <= 0.0 || alpha > 1.0) throw new IllegalArgumentException("alpha 必须在 (0,1]");
            int n = arr.length;
            if (n == 0) return new double[0];
            double[] res = new double[n];
            double s = arr[0];
            res[0] = s;
            for (int i = 1; i < n; i++) {
                s = alpha * arr[i] + (1 - alpha) * s;
                res[i] = s;
            }
            return res;
        }

        /* ====================================================
           方法: arithmeticMeanStream(DoubleStream stream)
           作用: 使用 Java DoubleStream 计算平均(返回 OptionalDouble)
           参数: stream 非空(注意 stream 只可消费一次)
           返回: OptionalDouble(当流为空时为空)
           复杂度: O(n)
         ==================================================== */
        public static OptionalDouble arithmeticMeanStream(DoubleStream stream) {
            if (stream == null) throw new IllegalArgumentException("stream 不能为 null");
            return stream.average();
        }

        /* ====================================================
           方法: arithmeticMeanLongStream(LongStream stream)
           作用: 使用 LongStream 计算平均(返回 OptionalDouble)
         ==================================================== */
        public static OptionalDouble arithmeticMeanLongStream(LongStream stream) {
            if (stream == null) throw new IllegalArgumentException("stream 不能为 null");
            return stream.average();
        }
    } // end AverageUtils

    // ============================
    // 文件: OnlineStats.java
    // 说明: Welford 在线算法,支持更新与合并(可并行合并)
    // 维护三个量:count, mean, M2(用于计算方差)
    // ============================
    public static class OnlineStats {
        private long count;
        private double mean;
        private double m2; // sum of squares of differences from the mean (用于方差)

        public OnlineStats() {
            this.count = 0L;
            this.mean = 0.0;
            this.m2 = 0.0;
        }

        /**
         * 增加一个观测值 x,并更新 count, mean, m2
         * 时间复杂度: O(1)
         */
        public void add(double x) {
            count++;
            double delta = x - mean;
            mean += delta / count;
            double delta2 = x - mean;
            m2 += delta * delta2;
        }

        /**
         * 合并另一个 OnlineStats 到当前对象(并行可合并)
         * 时间复杂度: O(1)
         * 公式参考 Welford 的可合并形式
         */
        public void merge(OnlineStats other) {
            if (other == null) return;
            if (other.count == 0) return;
            if (this.count == 0) {
                this.count = other.count;
                this.mean = other.mean;
                this.m2 = other.m2;
                return;
            }
            long combinedCount = this.count + other.count;
            double delta = other.mean - this.mean;
            this.mean = (this.mean * this.count + other.mean * other.count) / combinedCount;
            this.m2 = this.m2 + other.m2 + delta * delta * ( (double)this.count * other.count / combinedCount );
            this.count = combinedCount;
        }

        public long getCount() { return count; }
        public double getMean() { return mean; }
        public double getPopulationVariance() { // sigma^2
            if (count == 0) return Double.NaN;
            return m2 / count;
        }
        public double getSampleVariance() { // s^2
            if (count < 2) return Double.NaN;
            return m2 / (count - 1);
        }
        public double getPopulationStdDev() { return Math.sqrt(getPopulationVariance()); }
        public double getSampleStdDev() { return Math.sqrt(getSampleVariance()); }
    } // end OnlineStats

    // ============================
    // 文件: StreamExamples.java
    // 展示如何用 Java Stream 与并行环境计算平均(用 OnlineStats 做 collect)
    // ============================
    public static class StreamExamples {

        /**
         * 使用 DoubleStream 和 OnlineStats 做并行收集器示例(伪代码式)
         * 这里直接展示如何借助 stream.reduce/collect 实现并行安全的平均与方差计算
         */
        public static OnlineStats computeStatsFromDoubleStream(DoubleStream stream) {
            if (stream == null) throw new IllegalArgumentException("stream 不能为 null");
            // 注意: 下面示例如果使用 stream.parallel() 则需要使用适合并行的 collector,这里演示思路:
            OnlineStats stats = stream.collect(
                    OnlineStats::new, // supplier
                    (s, v) -> s.add(v), // accumulator
                    (s1, s2) -> s1.merge(s2) // combiner
            );
            return stats;
        }
    }

    // ============================
    // 文件: DemoMain.java
    // main() 演示各种平均的使用与对比(包含耗时与数值差异)
    // ============================
    public static void main(String[] args) {
        System.out.println("=== AverageProject Demo ===");

        // 示例数据
        double[] data = {1.0, 2.0, 3.0, 4.0, 5.0, 1e18, -1e18}; // 包含大数测试累加误差
        System.out.println("\n数据: " + Arrays.toString(data));

        // 算术平均:Kahan vs plain
        long t0 = System.nanoTime();
        double meanKahan = AverageUtils.arithmeticMean(data);
        long t1 = System.nanoTime();
        double meanPlain = AverageUtils.arithmeticMeanPlain(data);
        long t2 = System.nanoTime();
        System.out.printf("arithmeticMean (Kahan) = %.12f (ns=%d)%n", meanKahan, (t1 - t0));
        System.out.printf("arithmeticMeanPlain     = %.12f (ns=%d)%n", meanPlain, (t2 - t1));

        // Welford 在线统计
        OnlineStats os = new OnlineStats();
        for (double v : data) os.add(v);
        System.out.printf("Online mean = %.12f, sample variance = %.6e%n", os.getMean(), os.getSampleVariance());

        // 加权平均
        double[] vals = {1.0, 2.0, 3.0};
        double[] wts = {0.1, 0.1, 0.8};
        System.out.printf("weightedMean = %.6f%n", AverageUtils.weightedMean(vals, wts));

        // 几何平均(正数)
        double[] pos = {1.0, 2.0, 3.0, 4.0};
        System.out.printf("geometricMean = %.6f%n", AverageUtils.geometricMean(pos));

        // 调和平均
        double[] speeds = {60.0, 40.0}; // 两段路程速度示例
        System.out.printf("harmonicMean = %.6f%n", AverageUtils.harmonicMean(speeds));

        // 中位数与截尾平均
        double[] skew = {1,2,3,1000,10000};
        System.out.printf("median = %.6f, trimmedMean(20%%) = %.6f%n",
                AverageUtils.median(skew), AverageUtils.trimmedMean(skew, 0.2));

        // SMA 与 EMA
        double[] series = {1,2,3,4,5,6,7,8,9,10};
        System.out.println("SMA (window 3): " + Arrays.toString(AverageUtils.simpleMovingAverage(series, 3)));
        System.out.println("EMA (alpha=0.3): " + Arrays.toString(AverageUtils.exponentialMovingAverage(series, 0.3)));

        // BigDecimal 平均示例
        BigDecimal[] big = { new BigDecimal("12345678901234567890"), new BigDecimal("98765432109876543210") };
        MathContext mc = new MathContext(20, RoundingMode.HALF_UP);
        System.out.println("BigDecimal arithmetic mean = " + AverageUtils.arithmeticMeanBigDecimal(big, mc).toPlainString());

        // Stream 示例:DoubleStream
        DoubleStream ds = DoubleStream.of(series);
        OptionalDouble od = AverageUtils.arithmeticMeanStream(ds);
        System.out.println("DoubleStream average = " + (od.isPresent() ? od.getAsDouble() : "empty"));

        // 并行统计示例(OnlineStats via Stream collector)
        double[] large = new double[1000000];
        Random rnd = new Random(123456);
        for (int i = 0; i < large.length; i++) large[i] = rnd.nextDouble();
        long t3 = System.nanoTime();
        OnlineStats stats = StreamExamples.computeStatsFromDoubleStream(Arrays.stream(large));
        long t4 = System.nanoTime();
        System.out.printf("Large sample mean = %.12f (ns=%d)%n", stats.getMean(), (t4 - t3));

        System.out.println("\n=== Demo End ===");
    }
}

代码详细解读

  • AverageUtils.arithmeticMean(double[] arr):使用 Kahan summation 计算双精度数组的算术平均,减少累加误差,适合常规数值场景。

  • AverageUtils.arithmeticMeanPlain(double[] arr):直接普通累加实现的算术平均,性能略优但数值误差更大,常用于数据范围受限时。

  • AverageUtils.arithmeticMeanLong(long[] arr):把 long 值转为 double 使用 Kahan 累加计算平均并返回 double,避免 long 直接累加溢出。

  • AverageUtils.arithmeticMeanBigDecimal(BigDecimal[] arr, MathContext mc):使用 BigDecimal 在指定精度下计算平均,适合对精度非常敏感的场景(金融、会计)。

  • AverageUtils.geometricMean(double[] arr):对数法实现的几何平均,首先对元素取 log 累加再 exp(sum/n),适用于正数比率与增长率场景。

  • AverageUtils.harmonicMean(double[] arr):计算调和平均 n / sum(1/x_i),适用于速率类问题,拒绝含 0 的元素。

  • AverageUtils.weightedMean(double[] values, double[] weights):使用 Kahan 补偿的加权平均,计算 sum(w_i * x_i) / sum(w_i),并校验权重和非零。

  • AverageUtils.weightedMeanBigDecimal(BigDecimal[] values, BigDecimal[] weights, MathContext mc):BigDecimal 版本的加权平均。

  • AverageUtils.trimmedMean(double[] arr, double trimFraction):截尾平均,实现为先排序再去掉两端比例数据后求平均,trimFraction ∈ [0, 0.5)。

  • AverageUtils.median(double[] arr):返回数组的中位数(排序实现)。

  • AverageUtils.simpleMovingAverage(double[] arr, int window):计算窗口大小为 window 的简单移动平均,返回同长度数组,前 window-1 项为 NaN。

  • AverageUtils.exponentialMovingAverage(double[] arr, double alpha):指数加权移动平均 (EMA),递归实现,alpha ∈ (0,1]。

  • AverageUtils.arithmeticMeanStream(DoubleStream stream)ArithmeticMeanLongStream(LongStream stream):利用 Java Stream 的 average() 计算平均,返回 OptionalDouble。

  • OnlineStats 类:实现 Welford 算法,提供 add(double) 更新、merge(OnlineStats other) 合并、以及获取 count, mean, population/sample variance/stddev 的方法,适合在线计算与并行合并。

  • StreamExamples.computeStatsFromDoubleStream(DoubleStream stream):展示如何用 Stream 的 collect 将数据收集到 OnlineStats(可并行)以获得稳定的均值与方差。


项目详细总结

  1. 功能覆盖:本文实现了常见与高级平均数算法的全套工具:算术平均(Kahan & plain)、几何/调和平均、加权平均、截尾平均、中位数、SMA/EMA、Welford 在线统计与并行合并、以及 BigDecimal 的高精度实现,满足教学与工程需求。

  2. 数值稳定性:采用了 Kahan summation 来减少双精度的累加误差,采用 Welford 算法来进行在线稳定均值与方差计算,并提供 BigDecimal 版本以满足高精度要求。

  3. 性能与规模:在大规模数据或流式环境推荐使用在线算法(Welford)与可合并统计,在并行/分布式场景下可以安全合并分片统计避免传输全部原始数据。

  4. API 易用性:方法均做严格参数校验并抛出有意义的异常,方便上层调用者快速定位错误。


项目常见问题及解答

Q1:为什么 Kahan summation 比普通累加更准确?
A1:Kahan 使用一个补偿项来记录被丢失的小量,从而在后续累加中补偿,这能显著减小舍入误差,尤其在把大量小数加到大数上时。

Q2:什么时候使用 BigDecimal?
A2:当需要严格的十进制精度(例如财务计算、会计、法律合规场景)或 double 精度无法满足时,使用 BigDecimal。其代价是计算速度明显变慢与内存开销增大。

Q3:几何平均为什么要对数变换?
A3:直接把所有数相乘会在多项式或指数增长下快速溢出或下溢。对数变换把乘法转为加法,避免中间溢出,同时数值更稳定。

Q4:Welford 算法为什么可合并?
A4:Welford 维护 mean 与 M2(sum of squared deviations),有封闭的合并公式可以把两组统计合并为一组统计,从而适配并行或 MapReduce 场景。

Q5:截尾平均与中位数哪个对异常值更稳健?
A5:两者都比算术平均对异常值更稳健。中位数对极端异常最稳健(不受数值大小影响),截尾平均在保留更多信息的前提下兼顾稳健性和效率,常用于稳健统计分析。


扩展方向与性能优化建议

  1. 并行化与批处理:在多核或分布式环境中使用 OnlineStatsmerge 接口作为聚合器进行分片并行处理,然后合并结果,避免数据移动与重复计算。

  2. 无对象原生数组实现:为极致性能,将所有 BigDecimal/Complex(如果有)等对象化实现改为原始 double[] 操作以减少 GC 开销。

  3. 选择算法替代排序:对于截尾平均或中位数,当数据量非常大时可使用选择算法(Quickselect)在平均线性时间内找到第 k 个元素以避免完整排序。

  4. 高精度对数/指数支持:在需要 BigDecimal 精度的几何平均场景下,建议集成成熟的多精度对数/指数库(例如 Apfloat、Big-Math 等)以准确实现 ln/exp

  5. 滑动窗口高效实现:对滑动窗口统计,可以用双端队列或环形缓冲 + 分块技术(例如 DEMA / T-Digest)来提高效率或处理分位数等。

  6. 鲁棒性检测:在工程中对输入分布做预检查(是否存在 Inf/NaN/极值)并在文档中明确边界条件和异常处理策略,以免下游误用。

Logo

为武汉地区的开发者提供学习、交流和合作的平台。社区聚集了众多技术爱好者和专业人士,涵盖了多个领域,包括人工智能、大数据、云计算、区块链等。社区定期举办技术分享、培训和活动,为开发者提供更多的学习和交流机会。

更多推荐