4、平稳性处理进阶:高阶差分、季节性分解与方差稳定化变换
各位同学,咱们接着聊。上一章我们把平稳性的基础概念和单位根检验讲透了。但说实话,实战中遇到的金融时间序列,往往比教科书上的例子要「野」得多。你可能会遇到这样的情况:一阶差分后序列看起来还是不太对劲,或者方差忽大忽小,又或者季节性波动像幽灵一样缠着你的模型。
嗯,这一章我们就来解决这些「硬骨头」。我会把我在处理国债收益率、股指期货高频数据时踩过的坑,都摊开来给你们看。
4.1 高阶差分:什么时候用?怎么用?
先问个问题:为什么有时候一阶差分不够用?
我个人习惯,拿到一个序列后,先做ADF检验。如果p值大于0.05,就做一阶差分。但有一次,我处理某个新兴市场的汇率数据,一阶差分后ADF检验居然还是非平稳。当时我有点懵,后来仔细一看——这个序列的走势像抛物线一样,一阶差分只能消除线性趋势,但二次趋势还在。
这时候就需要高阶差分。
说白了,d阶差分就是连续做d次一阶差分。数学上很简单:
一阶差分:Δy_t = y_t - y_{t-1}
二阶差分:Δ²y_t = Δy_t - Δy_{t-1} = y_t - 2y_{t-1} + y_{t-2}
三阶差分:Δ³y_t = Δ²y_t - Δ²y_{t-1} = ...
但我要提醒你:高阶差分不是万能的。我曾经在项目中为了追求平稳性,一路差分到四阶,结果序列是平稳了,但经济含义全丢了——差分后的数据完全无法解释,模型预测出来的值回译后也面目全非。
实战中,我一般这样判断差分阶数:
- 看时序图:如果趋势是直线型,d=1;如果是抛物线型,试试d=2
- 看ACF图:如果一阶差分后ACF衰减很慢,考虑二阶差分
- 用KPSS检验辅助:KPSS原假设是平稳,如果一阶差分后KPSS仍拒绝,说明需要更高阶
代码实现很简单,Python里直接用numpy.diff或者pandas.DataFrame.diff:
import pandas as pd
import numpy as np
# 假设data是原始序列
data_diff1 = data.diff().dropna() # 一阶差分
data_diff2 = data.diff().diff().dropna() # 二阶差分
# 或者用numpy
data_diff2_np = np.diff(data, n=2)
4.2 季节性分解:STL与X13-ARIMA-SEATS
说到季节性,做量化的人最容易忽略的就是它。你想想看,很多金融数据都有明显的季节性——比如国债回购利率在季末会飙升,股指期货的成交量在交割周会放大。如果不把这些季节性成分剥离出来,模型会学出很多「假规律」。
我个人最常用的两种方法是STL和X13-ARIMA-SEATS。它们各有千秋,我分别说说。
4.2.1 STL(Seasonal-Trend decomposition using LOESS)
STL是我个人最喜欢的分解方法。为什么?因为它够灵活,够鲁棒。
STL的核心思想是用LOESS(局部加权回归)来分别拟合趋势、季节性和残差。它的优点很明显:
- 可以处理任何频率的季节性(日、周、月、年)
- 对异常值不敏感(鲁棒性版本)
- 趋势和季节性可以平滑变化
我在处理某只商品的日度库存数据时,发现数据里有很多「尖刺」——比如某天库存突然暴增。如果用经典分解法,这些尖刺会污染整个季节性成分。但STL的鲁棒版本能很好地处理这种情况。
Python里用statsmodels实现:
from statsmodels.tsa.seasonal import STL
import matplotlib.pyplot as plt
# 假设data是月度数据,周期为12
stl = STL(data, period=12, robust=True)
result = stl.fit()
# 提取成分
trend = result.trend
seasonal = result.seasonal
residual = result.resid
# 可视化
result.plot()
plt.show()
robust=True建议默认开启。我在项目中对比过,开启后残差中的异常值明显减少,后续建模的稳定性提升不少。
4.2.2 X13-ARIMA-SEATS
如果说STL是「轻骑兵」,那X13-ARIMA-SEATS就是「重装甲」。这是美国人口普查局开发的官方季节性调整方法,被全球央行和统计局广泛使用。
它的核心是两阶段:
- X13-ARIMA:先用ARIMA模型对序列进行前向和后向预测,然后做移动平均分解
- SEATS:基于ARIMA模型的信号提取方法,把序列分解为趋势-周期、季节性、不规则成分
说实话,X13-ARIMA-SEATS的参数非常多,我刚开始用的时候也头大。但它的优势在于:能自动检测并修正日历效应(比如春节、复活节这种移动假日),这对处理中国市场的节假日效应特别有用。
Python里可以用statsmodels的X13ARIMAResults,但需要安装X13二进制文件。我一般用pandas-datareader配合x13库:
from statsmodels.tsa.x13 import x13_arima_analysis
# 需要先安装x13as二进制文件
results = x13_arima_analysis(data, maxorder=(2,1), maxdiff=(1,1))
seasonally_adjusted = results.seasadj
为了让你更直观地理解这两种方法的区别,我画了一张对比图:
4.3 方差稳定化变换:Box-Cox与Yeo-Johnson
平稳性不只是均值要稳定,方差也得稳定。你想想看,如果某只股票的波动率在牛市中放大、熊市中缩小,那它的收益率序列虽然均值平稳,但方差明显不平稳——这就是所谓的「异方差性」。
处理方差非平稳,最经典的方法就是Box-Cox变换和它的改进版Yeo-Johnson变换。
4.3.1 Box-Cox变换
Box-Cox变换的公式很简单:
y_t(λ) = (y_t^λ - 1) / λ, 当 λ ≠ 0
y_t(λ) = ln(y_t), 当 λ = 0
其中λ是一个待估计的参数。说白了,Box-Cox就是在找一个「最佳」的幂变换,让变换后的数据方差更稳定、更接近正态分布。
我在处理某只商品的期货价差时,发现价差序列的方差随着价格水平上升而增大。用Box-Cox变换后,方差明显稳定了,后续的GARCH模型拟合效果也好了很多。
但要注意:Box-Cox要求数据必须为正。如果数据有负值或零,就不能直接用。
from scipy.stats import boxcox
import numpy as np
# 假设data是正数序列
data_boxcox, lambda_opt = boxcox(data)
print(f"最优λ值: {lambda_opt}")
# 逆变换(恢复原始尺度)
data_original = (data_boxcox * lambda_opt + 1) ** (1 / lambda_opt)
4.3.2 Yeo-Johnson变换
Yeo-Johnson变换是Box-Cox的「升级版」。它解决了Box-Cox不能处理负值的问题。公式稍微复杂一点,但核心思想是一样的——通过一个参数λ来调整数据的分布形状。
我个人在实战中更常用Yeo-Johnson,因为金融数据经常有负值(比如收益率)。有一次我处理股指期货的5分钟收益率序列,里面有不少负值,用Box-Cox直接报错,换成Yeo-Johnson就完美解决了。
from sklearn.preprocessing import PowerTransformer
# Yeo-Johnson变换
pt = PowerTransformer(method='yeo-johnson')
data_yj = pt.fit_transform(data.values.reshape(-1, 1))
# 查看最优λ
print(f"最优λ值: {pt.lambdas_[0]}")
# 逆变换
data_original = pt.inverse_transform(data_yj)
- 如果数据全为正数:优先用Box-Cox,解释性更强
- 如果数据包含负值或零:用Yeo-Johnson
- 变换后记得做逆变换,否则预测结果无法解释
- 不要盲目变换——如果原始数据方差已经稳定,强行变换反而会破坏结构
4.4 实战中的组合策略
说了这么多,你可能想问:在实际项目中,这些方法到底怎么组合使用?
我一般遵循这样的流程:
- 先看时序图:判断是否有趋势、季节性、方差变化
- 方差稳定化:如果方差随时间变化,先做Box-Cox或Yeo-Johnson
- 季节性分解:如果存在明显季节性,用STL或X13-ARIMA-SEATS提取季节性成分
- 差分处理:对去除季节性后的序列做ADF检验,确定差分阶数
- 再次检验:对最终序列做白噪声检验,确保信息已被充分提取
举个例子,我曾经处理过一组中国10年期国债收益率的日度数据。先做Yeo-Johnson变换(因为收益率有负值),然后用STL分解出季节性(发现季末效应明显),最后做一阶差分。经过这三步处理后,序列的ADF检验p值从0.32降到了0.001以下,后续的ARIMA模型拟合效果非常好。
好了,这一章的内容就到这里。平稳性处理是时间序列预测的「地基工程」,地基打不好,上面盖的模型再漂亮也没用。希望你们在实际项目中,能灵活运用这些方法,少踩我踩过的坑。
公众号:蓝海资料掘金营,微信deep3321