时序分析入门:趋势与平稳性检验的核心原理与Python实战
1. 项目概述:从数据到洞察,时序分析的两大基石
拿到一份时间序列数据,比如过去十年的月度销售额、某城市每日的PM2.5浓度、或者某个服务器每小时的CPU负载,我们第一反应往往是画个折线图看看走势。但作为一名数据分析师或建模者,我们不能只停留在“看”的层面。时序数据分析的核心,在于从看似杂乱无章、上下波动的曲线中,提炼出确定性的规律,并识别出随机性的成分,为后续的预测、控制或决策提供坚实的依据。而这一切的起点,就是两个至关重要的“体检”项目:趋势性检验和平稳性检验。这就像是给数据做一次全面的健康检查,判断它是否有长期的发展方向(趋势),以及其内在的统计特性是否稳定(平稳)。只有明确了这些基本属性,我们才能选择合适的模型“对症下药”,否则,用错了模型,预测结果可能会南辕北辙。
我处理过不少时序项目,踩过最深的坑之一,就是拿到数据后,看到有明显的上升或下降形态,就迫不及待地套用ARIMA模型进行预测,结果模型拟合看似不错,但预测效果一塌糊涂。后来复盘才发现,数据中存在强烈的非线性趋势,而我没有先进行严格的趋势性检验和平稳性检验,直接默认数据是平稳的或者用简单的差分处理,导致模型根本抓不住数据的真实结构。所以,今天我想结合自己的实操经验,把这“两大检验”的来龙去脉、方法选择、操作步骤和避坑要点,系统地梳理一遍。无论你是刚开始接触数学建模的学生,还是需要在工作中分析业务指标的数据分析师,理解并掌握这两项检验,都能让你在时序分析的道路上,避开很多初级错误,建立起可靠的第一道防线。
2. 核心概念解析:趋势与平稳究竟在说什么?
在深入检验方法之前,我们必须把概念本身吃透。很多朋友容易混淆“趋势”和“非平稳”,其实它们既有联系又有区别。
2.1 趋势性:数据长期运动的“方向盘”
趋势,指的是时间序列在长期内表现出的一种持续的、单向的上升或下降的运动方向。它反映了数据在较长时期内的基本发展态势。比如,随着经济发展和消费升级,一个品牌的年销售额可能呈现长期的线性增长趋势;又比如,随着节能技术的普及,某地区的单位GDP能耗可能呈现长期的下降趋势。
从数学上,我们可以将一个时间序列 (Y_t) 分解为几个部分:趋势项 (T_t)、季节项 (S_t)、周期项 (C_t) 和随机误差项 (I_t)。趋势项 (T_t) 就是我们要检验和识别的对象。它可以是:
- 确定性趋势:可以用一个关于时间 (t) 的确定性函数来描述,例如线性趋势 (T_t = a + bt),二次型趋势 (T_t = a + bt + ct^2),或者指数趋势。这种趋势是固定的、非随机的。
- 随机性趋势(单位根过程):这种趋势的表现是,序列的当前值高度依赖于前一时刻的值加上一个随机冲击,并且冲击的影响会永久累积下去。最典型的例子就是随机游走模型:(Y_t = Y_{t-1} + \epsilon_t)。这种序列没有固定的趋势线,但其方差会随时间无限增长,表现出一种“随风飘荡”的长期记忆性。
注意:我们通常所说的趋势性检验,主要针对的是确定性趋势。而随机性趋势本质上是非平稳性的一种极端形式(带单位根),需要通过平稳性检验(如ADF检验)来识别。
2.2 平稳性:统计规律的“定海神针”
平稳性,是时间序列分析中一个基石性的假设。一个平稳的时间序列,其统计特性(如均值、方差、自协方差)不随时间推移而改变。这意味着,序列的行为在不同时间段内是一致的,我们可以用过去的信息来推断未来的规律。
严格来说,平稳性分为两种:
- 严平稳:序列的任意有限维联合概率分布随时间平移不变。这条件非常苛刻,在实际中难以验证和应用。
- 弱平稳(宽平稳):这是我们实际分析中最常使用的概念。它要求:
- 均值恒定:(E(Y_t) = \mu) (对所有 (t) 都成立)
- 方差恒定:(Var(Y_t) = \sigma^2) (对所有 (t) 都成立)
- 自协方差只与时间间隔有关:(Cov(Y_t, Y_{t+k}) = \gamma_k) (只依赖于滞后阶数 (k),与具体时间点 (t) 无关)
为什么平稳性如此重要?因为绝大多数经典的时序模型,如AR(自回归)、MA(移动平均)、ARMA(自回归移动平均)模型,都建立在数据是弱平稳的假设之上。如果数据不平稳,直接套用这些模型,会导致参数估计失效、统计推断(如假设检验)失真,最终使得预测结果变得不可靠。不平稳的数据通常表现为:有明显的趋势、有季节性、或者方差随时间变化(异方差)。
2.3 趋势与平稳的关系:剪不断,理还乱
两者关系可以概括为:存在确定性趋势,必然导致序列非平稳(因为均值随时间变化)。但序列非平稳,不一定是因为有确定性趋势,还可能是随机性趋势(单位根)、季节性或者结构突变等。
举个例子:
- 线性增长的数据:(Y_t = 0.5t + \epsilon_t)。这个序列的均值 (E(Y_t)=0.5t) 随时间增长,所以它是非平稳的,并且存在确定性(线性)趋势。
- 随机游走数据:(Y_t = Y_{t-1} + \epsilon_t)。这个序列的均值虽然可能恒定(如果初始值为0),但其方差 (Var(Y_t) = t\sigma^2) 随时间无限增长,所以它是非平稳的,它表现出的“趋势”是随机性趋势,而非确定性趋势。
因此,我们的分析流程通常是:先检验是否存在单位根(随机性趋势),如果存在,则通过差分使其平稳;对于差分后平稳的序列,再考虑是否含有确定性趋势,并决定是否在模型中引入趋势项。这个顺序很重要,不能乱。
3. 平稳性检验详解:ADF、KPSS与可视化诊断
平稳性检验是我们的首要任务。这里我重点介绍两种最常用、且互为补充的检验方法:ADF检验和KPSS检验。同时,结合图形化观察,形成综合判断。
3.1 ADF检验:揪出“单位根”的主力军
ADF检验的全称是Augmented Dickey-Fuller检验,它是Dickey-Fuller检验的增强版,用于检验序列是否存在单位根(即是否是非平稳的随机性趋势)。
核心原理:它通过估计一个包含滞后项的回归方程来进行检验。方程的基本形式有三种:
- 无常数项无趋势项:(\Delta Y_t = \gamma Y_{t-1} + \sum_{i=1}^{p}\beta_i \Delta Y_{t-i} + \epsilon_t)
- 有常数项无趋势项:(\Delta Y_t = \alpha + \gamma Y_{t-1} + \sum_{i=1}^{p}\beta_i \Delta Y_{t-i} + \epsilon_t)
- 有常数项有趋势项:(\Delta Y_t = \alpha + \delta t + \gamma Y_{t-1} + \sum_{i=1}^{p}\beta_i \Delta Y_{t-i} + \epsilon_t)
其中,(\Delta Y_t) 是序列的一阶差分,(p) 是滞后阶数,用于消除误差项的自相关性。
检验的假设:
- 原假设 (H_0):序列存在单位根(即 (\gamma = 0)),序列非平稳。
- 备择假设 (H_1):序列不存在单位根(即 (\gamma < 0)),序列平稳。
如何操作与解读?在Python中,我们可以使用statsmodels库的adfuller函数。关键步骤和参数选择至关重要。
import pandas as pd import numpy as np from statsmodels.tsa.stattools import adfuller import matplotlib.pyplot as plt # 假设我们有一个时间序列数据 `ts_data` # 1. 先进行可视化观察 ts_data.plot(title='原始时序数据') plt.show() # 2. 执行ADF检验 # 参数 `regression` 可选:'c'(仅常数), 'ct'(常数和趋势), 'ctt'(常数、线性和二次趋势), 'nc'(无) # 通常先尝试包含常数和趋势项的‘ct’,如果趋势不显著,再尝试‘c’。 # 参数 `autolag` 可选:'AIC', 'BIC', 't-stat' 或 None,用于自动选择最佳滞后阶数p,推荐‘AIC’。 result = adfuller(ts_data, regression='ct', autolag='AIC') # 3. 解析结果 print('ADF Statistic: %f' % result[0]) print('p-value: %f' % result[1]) print('Critical Values:') for key, value in result[4].items(): print('\t%s: %.3f' % (key, value)) # 4. 判断准则 # 如果 ADF统计量 小于 某个显著性水平(如1%)下的临界值,或者 p-value 小于 0.05,则拒绝原假设,认为序列平稳。 # 反之,则不能拒绝原假设,认为序列非平稳。 if result[1] < 0.05: print("p-value < 0.05,拒绝原假设,序列平稳。") else: print("p-value >= 0.05,无法拒绝原假设,序列可能存在单位根(非平稳)。")实操心得与避坑指南:
- 滞后阶数
p的选择:这是影响检验结果的关键。autolag='AIC'是常用且相对可靠的方法,它会自动选择一个使AIC信息准则最小的p值。你也可以通过观察序列的自相关图(ACF图),看看自相关系数在多少阶之后截尾或落入置信区间内,来手动设定一个合理的p。 - 回归方程形式的选择:我个人的经验是,对于有明显增长或下降形态的数据,先用
regression='ct'(包含常数和趋势项)。如果检验结果拒绝原假设(平稳),但趋势项t的系数不显著,可以再用regression='c'重新检验一次,看看是否只是带漂移的平稳过程。如果数据围绕0值波动,没有明显趋势,则直接用regression='c'。 - 不要只看p值:一定要结合临界值来看。有时ADF统计量非常接近临界值,p值可能在0.05边缘,这时下结论要谨慎。最好能结合KPSS检验和图形化观察进行综合判断。
- 注意检验的势(Power):ADF检验对于接近单位根但不是单位根的过程(如AR系数为0.95),检验势可能不高,容易犯第二类错误(即实际非平稳但误判为平稳)。因此,单一检验结果不是金科玉律。
3.2 KPSS检验:换个角度,检验“趋势平稳”
KPSS检验(Kwiatkowski-Phillips-Schmidt-Shin检验)的出发点与ADF检验正好相反。它原假设是序列平稳(或趋势平稳),备择假设是存在单位根。
核心原理:KPSS检验将序列分解为确定性趋势、随机游走和平稳误差三部分。通过检验随机游走分量的方差是否为零来构建检验统计量。
检验的假设:
- 原假设 (H_0):序列是水平平稳的(
regression='c')或趋势平稳的(regression='ct')。 - 备择假设 (H_1):序列存在单位根(非平稳)。
如何操作与解读?同样使用statsmodels库。
from statsmodels.tsa.stattools import kpss # 执行KPSS检验 # 参数 `regression`:'c' 检验水平平稳,'ct' 检验趋势平稳。 # 参数 `nlags`:用于计算统计量时使用的滞后阶数,可选‘auto’或指定整数。 result_kpss = kpss(ts_data, regression='ct', nlags='auto') print('KPSS Statistic: %f' % result_kpss[0]) print('p-value: %f' % result_kpss[1]) print('Critical Values:') for key, value in result_kpss[3].items(): print('\t%s: %.3f' % (key, value)) # 判断准则:与ADF检验相反! # 如果 KPSS统计量 大于 某个显著性水平下的临界值,或者 p-value 小于 0.05,则拒绝原假设,认为序列非平稳。 # 反之,则不能拒绝原假设,认为序列是(趋势)平稳的。 if result_kpss[1] < 0.05: print("p-value < 0.05,拒绝原假设,序列非平稳。") else: print("p-value >= 0.05,无法拒绝原假设,序列可能是趋势平稳的。")ADF与KPSS的联合使用策略: 将两者结合使用,可以得出更稳健的结论。通常有以下四种情况:
| ADF检验结果 (H0: 非平稳) | KPSS检验结果 (H0: 平稳) | 综合结论 |
|---|---|---|
| 拒绝 H0 (平稳) | 不拒绝 H0 (平稳) | 序列平稳。这是最理想的情况,两者结论一致。 |
| 不拒绝 H0 (非平稳) | 拒绝 H0 (非平稳) | 序列非平稳。两者结论一致,需要进一步处理(如差分)。 |
| 拒绝 H0 (平稳) | 拒绝 H0 (非平稳) | 结论矛盾。可能序列是趋势平稳过程。即序列围绕一个确定性趋势波动,去除趋势后是平稳的。这时应优先相信KPSS检验(拒绝平稳),认为序列非平稳,并考虑去趋势。 |
| 不拒绝 H0 (非平稳) | 不拒绝 H0 (平稳) | 结论矛盾。可能检验势不足,或者序列具有长记忆性等复杂特征。需要增加样本量,或结合更专业的检验(如PP检验),并重点依赖图形化观察。 |
3.3 图形化观察:最直观的辅助工具
在运行统计检验之前和之后,绘制以下图形是必不可少的步骤:
- 时序图:直接观察序列是否存在明显的上升/下降趋势、季节性波动、方差是否恒定、是否存在异常点或结构突变。
- 自相关图:平稳序列的自相关系数通常会快速衰减至0附近(呈指数衰减或震荡衰减)。而非平稳序列的自相关系数则衰减得非常慢,或者长期保持较高正值。
- 滚动统计量图:计算并绘制滚动均值、滚动标准差。如果序列平稳,这两条线应该大致是水平的直线。如果滚动均值有明显趋势,或滚动标准差变化很大,则提示非平稳。
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf # 绘制自相关图 fig, axes = plt.subplots(2, 1, figsize=(12, 8)) plot_acf(ts_data, lags=40, ax=axes[0]) plot_pacf(ts_data, lags=40, ax=axes[1]) plt.show() # 绘制滚动统计量 rolling_mean = ts_data.rolling(window=12).mean() # 窗口大小根据数据频率调整 rolling_std = ts_data.rolling(window=12).std() plt.figure(figsize=(12,6)) plt.plot(ts_data, label='Original', color='blue', alpha=0.5) plt.plot(rolling_mean, label='Rolling Mean', color='red') plt.plot(rolling_std, label='Rolling Std', color='black') plt.legend() plt.show()4. 趋势性检验详解:从参数化到非参数化方法
当我们通过平稳性检验(特别是ADF检验)确认序列非平稳,且怀疑或观察到存在确定性趋势时,就需要进行趋势性检验。这里的目的是判断趋势是否显著存在,以及可能是什么形式的趋势。
4.1 参数化方法:基于模型拟合的检验
参数化方法需要先假设趋势的具体函数形式(如线性、二次型、指数型),然后通过统计检验判断该趋势项的系数是否显著不为零。
1. 线性趋势检验(t检验)这是最常用、最直观的方法。我们构建一个线性回归模型: [ Y_t = \alpha + \beta t + \epsilon_t ] 其中,(t) 是时间索引(如1, 2, 3, ...)。检验的原假设 (H_0: \beta = 0),备择假设 (H_1: \beta \neq 0)。我们可以用普通最小二乘法估计模型,然后对系数 (\beta) 进行t检验。如果p值小于显著性水平(如0.05),则拒绝原假设,认为存在显著的线性趋势。
import statsmodels.api as sm # 为时间序列添加时间趋势项 ts_data_df = pd.DataFrame({'y': ts_data}) ts_data_df['t'] = np.arange(1, len(ts_data_df) + 1) # 创建时间索引 # 构建OLS模型(添加常数项) X = sm.add_constant(ts_data_df['t']) # X包含常数项和时间趋势项 model = sm.OLS(ts_data_df['y'], X) results = model.fit() # 查看回归摘要,重点关注时间趋势项‘t’的系数、t统计量和p值 print(results.summary()) # 也可以直接提取p值进行判断 p_value_trend = results.pvalues['t'] if p_value_trend < 0.05: print(f"线性趋势项的p值为{p_value_trend:.4f},小于0.05,存在显著的线性趋势。") else: print(f"线性趋势项的p值为{p_value_trend:.4f},无法拒绝‘无线性趋势’的原假设。")2. 高阶多项式趋势检验(F检验)如果时序图显示趋势可能是曲线形的,可以尝试拟合多项式趋势: [ Y_t = \alpha + \beta_1 t + \beta_2 t^2 + ... + \beta_k t^k + \epsilon_t ] 我们可以通过F检验来比较包含趋势项的模型和不包含趋势项的模型(只有常数项),判断增加的趋势项是否显著改善了模型拟合。在statsmodels的回归摘要中,会给出整个模型的F检验p值,但更精确的是做模型比较的似然比检验或直接查看高阶项系数的t检验。
实操心得:
- 警惕伪回归:如果序列本身存在单位根(非平稳),直接对其做关于时间的回归,即使趋势看起来显著,也可能产生“伪回归”问题,即t统计量失真。因此,进行参数化趋势检验的前提,最好是序列已经是趋势平稳的,或者已经通过差分消除了单位根。一种常见的做法是:先对原始序列做ADF检验,如果非平稳,则进行一阶差分;对差分后的平稳序列,再检验其是否含有常数项(即漂移),这等价于检验原序列是否存在线性趋势。
- 趋势形式的预判:图形观察很重要。线性趋势是一条直线,二次趋势是一个抛物线。如果拿不准,可以分别拟合线性、二次模型,然后比较它们的调整后R方或AIC/BIC,选择更优的模型。AIC/BIC越小越好。
4.2 非参数化方法:Mann-Kendall趋势检验
当数据不满足正态分布假设,或者存在异常值、数据是序数尺度时,参数化方法可能失效。这时,Mann-Kendall趋势检验这种非参数方法就显示出其优势。它不假设趋势的具体形式,也不要求数据服从特定分布,稳健性很强。
核心原理:M-K检验通过比较序列中所有可能的数据对 ((Y_i, Y_j), i<j),计算其中 (Y_j > Y_i) 的对数减去 (Y_j < Y_i) 的对数,得到一个标准化统计量 (Z)。该统计量服从标准正态分布。
- 原假设 (H_0):序列没有单调趋势。
- 备择假设 (H_1):序列存在单调上升或下降趋势。
如何操作与解读?可以使用pymannkendall这个方便的库。
# 安装:pip install pymannkendall import pymannkendall as mk # 执行Mann-Kendall趋势检验 result_mk = mk.original_test(ts_data) print(f"趋势类型: {result_mk.trend}") print(f"p值: {result_mk.p}") print(f"统计量S: {result_mk.s}") print(f"斜率(Sen‘s Slope): {result_mk.slope}") # 趋势的估计斜率,非常有用! print(f"截距: {result_mk.intercept}") # 判断 if result_mk.p < 0.05: print(f"p值小于0.05,存在显著的{result_mk.trend}趋势。") print(f"估计的Sen斜率为{result_mk.slope:.4f},表示平均每单位时间序列变化{result_mk.slope:.4f}。") else: print("p值大于等于0.05,无法拒绝‘无单调趋势’的原假设。")M-K检验的独特优势:
- Sen‘s Slope(森斜率):这是M-K检验附带的一个非常有用的估计量。它表示趋势的中位数斜率,对异常值不敏感,能更稳健地反映趋势的强度。例如,Sen‘s Slope = 0.5,意味着序列值平均每个时间单位增加0.5。
- 无需分布假设:适用于各种类型的数据。
- 对单调趋势敏感:无论是线性还是非线性的单调趋势,都能检测出来。
注意事项:
- M-K检验只能检测单调趋势(一直上升或一直下降)。对于先升后降或更复杂的非单调趋势,它可能无法有效识别,或者会得出“无趋势”的结论。
- 序列存在自相关性会严重影响M-K检验的结果,使其更容易拒绝原假设(即更容易误判为有趋势)。因此,在应用前最好先检查序列的自相关性。如果存在显著自相关,需要使用改进的M-K检验方法(如
pymannkendall库中的hamed_rao_modification_test)。
5. 综合实战流程与案例拆解
理论说再多,不如一个完整的案例来得实在。假设我们拿到某电商平台2010年至2023年的月度GMV数据,我们的目标是分析其长期趋势,并为后续的预测模型做准备。
5.1 步骤一:数据导入与初步观察
import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from statsmodels.tsa.stattools import adfuller, kpss import statsmodels.api as sm import pymannkendall as mk import warnings warnings.filterwarnings('ignore') # 1. 加载数据(假设数据已处理好为时间索引的Series) # df = pd.read_csv('gmv_monthly.csv', parse_dates=['date'], index_col='date') # ts = df['gmv'] # 这里我们模拟生成一组带有线性增长趋势和季节性的非平稳数据 np.random.seed(42) n_periods = 168 # 14年 * 12个月 t = np.arange(n_periods) trend = 0.8 * t # 线性趋势 seasonal = 10 * np.sin(2 * np.pi * t / 12) # 年度季节性 noise = np.random.normal(0, 5, n_periods) # 随机噪声 ts = pd.Series(trend + seasonal + noise, index=pd.date_range(start='2010-01', periods=n_periods, freq='MS')) ts.name = 'Simulated_GMV' # 2. 绘制时序图 plt.figure(figsize=(14, 6)) plt.plot(ts) plt.title('Simulated Monthly GMV Data (2010-2023)') plt.xlabel('Date') plt.ylabel('GMV') plt.grid(True) plt.show()通过时序图,我们能清晰地看到数据具有强烈的上升趋势和规律的年度周期性波动。初步判断,该序列极有可能是非平稳的。
5.2 步骤二:平稳性检验综合诊断
我们同时运用ADF检验、KPSS检验和图形化工具。
# 1. ADF检验 (包含常数和趋势项) print("="*50) print("ADF检验结果:") adf_result = adfuller(ts, regression='ct', autolag='AIC') print(f'ADF Statistic: {adf_result[0]:.4f}') print(f'p-value: {adf_result[1]:.4f}') print('Critical Values:') for key, val in adf_result[4].items(): print(f'\t{key}: {val:.3f}') if adf_result[1] < 0.05: print("结论:拒绝原假设,序列平稳。") else: print("结论:无法拒绝原假设,序列可能存在单位根(非平稳)。") # 2. KPSS检验 (检验趋势平稳) print("\n" + "="*50) print("KPSS检验结果:") kpss_result = kpss(ts, regression='ct', nlags='auto') print(f'KPSS Statistic: {kpss_result[0]:.4f}') print(f'p-value: {kpss_result[1]:.4f}') print('Critical Values:') for key, val in kpss_result[3].items(): print(f'\t{key}: {val:.3f}') if kpss_result[1] < 0.05: print("结论:拒绝原假设,序列非平稳。") else: print("结论:无法拒绝原假设,序列可能是趋势平稳的。") # 3. 绘制自相关图 from statsmodels.graphics.tsaplots import plot_acf fig, ax = plt.subplots(figsize=(10,4)) plot_acf(ts, lags=40, ax=ax) plt.title('Autocorrelation Function (ACF) of Original Series') plt.show() # 4. 绘制滚动统计量 rolling_window = 12 rolling_mean = ts.rolling(window=rolling_window).mean() rolling_std = ts.rolling(window=rolling_window).std() fig, axes = plt.subplots(2, 1, figsize=(14, 8)) axes[0].plot(ts, label='Original', alpha=0.7) axes[0].plot(rolling_mean, label=f'{rolling_window}-Month Rolling Mean', color='red', linewidth=2) axes[0].set_title('Original Series and Rolling Mean') axes[0].legend() axes[0].grid(True) axes[1].plot(rolling_std, label=f'{rolling_window}-Month Rolling Std', color='darkgreen', linewidth=2) axes[1].set_title('Rolling Standard Deviation') axes[1].legend() axes[1].grid(True) plt.tight_layout() plt.show()结果分析与决策:
- ADF检验:p值很可能远大于0.05(例如0.9以上),无法拒绝“存在单位根”的原假设,提示序列非平稳。
- KPSS检验:p值很可能小于0.05,拒绝“序列趋势平稳”的原假设,同样提示序列非平稳。
- 自相关图:ACF衰减非常缓慢,在很大滞后阶数下仍显著不为0,这是非平稳序列的典型特征。
- 滚动统计量:滚动均值呈现明显的上升趋势,滚动标准差虽有一定波动但相对稳定。
综合结论:模拟的GMV序列是非平稳的。其非平稳性主要来源于确定性线性趋势和季节性。由于ADF和KPSS结论一致,我们不需要纠结于“趋势平稳”的复杂情况,直接进入下一步:差分处理。
5.3 步骤三:差分处理与再次检验
对于同时含有趋势和季节性的序列,我们通常先进行一阶季节性差分(消除季节性),再观察是否需要进行一阶常规差分(消除趋势)。
# 1. 进行一阶季节性差分(周期为12个月) ts_diff_seasonal = ts.diff(periods=12).dropna() # 2. 对季节性差分后的序列再次进行平稳性检验(ADF) print("="*50) print("对季节性差分后序列进行ADF检验:") adf_result_diff_s = adfuller(ts_diff_seasonal, regression='c', autolag='AIC') # 差分后可能无趋势,用‘c’ print(f'ADF Statistic: {adf_result_diff_s[0]:.4f}') print(f'p-value: {adf_result_diff_s[1]:.4f}') if adf_result_diff_s[1] < 0.05: print("结论:季节性差分后序列已平稳。") else: print("结论:季节性差分后序列仍非平稳,需要进一步做一阶常规差分。") ts_diff_final = ts_diff_seasonal.diff().dropna() print("\n对季节性差分序列再做一阶常规差分:") adf_result_final = adfuller(ts_diff_final, regression='c', autolag='AIC') print(f'ADF Statistic: {adf_result_final[0]:.4f}') print(f'p-value: {adf_result_final[1]:.4f}') if adf_result_final[1] < 0.05: print("结论:经过‘季节性差分+常规差分’后序列平稳。") final_stationary_series = ts_diff_final else: print("警告:多次差分后仍不平稳,需检查数据或考虑其他模型(如结构突变)。") final_stationary_series = ts_diff_final # 暂用 else: final_stationary_series = ts_diff_seasonal # 3. 绘制处理后的序列 plt.figure(figsize=(14,4)) plt.plot(final_stationary_series) plt.title('Stationary Series After Differencing') plt.grid(True) plt.show()在这个模拟案例中,由于我们构造的数据同时包含线性趋势和强季节性,很可能需要diff(12)后再diff(1)才能达到平稳。最终我们得到一个围绕0值波动的平稳序列。
5.4 步骤四:趋势性检验(在平稳序列或原序列上)
现在,我们对已经平稳的序列(或如果我们想直接检验原序列的趋势),进行趋势性检验。
方法A:对原序列进行Mann-Kendall检验(非参数,稳健)
print("="*50) print("对原始序列进行Mann-Kendall趋势检验:") result_mk_original = mk.original_test(ts) print(f"趋势方向: {result_mk_original.trend}") print(f"p值: {result_mk_original.p:.6f}") print(f"Sen‘s Slope: {result_mk_original.slope:.4f}") if result_mk_original.p < 0.05: print(f"存在显著的{result_mk_original.trend}趋势,平均每月变化{result_mk_original.slope:.4f}单位。")这能直接告诉我们原始数据是否存在单调上升趋势及其强度。
方法B:对差分后平稳序列检验“漂移”对于经过差分(比如一阶差分ts.diff().dropna())后平稳的序列,我们可以检验其均值(即漂移项)是否为0。如果差分后序列的均值显著不为0,则意味着原序列存在确定性线性趋势。
# 假设我们只做了一阶常规差分就平稳了 ts_diff_1 = ts.diff().dropna() # 检验 ts_diff_1 的均值是否为0 (使用单样本t检验) from scipy import stats t_stat, p_val = stats.ttest_1samp(ts_diff_1, 0) print(f"\n一阶差分序列的均值t检验: t={t_stat:.4f}, p={p_val:.4f}") if p_val < 0.05: print("差分序列均值显著不为0,表明原序列存在确定性线性趋势。") print(f"原序列的线性趋势斜率约为: {ts_diff_1.mean():.4f}") # 差分序列的均值近似等于原序列的趋势斜率方法C:在平稳序列上拟合带时间趋势的模型如果我们最终用于建模的序列是平稳的(比如final_stationary_series),我们可以在ARMA等模型中加入常数项,如果常数项显著,也暗示原序列可能存在某种趋势(需结合差分次数解释)。
5.5 步骤五:结果整合与建模指导
通过以上流程,我们得出关于模拟GMV数据的完整“体检报告”:
- 平稳性:原始序列非平稳。需要经过一阶季节性差分和一阶常规差分才能达到平稳。
- 趋势性:
- Mann-Kendall检验直接确认原始序列存在显著的单调上升趋势。
- 对一阶差分序列的均值检验也证实了确定性线性趋势的存在,并估计了趋势斜率。
- 参数化线性回归(需在平稳序列或考虑协整关系下谨慎进行)也能得到显著的线性趋势系数。
对后续建模的指导意义:
- 模型选择:由于数据同时具有趋势和季节性,且经过差分后平稳,最适合的模型族是季节性ARIMA,即SARIMA模型。其阶数表示为
SARIMA(p, d, q)(P, D, Q, s),其中:d=1:因为我们进行了一阶常规差分。D=1:因为我们进行了一阶季节性差分(s=12)。p, q, P, Q需要通过ACF/PACF图或网格搜索来确定。
- 建模序列:我们将使用
final_stationary_series这个平稳序列来识别和拟合ARMA部分。 - 预测还原:使用拟合的SARIMA模型进行预测后,需要将差分和季节差分的操作逆向进行,才能得到原始GMV尺度上的预测值。
6. 常见问题、陷阱与排查技巧实录
在实际操作中,你会遇到各种各样的问题。下面是我总结的一些典型坑点和解决思路。
6.1 检验结果矛盾或不确定怎么办?
这是最常见的问题。比如ADF检验p值=0.06(勉强不拒绝非平稳),KPSS检验p值=0.04(拒绝平稳)。
- 首先,信任图形证据:滚动均值/标准差图、ACF图比单一的p值更可靠。如果图形强烈显示有趋势或方差变化,那就应该当作非平稳处理。
- 其次,考虑检验的势和样本量:样本量小的时候,检验功效低,容易得出“无法拒绝”的模糊结论。尽可能增加数据量。
- 采取保守策略:当结果模棱两可时,优先认为序列是非平稳的,并进行差分。因为对平稳序列做差分(称为过差分)虽然会引入额外的移动平均结构,但通常比用平稳模型去拟合非平稳序列(欠差分)带来的危害要小。过差分后的序列ACF图可能会出现明显的负相关截尾,可以帮助我们识别。
- 尝试不同的检验设定:调整ADF检验的滞后阶数(
maxlag),或者使用其他单位根检验如PP检验,看结论是否一致。
6.2 数据存在季节性,如何检验趋势?
季节性本身就会导致序列非平稳(周期性均值变化)。直接对强季节性数据做趋势检验或ADF检验,结果会受到严重干扰。
- 正确做法:先进行季节性差分,消除季节性影响后,再对差分后的序列进行趋势性检验和平稳性检验。或者,使用能够同时处理趋势和季节性的模型(如TBATS、Prophet)进行分解,直接观察趋势分量。
6.3 趋势不是线性的怎么办?
Mann-Kendall检验只能检测单调趋势。如果趋势是二次的(先升后降)或更复杂:
- 分段检验:如果知道结构突变点,可以分段进行M-K检验或线性拟合。
- 使用非线性拟合:尝试用二次、指数、对数函数拟合,并通过比较R方、AIC等指标判断哪种形式更好。同时,可以检验非线性项(如t²)的系数是否显著。
- 数据变换:有时对数据取对数,可以将指数增长趋势转化为线性趋势,便于分析和检验。
6.4 序列存在自相关,对M-K检验有何影响?
如前所述,正自相关会严重增大M-K检验犯第一类错误(假阳性)的概率。如果你的数据是月度的经济或环境数据,自相关很常见。
- 解决方案:使用考虑了自相关的改进版M-K检验,如预白化处理后再检验,或使用
pymannkendall中的hamed_rao_modification_test等函数。
6.5 差分后序列均值为0,但ADF检验仍说不平稳?
这可能意味着序列存在高阶单位根(即需要多次差分)。例如,随机游走的一阶差分是平稳的,但随机游走的一阶差分序列再差分一次,可能就过差分
