简介:这份资源是面向时间序列分析初学者与Java开发者的ARMA、ARIMA模型实现例程,帮助读者在项目中快速复用自回归、移动平均及差分整合等核心算法,解决趋势与周期性数据的建模预测问题。压缩包共43个文件,约8.83MB,以java源码与class编译文件为主,另含jar依赖库、classpath与project工程配置、md说明文档,以及ppt、docx、pdf等辅助学习材料,目录按ARMA与ARIMA两个工程分别组织,便于对照阅读。目前已有1679人学习下载。读者可从中获得可直接运行的main入口、模型拟合与预测代码、示例数据集,以及JFreeChart绘图与Apache Commons Math统计计算的集成思路,适合作为课程设计、学术研究或业务预测的起步模板。
1. 时间序列预测选型:为什么 Java 工程师绕不开 ARMA 与 ARIMA
电商大促前两周,运营丢过来一份日订单量 CSV,问下周备多少库存。你打开一看,数据有趋势、有周期,还有几个明显异常点。用移动平均太糙,上 LSTM 又杀鸡用牛刀,这时候 ARMA 和 ARIMA 就是最务实的选择。它们属于经典统计时序模型,参数少、可解释、训练快,在 Java 生态里完全能落地,不需要为了一个预测任务硬切到 Python。
ARMA 处理的是平稳序列,ARIMA 多了一个差分步骤,专门对付非平稳序列。很多 Java 工程师第一次接触这两个模型,是在面试题或者算法课里,真到项目里反而不知道怎么下手。这篇笔记就按我实际做过的路径来:先讲清楚模型在算什么,再给出 Java 可运行的实现,最后把参数怎么调、坑在哪说透。适合有 Java 基础、需要做指标预测的 backend 工程师,也适合正在准备算法相关面试的 java 工程师。
2. ARMA 与 ARIMA 的数学骨架:先搞懂在拟合什么
2.1 自回归与移动平均的直观含义
ARMA 由两部分组成:AR(自回归)和 MA(移动平均)。AR 的思路是“今天的值跟昨天、前天的值有关”,用历史观测值的线性组合来预测当前值。MA 的思路是“今天的值跟过去几个时刻的预测误差有关”,用历史白噪声的线性组合来修正。
写成公式,ARMA(p, q) 的形式是:
x_t = c + φ₁x_{t-1} + … + φ_p x_{t-p} + ε_t + θ₁ε_{t-1} + … + θ_q ε_{t-q}
其中 p 是自回归阶数,q 是移动平均阶数,ε 是白噪声误差。p 决定回看多少个历史值,q 决定回看多少个历史误差。这两个参数不是拍脑袋定的,后面会讲怎么选。
ARIMA(p, d, q) 在 ARMA 基础上多了 d,表示差分次数。如果原始序列有趋势,比如订单量逐日上升,直接套 ARMA 会失效,因为均值在变。做一次差分后,序列变成增量序列,趋势被消除,再套 ARMA 就合理了。d 通常取 0、1、2,绝大多数业务场景 d=1 就够。
2.2 平稳性检验:ADF 检验的 Java 实现
在定阶之前,必须先确认序列是否平稳。常用方法是 ADF(Augmented Dickey-Fuller)检验。原假设是序列存在单位根,即非平稳。如果检验统计量小于临界值,或者 p 值小于显著性水平(通常 0.05),就拒绝原假设,认为序列平稳。
Java 里没有现成的 ADF 库,我一般自己实现简化版。核心是构造差分回归方程,用最小二乘估计系数,再算 t 统计量。下面是一个可运行的实现:
import org.apache.commons.math3.linear.*; import org.apache.commons.math3.stat.regression.OLSMultipleLinearRegression; public class ADFTest { /** * 简化版 ADF 检验 * @param series 原始序列 * @param maxLag 最大滞后阶数 * @return t 统计量(越负越平稳) */ public static double adfStat(double[] series, int maxLag) { int n = series.length; // 构造差分序列 double[] diff = new double[n - 1]; for (int i = 1; i < n; i++) { diff[i - 1] = series[i] - series[i - 1]; } int effectiveN = diff.length - maxLag; double[] y = new double[effectiveN]; double[][] x = new double[effectiveN][maxLag + 2]; for (int i = 0; i < effectiveN; i++) { int idx = i + maxLag; y[i] = diff[idx]; // 滞后项 for (int j = 0; j < maxLag; j++) { x[i][j] = diff[idx - 1 - j]; } // 常数项和趋势项 x[i][maxLag] = 1.0; x[i][maxLag + 1] = idx; } OLSMultipleLinearRegression regression = new OLSMultipleLinearRegression(); regression.newSampleData(y, x); double[] beta = regression.estimateRegressionParameters(); double[] se = regression.estimateRegressionParametersStandardErrors(); // 返回滞后一阶系数的 t 统计量 return beta[0] / se[0]; } }这段代码的逻辑是:对差分序列做回归,自变量包括滞后差分项、常数项和时间趋势项。回归系数的 t 统计量就是 ADF 统计量。参数 maxLag 一般取 1 到 3,太小会残留自相关,太大会损失样本量。如果算出来的 t 统计量小于 -2.86(5% 显著性水平下的临界值,样本量大于 100 时),就可以认为序列平稳。
注意:这个简化版没有完全复现标准 ADF 的临界值表,实际使用时建议对照 MacKinnon 临界值。如果只是做工程判断,t 统计量小于 -3 基本可以放心。
2.3 差分次数 d 的确定与过差分问题
如果 ADF 检验显示非平稳,就做一次差分,再检验。通常一次差分后就能平稳。但不要过度差分,过差分会让序列的方差变大,模型反而变差。判断方法是:差分后序列的方差如果比差分前还大,说明差过头了。
我一般会写一个循环,最多差分两次,每次记录方差和 ADF 统计量:
public static int determineD(double[] series) { double[] current = series; for (int d = 0; d <= 2; d++) { double adf = ADFTest.adfStat(current, 2); double variance = variance(current); System.out.printf("d=%d, ADF=%.3f, variance=%.3f%n", d, adf, variance); if (adf < -2.86) { return d; } current = diff(current); } return 2; }参数说明:adfStat 返回的 t 统计量越小越平稳,variance 用来辅助判断是否过差分。如果 d=1 时 ADF 已经小于 -2.86,且方差没有明显膨胀,就选 d=1。
3. Java 实现 ARMA/ARIMA:从参数估计到预测
3.1 用最小二乘估计 ARMA 参数
ARMA 的参数估计可以用条件最小二乘。思路是:把 MA 部分的误差项用递归方式展开,然后对 AR 系数和 MA 系数做线性回归。下面是一个可运行的 ARMA 拟合类:
import org.apache.commons.math3.linear.*; import org.apache.commons.math3.stat.regression.OLSMultipleLinearRegression; public class ARMA { private double[] arCoeffs; private double[] maCoeffs; private double intercept; private double[] residuals; /** * 拟合 ARMA(p, q) * @param series 平稳序列 * @param p AR 阶数 * @param q MA 阶数 */ public void fit(double[] series, int p, int q) { int n = series.length; int start = Math.max(p, q); int rows = n - start; double[] y = new double[rows]; double[][] x = new double[rows][p + q + 1]; // 初始化残差为 0 residuals = new double[n]; for (int i = 0; i < rows; i++) { int t = i + start; y[i] = series[t]; // AR 项 for (int j = 0; j < p; j++) { x[i][j] = series[t - 1 - j]; } // MA 项(用历史残差近似) for (int j = 0; j < q; j++) { x[i][p + j] = residuals[t - 1 - j]; } // 常数项 x[i][p + q] = 1.0; } OLSMultipleLinearRegression regression = new OLSMultipleLinearRegression(); regression.newSampleData(y, x); double[] beta = regression.estimateRegressionParameters(); arCoeffs = new double[p]; maCoeffs = new double[q]; System.arraycopy(beta, 0, arCoeffs, 0, p); System.arraycopy(beta, p, maCoeffs, 0, q); intercept = beta[p + q]; // 重新计算残差 for (int t = start; t < n; t++) { double pred = intercept; for (int j = 0; j < p; j++) { pred += arCoeffs[j] * series[t - 1 - j]; } for (int j = 0; j < q; j++) { pred += maCoeffs[j] * residuals[t - 1 - j]; } residuals[t] = series[t] - pred; } } public double[] getArCoeffs() { return arCoeffs; } public double[] getMaCoeffs() { return maCoeffs; } public double getIntercept() { return intercept; } public double[] getResiduals() { return residuals; } }逻辑说明:先假设残差为 0,构造设计矩阵,用 OLS 估计参数。然后用估计出的参数重新计算残差,迭代一次通常就够。参数 p 和 q 决定矩阵列数,p+q+1 是总参数量。如果样本量小于 50,p 和 q 都不宜超过 2。
3.2 用 AIC 准则定阶 p 和 q
p 和 q 不能随便选。常用 AIC(赤池信息量准则):AIC = -2ln(L) + 2k,其中 L 是似然函数,k 是参数个数。AIC 越小越好,它平衡了拟合优度和模型复杂度。
在 Java 里,可以用残差平方和近似计算 AIC:
public static double aic(double[] residuals, int p, int q) { int n = residuals.length; double rss = 0; for (double r : residuals) { rss += r * r; } double logLikelihood = -0.5 * n * (Math.log(2 * Math.PI * rss / n) + 1); int k = p + q + 1; return -2 * logLikelihood + 2 * k; }定阶时,遍历 p 从 0 到 3,q 从 0 到 3,选 AIC 最小的组合。注意 p 和 q 不要同时为 0,否则模型没有意义。下面是一个定阶循环:
public static int[] selectOrder(double[] series) { double bestAic = Double.MAX_VALUE; int bestP = 0, bestQ = 0; for (int p = 0; p <= 3; p++) { for (int q = 0; q <= 3; q++) { if (p == 0 && q == 0) continue; ARMA model = new ARMA(); model.fit(series, p, q); double aic = aic(model.getResiduals(), p, q); if (aic < bestAic) { bestAic = aic; bestP = p; bestQ = q; } } } return new int[]{bestP, bestQ}; }参数说明:p 和 q 的上限设为 3 是经验值,业务序列通常不会超过这个范围。如果数据量很大且周期性强,可以放宽到 5,但要注意过拟合。
3.3 多步预测与置信区间
拟合好模型后,预测就是递归计算。一步预测直接用公式,多步预测把预测值当作已知值继续代入。下面是一个预测方法:
public double[] forecast(double[] series, int steps) { int n = series.length; double[] result = new double[steps]; double[] extended = new double[n + steps]; System.arraycopy(series, 0, extended, 0, n); double[] extResiduals = new double[n + steps]; System.arraycopy(residuals, 0, extResiduals, 0, n); for (int s = 0; s < steps; s++) { int t = n + s; double pred = intercept; for (int j = 0; j < arCoeffs.length; j++) { pred += arCoeffs[j] * extended[t - 1 - j]; } for (int j = 0; j < maCoeffs.length; j++) { pred += maCoeffs[j] * extResiduals[t - 1 - j]; } extended[t] = pred; extResiduals[t] = 0; // 未来残差期望为 0 result[s] = pred; } return result; }逻辑说明:extended 数组保存历史值和预测值,extResiduals 保存历史残差,未来残差设为 0。参数 steps 是预测步数,一般不超过 10,步数越多误差累积越大。置信区间可以用残差标准差乘以 1.96 近似:
double sigma = Math.sqrt(rss / (n - p - q - 1)); double lower = pred - 1.96 * sigma; double upper = pred + 1.96 * sigma;注意:多步预测的置信区间会随步数增大而变宽,实际业务中建议只取前 3 步做决策参考。
4. 避坑与排查:ARIMA 落地时最容易翻车的 5 个点
4.1 现象:预测结果是一条直线
原因:差分次数 d 选大了,或者 p、q 都选了 0。过差分后序列变成白噪声,模型学不到任何模式,只能输出均值。
解决:回看 ADF 检验结果,确认 d 是否必要。如果 d=1 后方差明显变大,改回 d=0。同时检查定阶循环,确保 p 和 q 至少有一个大于 0。
4.2 现象:AIC 选出的阶数很大,但预测效果差
原因:AIC 在小样本下容易过拟合,选出的 p、q 偏大。另外,如果序列有周期性,ARMA 本身无法捕捉,需要先做季节差分。
解决:加一个约束,p+q 不超过样本量的平方根。如果序列有明显周期(比如 7 天),先做季节差分再拟合。或者改用 SARIMA,但 Java 实现复杂度会上升。
4.3 现象:残差还有自相关,模型没学干净
原因:p 或 q 选小了,或者序列有非线性成分。ARMA 只能捕捉线性关系。
解决:用 Ljung-Box 检验残差。如果残差还有自相关,增大 p 或 q。如果增大后仍不行,说明线性模型到头了,考虑树模型或神经网络。
4.4 现象:预测值偏移实际值很远
原因:没有做去均值或标准化。ARMA 假设序列均值稳定,如果原始序列均值很大,截距项会吸收大部分方差,导致预测偏移。
解决:拟合前先减去均值,预测后再加回来。代码里加一行:
double mean = Arrays.stream(series).average().orElse(0); double[] centered = Arrays.stream(series).map(v -> v - mean).toArray(); // 拟合 centered,预测结果再加 mean4.5 现象:Java 进程内存溢出,报 OutOfMemoryError
原因:定阶循环里反复创建大矩阵,或者序列太长导致 OLS 矩阵过大。热词里提到的“进程堆大小调整为 8000 还是报错”就是这个场景。
解决:限制 p、q 上限,复用矩阵对象,或者对长序列做降采样。如果序列超过 10000 个点,先聚合到小时或天级别再拟合。
5. 进阶技巧:用滚动预测验证模型真实效果
5.1 滚动预测的实现与评估指标
一次性划分训练集和测试集,在时序任务里容易高估效果。更可靠的做法是滚动预测:每次用前 N 个点训练,预测第 N+1 个点,然后窗口向前滑动。下面是一个滚动验证的框架:
public static double rollingValidate(double[] series, int p, int q, int windowSize) { int n = series.length; double totalError = 0; int count = 0; for (int i = windowSize; i < n; i++) { double[] train = Arrays.copyOfRange(series, i - windowSize, i); ARMA model = new ARMA(); model.fit(train, p, q); double[] pred = model.forecast(train, 1); totalError += Math.abs(pred[0] - series[i]); count++; } return totalError / count; // 返回 MAE }参数说明:windowSize 是训练窗口大小,一般取 30 到 100。太小模型不稳定,太大对近期变化不敏感。返回的 MAE 越小越好,可以跟移动平均的 MAE 对比,确认 ARMA 是否真的更优。
5.2 模型持久化与线上加载
Java 里可以把 AR 系数、MA 系数、截距和残差标准差序列化成 JSON 或二进制,线上直接加载预测,不需要每次重新拟合。我一般用 Jackson:
ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(new File("arma_model.json"), model); // 线上加载 ARMA loaded = mapper.readValue(new File("arma_model.json"), ARMA.class);注意:序列化时要确保 ARMA 类有无参构造函数,字段有 getter/setter。线上预测只做前向计算,耗时在微秒级,完全能扛住实时请求。
5.3 我踩过的坑与固定习惯
血泪经验有两条。第一,永远先画图再拟合。我曾經直接对一份有明显周期性的数据套 ARIMA,结果 AIC 选出的阶数很大,预测却完全跟不上周期波动,后来加了季节差分才解决。第二,永远保留一份基线模型。移动平均虽然简单,但它是检验 ARIMA 是否值得上线的标尺。如果 ARIMA 的 MAE 比移动平均还差,说明参数没调好或者数据不适合。
现在我的固定习惯是:拿到序列先跑 ADF,再画 ACF/PACF 图辅助定阶,然后用 AIC 循环确认,最后滚动验证。这套流程跑下来,ARMA/ARIMA 在 Java 里落地并不玄学,参数可解释,结果可复现。希望帮到你。
本文还有配套的精品资源,点击获取