时序数据分析:趋势性与平稳性检验的实战指南
2026/8/12 13:07:12 网站建设 项目流程

1. 项目概述:从数据波动中洞察规律

做数据分析,尤其是面对按时间顺序排列的数据时,我们常常会感到困惑:这些数据呈现的上升或下降,是真实的长期趋势,还是仅仅是随机波动?数据背后的规律是否稳定,能否用经典的统计模型来捕捉和预测?这两个核心问题,恰恰对应了时序数据分析中两个至关重要的前置步骤:趋势性检验和平稳性检验。这不仅仅是教科书里的理论,更是每个数据分析师、研究员在构建预测模型、评估市场走势、分析业务指标前,必须亲手“摸一摸”数据底色的基本功。

我处理过大量来自金融、气象、工业监控等领域的时序数据,一个深刻的体会是:跳过检验直接上模型,无异于在未知地基上盖楼,模型可能看起来很美,但预测结果往往南辕北辙。趋势性检验帮你判断数据是否存在一个明确的长期发展方向,比如销售额是否在持续增长,气温是否在逐年升高。而平稳性检验则更深入一层,它检查数据的基本统计特性(如均值、方差)是否随时间推移而保持恒定。很多强大的时序模型,比如ARIMA,其核心假设就是数据是平稳的,或者通过差分等操作后能变得平稳。

简单来说,趋势性检验回答“数据在往哪个方向走?”平稳性检验回答“数据的‘脾气’是否稳定?”。本篇文章,我将结合多年实战经验,为你系统拆解这两类检验的核心思想、常用方法、实操步骤以及避坑指南。无论你是正在备战数学建模竞赛的学生,还是需要分析业务时间序列的从业者,掌握这套“体检”流程,都能让你对数据有更清醒的认识,为后续的建模与预测打下坚实可靠的基础。

2. 核心思路与检验方法论解析

在动手进行任何检验之前,理清逻辑脉络至关重要。趋势性检验和平稳性检验并非孤立存在,它们共同构成了评估时序数据特性的“侦察兵”。我的习惯是,先通过可视化进行定性观察,再选择合适的统计检验进行定量判断,两者结合,结论才更可靠。

2.1 检验的逻辑框架与顺序选择

面对一份时序数据,我推荐的诊断流程是:先看趋势,再查平稳。为什么是这个顺序?因为一个存在强烈趋势(如线性增长)的数据,其均值显然是随时间变化的,这直接违反了平稳性中“均值恒定”的基本要求。此时,如果你直接对原始数据做平稳性检验,几乎百分之百会拒绝“数据平稳”的原假设。但这个“不平稳”是由趋势造成的,而非数据内在的波动模式不稳定。

因此,更合理的做法是:

  1. 趋势性检验:首先确认数据是否存在统计上显著的趋势。如果存在,则需要先考虑“剔除”趋势的影响。剔除趋势不是简单地把数据变平,而是为了暴露出数据底层可能存在的、围绕某个中心稳定波动的结构。
  2. 平稳性检验:对“剔除趋势后”的数据(或确认无趋势的原始数据)进行平稳性检验。如果平稳,皆大欢喜,可以直接应用许多经典模型。如果不平稳,则可能需要进一步的变换,如差分(计算相邻数据的差值),直到数据变得平稳为止。

这个流程体现了数据分析的层次性:先解决宏观的结构性问题(趋势),再处理微观的稳定性问题(平稳性)。跳过第一步,很可能导致你对平稳性做出误判,进而选用错误的模型处理方法。

2.2 主流检验方法及其适用场景

两类检验都有多种统计方法,选择哪种取决于数据特点和你的分析目标。

对于趋势性检验,常用方法有:

  • Mann-Kendall (MK) 检验:这是非参数检验的“明星方法”,也是我使用频率最高的一种。它不要求数据服从正态分布,对异常值也不敏感,适用性非常广。其核心思想是检验数据序列随时间是否存在单调上升或下降的趋势(不一定是线性的)。它通过计算所有后期数据与前期数据比较的符号(是增是减)来构建统计量。在Python的pymannkendall库或R语言的trend包中都能方便调用。
  • Sen‘s Slope 估计:通常与MK检验配合使用。MK检验告诉你趋势是否存在,而Sen’s Slope则告诉你这个趋势的斜率有多大。它通过计算所有两两数据点之间斜率的中位数,得到一个稳健的趋势斜率估计,同样不受异常值影响。
  • 线性回归t检验:这是一种参数方法。其思路是拟合一个“数据 = 截距 + 斜率×时间 + 误差”的线性模型,然后检验斜率系数是否显著不为零。这种方法简单直观,但前提是假设残差独立同分布且符合正态性,对于波动复杂的数据可能不太稳健。

对于平稳性检验,经典方法包括:

  • Augmented Dickey-Fuller (ADF) 检验:这是平稳性检验的“标准装备”,几乎必用。它检验的原假设是“序列存在单位根”(即不平稳)。如果检验得到的p值小于显著性水平(如0.05),我们就拒绝原假设,认为序列是平稳的。ADF检验的强大之处在于它通过引入滞后项,可以处理更一般的自相关情况。在Python的statsmodels库或R语言的tseries包中都有实现。
  • Kwiatkowski-Phillips-Schmidt-Shin (KPSS) 检验:这个检验的原假设与ADF检验相反,其原假设是“序列是平稳的”(或趋势平稳)。因此,它和ADF检验可以互为补充。一个常见的做法是同时进行ADF和KPSS检验:如果ADF拒绝(平稳)且KPSS不拒绝(平稳),则证据最强;如果两者结论矛盾,则需要仔细分析数据特征。
  • Phillips-Perron (PP) 检验:与ADF检验解决类似的问题,但它在处理序列相关和异方差时的修正方法不同。在实际应用中,ADF检验更为普遍。

注意:选择检验方法时,务必理解其原假设。例如,ADF检验中“拒绝原假设”意味着支持平稳性,这是一个容易混淆的点。我的经验是,在报告结果时,永远写明“在X的显著性水平下,拒绝/不拒绝‘序列存在单位根’的原假设”,避免歧义。

3. 实战演练:从数据到结论的完整过程

理论说得再多,不如亲手跑一遍代码。下面我将用一个模拟的、具有轻微线性增长趋势和季节波动的月度数据为例,展示完整的检验流程。我们使用Python环境,主要借助pandas,statsmodels,pymannkendall等库。

3.1 数据准备与初步可视化

任何分析的第一步都是“看”数据。可视化能直观地揭示趋势、季节性和异常点。

import pandas as pd import numpy as np import matplotlib.pyplot as plt import statsmodels.api as sm from statsmodels.tsa.stattools import adfuller, kpss import pymannkendall as mk # 1. 生成模拟数据:一个线性增长趋势 + 季节性波动 + 随机噪声 np.random.seed(42) # 确保结果可复现 time_index = pd.date_range(start='2018-01-01', periods=60, freq='M') trend = 0.5 * np.arange(60) # 线性趋势 seasonality = 10 * np.sin(2 * np.pi * np.arange(60) / 12) # 年度季节性 noise = np.random.normal(0, 3, 60) # 随机噪声 data = trend + seasonality + noise ts = pd.Series(data, index=time_index, name='Simulated_Series') # 2. 绘制时序图 plt.figure(figsize=(12, 6)) plt.plot(ts, marker='o', linestyle='-', linewidth=1, markersize=3) plt.title('Simulated Monthly Time Series Data') plt.xlabel('Date') plt.ylabel('Value') plt.grid(True, which='both', linestyle='--', linewidth=0.5, alpha=0.7) plt.tight_layout() plt.show()

通过这张图,我们可以定性看到数据整体有一个向上的走势(趋势),同时伴随着规律的波浪形摆动(季节性)。这初步印证了我们的数据生成过程。接下来进行定量检验。

3.2 执行趋势性检验(Mann-Kendall + Sen‘s Slope)

我们使用非参数的MK检验,因为它对我们的数据分布没有要求。

# 执行Mann-Kendall趋势检验 mk_result = mk.original_test(ts) print("=== Mann-Kendall Trend Test Results ===") print(f"Trend: {mk_result.trend}") # 显示趋势方向(increasing, decreasing, no trend) print(f"H-statistic (S): {mk_result.s}") print(f"p-value: {mk_result.p:.6f}") print(f"Sen's Slope: {mk_result.slope:.4f}") # 趋势斜率 print(f"Intercept: {mk_result.intercept:.4f}") # 判断标准:通常以p-value < 0.05 为存在显著趋势 alpha = 0.05 if mk_result.p < alpha: print(f"\n结论:在{alpha}的显著性水平下,拒绝原假设(无趋势)。该序列存在显著的{mk_result.trend}趋势。") print(f"趋势的估计斜率(Sen's Slope)为:{mk_result.slope:.4f},即平均每时间单位增加{mk_result.slope:.4f}。") else: print(f"\n结论:在{alpha}的显著性水平下,没有足够证据拒绝原假设。该序列未检测到显著趋势。")

运行这段代码,你很可能得到一个p值远小于0.05的结果,并判断存在显著的上升趋势。Sen‘s Slope会给出一个具体的斜率值,量化了趋势的强度。

3.3 执行平稳性检验(ADF与KPSS)

现在我们对原始数据(包含趋势)进行平稳性检验,预期结果应该是不平稳。

# 执行Augmented Dickey-Fuller (ADF) 检验 print("\n=== Augmented Dickey-Fuller Test (on Original Data) ===") adf_result = adfuller(ts, autolag='AIC') # 使用AIC准则自动选择最佳滞后阶数 adf_output = pd.Series(adf_result[0:4], index=['Test Statistic', 'p-value', '#Lags Used', 'Number of Observations Used']) for key, value in adf_result[4].items(): adf_output[f'Critical Value ({key})'] = value print(adf_output) if adf_result[1] < 0.05: print("结论:p值 < 0.05,拒绝原假设。序列平稳。") else: print("结论:p值 >= 0.05,无法拒绝原假设。序列可能非平稳。") # 执行KPSS检验(注意:statsmodels中默认原假设为水平平稳,trend='c') print("\n=== KPSS Test (on Original Data) ===") kpss_result = kpss(ts, regression='c', nlags='auto') # 'c'表示检验水平平稳性 kpss_output = pd.Series(kpss_result[0:3], index=['Test Statistic', 'p-value', '#Lags Used']) for key, value in kpss_result[3].items(): kpss_output[f'Critical Value ({key})'] = value print(kpss_output) if kpss_result[1] < 0.05: print("结论:p值 < 0.05,拒绝原假设。序列非平稳。") else: print("结论:p值 >= 0.05,无法拒绝原假设。序列可能平稳。")

对于我们的原始数据,ADF检验的p值很可能大于0.05(无法拒绝“不平稳”),而KPSS检验的p值很可能小于0.05(拒绝“平稳”)。两者结论一致:原始序列非平稳。这主要是由其中包含的线性趋势导致的。

3.4 处理趋势后的再检验

既然检测到了趋势,一个标准的预处理方法是“差分”(Differencing),即计算当前值与前一个值的差值。一阶差分通常可以消除线性趋势。

# 计算一阶差分 ts_diff = ts.diff().dropna() # 绘制差分后的序列 plt.figure(figsize=(12, 6)) plt.plot(ts_diff, marker='o', linestyle='-', linewidth=1, markersize=3) plt.title('Time Series After First-Order Differencing (Removed Trend)') plt.xlabel('Date') plt.ylabel('Differenced Value') plt.grid(True, linestyle='--', linewidth=0.5, alpha=0.7) plt.axhline(y=0, color='r', linestyle='-', linewidth=0.5) # 添加零线 plt.tight_layout() plt.show() # 对差分后的数据再次进行ADF检验 print("\n=== ADF Test (on Differenced Data) ===") adf_result_diff = adfuller(ts_diff, autolag='AIC') if adf_result_diff[1] < 0.05: print(f"p-value: {adf_result_diff[1]:.6f}。结论:差分后序列平稳。") else: print(f"p-value: {adf_result_diff[1]:.6f}。结论:差分后序列仍可能非平稳,可能需要更高阶差分或其他处理。")

观察差分后的时序图,数据不再呈现明显的上升或下降趋势,而是围绕零值上下波动。此时再进行ADF检验,p值通常会变得非常小(<0.05),表明我们成功地去除了趋势,得到了一个平稳的序列。这个平稳序列(ts_diff)就可以作为后续ARIMA等模型的输入了。

4. 深度解析:检验背后的原理与参数抉择

知其然,更要知其所以然。了解检验方法背后的统计思想,能帮助你在复杂情况下做出正确判断,而理解关键参数的选择,则直接关系到检验结果的可靠性。

4.1 Mann-Kendall检验原理浅析

MK检验是一种非参数检验,它不关心数据的具体数值大小,只关心其随时间变化的顺序。其核心统计量S的计算基于所有可能的“数据对”(x_i, x_j, 其中 i < j)。对于每一对数据,计算符号函数:如果x_j > x_i,记+1;如果x_j < x_i,记-1;相等则记0。然后将所有符号值求和得到S。

  • 如果S是一个很大的正数,说明后期值普遍大于前期值,存在上升趋势。
  • 如果S是一个很大的负数,则存在下降趋势。
  • 如果S接近0,则无趋势。

通过将S标准化,并与标准正态分布比较,就可以计算出p值。它的优点是对数据的分布没有要求,且对异常值稳健,因为异常值只是众多数据对中的一部分,不会像线性回归那样被一条直线过度“吸引”。

4.2 ADF检验中的滞后阶数选择

这是ADF检验中最关键的一个参数,它决定了检验方程中包含多少期滞后项来解释序列的自相关。选择不当会导致检验效力降低(容易犯第二类错误)或尺寸扭曲(容易犯第一类错误)。

常见的自动选择准则有:

  • AIC (Akaike Information Criterion):在模型复杂度和拟合优度之间取得平衡,倾向于选择更简洁的模型。autolag='AIC'是常用选项。
  • BIC (Bayesian Information Criterion):比AIC对模型复杂度惩罚更重,倾向于选择更简单的模型。
  • 固定滞后:基于对数据自相关性的先验知识手动设定。例如,月度数据可能考虑12期滞后以捕捉年度效应。

statsmodelsadfuller函数中,设置autolag='AIC''BIC'可以让库自动从0到一个最大滞后值(默认基于样本量计算)之间,选择使信息准则最小的滞后阶数。我的经验是,对于没有明显长期周期的数据,使用‘AIC’自动选择通常是一个稳健的起点。如果结果处于临界值附近(p值在0.05左右),可以尝试不同的准则或手动调整最大滞后参数(maxlag)进行敏感性分析,看结论是否稳定。

4.3 差分:消除趋势与实现平稳的利器

差分是使非平稳序列平稳化的最常用方法。一阶差分消除线性趋势,二阶差分消除二次曲线趋势,以此类推。对于具有季节性周期(如月度数据中的年度周期)的数据,还需要进行季节性差分。

  • 一阶差分Y_t' = Y_t - Y_{t-1}
  • 季节性差分(周期为s)Y_t' = Y_t - Y_{t-s}

在Python中,pandas.diff(periods=1)函数可以方便地实现差分。periods参数控制差分的步长。例如,对于月度数据,ts.diff(periods=12)就是季节性差分。

重要心得:差分不是越多越好。每做一次差分,都会损失一个数据点,并且可能引入额外的相关性或使序列的方差变得不稳定。通常,先做一阶差分,检验平稳性;如果不平稳,再考虑二阶或季节性差分。也可以通过观察自相关图(ACF)来判断:如果原始序列的ACF衰减非常缓慢,而差分后的序列ACF快速衰减到零附近,则说明差分是有效的。

5. 常见陷阱、问题排查与实战技巧

在实际操作中,你绝不会总是一帆风顺。下面分享一些我踩过的坑和总结的技巧。

5.1 检验结果矛盾或处于临界值怎么办?

  • 场景:ADF检验p值为0.06(略大于0.05),KPSS检验p值为0.03(小于0.05),一个说“可能平稳”,一个说“非平稳”。
  • 排查与解决
    1. 检查数据长度:样本量太小会导致检验功效不足,结论不可靠。尽量保证有足够多的数据点(例如,年度数据至少20-30个点,月度数据至少60-80个点)。
    2. 审视数据图形:回到时序图。如果图形显示有轻微趋势或波动幅度随时间明显变化,那么KPSS的结果(拒绝平稳)可能更可信。ADF检验对某些类型的非平稳(如结构突变)可能不敏感。
    3. 尝试不同的差分阶数:对数据做一阶差分后再检验。如果差分后两个检验都支持平稳,那么原始序列就是一个“差分平稳”序列。
    4. 考虑结构突变:如果序列的均值或方差在某个时间点发生了突然、永久性的改变(例如,政策变更前后),这也会导致非平稳。这时需要更复杂的模型(如带结构突变的单位根检验)或分段建模。
    5. 调整检验参数:在ADF检验中尝试不同的滞后阶数选择标准(AIC/BIC)或手动设置一个更大的maxlag,看p值是否发生显著变化。

5.2 存在季节性时如何检验趋势和平稳性?

季节性强的数据(如月度销售额、每日气温)会给检验带来干扰。

  • 对于趋势检验(MK检验):MK检验本身对季节性不敏感,因为它基于所有数据对的秩次比较。季节性波动是周期性的,不会产生单调的趋势信号。因此,MK检验可以直接用于有季节性的数据来检测长期趋势。
  • 对于平稳性检验:季节性本身意味着均值在不同季节是不同的,这违反了平稳性定义。因此,在检验前通常需要先消除季节性
    • 方法一:季节性差分。先进行周期为s的季节性差分(如月度数据s=12),然后再对差分后的序列进行ADF检验。
    • 方法二:先分解,后检验。使用时间序列分解方法(如STL分解、经典分解法)将序列拆分为趋势(Trend)、季节性(Seasonal)和残差(Residual)三部分。然后对残差部分进行平稳性检验。如果残差平稳,则称原序列为“趋势和季节平稳”序列。

5.3 如何自动化与报告检验结果?

在需要批量处理多个时间序列或定期报告中,手动操作效率低下。可以编写一个封装函数。

def ts_diagnostic_report(series, series_name="Series", alpha=0.05): """ 生成时序数据趋势性与平稳性诊断报告。 参数: series: pd.Series, 时间序列数据。 series_name: str, 序列名称。 alpha: float, 显著性水平。 返回: dict, 包含主要检验结果的字典。 """ report = {'Name': series_name} # 1. Mann-Kendall趋势检验 try: mk_res = mk.original_test(series) report['MK_Trend'] = mk_res.trend report['MK_p_value'] = mk_res.p report['MK_Slope'] = mk_res.slope report['Has_Significant_Trend'] = mk_res.p < alpha except Exception as e: report['MK_Error'] = str(e) # 2. ADF平稳性检验 (原始数据) try: adf_res = adfuller(series, autolag='AIC') report['ADF_Stat'] = adf_res[0] report['ADF_p_value'] = adf_res[1] report['ADF_Stationary'] = adf_res[1] < alpha report['ADF_Lags'] = adf_res[2] except Exception as e: report['ADF_Error'] = str(e) # 3. 如果有趋势,给出差分后的ADF检验结果 if report.get('Has_Significant_Trend', False): series_diff = series.diff().dropna() if len(series_diff) > 1: # 确保差分后还有数据 try: adf_res_diff = adfuller(series_diff, autolag='AIC') report['ADF_diff_p_value'] = adf_res_diff[1] report['ADF_diff_Stationary'] = adf_res_diff[1] < alpha except Exception as e: report['ADF_diff_Error'] = str(e) # 打印简要报告 print(f"诊断报告 - {series_name}") print("-" * 40) print(f"趋势检验 (Mann-Kendall): p值 = {report.get('MK_p_value', 'N/A'):.4f}, 趋势方向 = {report.get('MK_Trend', 'N/A')}") print(f"平稳性检验 (ADF-原始): p值 = {report.get('ADF_p_value', 'N/A'):.4f}") if 'ADF_diff_p_value' in report: print(f"平稳性检验 (ADF-一阶差分后): p值 = {report['ADF_diff_p_value']:.4f}") print("-" * 40) return report # 使用示例 report = ts_diagnostic_report(ts, series_name="模拟月度数据")

这个函数提供了一个快速生成标准化报告的方法,便于在项目文档或分析日志中记录关键诊断信息。

5.4 检验只是起点,而非终点

最后,也是最重要的一点:不要机械地依赖检验的p值。统计检验是一个辅助决策的工具,而不是绝对的真理判决书。务必结合图形观察、业务背景和常识进行综合判断。

  • 一个统计上显著但斜率极小的趋势,在业务上可能毫无意义。
  • 一个因为个别极端值而导致ADF检验拒绝平稳性的序列,可能需要先处理异常值。
  • 对于明显存在趋势或季节性的数据,即使ADF检验因为某些原因(如样本量小、参数设置)给出了“平稳”的结论,你也应该优先相信自己的眼睛和业务逻辑,对数据进行适当的预处理。

趋势性检验和平稳性检验,就像医生手中的听诊器和血压计,它们提供关键的诊断指标,但最终的“治疗方案”——是直接建模,还是先差分、去季节、去趋势——需要你这位“数据医生”基于全面的信息做出专业的判断。掌握了这些检验方法,并理解了其背后的原理与局限,你就拥有了洞察时序数据内在规律的第一个,也是最重要的工具。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询