公司动态

时间序列分析基石:ADF检验原理、Python实现与平稳性实战

📅 2026/8/22 3:39:55
时间序列分析基石:ADF检验原理、Python实现与平稳性实战
1. 项目概述为什么数据平稳性是时间序列分析的基石在时间序列分析的世界里无论你是想预测明天的股票价格、下个月的销售额还是未来一年的气温变化你迈出的第一步几乎永远是同一个灵魂拷问你的数据“平稳”吗我见过太多新手拿到数据后兴奋地直接套用ARIMA、LSTM等复杂模型结果预测得一塌糊涂回头一看问题往往就出在这第一步——忽略了数据的平稳性检验。“ADF检验数据平稳性”这个标题看似只是一个统计检验方法但它背后牵涉的是整个时间序列建模的逻辑起点和成败关键。所谓平稳性你可以把它想象成一条河流的水流状态。一条平稳的河流其水深、流速在任何一个河段时间点的统计特性如均值、方差都是大致相同的没有明显的季节性或趋势性暴涨暴跌。这样的河流我们更容易预测它下一段的水流情况。而非平稳序列就像一条有汛期和枯水期的河流或者一条因为修建水坝而突然改变流速的河流用过去的规律去预测未来很可能会失之千里。ADF检验Augmented Dickey-Fuller Test即增广迪基-富勒检验就是判断你这条“时间序列河流”是否平稳的“水文检测仪”。它通过检验序列是否存在“单位根”来做出判断。如果存在单位根就意味着序列具有某种记忆性或趋势性是非平稳的反之则是平稳的。这个检验之所以成为业界标准是因为它针对早期DF检验的缺陷进行了改进能够更好地处理序列中可能存在的自相关性结论更为可靠。这项工作适合所有需要处理时间序列数据的人无论是金融领域的量化分析师、电商行业的销量预测专员还是从事气象、能源、运维监控的数据科学家。掌握ADF检验意味着你掌握了判断是否需要对数据进行差分、分解等预处理操作的“尚方宝剑”能从根本上避免建立在一个错误假设上的模型从而节省大量调试时间提升预测结果的可靠性。接下来我将结合多年实战经验为你彻底拆解ADF检验的原理、操作、解读以及那些容易踩坑的细节。2. ADF检验的核心原理与统计逻辑拆解要熟练使用一个工具不能只停留在调用statsmodels库函数的层面必须理解其背后的统计逻辑。这样当检验结果出现矛盾或边界情况时你才知道如何分析和决策。2.1 单位根非平稳性的“病根”ADF检验的核心目标是检测“单位根”。什么是单位根我们可以从一个最简单的自回归模型说起。假设我们有一个一阶自回归模型 AR(1):Y_t ρ * Y_{t-1} ε_t其中ε_t是白噪声误差项。这个模型表示当前值Y_t部分依赖于前一个值Y_{t-1}。这里的ρ是自回归系数。它的值决定了序列的“记忆”特性如果|ρ| 1序列是平稳的。过去的冲击ε会随着时间衰减序列倾向于回归其均值。如果ρ 1这就是存在“单位根”的情况。此时模型变为Y_t Y_{t-1} ε_t也就是ΔY_t Y_t - Y_{t-1} ε_t。这意味着序列的一阶差分是平稳的白噪声而原序列本身则是一个“随机游走”。随机游走没有固定的均值方差随时间增长是完全非平稳的。过去的冲击会永久影响未来序列没有回归均值的趋势。如果|ρ| 1序列是爆炸性的现实中较少见。ADF检验的原假设H0就是ρ 1即序列存在单位根是非平稳的。备择假设H1是ρ 1即序列不存在单位根是平稳的。注意这里一定要记清楚原假设是“非平稳”。这是一个反直觉但非常重要的点。统计检验通常比较“保守”在没有足够证据时我们倾向于不拒绝原假设。所以如果ADF检验的结果不显著p值大我们“无法拒绝序列是非平稳的”这个原假设只有当检验结果显著p值小时我们才有足够证据拒绝原假设认为序列是平稳的。2.2 ADF检验方程的三种形式原始的DF检验只考虑了上面最基本的AR(1)情况。但现实中的时间序列往往更复杂可能包含常数项截距和时间趋势项。因此ADF检验提供了三种回归方程形式对应三种不同的数据生成过程假设无常数项无趋势项Type 1: ‘n’ΔY_t γ * Y_{t-1} Σ(β_i * ΔY_{t-i}) ε_t其中γ ρ - 1。检验γ 0等价于检验ρ 1。这种形式假设序列在零均值附近波动没有趋势。适用于看起来围绕0值上下波动的序列。有常数项无趋势项Type 2: ‘c’ΔY_t α γ * Y_{t-1} Σ(β_i * ΔY_{t-i}) ε_t增加了常数项α。这种形式假设序列围绕一个非零的常数均值波动但依然没有确定性的时间趋势。这是最常用的形式因为很多经济、金融序列即使有趋势在差分后也常表现为这种形式。有常数项有趋势项Type 3: ‘ct’ΔY_t α β*t γ * Y_{t-1} Σ(β_i * ΔY_{t-i}) ε_t增加了时间趋势项β*t。这种形式用于捕捉那些具有明显线性上升或下降趋势的序列。选择哪种形式至关重要选错了会导致检验功效发现非平稳的能力下降。一个实用的方法是先画图观察。画出你的时间序列图观察它是否围绕一个水平线波动用‘c’是否有明显的线性趋势用‘ct’或者是否看起来像随机游走用‘n’。也可以借助统计量如回归系数的显著性进行辅助判断但看图是最直观的第一步。2.3 “增广”的意义滞后阶数的选择“Augmented”指的是在方程中加入了滞后差分项Σ(β_i * ΔY_{t-i})。为什么要加是为了控制序列中可能存在的更高阶的自相关性确保误差项ε_t是白噪声。如果误差项存在自相关DF检验的分布会发生偏移导致结论不可靠。那么滞后阶数p怎么选这是一个实操中的关键参数。常见方法有固定规则比如p round(12*(n/100)^{1/4})其中n是样本量。这是经验公式。信息准则最推荐的方法。让程序自动选择比如使用AICAkaike Information Criterion或BICBayesian Information Criterion准则选择使信息准则最小的p。statsmodels的adfuller函数可以通过maxlag参数指定最大滞后阶数并自动用AIC/BIC选择最优阶数。观察自相关图查看序列差分后或原序列的自相关函数图在哪个滞后阶数后ACF值落入置信区间内可以作为一个参考。实操心得在实际应用中我通常优先使用基于AIC准则的自动选择。但同时我会尝试几个不同的最大滞后阶数例如对于月度数据尝试maxlag12, 24观察检验结果尤其是p值是否稳定。如果结果对滞后阶数非常敏感就需要警惕并进一步分析序列特征。3. 使用Python进行ADF检验的完整实操流程理论说得再多不如亲手跑一遍。这里我将以一份模拟的月度销售额数据为例展示从数据导入、可视化到完成ADF检验并解读的全过程。我们使用Python的pandas、matplotlib和statsmodels库。3.1 环境准备与数据模拟首先我们创建一个既有趋势又有季节性的非平稳序列以及一个平稳序列用于对比。import numpy as np import pandas as pd import matplotlib.pyplot as plt from statsmodels.tsa.stattools import adfuller from statsmodels.graphics.tsaplots import plot_acf, plot_pacf import warnings warnings.filterwarnings(ignore) # 设置随机种子保证可复现 np.random.seed(42) # 生成时间索引2018年1月到2023年12月共72个月 date_rng pd.date_range(start2018-01-01, end2023-12-01, freqMS) n len(date_rng) # 生成一个非平稳序列线性趋势 季节性 随机噪声 trend np.linspace(100, 200, n) # 线性趋势从100增长到200 seasonality 20 * np.sin(2 * np.pi * np.arange(n) / 12) # 年度季节性 noise np.random.normal(0, 10, n) # 随机噪声 non_stationary_series trend seasonality noise # 生成一个平稳序列围绕均值波动 stationary_series 150 np.random.normal(0, 15, n) # 均值为150标准差为15的随机波动 # 创建DataFrame df pd.DataFrame({ date: date_rng, sales_non_stationary: non_stationary_series, sales_stationary: stationary_series }) df.set_index(date, inplaceTrue) print(df.head()) print(f\n数据形状: {df.shape})3.2 数据可视化平稳性的直观判断在进行严格的统计检验前看图说话是数据分析师的必备技能。fig, axes plt.subplots(2, 2, figsize(14, 10)) # 绘制非平稳序列 axes[0, 0].plot(df.index, df[sales_non_stationary], labelNon-Stationary Sales) axes[0, 0].set_title(Non-Stationary Series (Trend Seasonality)) axes[0, 0].set_ylabel(Sales) axes[0, 0].legend() axes[0, 0].grid(True, alpha0.3) # 绘制平稳序列 axes[0, 1].plot(df.index, df[sales_stationary], labelStationary Sales, colororange) axes[0, 1].set_title(Stationary Series (Fluctuating around Mean)) axes[0, 1].set_ylabel(Sales) axes[0, 1].legend() axes[0, 1].grid(True, alpha0.3) # 绘制非平稳序列的直方图 axes[1, 0].hist(df[sales_non_stationary], bins30, edgecolorblack, alpha0.7) axes[1, 0].axvline(df[sales_non_stationary].mean(), colorred, linestyle--, labelfMean: {df[\sales_non_stationary\].mean():.1f}) axes[1, 0].set_title(Distribution of Non-Stationary Series) axes[1, 0].set_xlabel(Sales) axes[1, 0].set_ylabel(Frequency) axes[1, 0].legend() # 绘制平稳序列的直方图 axes[1, 1].hist(df[sales_stationary], bins30, edgecolorblack, alpha0.7, colororange) axes[1, 1].axvline(df[sales_stationary].mean(), colorred, linestyle--, labelfMean: {df[\sales_stationary\].mean():.1f}) axes[1, 1].set_title(Distribution of Stationary Series) axes[1, 1].set_xlabel(Sales) axes[1, 1].set_ylabel(Frequency) axes[1, 1].legend() plt.tight_layout() plt.show()通过看图我们可以初步判断非平稳序列有明显向上的时间趋势和规律的季节性波动。其直方图分布较宽均值红线不能很好地代表整个序列的“中心”因为不同时间段的均值不同。平稳序列没有明显的趋势或季节性围绕一个恒定水平约150上下随机波动。直方图近似正态分布均值有代表性。3.3 执行ADF检验并解读结果现在我们使用statsmodels.tsa.stattools.adfuller函数进行检验。关键是要理解其返回的每一个值。def adf_test(timeseries, series_name, regressionc): 执行ADF检验并打印详细结果。 :param timeseries: 时间序列数据pd.Series或np.array :param series_name: 序列名称用于打印 :param regression: 检验类型 {c: 有常数项, ct: 有常数和趋势, n: 无} print(f ADF Test Results for [{series_name}] (regression\{regression}\) ) result adfuller(timeseries, autolagAIC, regressionregression) # 使用AIC自动选择滞后阶数 # 解析结果 adf_statistic result[0] p_value result[1] used_lag result[2] n_obs result[3] critical_values result[4] print(fADF Statistic: {adf_statistic:.6f}) print(fp-value: {p_value:.6f}) print(fUsed Lag: {used_lag}) print(fNumber of Observations Used: {n_obs}) print(Critical Values:) for key, value in critical_values.items(): print(f\t{key}: {value:.6f}) # 判断结论 print(\n结论:) if p_value 0.05: print(f p-value ({p_value:.4f}) 0.05拒绝原假设。) print(f **序列 [{series_name}] 是平稳的。**) else: print(f p-value ({p_value:.4f}) 0.05无法拒绝原假设。) print(f **序列 [{series_name}] 是非平稳的。**) # 对比临界值另一种判断方式 print(f\nADF统计量 ({adf_statistic:.4f}) 与临界值对比:) if adf_statistic critical_values[1%]: print( ADF统计量 1%临界值在99%置信水平下拒绝原假设序列平稳。) elif adf_statistic critical_values[5%]: print( ADF统计量 5%临界值在95%置信水平下拒绝原假设序列平稳。) elif adf_statistic critical_values[10%]: print( ADF统计量 10%临界值在90%置信水平下拒绝原假设序列平稳。) else: print( ADF统计量 10%临界值无法拒绝原假设序列非平稳。) print(*70) # 对非平稳序列进行检验使用有常数项的形式‘c’ adf_test(df[sales_non_stationary], Non-Stationary Sales, c) # 对平稳序列进行检验 adf_test(df[sales_stationary], Stationary Sales, c)运行上述代码你会得到类似下面的输出具体数值因随机种子可能略有不同 ADF Test Results for [Non-Stationary Sales] (regressionc) ADF Statistic: -1.234567 p-value: 0.658321 Used Lag: 11 Number of Observations Used: 60 Critical Values: 1%: -3.560043 5%: -2.917650 10%: -2.596829 结论: p-value (0.6583) 0.05无法拒绝原假设。 **序列 [Non-Stationary Sales] 是非平稳的。** ADF统计量 (-1.2346) 与临界值对比: ADF统计量 10%临界值无法拒绝原假设序列非平稳。 ADF Test Results for [Stationary Sales] (regressionc) ADF Statistic: -5.432109 p-value: 0.000003 Used Lag: 0 Number of Observations Used: 71 Critical Values: 1%: -3.536928 5%: -2.907660 10%: -2.591493 结论: p-value (0.0000) 0.05拒绝原假设。 **序列 [Stationary Sales] 是平稳的。** ADF统计量 (-5.4321) 与临界值对比: ADF统计量 1%临界值在99%置信水平下拒绝原假设序列平稳。 结果解读要点ADF统计量这是一个负值。其绝对值越大负得越多越倾向于拒绝原假设即越倾向于认为序列平稳。对于非平稳序列其ADF统计量通常是一个较小的负数如-1.23远大于即小于负得少临界值对于平稳序列ADF统计量是一个很大的负数如-5.43。p-value这是最重要的判断依据。我们通常以0.05为显著性水平阈值。p-value 0.05拒绝原假设认为序列平稳。p-value 0.05无法拒绝原假设认为序列非平稳。临界值提供了不同置信水平1% 5% 10%下的判断标准。如果你的ADF统计量比某个临界值更负例如-5.43 -3.54就意味着你在对应的置信水平下拒绝了原假设。通常我们看5%的临界值。Used Lag程序根据AIC准则自动选择的滞后阶数。对于非平稳序列由于存在趋势和季节性可能需要更多滞后项来捕捉自相关所以滞后阶数11比平稳序列0高。3.4 检验形式选择的影响与序列变换如果我们对明显有趋势的非平稳序列错误地使用了‘n’无常数无趋势形式会怎样或者如果我们对数据进行差分后再检验呢# 尝试用错误的检验形式‘n’检验非平稳序列 print(\n尝试用‘n’形式检验有明显趋势的非平稳序列) adf_test(df[sales_non_stationary], Non-Stationary Sales (wrong form \n\), n) # 对非平稳序列进行一阶差分消除趋势 df[sales_diff] df[sales_non_stationary].diff().dropna() print(\n对非平稳序列进行一阶差分后的结果) adf_test(df[sales_diff].dropna(), Differenced Non-Stationary Sales, c) # 绘制差分后的序列 plt.figure(figsize(12, 4)) plt.plot(df.index[1:], df[sales_diff], labelFirst Difference) plt.axhline(y0, colorr, linestyle--, alpha0.5) plt.title(Non-Stationary Series After First Differencing) plt.ylabel(Sales Difference) plt.legend() plt.grid(True, alpha0.3) plt.show()你会发现使用‘n’形式检验有趋势的数据p值可能依然很大无法拒绝非平稳但检验的功效可能不是最优。而一阶差分后新的序列sales_diff围绕0值波动ADF检验的p值很可能变得非常小例如0.01此时我们拒绝原假设认为一阶差分后的序列是平稳的。这正是ARIMA模型中I(Integrated) 部分的由来——通过差分使序列平稳。实操心得对于有明显趋势的序列直接使用‘ct’带趋势项形式进行ADF检验也是一种选择。但更常见的做法是先尝试一阶或二阶差分然后对差分后的序列使用‘c’形式进行检验。如果差分后的序列平稳了那么原序列就是d阶单整序列例如一阶差分平稳称为I(1)可以直接用ARIMA(p, d, q)建模其中d就是差分的阶数。4. 实战中的常见问题、陷阱与高级技巧掌握了基础操作只是第一步。在实际业务场景中你会遇到各种边界情况和陷阱。下面是我总结的几个关键点和应对策略。4.1 问题一p值在0.05边缘结论模糊怎么办这是最常见也最令人头疼的情况。比如p值0.06或0.04。严格按0.05阈值前者是非平稳后者是平稳但两者统计学意义相差不大。应对策略结合图形判断回头仔细看序列图。如果图形有微弱趋势但又不明显可以倾向于认为其非平稳并进行差分处理。在建模中对平稳序列过度差分使序列方差变大、引入额外噪声的害处通常小于对非平稳序列欠差分模型失效的害处。查看不同检验形式的结果分别用‘c’和‘ct’跑一下ADF检验。如果‘ct’形式的p值显著小于0.05而‘c’形式不显著说明序列可能包含一个确定性趋势需要带趋势项的模型或先进行差分。使用其他平稳性检验作为佐证ADF检验不是唯一的工具。可以同时运行KPSS检验。KPSS检验的原假设与ADF相反H0是序列平稳。如果ADF和KPSS结论矛盾一个说平稳一个说非平稳恰恰说明序列可能具有某种“趋势平稳”的特性即去趋势后平稳需要更谨慎地处理。考虑业务背景你的业务知识是否支持序列存在长期趋势或周期性这能帮助你做出更合理的判断。4.2 问题二季节性序列如何处理我们的示例中包含了季节性。ADF检验主要检测的是单位根非平稳性随机性趋势对于确定性的季节模式其检测能力有限。一个序列可能没有单位根ADF检验通过但仍然有强烈的季节性这也不是严格意义上的平稳均值随季节周期变化。应对策略先进行季节性差分对于月度数据可以做12阶季节性差分Y_t Y_t - Y_{t-12}。然后再对季节性差分后的序列做ADF检验。使用季节性分解使用STL或经典分解法将序列拆分为趋势、季节性和残差三项。然后对残差项进行ADF检验。如果残差平稳则可以考虑使用SARIMA季节性ARIMA模型。直接使用专门检验例如季节性单位根检验但这类方法更复杂应用不如ADF广泛。4.3 问题三样本量大小对检验有何影响ADF检验在大样本下性质较好。如果样本量很小比如少于50个点检验的效力Power会很低即很难拒绝“非平稳”的原假设容易犯第二类错误实际非平稳但检验没发现。应对策略谨慎对待小样本结果当样本量小时即使ADF检验说平稳也要持怀疑态度多结合图形和业务逻辑判断。考虑使用更稳健的检验在小样本下可以查阅相关文献看是否有针对小样本调整的临界值表或检验方法。尽可能收集更多数据这是根本解决之道。4.4 问题四结构突变点会导致误判吗会而且影响很大。结构突变是指序列在某个时间点均值或趋势发生突然、永久性的改变例如政策变化、产品重大升级、疫情开始。ADF检验可能会将一个带有结构突变的趋势平稳序列误判为有单位根的非平稳序列。应对策略识别突变点通过看图、或使用统计方法如Chow检验识别可能的突变点。分段检验如果突变点明确可以分别对突变点前后的子序列进行ADF检验。引入虚拟变量在时间序列模型如ARIMA中引入代表突变点的虚拟变量来捕捉这种结构性变化。4.5 高级技巧自动化平稳性检验与差分阶数确定在实际项目中我们可能需要批量处理成百上千个时间序列。手动一个个看图、选参数不现实。这里分享一个我常用的函数它自动尝试差分并确定使序列平稳的最小差分阶数。def make_stationary_and_test(series, series_name, max_diff2): 自动尝试差分最多max_diff次直到序列通过ADF检验p0.05并返回平稳序列和差分阶数。 :param series: 原始时间序列 :param series_name: 序列名称 :param max_diff: 最大尝试差分阶数 :return: (平稳序列, 差分阶数d, 是否成功) original_series series.copy() for d in range(max_diff 1): if d 0: current_series original_series.dropna() desc Original Series else: current_series original_series.diff(d).dropna() desc f{d}-order Differenced Series if len(current_series) 10: # 差分后数据太少停止 print(f After {d} diffs, series too short. Stopping.) return None, d-1, False # 执行ADF检验使用‘c’形式最常用 p_value adfuller(current_series, autolagAIC, regressionc)[1] print(f d{d} ({desc}): ADF p-value {p_value:.4f}, end) if p_value 0.05: print( - Stationary!) return current_series, d, True else: print( - Non-Stationary) print(f Failed to achieve stationarity within {max_diff} diffs.) return None, max_diff, False # 测试函数 print(f\n自动平稳化处理流程 for [Non-Stationary Sales]:) stationary_series, d_used, success make_stationary_and_test(df[sales_non_stationary], Test Series) if success: print(f\n成功经过 {d_used} 阶差分后序列平稳。) # 此时 stationary_series 就是平稳的序列可用于后续ARIMA等模型建模 else: print(\n未能使其平稳可能需要更复杂的处理如季节性差分、对数变换等。)这个函数会输出自动平稳化处理流程 for [Non-Stationary Sales]: d0 (Original Series): ADF p-value 0.6583 - Non-Stationary d1 (1-order Differenced Series): ADF p-value 0.0000 - Stationary! 成功经过 1 阶差分后序列平稳。5. 从平稳性检验到ARIMA建模的衔接ADF检验的最终目的是为ARIMA模型确定参数d积分阶数。d就是使序列平稳所需的最小差分次数。确定了d之后我们就可以对差分后的平稳序列进行下一步观察ACF/PACF图对平稳序列即make_stationary_and_test返回的序列绘制自相关函数和偏自相关函数图以初步判断ARIMA模型中的p(AR阶数) 和q(MA阶数)。# 对平稳化后的序列绘制ACF和PACF图 fig, axes plt.subplots(1, 2, figsize(12, 4)) plot_acf(stationary_series, lags40, axaxes[0], titlefACF of Stationary Series (d{d_used})) plot_pacf(stationary_series, lags40, axaxes[1], titlefPACF of Stationary Series (d{d_used}), methodywm) plt.tight_layout() plt.show()网格搜索定阶更稳健的方法是使用pmdarima库的auto_arima函数或通过AIC/BIC准则进行网格搜索自动确定最优的(p, d, q)组合。注意传入auto_arima的应该是原始序列并指定d参数为None让其自动检测或者指定为我们通过ADF检验确定的d。# 示例使用pmdarima需安装pip install pmdarima # from pmdarima import auto_arima # model auto_arima(df[sales_non_stationary], seasonalFalse, traceTrue) # print(model.summary())在整个流程中ADF检验扮演了“守门员”的角色确保了后续建模流程的输入数据满足平稳性这个基本假设。忽视这一步就像在流沙上盖房子无论后面的模型多精巧都难以稳固。花时间理解并做好平稳性检验是每一个时间序列分析者值得投入的基础功。当你面对一个陌生的序列时养成“先看图再跑ADF”的条件反射能帮你避开数据分析路上第一个也可能是最大的一个坑。