ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ARIMA残差修正实战:用加权马尔可夫链提升时间序列预测精度

ARIMA残差修正实战:用加权马尔可夫链提升时间序列预测精度 简介基于加权马尔可夫链修正的ARIMA预测模型研究论文适合时间序列分析、设备状态监测及机器学习算法融合方向的研究人员与开发者。论文针对传统ARIMA模型在处理非线性、非平稳数据时的偏差与不稳定性引入加权马尔可夫链对残差序列进行状态划分与概率建模再结合状态特征值和线性插值法将预测残差转化为具体修正值最终以船舶海水出口温度预测为实例详细对比了单一ARIMA模型与修正模型的预测效果。资源为pdf格式共1个文件压缩包约767KB已有513人学习。阅读后可系统掌握组合模型构建、残差修正流程、实验结果评估及设备视情维护的实现思路也可将方法推广至金融、能源、交通等其他复杂系统预测场景是学术研究与工程应用的有益参考。1. 为什么ARIMA的预测残差里还藏着信息加权马尔可夫链修正思路做时间序列预测的人都有过这种体验用ARIMA拟合完历史数据残差图看起来白得发亮但一旦多步往前走预测值就开始“懒”——该拐弯的地方直着走该震荡的地方平着滑。原因不是ARIMA不行而是它把能拆的线性趋势和自相关结构都拆完了剩下的残差里还残存着状态转移的规律。加权马尔可夫链就是干这个的把ARIMA残差按状态切分用转移概率算期望再按权重修正ARIMA的预测值。这个思路特别适合销量、负荷、流量这类非线性波动明显、但状态之间又有规律的序列。这篇实战笔记会把从ARIMA建基准到加权马尔可夫链修正的完整路径拆开参数怎么设、坑在哪一条条说清楚。2. 先建ARIMA基准平稳性检验、定阶参数与残差提取2.1 为什么先跑ARIMA而不是直接上马尔可夫链常见做法是先把ARIMA建好再对残差做加权马尔可夫链而不是跳过ARIMA直接用马尔可夫链预测原序列。原因很直接马尔可夫链要求状态划分稳定而原始序列如果带趋势或者季节性同一数值在不同时间段的含义完全不同状态去划分它转移矩阵会被极端值污染。ARIMA先把确定性的线性部分拿走残差围绕零附近波动状态边界才能有统计意义。另外ARIMA和马尔可夫链的分工本来就不重叠。ARIMA擅长捕捉线性关系包括自回归项、差分项和移动平均项马尔可夫链擅长描述离散状态之间的转移概率。把两者拼在一起本质上是“线性主体 非线性残差修正”。要是原始序列本身就是随机游走ARIMA拟合完残差还是随机游走那加权马尔可夫链也救不了——这个先决条件在第一阶段就能查出来。整套流程的落地顺序是先做平稳性检验再定阶再拟合再提取残差再检验残差是否还有自相关。残差如果有显著自相关说明ARIMA模型没吸收干净要么升阶要么换模型而不是急着做加权马尔可夫链。这一点放在最前面是为了避免你花了一晚上调转移矩阵最后发现问题出在ARIMA的阶数上。2.2 用ADF检验确认差分阶数平稳性是ARIMA建模的第一道门槛。我一般用ADF检验判断序列是否平稳p值小于0.05说明拒绝单位根假设序列可以视为平稳。不平稳就先差分每差分一次检验一次。import pandas as pd import numpy as np from statsmodels.tsa.stattools import adfuller # 读入序列假设是单列时间序列 series pd.read_csv(sales.csv, parse_dates[date], index_coldate)[value] # 原序列ADF检验 adf_org adfuller(series.dropna()) print(f原始序列 ADF p值: {adf_org[1]:.4f}) # 若p值大于0.05先做一阶差分再检验 if adf_org[1] 0.05: series_diff series.diff().dropna() adf_diff adfuller(series_diff) print(f一阶差分 ADF p值: {adf_diff[1]:.4f})这段代码先对原序列做ADF检验p值大于0.05就执行差分。注意series.diff()产生的第一个值是NaN必须用dropna()清掉否则后面的拟合会报错。差分阶数建议最多到二阶三阶以上虽然能通过平稳性检验但会丢失大量信息预测出来基本是平的。2.3 定阶与拟合AIC/BIC最小化定阶有两条路一条是画ACF和PACF图人工判断适合快速看个大概另一条是遍历(p,d,q)组合选AIC或BIC最小的那组。批量定阶时我一般用pmdarima的auto_arima它会自动遍历候选参数省去手写循环的工夫。from pmdarima import auto_arima # 在1到5范围内搜索p和qd固定为1 model auto_arima( series, start_p1, max_p5, start_q1, max_q5, d1, seasonalFalse, traceTrue, stepwiseTrue, information_criterionaic, suppress_warningsTrue ) print(model.summary())auto_arima的traceTrue会打印每一步尝试的AIC值可以看到模型在哪些参数组合上收敛得更快。information_criterionaic在多步预测场景下通常比BIC更合适因为AIC对大阶数模型更宽容能保留一些弱自相关结构。如果你的序列明显有月度或者周度周期把seasonalTrue打开并给m传入周期长度否则季节性会被当成趋势处理残差会大得离谱。2.4 残差提取与自相关检查ARIMA拟合完残差是后面加权马尔可夫链的原料。提取残差这一步要小心model.resid()在部分库版本里会把NaN值一起保留直接影响状态划分。我习惯先转成numpy数组再用np.isfinite过滤一遍。import numpy as np from statsmodels.stats.diagnostic import acorr_ljungbox # 提取残差并过滤无效值 resid np.asarray(model.resid()) valid_mask np.isfinite(resid) resid resid[valid_mask] # Ljung-Box检验p值小于0.05说明残差仍有自相关 lb_test acorr_ljungbox(resid, lags[10], return_dfTrue) print(lb_test) # 保存残差序列供马尔可夫链阶段使用 np.save(resid.npy, resid)Ljung-Box检验是判断残差是否还有自相关的快速口子。p值大于0.05说明模型已经把线性信息吸收干净了可以做下一步p值小于0.05返回去调整ARIMA的阶数。很多人在这一步偷懒残差带着明显的自相关就去划分状态结果马尔可夫链把ARIMA没算完的线性规律又“修正”了一遍预测值反而偏离真实值更远。注意残差量级会影响状态划分的参数。如果残差标准差特别小比如小于0.01状态划分后每个状态的样本量可能不够转移矩阵稀疏得没法看。这时先检查原始序列是否被放缩过建议建模前用StandardScaler把序列标准化预测完再反向变换回去。3. 加权马尔可夫链修正残差状态划分、转移矩阵与权重函数3.1 残差状态划分均值标准差法与分位数法把残差变成离散状态是加权马尔可夫链最关键的一步。常用做法是按均值加减k倍标准差来划分区间。比如分4个状态小于均值减一个标准差为状态1均值正负一个标准差之间为状态2和状态3大于均值加一个标准差为状态4。这样划分的好处是边界稳定而且能保留极端值的影响。分位数法则更适合残差分布偏斜的场景。用25%、50%、75%分位数做切分线能保证每个状态里的样本数量大致相同但极端值的区分度会弱一些。两种方案没有绝对优劣我一般先画残差的直方图接近正态就用均值标准差法明显偏斜就用分位数法。import numpy as np resid np.load(resid.npy) mean_r np.mean(resid) std_r np.std(resid) # 4状态划分状态1/2/3/4 states np.zeros_like(resid, dtypeint) states[resid mean_r - std_r] 1 states[(resid mean_r - std_r) (resid mean_r)] 2 states[(resid mean_r) (resid mean_r std_r)] 3 states[resid mean_r std_r] 4 # 统计每个状态的样本量 for s in range(1, 5): print(f状态 {s}: {(states s).sum()} 个样本)状态数的选择直接影响转移矩阵的稠密程度。状态太少修正精度上不去状态太多每个状态里的样本被稀释转移概率估计不稳定。4到5个状态是常见做法如果你有上千个历史数据点可以试试6个状态但低于500个点还是老实停在4个状态。还有一点状态划分的边界要固定下来测试集和训练集用同一套边界不能重新算否则就犯了数据泄露的错。3.2 转移频率矩阵与概率矩阵状态序列出来后统计一步转移概率矩阵。所谓一步转移就是今天在状态i、明天在状态j的比例。计算方式很朴素先统计转移频次再把每个状态i的频次按行归一化。num_states 4 trans_count np.zeros((num_states, num_states)) for t in range(len(states) - 1): i states[t] - 1 j states[t 1] - 1 trans_count[i, j] 1 # 行归一化得到转移概率矩阵 trans_prob trans_count / trans_count.sum(axis1, keepdimsTrue) print(转移概率矩阵) print(np.round(trans_prob, 3))这里有个容易被忽略的细节最后一个状态没有“下一时刻”所以range(len(states) - 1)不能写成range(len(states))否则下标越界。归一化时如果某一行全为零除零会产生NaN后续加权计算全部失效。我一般加一行保护trans_count[trans_count.sum(axis1) 0] np.ones(num_states)把零行改成均匀分布避免崩溃。3.3 三套权重方案频率权重、距离权重、概率权重加权马尔可夫链和普通马尔可夫链的区别就在“加权”两个字上。普通马尔可夫链只用最近一步的状态去查转移概率加权版本则把过去k步的状态都考虑进来给历史更近的步骤更高的权。常见做法有三套方案频率权重、距离权重、概率权重。频率权重看历史中每个状态出现的次数占比出现多的状态占比高这种方案适合状态分布严重不平衡的残差距离权重按时间衰减越近的状态权越大公式上常见的是w(t) 1 / (t 1)或指数衰减w(t) alpha^t适合残差状态快速变化的序列概率权重则把每个状态的转移概率大小作为权重参考转移概率越确定的状态越值得信任。表格里列一下三套方案的适用场景权重方案计算方式适用场景注意点频率权重状态出现次数 / 总次数状态分布不均衡对近期变化不敏感距离权重1/(t1) 或 alpha^t状态快速切换alpha取值要调参概率权重转移概率的期望或方差状态转移规律明显方差大时要降权实际项目中我不会只选一套而是把三套都算出来然后看哪套在验证集上RMSE最小。距离权重里的alpha通常在0.3到0.9之间网格搜索每步0.1用滚动验证选最优。3.4 单步与多步修正加权马尔可夫链怎么输出修正值加权马尔可夫链的输出不是一个状态而是所有状态的加权期望值。具体来说把过去k步的状态转为概率分布和转移概率矩阵相乘得到未来状态的分布再用每个状态的“代表值”求期望。这个期望就是残差的修正值。def weighted_markov_correction(states, trans_prob, k3, alpha0.5): 基于距离权重修正残差 states: 分箱后的状态序列 trans_prob: 一步转移概率矩阵 k: 回看步数 alpha: 距离衰减系数 # 最近k步的状态概率分布 recent_states states[-k:] weights np.array([alpha ** (k - i) for i in range(k)]) weights / weights.sum() # 过去k步的状态分布 state_dist np.zeros(trans_prob.shape[0]) for idx, s in enumerate(recent_states): state_dist[s - 1] weights[idx] # 预测下一个状态分布当前状态分布乘转移矩阵 next_dist state_dist trans_prob # 用每状态均值作为代表值求期望 state_repr [] for s in range(1, trans_prob.shape[0] 1): mask (states s) state_repr.append(np.mean(resid[mask]) if mask.sum() 0 else 0.0) correction np.dot(next_dist, state_repr) return correction这段代码把加权马尔可夫链的核心逻辑浓缩了。权重值alpha ** (k - i)让最近的状态拿最高的权最远的状态拿最低的权归一化之后保证总和为1。state_dist trans_prob是矩阵乘法维度是(4,)乘(4,4)输出(4,)含义是预测下一步落在每个状态的概率。最后用该状态残差均值求期望得到修正值。注意state_repr里的每个状态必须有足够样本才有意义少于10个样本的状态均值的方差太大修正值会很不稳定。4. 把修正值叠回ARIMA预测合成流程与验证指标4.1 合成公式预测值等于ARIMA加修正值加权马尔可夫链输出的修正值直接加到ARIMA的预测值上。合成公式只有一行final_prediction arima_forecast correction。但要注意ARIMA预测多步之后修正值要逐点计算。因为马尔可夫链是状态序列驱动的每走一步状态分布就更新一次不能用同一个修正值去加后面所有步。多步修正的循环逻辑是先用ARIMA预测第t1步得到残差初始状态代入加权马尔可夫链计算修正值把修正后的值作为新输入更新状态序列再预测t2步。这本质上是“滚动修正”不是一次算完。4.2 滚动预测的验证框架要验证加权马尔可夫链有没有真的提升ARIMA的精度就不能只做一次训练集测试集划分。我一般用滚动窗口验证把训练集起始点固定终点不断前移每移一步就重训一次模型预测下一步收集预测误差。这样一个测试数据点都不浪费而且能看出模型在不同时期的稳定性。from statsmodels.tsa.arima.model import ARIMA def rolling_forecast(series, resid, horizon12, k3, alpha0.5): 滚动预测每次重训ARIMA并用加权马尔可夫链修正 series: 原始序列已标准化 resid: 全量残差用于统计状态拟合参数 horizon: 预测步数 forecasts [] corrections [] for t in range(len(series) - horizon): # 用截至t的数据拟合ARIMA train series[:t 1] arima_model ARIMA(train, order(2, 1, 1)).fit() # 预测下一步 fc arima_model.forecast(1)[0] forecasts.append(fc) # 用当前残差状态做单步修正 # 实际项目里这里会维护一个实时状态序列 correction weighted_markov_correction(resid, trans_prob, k, alpha) corrections.append(correction) return np.array(forecasts) np.array(corrections)这个示例代码暴露了一个工程问题实时滚动时残差状态序列是逐点更新的而weighted_markov_correction里用的states[-k:]必须是截至当前滚动点的状态序列不能拿全量残差去截。更稳妥的做法是在循环里维护一个states_buffer每预测完一步把真实值减去ARIMA预测值得到新残差更新状态后塞进buffer再继续下一步。4.3 指标对比RMSE/MAE/MAPE与提升幅度合成了预测结果用RMSE、MAE、MAPE三个指标对比纯ARIMA和ARIMA加加权马尔可夫链。计算公式不复杂但读结果时要有一个概念提升幅度在5%以内属于正常波动10%以上说明马尔可夫链真的抓到了残差里的状态规律3%以下的话多半是状态划分或权重没调好。def rmse(y_true, y_pred): return np.sqrt(np.mean((y_true - y_pred) ** 2)) def mape(y_true, y_pred): return np.mean(np.abs((y_true - y_pred) / y_true)) * 100 # 真实值与两组预测值 y_true series[-horizon:] pred_arima forecasts pred_hybrid forecasts corrections print(fARIMA RMSE: {rmse(y_true, pred_arima):.4f}) print(fHybrid RMSE: {rmse(y_true, pred_hybrid):.4f}) print(fMAPE 提升: {mape(y_true, pred_arima) - mape(y_true, pred_hybrid):.2f}%)对照实验要做三组纯ARIMA、ARIMA加固定频率权重、ARIMA加距离权重。三组在同一个验证集上比不然sample不同提升幅度没有可比性。还有一个容易被忽略的问题MAPE在真实值接近零时会爆掉如果序列里有周期性低值比如夜间零销量优先看RMSE和MAE。5. 加权马尔可夫链加ARIMA的5个踩坑记录5.1 状态数拍脑袋定太多转移矩阵全是零现象分6个状态统计转移概率矩阵时发现第三行和第五行全是0算出来的修正值永远是同一个数。原因样本量不够特别是极端状态被分到边缘后出现次数少得可怜几乎没有机会转移到别的状态。转移矩阵里零行意味着这个状态是“死状态”。解决减少状态数到4或者改用分位数法切分保证每个状态至少占到总样本的10%以上。如果必须保留6个状态就引入拉普拉斯平滑给转移矩阵对角线加上一个较小的epsilon值避免零概率。5.2 残差没通过白噪声检验就强行修正现象ARIMA跑完Ljung-Box检验的p值只有0.01明显小于0.05。加权马尔可夫链修正后预测误差反而变大。原因残差里还有线性自相关信息马尔可夫链本身能捕捉这种自相关但它捕捉的方式是离散状态转移精度远不如ARIMA直接用ACF和PACF建模来得高。两步都在炒同一盘菜锅底糊了。解决回到ARIMA定阶环节把p或q加一阶或者考虑加入季节项。等Ljung-Box检验p值大于0.05再继续。如果加了阶数还是不通过检查是否漏了外部变量比如节假日哑变量。5.3 权重没归一化修正值整体漂移现象修正值恒为正值预测曲线整体比真实值高出一截。原因距离权重的计算公式w(t) 1/(t1)是递降的但把k步权重直接累加后没有归一化导致修正值的期望被拉高。加权马尔可夫链的所有权重方案如果不归一化输出就是有偏的。解决每计算一组权重立刻除以权重总和。更省事的办法是对权重做softmax让权重本身保持在0到1之间且总和为1。我一般用weights np.exp(-alpha * np.arange(k))再归一化这样alpha的调节空间更大。5.4 多步预测时修正值被反复叠加放大现象滚动预测到第5步之后预测值开始发散误差越来越大。原因每走一步修正值都被加到下一轮的状态更新里相当于是对残差预测做了递归叠加。如果修正方向持续同向轻微的系统性偏差就会被不断放大。解决限制修正幅度。常见做法是对修正值乘以一个收缩系数比如correction * 0.7或者设置修正值绝对值上限为残差标准差的1.5倍。另一个做法是在多步预测的前半段使用加权马尔可夫链修正后半段只保留ARIMA原预测避免后期累积误差。5.5 测试集和训练集共用状态划分边界现象验证集上效果爆好但上线后预测效果悬崖式下跌。原因状态划分的均值和标准差是在全量数据上算的等于把未来信息泄露进了模型。测试集里的残差相对训练集自然会落在“正常范围”内修正值当然好看。解决严格按时间顺序切分数据状态边界的均值、标准差、分位数全部只用训练集计算保存这些边界参数测试时直接套用。用一个简单函数固化边界def fit_state_boundaries(resid_train, num_states4): mean_r np.mean(resid_train) std_r np.std(resid_train) if num_states 4: bounds [mean_r - std_r, mean_r, mean_r std_r] return bounds # 测试集直接用这套边界映射状态上线后每次跑新数据也是用历史训练固定下来的边界不允许在线重新拟合。这算是我踩过的最贵的坑没有之一。6. 检验加权马尔可夫链值不值得加一个小样本对照法与其相信“加了总比不加好”不如做一个严格的样本外对照来验证。操作很简单选最后N个样本作为测试区从测试区之前的数据里截出训练区用训练区拟合状态边界和转移矩阵然后滚动预测整个测试区。年份跨度至少覆盖一个完整波动周期比如月度数据就至少12个月。对照结果看三点修正值序列的标准差是否明显大于零——如果修正值长期贴着零走说明马尔可夫链没抓到东西RMSE提升是否稳定——不是某一年提升、其他年份下降的过山车行情转移矩阵对角线是否明显大于非对角线——这条最重要如果各状态自转移概率都在0.8以上说明残差状态惰性强不做修正也影响不大。# 小样本对照的最终输出 lift_ratio (rmse_arima - rmse_hybrid) / rmse_arima print(f提升比例: {lift_ratio * 100:.1f}%) # 阈值建议 5% 不加5%-10% 结合稳定性判断 10% 值得上生产我自己做这类模型有个习惯先把纯ARIMA的预测结果打印出来贴墙上再迭代加权马尔可夫链每次调参都回到那张图上看残差形态有没有改变。没有跌宕起伏的推导公式只有这个笨办法帮我避开了绝大多数无效调参。加权马尔可夫链不是银弹但它给ARIMA留了一个吸收非线性残差的出口只要状态边界不泄漏、权重不漂移、修正不叠加就值得一试。希望帮到你。本文还有配套的精品资源点击获取
返回列表