简介:一份面向数据分析与统计建模初学者的 PDF 教程,聚焦 Python 时间序列分析中自相关图(ACF)与偏自相关图(PACF)的绘制与解读。内容以 statsmodels 库的 plot_acf、plot_pacf 为主线,结合示例说明如何通过图形判断序列的拖尾与截尾特征,并为 ARIMA 模型定阶提供参考;同时补充 seaborn 热力图实现变量间相关性可视化的方法。全包仅包含 1 个 PDF 文件,大小约 77KB,便于离线阅读与快速查阅。教程还讨论了 ACF 对趋势和季节性的识别价值,以及 PACF 在检测线性依赖中的作用,帮助读者理解滞后阶数与自相关系数、偏自相关系数之间的关系。已有 11436 人学习下载,适合需要快速上手时间序列建模、开展数据相关性探索的 Python 使用者。
1. 拿到时间序列的第一件事:先画 acf 和 pacf
上个月我接到一个销售数据的预测需求,第一版模型直接上了 LSTM,训练完发现残差里还藏着明显的 7 日周期。后来才发现,问题不在模型,在数据进模型之前我连自相关结构都没看。时间序列分析里,python 画自相关图(acf)和偏自相关图(pacf)是建模前必须做的一步:它告诉你当前时刻和过去时刻之间到底有没有关系、关系能延续多远、该用 AR 还是 MA 项来吸收这些关系。
这篇教程面向要动手建模的人——不管是做时间序列预测、异常检测,还是刚把 pandas 的 Series 拿在手里不知道怎么开头,我都按自己的操作路径来写:先用 statsmodels 把 acf、pacf 算出来并画出来,再讲怎么从图里判断截尾和拖尾,最后给出定阶和避坑的具体经验。图不是画出来好看的,是要在建模前替你回答“数据里到底有没有残留的相关性”这个问题的。
2. 自相关图 ACF:先搞懂它在算什么,再动手画图
2.1 自相关的直觉:昨天的销量和今天的销量有关系吗
自相关(Autocorrelation)衡量的是同一个序列在不同时间点上的相关性。具体说,acf(k) 是序列在 t 时刻和 t-k 时刻之间的相关系数,k 是滞后阶数。如果是日销量数据,acf(1) 就是“今天销量和昨天销量有多像”,acf(7) 就是“今天和上周同一天有多像”。
数学定义是:
acf(k) = Cov(y_t, y_{t-k}) / Var(y_t)其中 Cov 是协方差,Var 是方差,所以 acf 的值被归一化到 [-1, 1] 之间。这个定义和普通的样本相关系数几乎一样,只是把“两个不同变量”换成了“同一个变量的不同时刻”。但要注意:acf 计算的不是边缘相关,而是“无条件的相关”,也就是说 acf(2) 描述的是一阶相关性没有被剔除时的结果,这也是后面 pacf 存在的意义。
在 python 里计算 acf,statsmodels 是最常用的库,因为它同时实现了计算和绘图两个功能,还带了置信区间,不需要自己手写协方差求和。下面是最小的可运行代码。
import numpy as np import pandas as pd import matplotlib.pyplot as plt from statsmodels.graphics.tsaplots import plot_acf # 构造一个月度销售数据,带趋势和季节性 np.random.seed(42) t = np.arange(60) sales = 20 + 0.5 * t + 8 * np.sin(2 * np.pi * t / 12) + np.random.normal(0, 1, size=60) # 画自相关图,滞后阶数取 24(至少覆盖两个季节性周期) fig = plot_acf(sales, lags=24, alpha=0.05, zero=False) plt.xlabel("Lag (月)") plt.ylabel("ACF 值") plt.title("月度销售数据的自相关图") plt.show()这段代码里 plot_acf 是核心函数。lags=24 表示画 1 到 24 阶的自相关系数,zero=False 表示不画第 0 阶——第 0 阶恒等于 1 且没有分析价值,画出来会把纵轴压缩得很厉害。alpha=0.05 表示置信水平 95%,阴影条带就是置信区间,落在区间之外的柱子通常认为是统计显著的。
从这张图里,你会看到几个典型特征:lags=12 和 lags=24 附近出现高峰,说明数据存在 12 个月的强季节周期;整体拖尾而不是快速衰减到 0,说明序列本身不平稳,可能有必要先做差分或去趋势。这其实就是建模前最重要的信息:我知道这个序列有周期性,那特征工程里就要加入月份、滞后项或者季节差分,否则任何模型都学不到这种规律。
2.2 为什么要在建模前先看 acf:白噪声检验、MA 阶数、季节周期
acf 图有三个核心用途。第一是检验残差是否为白噪声:如果模型拟合得够好,残差的自相关在任意滞后阶数都应该接近 0,全部落在置信区间内。如果你在残差的 acf 图上看到 lag=1 处有漂亮的尖峰,说明模型没有把一阶自相关吸收掉,还有信息可以榨取,模型结构或者滞后阶数一定不对。
第二是识别 MA(q) 模型的阶数。MA 模型有一个很特别的统计性质:理论上 MA(q) 过程的自相关函数在滞后 q 之后会“截尾”,也就是 q 以后的 acf 系数直接变成 0。因此如果你看到 acf 图在 lag=2 之后突然掉进置信区间,那模型大概率是 MA(2)。这比靠 AIC 硬调参多给了你一个可解释的角度。
第三是识别季节周期。我之前在做一个电力负荷数据时,acf 在 lag=48 处出现高峰,那是 48 个 15 分钟间隔,对应一天前的负荷水平。图上的这个规律告诉我要加的滞后项是 48 而不是 49 或者 72。这里想强调一点:acf 图适合用来发现“周期性重复”的相关结构,但要把周期长度精确化,最好同时用快速傅里叶变换验证,acf 给出的是时域视角。
下面这段代码演示用 statsmodels 的 acf 函数直接拿数值,方便你打印出来做进一步判断,不只是看图。
from statsmodels.tsa.stattools import acf # 返回从 lag0 开始的 acf 数值数组 acf_vals = acf(sales, nlags=24, fft=True, adjusted=True) # 只打印 1-6 阶的系数和对应是否显著 for i in range(1, 7): significant = "显著" if abs(acf_vals[i]) > 1.96 / np.sqrt(len(sales)) else "不显著" print(f"lag {i}: acf={acf_vals[i]:.4f} ({significant})")这里 fft=True 会启用快速傅里叶算法来加速计算。样本量大时、nlags 也大时,fft 计算比直接求和快很多,但对样本量很小(比如 n 小于 50)的情况,直接求和反而更稳定。adjusted=True 表示使用的是调整后的自相关估计,默认是 False,那么算出来是未调整的系数;两者在样本量小的时候差别明显,调整后会对有限样本偏差做一些修正。
还有一个容易忽略的关键参数 nlags。我一般取 min(24, n//2-1),也就是说当数据只有 60 个点时,nlags 最多取 29。取值太大,后面的滞后阶数对应的样本对很少,估计方差变大,图上会出现大量虚假的“显著”尖刺;取值太小,季节性周期可能完全暴露不出来。对月度数据,至少覆盖 12 个月才能看到年度周期;对日粒度数据,我会取 30、60、90 这种覆盖自然周期的值,而不是随便填个 20。
3. 偏自相关图 PACF:它剔除的到底是什么
3.1 从 AR(1) 说起:为什么光看 acf 会误判
偏自相关(Partial Autocorrelation)这个名字里的“偏”字,说的是它在计算 y_t 与 y_{t-k} 的相关性时,剔除了中间 k-1 个滞后项 y_{t-1} ... y_{t-k+1} 的间接影响。通俗讲,pacf(k) 告诉你的是“多往前看 k 步,除了已经被中间那几步解释掉的部分,你还能获得多少额外信息”。
这里有一个常见误判:对 AR(1) 过程,acf(2) 并不等于 0,因为 y_{t-2} 可以通过 y_{t-1} 间接影响 y_t。如果你只看 acf 图,看到 lag 2 也显著,就可能误判成 MA(2)。而 pacf(2) 会把这个间接影响剔除掉,对 AR(1) 来说 pacf(2) 几乎为 0。这就解释了为什么定阶不能只看一张图,必须把 pacf 和 acf 对照着看,一个看直接效应,一个看总效应。
pacf 的计算在理论上依赖 Yule-Walker 方程,通过求解自回归系数得到。statsmodels 里 pacf 函数的 method 参数有几种选择,默认方法是 ywunbiased(Yule-Walker 无偏估计),它适用于样本量较大、序列是平稳 AR 过程的情况。如果你拿不准就用默认,大多数场景下结果差别不大。
下面这句代码是画 pacf 图的最小写法:
from statsmodels.graphics.tsaplots import plot_pacf # method="ywmle" 是 Yule-Walker 的极大似然估计版本 fig = plot_pacf(sales, lags=24, method="ywmle", alpha=0.05, zero=False) plt.xlabel("Lag (月)") plt.ylabel("PACF 值") plt.title("月度销售数据的偏自相关图") plt.show()method 参数在 statsmodels 新版本里可以直接传 "ywunbiased"、"/ywmle"、"ols" 或 "ldung-box"。ols 是用普通最小二乘回归逼近 pacf,样本量大的时候慢一点,但有时会比 Yule-Walker 更稳定——特别是数据带有轻微非平稳性的时候。ansley 相关的方法能对 MA 过程得到更精确的估计,但一般建模时用不到,知道有这么个参数就行。
3.2 pacf 图怎么看:AR 阶数的显微镜
pacf 图最大的价值在于确定 AR(p) 模型的阶数。因为偏自相关在理论上会在滞后 p 阶之后“截尾”为 0,所以如果 pacf 图在 lag=3 之后全部落在置信区间内,那 AR(3) 模型就足够了。这里的逻辑和 MA 定阶对称:acf 截尾指向 MA,pacf 截尾指向 AR。
但截尾这件事,在实际数据上从来不会表现得特别干净。统计显著性受样本量影响很大,样本小的时候置信区间很宽,截尾点的位置可能模糊不清;样本大的时候,真实为 0 的系数也可能因为随机波动偶尔冒出一个超出置信区间的点。我一般不看单根柱子,而是看连续 3 个大滞后是否都落在区间内,这样判断截尾比单点判断稳健得多。
另外要注意,pacf 图对非平稳序列几乎完全失去意义。因为非平稳序列的滞后相关性会被趋势主导,pacf 即使剔除了中间项,也无法消除趋势带来的“伪相关”。你在画 pacf 之前,至少要用 adfuller 做一次 ADF 检验,或者直接用差分后的序列来画图。否则 pacf 图里出现的可能只是趋势的影子,不是真实的自回归结构。
4. 从图到定阶:截尾、拖尾对照表与完整案例
4.1 四种典型形态:AR、MA、ARMA 还是白噪声
把 acf 和 pacf 拼在一起看,最经典的就是下面这张判断表:
| 形态 | ACF | PACF | 推荐模型 |
|---|---|---|---|
| AR(p) | 拖尾(指数衰减或震荡衰减) | p 阶截尾 | AR(p) |
| MA(q) | q 阶截尾 | 拖尾 | MA(q) |
| ARMA(p,q) | 拖尾 | 拖尾 | ARMA(p,q) |
| 白噪声 | 全部落在置信区间内 | 全部落在置信区间内 | 无需建模 |
我对这张表的理解是:acf 的尾巴就像“剩余信息含量”,pacf 的截尾点就像“最远的直接依赖距离”。如果 acf 拖尾的同时 pacf 在 2 阶截尾,说明这个序列主要被过去两个时刻直接决定,更多滞后时刻的影响都是通过这两个时刻间接传递的,模型用 AR(2) 就够了。
拖尾的具体形态有两种:一种是正的指数衰减,走的是 AR(1) 的典型路径;另一种是震荡衰减,比如正、负、正、负交替,这经常是 AR(2) 的表现。看图时别只盯前两三根柱子,要看整个拖尾的模式,必要时把 lag 数调大到 30 甚至 40,才能分辨清楚是指数衰减还是正弦型衰减。
4.2 用模拟数据走一遍完整定阶流程
下面我给一个完整的操作流程,从数据生成到定阶衔接后续时间序列预测模型。先模拟一个 AR(2) 过程,然后带大家用 acf 和 pacf 把它识别出来。
import numpy as np import pandas as pd from statsmodels.tsa.arima_process import arma_generate_sample from statsmodels.tsa.stattools import adfuller from statsmodels.graphics.tsaplots import plot_acf, plot_pacf import matplotlib.pyplot as plt # 模拟 AR(2): y_t = 0.6*y_{t-1} - 0.4*y_{t-2} + e_t np.random.seed(1) ar_params = np.array([1, -0.6, 0.4]) # 注意 arma_generate_sample 的符号约定 ma_params = np.array([1, 0]) data = arma_generate_sample(ar=ar_params, ma=ma_params, nsample=200) # 先做平稳性检验 adf_res = adfuller(data) print(f"ADF p-value: {adf_res[1]:.4f}") # 画图 fig, axes = plt.subplots(2, 1, figsize=(10, 8)) plot_acf(data, lags=20, ax=axes[0], zero=False) plot_pacf(data, lags=20, ax=axes[1], method="ywmle", zero=False) plt.show()这段代码里有几个容易翻车的细节:arma_generate_sample 里的 ar 参数用的是“1 - phi1 - phi2”这种约定,所以想要 y_t = 0.6*y_{t-1} - 0.4*y_{t-2},ar_params 要写 [1, -0.6, 0.4],而不是 [0.6, -0.4]。写成后者会得到完全不同的序列,acf 和 pacf 图也会对不上。ADF 检验的 p 值如果小于 0.05 就说明序列平稳,可以不用差分直接定阶。
看图时会发现 pacf 在 lag=2 处有一个明显的尖峰,lag=3 及以后全部落进置信区间;acf 则衰减得比较慢,呈震荡形态。按对照表,这就是 AR(2)。判断完成以后,我一般会用 ARIMA(2, 0, 0) 或 ARIMA(1, 0, 2) 这类候选去跑 AIC 对比,但初始方向已经由 acf/pacf 定死了。这样做的价值是:你不再是黑匣子式地把 p、q 全部交给自动搜索,而是先通过图建立结构上的判断,再让自动搜索在邻近区域里微调。
值得一提的是,acf/pacf 定阶对后续用 ARIMA 做预测、甚至对 LSTM 做时间序列预测都有帮助。LSTM 虽然理论上能自动学滞后关系,但如果你先用 acf 发现残差中还有 7 阶峰值,就知道该把 input window 至少提到 7,或者干脆做一次季节差分。我见过很多 LSTM 模型效果差,不是网络结构问题,而是输入窗口根本没覆盖数据依赖的最远距离。
5. 避坑:acf/pacf 使用中最常见的 4 个翻车点
5.1 不平稳数据直接画图,拖尾全是假象
现象:acf 图从 lag 1 开始就是接近 1 的系数,然后以极慢的速度单调递减,30 个滞后全部超出置信区间,看起来像是“无限阶拖尾”。pacf 图则出现一个极大的 lag 1 值,其余全部不显著。
原因:序列里有明显的趋势或随机游走成分。趋势会让所有滞后都产生正向相关性,acf 捕捉到的是这个虚假趋势相关,而不是真实的自相关结构。这种情况下强行看拖尾截尾没有任何意义。
解决:先做 ADF 检验,确认 p 值 < 0.05 或 < 0.01。如果不平稳,对原始序列做一阶差分,或者对带季节性的数据做季节差分,然后对差分后的序列重新画 acf 和 pacf。
5.2 lags 参数随便填,季节周期被淹没或假峰值满天飞
现象:把 lags 设得太小,比如 lags=10,结果月度数据里年周期完全看不见;或者设得太大,比如 100 条数据画 60 个滞后,尾部一大片“显著”尖峰,看着像有复杂结构,其实都是噪声。
原因:每个滞后的 acf 值是基于 t-k 与 t 的样本对计算的。滞后越大,有效样本量越小,估计方差越大。尾部的高峰往往只是少数几个异常点造成的,没有统计意义。
解决:lags 取 min(24, n//2-1) 或者 min(36, n//2-1)。日粒度数据直接考虑用 7 的倍数,月度数据用 12 的倍数。判读时重点看前 1/3 的滞后区域,后 2/3 的区域只用来识别季节性长周期,不作为定阶依据。
5.3 样本量太小:无限数据才有的截尾,在 30 条数据里不可能出现
现象:样本量只有 30-40,acf 和 pacf 图里的置信区间宽到几乎覆盖所有柱子,任何滞后都不显著,你可能会误判成白噪声。另一种情况是偶尔有一两根柱子冒出来“显著”,但你换一组随机数又完全变了。
原因:置信区间公式大致是 ±1.96/sqrt(n),n=30 时区间宽度超过 ±0.35,acf 值很难突破这个范围。另外,小样本下 Yule-Walker 估计的偏差也比大样本更大,pacf 结果更不稳定。
解决:如果样本量不足 50,acf/pacf 图只做参考,不要只依赖图。改用 Ljung-Box 检验做白噪声判断,同时用 AIC/BIC 对几个候选阶数做交叉验证,三种手段互相印证。如果数据已经不可能增加,考虑引入先验知识,比如业务上确定有天周期,就硬编码这些滞后特征。
5.4 statsmodels 版本差异:plot_acf 的返回值在不同版本里不一样
现象:在旧版 statsmodels(0.12 及更早)里,plot_acf 返回一个图形对象;到了新版 0.13+,返回的是包含图和实例的元组。如果你写成ax = plot_acf(series)并直接操作 ax,新版本会报错或者拿到一个不可预期的对象。
原因:这是库官方调整了返回值类型,为了兼容多个绘图对象的访问方式。这类问题在升级环境后最容易遇到,网上旧的教程代码直接跑不起来。
解决:统一写成fig_result = plot_acf(series, lags=..., ax=ax),然后把数据传进预先建好的 subplot。或者先result = plot_acf(...),再用fig = result[0] if isinstance(result, tuple) else result做兼容。更稳妥的做法是使用 plt.gcf() 获取当前图对象,不依赖返回值类型。
5.5 缺失值处理不当:pandas 对齐错误导致 acf 全偏
现象:用 resample 聚合日数据变成周数据时,某几周没有销量,直接填入 0 或前向填充后画 acf,结果出现诡异的负相关尖峰。
原因:填充方式破坏了数据自相关结构。填 0 会制造大量“这个点低、下一个点高”的伪交替,负相关就出来了;前向填充会制造相同值重复的块,正相关性被高估。
解决:最稳妥的做法是保留原始时间索引,用 dropna 丢弃缺失位置,或者用插值后再画图。如果缺失比例超过 20%,建议不要做 acf/pacf 分析,因为插值算法本身也会改变序列的相关性结构。还有一种偷懒但实践中有效的方式:用原始时间戳的间隔信息,把 acf 的滞后对齐到真实时间间隔,而不只是按样本序号对齐。
6. 把 acf/pacf 封装进一个函数:从画图到自动给出定阶建议
最后一节,我分享一个自己常用的做法:把 acf/pacf 分析和 Ljung-Box 白噪声检验封装成一个辅助函数,同时输出图和数值结果。这样每次拿到新数据,跑一段代码就能得到全部需要的信息,不再每次重复写同样的绘图流程。
import numpy as np import pandas as pd import matplotlib.pyplot as plt from statsmodels.graphics.tsaplots import plot_acf, plot_pacf from statsmodels.stats.diagnostic import acorr_ljungbox from statsmodels.tsa.stattools import adfuller def ts_acf_pacf_report(series, lags=24, diff=True): """给一个 pandas Series,输出平稳性检验 + acf/pacf 图 + 白噪声检验""" s = series.copy() if diff: s = s.diff().dropna() # 平稳性 adf_p = adfuller(s)[1] # Ljung-Box 白噪声检验,考察前 10 个滞后 lb_res = acorr_ljungbox(s, lags=[10], return_df=True) lb_p = lb_res["lb_pvalue"].iloc[0] print(f"ADF p-value: {adf_p:.4f}, Ljung-Box p-value: {lb_p:.4f}") # 绘图 fig, axes = plt.subplots(2, 1, figsize=(10, 8)) plot_acf(s, lags=lags, ax=axes[0], zero=False) plot_pacf(s, lags=lags, ax=axes[1], method="ywmle", zero=False) plt.tight_layout() plt.show()参数 diff 默认为 True,意味着默认对原始序列做一阶差分,这适合绝大多数非平稳业务数据。如果你已经处理过平稳性,调成 False 即可。这样封装的思路是:绘图只是中间产物,最终目的是得到三个结论——序列是否平稳(ADF)、是否白噪声(Ljung-Box)、自回归结构是什么(acf/pacf 图的视觉判断)。
Ljung-Box 检验的 p 值如果大于 0.05,说明残差没有显著自相关,可以认为是白噪声;p 值很小说明显著性很强,还有信息没被提取。不过这只是一个整体检验,无法告诉你具体是哪个滞后造成的,所以仍然需要配合 acf 图定位。
再分享一个进阶用法:在 LSTM 时间序列预测任务里,acf/pacf 不只是用来定阶,更重要的是检查训练集和测试集切分的合理性。有一次我在做传感器异常检测,数据里存在明显的日周期,但切分 train/test 时直接按顺序切,导致训练集覆盖不了完整的周期相位,acf 图显示训练集的滞后相关性结构和测试集不一致。后来我改成按周期对齐切分,模型的效果才算正常。
对于做时间序列异常检测的人来说,acf/pacf 还有一个特别实用的场景:检测指标的“自相关突变”。正常运行数据如果存在稳定的周期自相关,acf 图的峰值位置是固定的;当系统发生异常,自相关结构会显著变化,比如原本 24 阶峰值突然消失,这说明日周期被破坏了。这个特征比单纯看数值异常更稳健,因为它直接反映了系统动态的变化。
我现在拿到任何一段新的时序数据,第一件事就是跑一遍 ts_acf_pacf_report,亲眼确认三个问题:序列平稳吗、是白噪声吗、依赖在哪几个滞后。姿势固定下来之后,后面无论是传统 ARIMA 还是深度学习模型,我都能在几分钟内说清楚这个数据的自相关结构长什么样,而不是把数据扔给黑匣子模型去猜。这几个函数和读图习惯是踩过不少坑换来的血泪经验,希望帮到你。
本文还有配套的精品资源,点击获取