1. 概述

移动平均数是分析数据趋势和模式的基础工具,在金融、经济和工程领域应用广泛。

它能有效平滑短期波动,揭示数据的潜在趋势,使分析结果更易解读。

本教程将探讨多种计算移动平均数的方法,从传统实现到现代库和Stream API方案。

2. 计算移动平均数的常用方法

本节介绍三种主流实现方式,各有适用场景。

2.1. 使用Apache Commons Math库

Apache Commons Math是功能强大的Java数学库,内置统计功能包括移动平均数计算。

通过其DescriptiveStatistics类,可简化计算流程并利用优化算法提升性能。核心思路是:添加数据点到统计对象后直接获取均值作为移动平均数。

public class MovingAverageWithApacheCommonsMath {

    private final DescriptiveStatistics stats;

    public MovingAverageWithApacheCommonsMath(int windowSize) {
        this.stats = new DescriptiveStatistics(windowSize);
    }

    public void add(double value) {
        stats.addValue(value);
    }

    public double getMovingAverage() {
        return stats.getMean();
    }
}

测试验证:

@Test
public void whenValuesAreAdded_shouldUpdateAverageCorrectly() {
    MovingAverageWithApacheCommonsMath movingAverageCalculator = new MovingAverageWithApacheCommonsMath(3);
    movingAverageCalculator.add(10);
    assertEquals(10.0, movingAverageCalculator.getMovingAverage(), 0.001);
    movingAverageCalculator.add(20);
    assertEquals(15.0, movingAverageCalculator.getMovingAverage(), 0.001);
    movingAverageCalculator.add(30);
    assertEquals(20.0, movingAverageCalculator.getMovingAverage(), 0.001);
}

创建窗口大小为3的实例后,依次添加10/20/30三个值,验证均值计算正确性。

2.2. 循环缓冲区方案

循环缓冲区是经典实现方式,以内存高效著称。当不想引入外部依赖时,这种简单粗暴的方案往往更优。

核心机制:新数据覆盖最旧数据,基于当前缓冲区元素计算平均值。

通过循环覆盖机制,每次更新可达O(1)时间复杂度,特别适合实时数据处理场景。

public class MovingAverageByCircularBuffer {

    private final double[] buffer;
    private int head;
    private int count;

    public MovingAverageByCircularBuffer(int windowSize) {
        this.buffer = new double[windowSize];
    }

    public void add(double value) {
        buffer[head] = value;
        head = (head + 1) % buffer.length;
        if (count < buffer.length) {
            count++;
        }
    }

    public double getMovingAverage() {
        if (count == 0) {
            return Double.NaN;
        }
        double sum = 0;
        for (int i = 0; i < count; i++) {
            sum += buffer[i];
        }
        return sum / count;
    }
}

测试用例验证:

@Test
public void whenValuesAreAdded_shouldUpdateAverageCorrectly() {
    MovingAverageByCircularBuffer ma = new MovingAverageByCircularBuffer(3);
    ma.add(10);
    assertEquals(10.0, ma.getMovingAverage(), 0.001);
    ma.add(20);
    assertEquals(15.0, ma.getMovingAverage(), 0.001);
    ma.add(30);
    assertEquals(20.0, ma.getMovingAverage(), 0.001);
}

创建窗口大小为3的实例,添加值后验证移动平均数与预期值匹配(容差0.001)。

2.3. 指数移动平均数

另一种方案是使用指数平滑计算移动平均数。

指数平滑为历史观测值分配指数衰减权重,能快速响应数据变化并捕捉趋势:

public class ExponentialMovingAverage {

    private double alpha;
    private Double previousEMA;

    public ExponentialMovingAverage(double alpha) {
        if (alpha <= 0 || alpha > 1) {
            throw new IllegalArgumentException("Alpha must be in the range (0, 1]");
        }
        this.alpha = alpha;
        this.previousEMA = null;
    }

    public double calculateEMA(double newValue) {
        if (previousEMA == null) {
            previousEMA = newValue;
        } else {
            previousEMA = alpha * newValue + (1 - alpha) * previousEMA;
        }
        return previousEMA;
    }
}

alpha参数控制衰减速率,值越小近期数据权重越高。

指数移动平均数在需要快速响应变化同时保留长期趋势时特别有用。

测试验证:

@Test
public void whenValuesAreAdded_shouldUpdateExponentialMovingAverageCorrectly() {
    ExponentialMovingAverage ema = new ExponentialMovingAverage(0.4);
    assertEquals(10.0, ema.calculateEMA(10.0), 0.001);
    assertEquals(14.0, ema.calculateEMA(20.0), 0.001);
    assertEquals(20.4, ema.calculateEMA(30.0), 0.001);
}

创建alpha=0.4的实例,添加值后验证EMA计算结果(容差0.001)。

2.4. 基于Stream的方案

利用Stream API可实现函数式风格的移动平均数计算,特别适合处理数据流或集合。

简化实现示例:

public class MovingAverageWithStreamBasedApproach {
    private int windowSize;

    public MovingAverageWithStreamBasedApproach(int windowSize) {
        this.windowSize = windowSize;
    }
    public double calculateAverage(double[] data) {
        return DoubleStream.of(data)
                .skip(Math.max(0, data.length - windowSize))
                .limit(Math.min(data.length, windowSize))
                .summaryStatistics()
                .getAverage();
    }
}

核心逻辑:从数据数组创建流,跳过窗口外元素,限制窗口大小,使用*summaryStatistics()*计算均值。

这种方案充分利用了Java Streams API的函数式特性,实现简洁高效。

测试验证:

@Test
public void whenValidDataIsPassed_shouldReturnCorrectAverage() {
    double[] data = {10, 20, 30, 40, 50};
    int windowSize = 3;
    double expectedAverage = 40;
    MovingAverageWithStreamBasedApproach calculator = new MovingAverageWithStreamBasedApproach(windowSize);
    double actualAverage = calculator.calculateAverage(data);
    assertEquals(expectedAverage, actualAverage);
}

验证*calculateAverage()*方法对有效数据和窗口大小的处理正确性。

3. 其他方案

除上述方法外,根据具体需求还可考虑以下替代方案。

3.1. 并行处理

当性能是首要目标且有多核CPU可用时,可采用并行处理提升计算效率。

Java的并行流可自动将计算分配到多线程。

3.2. 加权移动平均数

加权移动平均数(WMA)为窗口内数据点分配不同权重。

权重通常基于预设标准(如重要性、相关性或与窗口中心的距离)确定。

3.3. 累积移动平均数

累积移动平均数(CMA)计算到特定时间点所有数据的平均值。与其他方法不同,它不使用固定窗口,而是包含所有历史数据。

4. 结论

移动平均数计算是时间序列分析的基础,在金融、经济和工程等领域有广泛应用。

通过Apache Commons Math、循环缓冲区和指数移动平均数等技术,分析师能有效挖掘数据的潜在趋势和模式。

此外,加权和累积移动平均数扩展了分析工具箱,支持更复杂的时间序列数据分析。

最终方案选择完全取决于项目具体需求和偏好。

源代码可在GitHub获取。


原始标题:Calculating Moving Averages in Java