1. 这不是教科书里的概念罗列,而是你真正建模时每天要掰扯清楚的“语言地图”
做时间序列建模,最常遇到的不是代码报错,而是开会时听不懂同事说的“非平稳性”到底指什么,写报告时卡在“ARIMA的d阶差分到底是为了解决哪个具体问题”,或者调试模型时发现RMSE很低但业务方盯着预测曲线直摇头——说这走势“不像真实发生的”。这些卡点,90%以上都源于对基础术语和概念的理解停留在字面,没和实际数据、真实场景、建模动作一一对应起来。我带过二十多个工业预测项目,从风电功率预测到电商GMV周度拆解,踩过最多坑的地方,从来不是算法选型,而是团队里有人把“自相关”当成“因果关系”,把“残差白噪声检验”当成走形式,结果模型上线后连续三周预测值系统性偏高,复盘才发现训练集里根本没做季节性分解,原始序列带着强年度周期,却直接喂给了一个默认假设“平稳”的LSTM。这篇内容,就是帮你把“Time Series Modeling: Related Terms and Concepts”这个看似枯燥的标题,变成一张可操作、可验证、能救命的实战语言地图。它不讲定义背诵,只讲每个词背后对应着你鼠标点下的哪一步操作、数据里哪一段异常波动、模型输出里哪一个诊断图。适合刚学完ARIMA公式但一上手就懵的新手,也适合做了三年预测但总在模型解释环节被业务方问住的工程师。核心关键词——时间序列、平稳性、自相关、差分、白噪声、季节性、趋势、残差分析、模型诊断——每一个都会落到你明天打开Jupyter Notebook时要写的那行代码、要看的那个plot上。
2. 内容整体设计与思路拆解:为什么必须用“问题驱动”来重构概念体系
2.1 拒绝字典式学习:概念必须绑定具体故障场景
传统教材或课程讲时间序列概念,习惯按“定义→性质→数学表达”推进。比如讲“平稳性”,先给严平稳/宽平稳的数学定义,再推导协方差函数。这种路径在考试中有效,但在真实建模中是灾难性的。我见过太多人能默写出宽平稳的三个条件(均值恒定、方差有限、自协方差仅与滞后有关),但拿到一份销售数据,看到Q4销量突然翻倍,第一反应却是“赶紧调高LSTM的隐藏层维度”,而不是意识到:“等等,这个突变大概率破坏了二阶平稳性,得先做季节性调整或引入外生变量”。所以本篇的设计逻辑彻底倒置:所有概念,必须从一个具体的、高频发生的建模故障出发。例如,“差分”这个概念,我们不从“使序列平稳”这个目标讲起,而是从一个真实案例切入:某物流公司的日均订单量序列,用ARIMA(1,1,1)拟合后,AIC值很低,但预测未来7天时,第3天开始预测值一路向下崩塌,最后一天预测值接近零。复盘发现,原始序列存在缓慢上升的线性趋势,一阶差分后得到的“变化量”序列本身又出现了新的、更隐蔽的二次趋势。这时候,“差分”就不再是抽象操作,而是你必须判断“差几次?差完还稳不稳?差完的序列有没有新问题?”的决策链条。每一个术语,都对应着你在建模流水线上必须停下来的质检站。
2.2 概念分层:区分“数据固有属性”、“人为处理动作”和“模型诊断信号”
时间序列领域大量术语混用,导致沟通低效。比如“趋势”这个词,在不同语境下指代完全不同东西:有时指原始数据中肉眼可见的长期上扬(数据固有属性),有时指STL分解后提取出的趋势分量(人为处理动作的结果),有时又指模型残差图中持续存在的斜线(模型诊断信号)。如果不做严格区分,团队协作时就会出现鸡同鸭讲。因此,本篇将全部核心概念划分为三个互斥层级:
数据固有属性层:描述原始序列本身携带的统计特征,如趋势、季节性、周期性、非平稳性、自相关性。它们是客观存在的,不因你的建模动作而改变,但会决定你后续所有动作的方向。判断它们,依赖的是可视化(时序图、ACF/PACF图)和统计检验(ADF、KPSS)。
人为处理动作层:你为了满足模型假设或提升效果而主动施加的操作,如差分、对数变换、STL分解、Box-Cox变换、滑动窗口标准化。每一个动作都有明确的前置条件(比如“只有当ADF检验p值>0.05时,才需要一阶差分”)和后置验证(“差分后必须重新画ACF图,确认拖尾现象消失”)。
模型诊断信号层:模型训练完成后,从残差中读取的反馈信息,如白噪声、残差自相关、异方差性、残差正态性。它们不是数据本身的属性,而是模型是否“学到位”的体检报告。比如残差呈现明显自相关,说明模型漏掉了序列中的某种动态模式,必须回头检查是否遗漏了季节性项或外部变量。
这种分层,直接决定了你每天的工作流:先花80%时间在“数据固有属性层”做勘探(看图、检验、记录结论),再用15%时间在“人为处理动作层”做施工(选方法、调参数、验证效果),最后用5%时间在“模型诊断信号层”做验收(画残差图、跑Ljung-Box检验)。我经手的项目里,凡是跳过第一层、直接冲进第二层的,100%返工。
2.3 工具链锚定:每个概念必须关联到Python中一行可执行的代码
概念落地的终极标准,是你能否用代码把它“揪出来”。本篇拒绝空谈,所有关键概念都绑定到statsmodels、pmdarima、scikit-learn等主流库中最常用、最不易出错的实现方式。例如,“自相关”不只讲定义,而是明确告诉你:
- 看图:
from statsmodels.tsa.stattools import plot_acf; plot_acf(series, lags=40) - 定量:
from statsmodels.tsa.stattools import acf; acf_values = acf(series, nlags=20) - 检验:
from statsmodels.stats.diagnostic import acorr_ljungbox; lb_test = acorr_ljungbox(residuals, lags=[10], return_df=True)
这种绑定,确保你读完就能立刻在自己数据上验证。我坚持认为,一个没在Jupyter里亲手跑过adfuller()并看懂返回的p-value和test_statistic含义的人,就不算真正理解“平稳性”。概念的生命力,永远在代码的执行结果里,不在PPT的动画效果中。
3. 核心细节解析与实操要点:拆解7个高频误用概念的致命细节
3.1 平稳性:不是“看起来不动”,而是“统计规律不变”
“平稳性”是时间序列建模的基石,也是被误解最深的概念。新手常犯的错误是:把时序图上“波动不大”的序列当成平稳序列。我曾接手一个气象站的小时温度数据,时序图看起来非常“安静”,波动范围就±2℃,大家理所当然认为它是平稳的,直接上了ARMA模型。结果预测误差巨大。深入分析才发现,该序列存在极强的日周期(白天升温、夜晚降温),且这种周期的幅度随季节变化——夏天日温差大,冬天小。这就是典型的二阶非平稳:均值虽稳定,但方差(波动幅度)随时间系统性变化。真正的平稳性检验,必须通过三重验证:
可视化初筛:画原始序列图,重点看三点——均值线是否水平(无趋势)、波动带宽是否均匀(无异方差)、是否有重复出现的固定间隔模式(季节性)。注意:肉眼“觉得平稳”不可靠,尤其当数据量大时,微弱趋势会被噪声掩盖。
统计检验定论:必须跑至少两个互补检验。ADF检验(Augmented Dickey-Fuller)原假设是“存在单位根(即非平稳)”,p值<0.05才拒绝原假设,认为平稳;KPSS检验(Kwiatkowski-Phillips-Schmidt-Shin)原假设是“平稳”,p值>0.05才接受平稳。我坚持双检验,因为ADF对趋势敏感但对季节性不敏感,KPSS反之。当两者结论冲突(如ADF说平稳、KPSS说非平稳),说明数据存在复杂结构,必须进一步分解。
ACF图终审:平稳序列的ACF图必须快速衰减至零(通常2-3阶后进入置信区间)。如果ACF拖着长尾巴缓慢下降,无论ADF p值多小,都说明存在未被捕捉的长期依赖,需警惕。我在风电预测项目中就遇到过:ADF p=0.001,但ACF在lag=24(一天)处仍有显著峰值,最终发现是未处理的日周期残留。
提示:平稳性检验的样本量要求常被忽视。ADF检验在小样本(n<50)下功效极低,容易把非平稳序列误判为平稳。我的硬性规则是:样本量<100时,绝不单凭ADF下结论,必须结合STL分解看趋势分量。
3.2 自相关与偏自相关:ACF/PACF图不是装饰画,是模型阶数的密码本
ACF(自相关函数)和PACF(偏自相关函数)图是ARIMA建模的“罗盘”,但多数人只会死记硬背“AR看PACF截尾、MA看ACF截尾”。这在理想白噪声数据上可行,但在真实世界中,噪声从来不是白色的。我处理过一个零售SKU的周销量数据,ACF图显示lag=1到lag=5都显著,PACF在lag=1后缓慢衰减。按教科书,这该是AR过程,但强行拟合AR(5)后,残差检验失败。深挖发现,该SKU存在4周的促销周期,但促销效应有滞后——本周促销,影响持续到下周。这导致ACF在lag=4和lag=5处出现双峰。此时,ACF的“拖尾”不是AR的特征,而是未建模的季节性+滞后效应的混合信号。正确做法是:先用seasonal_decompose移除4周季节性,再看残差的ACF/PACF。结果残差ACF在lag=1截尾,PACF拖尾,这才符合MA(1)的特征。
实操中,解读ACF/PACF必须结合业务背景:
- lag=1显著:几乎必然存在短期惯性(如用户今日购买,明日复购概率高),这是MA(1)或AR(1)的强信号。
- lag=k显著(k>1):优先怀疑是否存在k周期的业务节奏(如周数据中lag=7、月数据中lag=12),而非直接套用ARMA阶数。
- ACF/PACF双拖尾:大概率存在未处理的趋势或季节性,或数据本身是分数阶差分(fractional differencing)过程,此时ARIMA可能不是最优选择,应转向Prophet或深度学习。
注意:ACF/PACF图的置信区间计算默认基于正态分布假设,但真实金融或IoT数据常有厚尾。我习惯手动加宽置信带:
plot_acf(series, lags=40, alpha=0.02)(alpha=0.02对应98%置信度),避免把噪声峰误判为信号。
3.3 差分:不是“差一次就万事大吉”,而是“差到刚好为止”的精细手术
差分是让非平稳序列变平稳的最常用手段,但“过度差分”比“差分不足”危害更大。过度差分的典型症状是:模型预测值震荡加剧,且预测区间(prediction interval)异常宽。这是因为差分本质是放大噪声——原始序列的微小测量误差,在差分后被转化为大幅波动。我在一个传感器振动频率预测项目中吃过亏:原始序列有缓慢上升趋势,ADF p=0.3,我果断一阶差分。差分后ADF p=0.002,看起来完美。但模型上线后,预测的振动频率在正常值上下剧烈抖动,远超设备安全阈值。复盘发现,一阶差分后序列的方差扩大了3.2倍,而模型把这部分放大的噪声当成了真实动态。解决方案是改用局部线性趋势拟合:用滚动窗口(如30天)拟合线性趋势,用原始值减去趋势值,而非简单差分。这样既消除了趋势,又最大程度保留了原始波动信息。
差分操作必须遵循“最小必要原则”:
- 目标明确:差分只为消除特定非平稳源。如果是线性趋势,一阶差分足够;如果是二次趋势,一阶差分后ACF仍拖尾,则需二阶差分;但二阶差分后若ACF在lag=1处出现负相关峰,就是过度差分的警报。
- 验证闭环:每次差分后,必须重新跑ADF/KPSS,并画ACF图。我有个铁律:差分后的ACF图,前3阶必须全部落入置信区间,且无明显模式。
- 逆变换谨慎:差分后的预测值需积分还原。
numpy.cumsum()是常用方法,但必须确保初始值准确。我习惯保存差分前的最后一个原始值y_t,预测差分值Δy_{t+1}后,用y_{t+1} = y_t + Δy_{t+1}还原,而非对整个差分序列cumsum,避免累积误差。
3.4 季节性与周期性:业务节奏才是你的“季节”,不是日历
“季节性”在统计学中常被简化为“固定周期的重复模式”,如月度数据的12周期、周度数据的7周期。但真实业务中,季节性远比日历复杂。我做过一个跨境电商平台的GMV预测,按日历,它有明显的“周季节性”(周末高、工作日低)和“年季节性”(黑五、圣诞高峰)。但上线模型后,Q2预测持续偏低。排查发现,该平台在Q2主推“春季焕新”活动,活动周期是每两周一轮,且每轮活动持续5天。这种由运营策略驱动的、非日历固定的“伪季节性”,在ACF图上表现为lag=14和lag=19(14+5)处的峰值,完全淹没在日历季节性的lag=7和lag=14中。此时,硬套SARIMA的12或7周期会失败。
破解业务季节性的关键是剥离日历,回归动作:
- 列出所有已知业务节奏:促销周期、产品上新节奏、财报发布日、行业展会时间、甚至员工发薪日(影响消费)。
- 用频谱分析定位真实周期:
scipy.signal.periodogram能找出序列功率谱中的主导频率。比ACF更鲁棒,尤其对非正弦波形的业务节奏。 - 外生变量替代:对无法用固定周期建模的节奏,直接构造二元变量(如
is_promotion_day=1)或强度变量(如promotion_discount_rate)作为模型输入。在XGBoost或神经网络中,这比强行塞进SARIMA的季节性参数更有效。
实操心得:不要迷信“自动季节性检测”。
pmdarima.auto_arima的m参数常推荐错误值。我一律关闭自动检测,先用seasonal_decompose目视确认,再用periodogram定量验证,最后用业务知识交叉核对。
3.5 白噪声:不是“看起来像噪声”,而是“统计上不可预测”
“白噪声”常被误认为“残差图看起来毛茸茸的、没规律”。但统计学上的白噪声有严格定义:均值为零、方差恒定、任意两时刻的值不相关(即自相关系数为零)。一个常见的致命错误是:模型残差ACF图上所有lags都在置信区间内,大家就欢呼“白噪声达成”,然后收工。但我在一个电力负荷预测项目中发现,残差ACF确实“干净”,但残差的绝对值序列(|residual|)却有强自相关——这意味着模型对大误差的预测不稳定,高估时总高估,低估时总低估。这违反了白噪声的“方差恒定”假设,属于条件异方差(Conditional Heteroskedasticity),是GARCH类模型的典型场景。
验证白噪声必须四步走:
- 均值检验:
ttest_1samp(residuals, popmean=0),p值>0.05。 - 方差稳定性:画残差滚动标准差图,或用
statsmodels.stats.api.het_arch检验ARCH效应。 - 自相关检验:
acorr_ljungbox(residuals, lags=[10,20,30], return_df=True),所有lags的p值都>0.05。 - 正态性辅助:
scipy.stats.shapiro(residuals),p>0.05(虽非白噪声必要条件,但正态残差让置信区间更可靠)。
注意:Ljung-Box检验对小样本敏感。当残差量<100时,我改用
acorr_breusch_godfrey(Breusch-Godfrey检验),它对小样本更稳健,且能检验高阶自相关。
3.6 趋势:不是“画条直线拟合”,而是“识别生成机制”
趋势常被简单等同于“线性增长”,于是大家一股脑用np.polyfit(x, y, 1)拟合一条直线。但真实数据的趋势生成机制千差万别:
- 线性趋势:如某工厂设备老化导致的每日产量微降(恒定速率)。
- 指数趋势:如病毒传播初期的感染人数(增长率恒定)。
- 分段趋势:如政策调整后,经济指标在某时间点发生永久性跃迁。
- 随机游走趋势:如股票价格,其“趋势”本质是累积的随机冲击,无法用确定性函数描述。
错误识别趋势机制,会导致灾难性外推。我处理过一个SaaS公司ARR(年度经常性收入)数据,过去3年呈近似直线增长,大家就用线性趋势外推明年。结果新CEO上任后砍掉低毛利产品线,ARR增速断崖下跌。问题根源在于,原趋势是“产品组合扩张”驱动的,而非公司内在增长引擎。正确的趋势分析,必须回答:“这个趋势是由什么业务动作驱动的?该动作在未来是否可持续?”——这需要和业务方深度访谈,而非只看数据。
技术上,我采用“机制导向”的趋势识别流程:
- 先看对数序列:若
log(series)呈线性,则原序列是指数趋势,应建模log(y_t)。 - 再看一阶差分:若
diff(series)平稳且均值≠0,则原序列是线性趋势,可用detrend或ARIMA的d=1。 - 最后用结构突变检验:
statsmodels.tsa.regime_switching.markov_regression或ruptures库检测趋势转折点。找到转折点后,分段建模比全局拟合更鲁棒。
3.7 残差分析:不是模型结束的句号,而是下一轮迭代的起点
残差分析常被当作建模的“收尾工作”,但在我这里,它是整个流程的“心脏监护仪”。一个健康的残差,必须同时满足四个维度:
- 统计维度:白噪声(前述四步检验)。
- 时间维度:残差在时间轴上无聚集性(如连续多天高估),用
runstest_1samp检验游程。 - 幅度维度:残差绝对值与预测值大小无关(无异方差),画
residuals vs fitted散点图,应呈均匀云状,而非喇叭形。 - 业务维度:残差在关键业务时段(如促销日、财报日)无系统性偏差。我必做分组t检验:
ttest_ind(residuals[is_promo], residuals[~is_promo]),若p<0.05,说明模型对促销场景建模不足。
一次深刻的教训:某快递公司的“次日达率”预测,残差统计检验全通过,但业务方反馈“大促期间预测总是太乐观”。我画了残差vs促销强度散点图,发现促销强度>0.8时,残差均值为-5%(系统性高估)。根源是模型没纳入“分拨中心饱和度”这一关键外生变量。加入后,残差在高促销强度区间的均值回归到-0.2%,业务方终于点头。
关键技巧:残差分析必须“分层下钻”。先看总体,再按时间(周几、月份)、按业务维度(渠道、区域、产品线)切片分析。我用
pandas.cut和groupby自动化这一步,每天生成残差健康报告,比任何KPI dashboard都更能预警模型衰减。
4. 实操过程与核心环节实现:从原始数据到可交付模型的完整流水线
4.1 流水线全景:7个不可跳过的硬性节点
我把时间序列建模固化为一条7节点流水线,每个节点都有明确输入、输出、通过标准和失败回滚路径。这不是理论框架,而是我每天在GitLab CI里跑的checklist:
| 节点 | 名称 | 输入 | 输出 | 通过标准 | 失败回滚 |
|---|---|---|---|---|---|
| 1 | 数据勘探 | 原始CSV | 探勘报告(含时序图、缺失值热力图、基础统计) | 报告中必须标注所有可疑点(如突变点、长缺失段) | 返回数据源,清洗或打标签 |
| 2 | 固有属性诊断 | 探勘报告 | 属性清单(趋势类型、季节性周期、平稳性结论、ACF/PACF关键lag) | 所有结论必须有图/表/检验p值支撑,无主观描述 | 重新运行诊断,或增加业务访谈 |
| 3 | 预处理决策 | 属性清单 | 预处理方案(差分阶数、变换类型、分解方法、外生变量列表) | 方案必须注明每一步的“为什么”,如“因KPSS p=0.01,故需一阶差分” | 回退到节点2,补充诊断 |
| 4 | 特征工程 | 预处理方案 | 特征矩阵(含滞后特征、滚动统计、外生变量) | 特征间相关系数矩阵最大值<0.95,无多重共线性 | 移除冗余特征,或改用PCA |
| 5 | 模型拟合 | 特征矩阵 | 训练好的模型对象、训练误差(RMSE/MAE) | 训练误差在历史基准范围内(如比简单移动平均低15%) | 切换模型族(如从ARIMA切到Prophet) |
| 6 | 残差深度诊断 | 模型对象、测试集 | 残差健康报告(四维检验结果、分组偏差表) | 所有四维检验通过,且关键业务分组偏差<1% | 返回节点3,调整预处理或节点4,增强特征 |
| 7 | 业务验证 | 残差报告、业务场景列表 | 业务方签字确认的验证记录 | 至少3个核心业务场景(如大促、新品上市)的预测误差<业务容忍阈值 | 返回节点5,微调模型或增加场景特化训练 |
这条流水线强制把模糊的概念讨论,转化为可审计、可追溯的动作。例如,“季节性”不再是一个名词,而是节点2中必须填写的“周期=7,来源=业务日历,验证=ACF lag=7峰值=0.62,p<0.001”。
4.2 节点2实操:固有属性诊断的完整代码与解读
以下是我每天运行的标准诊断脚本,已封装为ts_diagnose.py,输入原始序列series,输出结构化结论:
import numpy as np import pandas as pd import matplotlib.pyplot as plt from statsmodels.tsa.stattools import adfuller, kpss, acf, pacf from statsmodels.tsa.seasonal import seasonal_decompose from scipy import signal from statsmodels.stats.diagnostic import acorr_ljungbox def diagnose_series(series, freq='D', max_lag=50): """ 全面诊断时间序列固有属性 :param series: pd.Series, 索引为datetime :param freq: str, 数据频率 ('D'日, 'W'周, 'M'月) :param max_lag: int, ACF/PACF最大滞后阶数 :return: dict, 包含所有诊断结论 """ results = {} # 1. 可视化初筛 fig, axes = plt.subplots(2, 2, figsize=(15, 10)) series.plot(ax=axes[0,0], title="原始序列") axes[0,0].axhline(y=series.mean(), color='r', linestyle='--', alpha=0.7) # ACF/PACF plot_acf(series, ax=axes[0,1], lags=max_lag, alpha=0.02) axes[0,1].set_title("ACF图 (alpha=0.02)") plot_pacf(series, ax=axes[1,0], lags=max_lag, alpha=0.02) axes[1,0].set_title("PACF图 (alpha=0.02)") # 季节性分解(自动选择周期) if freq == 'D': period = 7 elif freq == 'W': period = 52 else: period = 12 try: decomp = seasonal_decompose(series, model='additive', period=period) decomp.trend.plot(ax=axes[1,1], title=f"趋势分量 (周期={period})") except: axes[1,1].text(0.5, 0.5, "分解失败\n尝试其他周期", ha='center', va='center') plt.tight_layout() plt.show() # 2. 平稳性检验 adf_result = adfuller(series) kpss_result = kpss(series) results['stationarity'] = { 'adf_pvalue': adf_result[1], 'adf_critical_values': adf_result[4], 'kpss_pvalue': kpss_result[1], 'kpss_critical_values': kpss_result[3] } # 3. 自相关量化 acf_vals = acf(series, nlags=max_lag) pacf_vals = pacf(series, nlags=max_lag) # 找显著lag(超出98%置信区间) acf_significant = np.where(np.abs(acf_vals) > 1.96/np.sqrt(len(series)))[0] pacf_significant = np.where(np.abs(pacf_vals) > 1.96/np.sqrt(len(series)))[0] results['autocorrelation'] = { 'significant_acf_lags': acf_significant.tolist(), 'significant_pacf_lags': pacf_significant.tolist(), 'max_acf_lag': int(acf_significant[0]) if len(acf_significant) > 0 else None } # 4. 季节性频谱分析 f, Pxx = signal.periodogram(series, fs=1, nfft=1024) # 找功率谱峰值(排除f=0的直流分量) peaks, _ = signal.find_peaks(Pxx[1:], height=np.max(Pxx[1:])*0.3) if len(peaks) > 0: dominant_period = int(1/f[peaks[0]+1]) results['seasonality'] = {'dominant_period': dominant_period, 'power': Pxx[peaks[0]+1]} else: results['seasonality'] = {'dominant_period': None} # 5. 趋势强度估算 # 线性拟合斜率 x = np.arange(len(series)) slope, _, _, _, _ = scipy.stats.linregress(x, series) results['trend'] = {'slope': slope, 'abs_slope_ratio': abs(slope)/series.std()} return results # 使用示例 # diagnosis = diagnose_series(your_series, freq='D') # print(diagnosis)这段代码的关键设计点:
- 可视化先行:强制先看图,避免“先入为主”。时序图上的红色均值线,能瞬间暴露趋势。
- 双检验绑定:ADF和KPSS结果同屏展示,结论冲突时一目了然。
- ACF/PACF置信度升级:
alpha=0.02(98%置信)比默认95%更严格,减少噪声误判。 - 季节性双验证:既用
seasonal_decompose的直观分解,又用periodogram的客观频谱,交叉验证。 - 趋势量化:不只给斜率,还给
abs_slope_ratio(斜率绝对值/标准差),衡量趋势强度相对于噪声的显著性。比单纯看斜率数值更有意义。
运行后,diagnosis字典就是你的“概念事实库”,每个键值对都对应一个可验证的结论。例如,diagnosis['stationarity']['adf_pvalue'] < 0.05就是“平稳性”概念的落地凭证。
4.3 节点6实操:残差深度诊断的四维检验代码
残差诊断是流水线中最易被简化的环节。以下是我生产环境使用的residual_diagnose.py,它把前述四维检验全部自动化:
import numpy as np import pandas as pd import matplotlib.pyplot as plt from scipy import stats from statsmodels.stats.diagnostic import acorr_ljungbox, acorr_breusch_godfrey from statsmodels.stats.api import het_arch from statsmodels.tsa.stattools import adfuller def residual_diagnosis(y_true, y_pred, residuals, groups=None): """ 残差四维深度诊断 :param y_true: 真实值数组 :param y_pred: 预测值数组 :param residuals: 残差数组 (y_true - y_pred) :param groups: 分组标签数组,用于业务维度检验 :return: dict, 诊断报告 """ report = {} # 维度1:统计检验(白噪声) # 1.1 均值检验 ttest = stats.ttest_1samp(residuals, popmean=0) report['mean_test'] = {'pvalue': ttest.pvalue, 'pass': ttest.pvalue > 0.05} # 1.2 Ljung-Box自相关检验(多阶) lb_test = acorr_ljungbox(residuals, lags=[10, 20, 30], return_df=True) report['lb_test'] = { 'pvalues': lb_test['lb_pvalue'].to_dict(), 'pass': lb_test['lb_pvalue'].min() > 0.05 } # 1.3 ARCH效应检验(方差稳定性) arch_test = het_arch(residuals) report['arch_test'] = {'pvalue': arch_test[1], 'pass': arch_test[1] > 0.05} # 维度2:时间维度(游程检验) # 将残差符号化:正为1,负为-1,零为0 signs = np.sign(residuals) # 计算游程数 runs = 1 for i in range(1, len(signs)): if signs[i] != signs[i-1]: runs += 1 # 游程检验 n_plus = np.sum(signs == 1) n_minus = np.sum(signs == -1) expected_runs = (2 * n_plus * n_minus) / (n_plus + n_minus) + 1 std_runs = np.sqrt((2 * n_plus * n_minus * (2 * n_plus * n_minus - n_plus - n_minus)) / ((n_plus + n_minus)**2 * (n_plus + n_minus - 1))) z_score = (runs - expected_runs) / std_runs run_pvalue = 2 * (1 - stats.norm.cdf(abs(z_score))) report['run_test'] = {'pvalue': run_pvalue, 'pass': run_pvalue > 0.05} # 维度3:幅度维度(异方差) # 残差绝对值 vs 预测值散点图 + Spearman相关 corr_coef, corr_p = stats.spearmanr(np.abs(residuals), y_pred) report['heteroskedasticity'] = { 'spearman_corr': corr_coef, 'pvalue': corr_p, 'pass': corr_p > 0.05 } # 维度4:业务维度(分组t检验) if groups is not None: unique_groups = np.unique(groups) group_tests = {} for g in unique_groups: group_resid = residuals[groups == g] other_resid = residuals[groups != g] ttest_group = stats.ttest_ind(group_resid, other_resid, equal_var=False) group_tests[str(g)] = { 'pvalue': ttest_group.pvalue, 'mean_diff': np.mean(group_resid) - np.mean(other_resid), 'pass': ttest_group.pvalue > 0.05 } report['business_test'] = group_tests # 综合结论 all_pass = True for dim in ['mean_test', 'lb_test', 'arch_test', 'run_test', 'heterosked