Java中计算移动平均线

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

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

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

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

1.使用 Apache Commons 数学库
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);
}

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

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

我们创建一个窗口大小为 3 的MovingAverageByCircularBuffer类的实例。添加每个值后,测试断言计算出的移动平均值与预期值匹配,容差为 0.001。

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 的ExponentialMovingAverage (EMA)实例。

然后,当添加每个值时,测试断言计算出的 EMA 与预期值相匹配,误差范围为 0.001。

4.基于流的方法
我们可以利用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();
    }
}

在这里,我们从输入数据数组创建一个流,跳过指定窗口大小之外的元素,将流限制在 windowSize 内,然后使用summaryStatistics()计算平均值。

这种方法利用Java Streams 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)的正确平均值。

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

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

Java提供了对并行流的支持,它可以自动将计算分布到多个线程上。

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

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

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

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

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

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

同样,选择完全取决于具体的项目要求和偏好。