用Java计算移动平均值

2025/04/10

1. 概述

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

它们有助于平滑短期波动并揭示潜在趋势,使数据更易于解释。

在本教程中,我们将探讨计算移动平均值的各种方法和技术,从传统方法到库和Stream API。

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

在本节中,我们将探讨计算移动平均值的三种常用方法。

2.1 使用Apache Commons Math库

Apache Commons Math是一个强大的Java库,提供广泛的数学和统计函数,包括用于计算移动平均值的工具。

通过利用Apache Commons Math库中的DescriptiveStatistics类,我们可以简化移动平均值计算过程,并利用优化的算法实现高效的数据处理,它将数据点添加到统计对象并检索代表移动平均值的平均值。

让我们使用DescriptiveStatistics类来计算具有windowSize的移动平均值:

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);
}

首先,我们创建一个MovingAverageWithApacheCommonsMath类的实例,窗口大小为3。然后,将3个值(10、20和30)分别添加到计算器中,并验证其平均值。

2.2 使用循环缓冲区方法

循环缓冲区方法是计算移动平均值的经典方法,以其高效的内存使用而闻名。这种方法很简单,在某些情况下可能会提供更好的性能,特别是当我们担心外部依赖的开销时。

在这种方法中,新的数据点会覆盖最旧的数据点,并且平均值是根据缓冲区中的当前元素计算的。

通过循环遍历缓冲区,我们可以实现每次更新的恒定时间复杂度,使其适用于实时数据处理应用程序

让我们使用循环缓冲区计算移动平均值:

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);
}

我们创建一个MovingAverageByCircularBuffer类的实例,窗口大小为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);
}

我们首先创建一个ExponentialMovingAverage(EMA)实例,其平滑因子(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 Stream API的函数编程功能以简洁高效的方式执行计算。

现在,让我们编写一些JUnit测试来确保我们的代码按预期工作:

@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()方法是否针对给定场景(例如有效数据和windowSize)返回正确的平均值。

3. 其他方法

虽然上述方法是Java中计算移动平均值的一些更方便、更有效的方法,但我们可以根据具体要求和约束考虑其他方法。在这里,我们将介绍两种这样的方法。

3.1 并行处理

如果性能是我们的首要任务,并且我们可以使用多个CPU核心,那么我们可以利用并行处理技术更有效地计算移动平均值。

Java提供对并行流的支持,它可以自动在多个线程之间分配计算。

3.2 加权移动平均值

加权移动平均值(WMA)是一种计算移动平均值的方法,它为窗口内的每个数据点分配不同的权重。

权重通常根据预定义的标准确定,例如重要性、相关性或与窗口中心的接近度。

3.3 累计移动平均值

累积移动平均(CMA)计算截至某一时间点的所有数据点的平均值,与其他移动平均方法不同,CMA不使用固定大小的窗口,而是包含所有可用数据。

4. 总结

计算移动平均值是时间序列分析的一个基本方面,其应用涉及金融、经济和工程等各个领域。

使用Apache Commons Math、循环缓冲区和指数移动平均技术,分析师可以深入了解其数据的潜在趋势和模式

此外,探索加权和累积移动平均值扩展了分析师的工具包,从而能够对时间序列数据进行更复杂的分析和解释。

再次强调,选择完全取决于具体项目的要求和偏好。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章