简介:面向Java开发者与时间序列分析学习者的ARMA、ARIMA模型实现例程,重点解决在业务项目中复用、调试自回归与移动平均建模代码的问题。包内以两个独立工程组织源码,提供Eclipse工程配置、Java实现类、编译产物及Jama等依赖库,并附带示例数据和说明文档,便于从入口程序运行并观察模型拟合、差分处理与预测流程;配套教程与讲义可辅助理解自回归阶数、移动平均阶数及平稳化等概念。全包共43个文件,以12个java源码、12个class、5个jar依赖为主,辅以md说明、txt数据及少量文档,压缩后约8.83MB,目录紧凑,适合边读代码边验证。已有1679人学习下载,可作为课程实验、算法复现或项目原型的参考素材,也方便替换自有序列做调试与结果对照。
1. 从一段销量序列说起:为什么要在 Java 里手搓 ARMA 和 ARIMA
手上的 Java 服务要做周度销量预测,离线侧的分析工具一行 ARIMA 就出结果,可线上不可能为了一个预测接口再挂一套分析运行时。这才是大部分人搜「ARMA ARIMA 模型 Java 实现」的真实处境:不是不会建模,而是要把建模过程搬进 JVM。这份例程走的是一条不依赖建模框架的路:用 JDK 原生数组把差分、平稳性检验、ACF/PACF 定阶、参数估计、预测还原和滚动回测串成一条能跑通的链路。每一行参数都能改,改完也知道后果,适合两类人——已经在用现成统计库但说不清系数怎么来的,以及想把定阶、约束、批量建模嵌进现有工程里的。
2. 差分与平稳性检验:d 定错,后面所有参数都是白算
ARIMA 里最容易被轻视的一步就是 d。很多人拿到序列先画个图,看趋势往下走就随手做一阶差分,接着定阶、估参、出预测,最后发现预测曲线在尾部一路往下冲。这类问题九成不在 ARMA 部分,而是差分阶数选错,导致模型把「趋势」当成了「记忆」去外推。这一章干的事就一件:把 d 从拍脑袋变成一个能算、能验证的决策。
2.1 差分的本质是把水平信息挪到增量上
原始序列里的趋势项是确定性的,ARMA 模型假设的是弱平稳——均值和自协方差都不随时间平移。做一次差分,等价于给序列套一个 $(1-B)$ 算子,把线性趋势变成常数、把二次趋势变成线性。问题是差分不是越多越好,每做一次差分就丢一个观测,同时放大高频噪声;差分过头,序列会变得不可逆,预测阶段就会出现「还原后越走越偏」的玄学现象。
我一般会先把差分写成独立工具类,不和建模代码耦合,因为后面还原时要靠它逐层累加。下面这段是整个例程里最短、但用得最多的代码。
public final class Differencer { /** d 阶差分,返回长度 n-d 的序列 */ public static double[] diff(double[] x, int d) { double[] cur = x.clone(); for (int k = 0; k < d; k++) { double[] next = new double[cur.length - 1]; for (int i = 1; i < cur.length; i++) { next[i - 1] = cur[i] - cur[i - 1]; // 一阶差分:本期减上期 } cur = next; } return cur; } /** tail[k] = 第 k 层差分序列(k=0 为原序列)的最后一个观测值,还原时当锚点用 */ public static double[] tail(double[] x, int d) { double[] t = new double[d]; double[] cur = x.clone(); t[0] = cur[cur.length - 1]; for (int k = 1; k < d; k++) { double[] next = new double[cur.length - 1]; for (int i = 1; i < cur.length; i++) next[i - 1] = cur[i] - cur[i - 1]; cur = next; t[k] = cur[cur.length - 1]; // 每层只留末值,够还原用 } return t; } }diff的循环写法比递归省事,cur每轮缩短 1,最后自然只剩最深层序列。tail单独抽出来是因为还原时你一定要知道「预测起点之前,每一层序列停在哪个数上」,这个数一旦取错,整个预测曲线会整体平移——这是后面避坑章节里最典型的一条。
参数上没什么可调的,真正要留意的是d的取值上限:样本量 200 以内的序列,d超过 2 基本没有统计意义,三层差分后剩下的点太少,ADF 回归的自由度都不够用。
2.2 ADF 回归用二十行最小二乘就能跑起来
判断平稳性最常用的是 ADF 检验。它的形式是一个带滞后差分项的回归:
$$\Delta x_t = \alpha + \beta t + \gamma x_{t-1} + \sum_{i=1}^{p}\delta_i \Delta x_{t-i} + \varepsilon_t$$
关心的只有 $\gamma$ 的 t 统计量。原假设是 $\gamma = 0$(存在单位根,序列不平稳),统计量越负越倾向拒绝。这个回归本身就是普通最小二乘,不需要拉任何统计包,自己写个求逆就能算。
public final class AdfTest { public static final class Result { public final double stat; // gamma 的 t 统计量 public final double sigma2; // 回归残差方差 public final int n; // 有效样本量 Result(double stat, double sigma2, int n) { this.stat = stat; this.sigma2 = sigma2; this.n = n; } /** 5% 显著性、含常数项不含趋势的临界值,n 在 100 附近时约 -2.89 */ public boolean stationaryAt5() { return stat < -2.89; } } /** * @param x 原始序列 * @param p 增广阶数,粗略取 floor(4*(n/100)^0.25),一般 1~4 足够 * @param trend 是否加入确定性时间趋势项 */ public static Result test(double[] x, int p, boolean trend) { int n = x.length; int cols = 2 + (trend ? 1 : 0) + p; int rows = n - 1 - p; if (rows <= cols + 2) { throw new IllegalArgumentException("样本太短,ADF 回归自由度不足"); } double[][] X = new double[rows][cols]; double[] y = new double[rows]; for (int i = 0; i < rows; i++) { int t = i + p + 1; y[i] = x[t] - x[t - 1]; // 被解释变量:一阶差分 int c = 0; X[i][c++] = 1.0; // 常数项 if (trend) X[i][c++] = t; // 时间趋势 X[i][c++] = x[t - 1]; // 水平项 x_{t-1} for (int j = 0; j < p; j++) { X[i][c++] = x[t - 1 - j] - x[t - 2 - j]; // 增广差分项 } } Ols ols = Ols.fit(X, y); int levelIdx = trend ? 2 : 1; // 水平项所在列 double stat = ols.beta[levelIdx] / ols.se[levelIdx]; return new Result(stat, ols.sigma2, rows); } }关键点有三个。第一,被解释变量是 $\Delta x_t$ 而不是 $x_t$,这是 ADF 和普通回归最容易写混的地方。第二,水平项一定要用 $x_{t-1}$,用当期值整个检验就失效了。第三,临界值不能用标准 t 分布的表,ADF 统计量服从的是非标准分布,必须查专门的临界值表,实现里直接硬编码了 5% 水平下的近似值:
| 样本量 | 含常数项(无趋势) | 含常数项 + 趋势 |
|---|---|---|
| n ≈ 50 | -2.92 | -3.50 |
| n ≈ 100 | -2.89 | -3.45 |
| n ≈ 250 | -2.88 | -3.43 |
| n ≈ 500 | -2.87 | -3.42 |
样本量越大临界值收敛越快,表格里这几行覆盖了绝大多数业务序列。我的习惯是先跑含趋势的版本,如果趋势项系数不显著再退回不含趋势的版本,两个版本的stat差得很远时,说明序列里确实有确定性趋势,此时应该先剥离趋势再建模,而不是加差分阶数。
2.3 过差分的三种识别信号
ADF 告诉你「差分后平不平稳」,但没告诉你「是不是差分多了」。这里有几条血泪经验换来的判据:
第一种信号是方差反弹。正常的差分会让序列波动幅度下降或持平,如果在d=2之后残差方差明显大于d=1,说明二阶差分把信号打散成了噪声。做法很简单,把每一阶差分后的样本方差打出来横向比一下,方差最小的那一阶通常就是合适的d。
第二种信号是 ACF 出现大的负值。做过头的序列在一阶滞后上会冒出一个接近 -0.5 的自相关系数,这是差分算子过度的典型特征。
第三种信号来自还原后的预测形状。拿d=1和d=2各跑一次预测、各自还原,看 12 期之后的走向:如果d=2的曲线呈明显抛物线外推,而业务上没有任何依据支持这种加速,就退回d=1。
季节性序列还要额外注意:周期为 12 的月度数据里,普通一阶差分往往不够,需要叠加季节差分 $(1-B^{12})$,但它会一次性丢掉 12 个点,样本少于 60 时基本别用。此时更稳的做法是先把季节成分用移动平均剥离出去,剩下的部分再走常规 ARIMA 流程。
3. ACF 与 PACF 的代码实现:让 p、q 从看图变成算数
定阶这件事,图上看是一回事,代码里写是另一回事。看图容易产生错觉,尤其是样本量小的时候,ACF 上一根稍微高一点的柱子,可能只是随机波动。这一章把 ACF、PACF、AIC/BIC 三条线都实现出来,目的是让你有三票可以互相校验,而不是只靠一张图下结论。
3.1 自协方差的分母之争:除 n 还是除 n-k
自协方差估计有两个流派:除以 $n$ 的有偏估计,和除以 $n-k$ 的无偏估计。单看数值,无偏版本更「正确」,但它在滞后阶数接近样本量时会剧烈震荡,甚至让整个自协方差矩阵不正定,后面的 Durbin-Levinson 递推直接崩掉。主流实现基本都选除 $n$,牺牲一点无偏性换数值稳定,例程里也这么写。
public final class Correlogram { /** 返回长度 maxLag+1 的自相关数组,r[0] 恒为 1 */ public static double[] acf(double[] x, int maxLag) { int n = x.length; double mean = 0; for (double v : x) mean += v; mean /= n; double[] acov = new double[maxLag + 1]; for (int k = 0; k <= maxLag; k++) { double s = 0; for (int t = k; t < n; t++) { s += (x[t] - mean) * (x[t - k] - mean); } acov[k] = s / n; // 统一除 n,保证自协方差矩阵正定 } double[] r = new double[maxLag + 1]; for (int k = 0; k <= maxLag; k++) r[k] = acov[k] / acov[0]; return r; } }maxLag的取法有讲究。经验值是 $\min(10 \log_{10} n,\ n/4)$:100 个点取 20 左右,250 个点取 24 左右。取得太短看不出一阶截尾,取得太长尾部全是噪声。算出来的r[k]可以直接和 $1.96/\sqrt{n}$ 比较,超出这条带子的滞后阶数才值得关注。
还有一种情况要提前处理:序列均值明显不为零时,去均值这一步必须做,否则 ACF 会整体偏正,所有滞后都显得「显著」。上面代码里先减mean再做乘积,就是在处理这个问题。
3.2 Durbin-Levinson 递推把 PACF 算出来
PACF 的直观含义是「扣掉中间滞后项影响后,$x_t$ 与 $x_{t-k}$ 的净相关」。直接算法是每阶都跑一次 k 元回归,样本大时慢得离谱。Durbin-Levinson 递推把它变成了 $O(K^2)$ 的迭代,而且顺带把 AR 系数也算出来了。
/** 输入 <p> <a href="https://download.csdn.net/download/bocai8058/10399921" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>