时间序列建模实战语言地图:从平稳性、自相关到残差诊断的可操作指南

📅 2026/7/14 12:48:41
时间序列建模实战语言地图:从平稳性、自相关到残差诊断的可操作指南
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, lags40)定量from statsmodels.tsa.stattools import acf; acf_values acf(series, nlags20)检验from statsmodels.stats.diagnostic import acorr_ljungbox; lb_test acorr_ljungbox(residuals, lags[10], return_dfTrue)这种绑定确保你读完就能立刻在自己数据上验证。我坚持认为一个没在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 p0.001但ACF在lag24一天处仍有显著峰值最终发现是未处理的日周期残留。提示平稳性检验的样本量要求常被忽视。ADF检验在小样本n50下功效极低容易把非平稳序列误判为平稳。我的硬性规则是样本量100时绝不单凭ADF下结论必须结合STL分解看趋势分量。3.2 自相关与偏自相关ACF/PACF图不是装饰画是模型阶数的密码本ACF自相关函数和PACF偏自相关函数图是ARIMA建模的“罗盘”但多数人只会死记硬背“AR看PACF截尾、MA看ACF截尾”。这在理想白噪声数据上可行但在真实世界中噪声从来不是白色的。我处理过一个零售SKU的周销量数据ACF图显示lag1到lag5都显著PACF在lag1后缓慢衰减。按教科书这该是AR过程但强行拟合AR(5)后残差检验失败。深挖发现该SKU存在4周的促销周期但促销效应有滞后——本周促销影响持续到下周。这导致ACF在lag4和lag5处出现双峰。此时ACF的“拖尾”不是AR的特征而是未建模的季节性滞后效应的混合信号。正确做法是先用seasonal_decompose移除4周季节性再看残差的ACF/PACF。结果残差ACF在lag1截尾PACF拖尾这才符合MA(1)的特征。实操中解读ACF/PACF必须结合业务背景lag1显著几乎必然存在短期惯性如用户今日购买明日复购概率高这是MA(1)或AR(1)的强信号。lagk显著k1优先怀疑是否存在k周期的业务节奏如周数据中lag7、月数据中lag12而非直接套用ARMA阶数。ACF/PACF双拖尾大概率存在未处理的趋势或季节性或数据本身是分数阶差分fractional differencing过程此时ARIMA可能不是最优选择应转向Prophet或深度学习。注意ACF/PACF图的置信区间计算默认基于正态分布假设但真实金融或IoT数据常有厚尾。我习惯手动加宽置信带plot_acf(series, lags40, alpha0.02)alpha0.02对应98%置信度避免把噪声峰误判为信号。3.3 差分不是“差一次就万事大吉”而是“差到刚好为止”的精细手术差分是让非平稳序列变平稳的最常用手段但“过度差分”比“差分不足”危害更大。过度差分的典型症状是模型预测值震荡加剧且预测区间prediction interval异常宽。这是因为差分本质是放大噪声——原始序列的微小测量误差在差分后被转化为大幅波动。我在一个传感器振动频率预测项目中吃过亏原始序列有缓慢上升趋势ADF p0.3我果断一阶差分。差分后ADF p0.002看起来完美。但模型上线后预测的振动频率在正常值上下剧烈抖动远超设备安全阈值。复盘发现一阶差分后序列的方差扩大了3.2倍而模型把这部分放大的噪声当成了真实动态。解决方案是改用局部线性趋势拟合用滚动窗口如30天拟合线性趋势用原始值减去趋势值而非简单差分。这样既消除了趋势又最大程度保留了原始波动信息。差分操作必须遵循“最小必要原则”目标明确差分只为消除特定非平稳源。如果是线性趋势一阶差分足够如果是二次趋势一阶差分后ACF仍拖尾则需二阶差分但二阶差分后若ACF在lag1处出现负相关峰就是过度差分的警报。验证闭环每次差分后必须重新跑ADF/KPSS并画ACF图。我有个铁律差分后的ACF图前3阶必须全部落入置信区间且无明显模式。逆变换谨慎差分后的预测值需积分还原。numpy.cumsum()是常用方法但必须确保初始值准确。我习惯保存差分前的最后一个原始值y_t预测差分值Δy_{t1}后用y_{t1} y_t Δy_{t1}还原而非对整个差分序列cumsum避免累积误差。3.4 季节性与周期性业务节奏才是你的“季节”不是日历“季节性”在统计学中常被简化为“固定周期的重复模式”如月度数据的12周期、周度数据的7周期。但真实业务中季节性远比日历复杂。我做过一个跨境电商平台的GMV预测按日历它有明显的“周季节性”周末高、工作日低和“年季节性”黑五、圣诞高峰。但上线模型后Q2预测持续偏低。排查发现该平台在Q2主推“春季焕新”活动活动周期是每两周一轮且每轮活动持续5天。这种由运营策略驱动的、非日历固定的“伪季节性”在ACF图上表现为lag14和lag19145处的峰值完全淹没在日历季节性的lag7和lag14中。此时硬套SARIMA的12或7周期会失败。破解业务季节性的关键是剥离日历回归动作列出所有已知业务节奏促销周期、产品上新节奏、财报发布日、行业展会时间、甚至员工发薪日影响消费。用频谱分析定位真实周期scipy.signal.periodogram能找出序列功率谱中的主导频率。比ACF更鲁棒尤其对非正弦波形的业务节奏。外生变量替代对无法用固定周期建模的节奏直接构造二元变量如is_promotion_day1或强度变量如promotion_discount_rate作为模型输入。在XGBoost或神经网络中这比强行塞进SARIMA的季节性参数更有效。实操心得不要迷信“自动季节性检测”。pmdarima.auto_arima的m参数常推荐错误值。我一律关闭自动检测先用seasonal_decompose目视确认再用periodogram定量验证最后用业务知识交叉核对。3.5 白噪声不是“看起来像噪声”而是“统计上不可预测”“白噪声”常被误认为“残差图看起来毛茸茸的、没规律”。但统计学上的白噪声有严格定义均值为零、方差恒定、任意两时刻的值不相关即自相关系数为零。一个常见的致命错误是模型残差ACF图上所有lags都在置信区间内大家就欢呼“白噪声达成”然后收工。但我在一个电力负荷预测项目中发现残差ACF确实“干净”但残差的绝对值序列|residual|却有强自相关——这意味着模型对大误差的预测不稳定高估时总高估低估时总低估。这违反了白噪声的“方差恒定”假设属于条件异方差Conditional Heteroskedasticity是GARCH类模型的典型场景。验证白噪声必须四步走均值检验ttest_1samp(residuals, popmean0)p值0.05。方差稳定性画残差滚动标准差图或用statsmodels.stats.api.het_arch检验ARCH效应。自相关检验acorr_ljungbox(residuals, lags[10,20,30], return_dfTrue)所有lags的p值都0.05。正态性辅助scipy.stats.shapiro(residuals)p0.05虽非白噪声必要条件但正态残差让置信区间更可靠。注意Ljung-Box检验对小样本敏感。当残差量100时我改用acorr_breusch_godfreyBreusch-Godfrey检验它对小样本更稳健且能检验高阶自相关。3.6 趋势不是“画条直线拟合”而是“识别生成机制”趋势常被简单等同于“线性增长”于是大家一股脑用np.polyfit(x, y, 1)拟合一条直线。但真实数据的趋势生成机制千差万别线性趋势如某工厂设备老化导致的每日产量微降恒定速率。指数趋势如病毒传播初期的感染人数增长率恒定。分段趋势如政策调整后经济指标在某时间点发生永久性跃迁。随机游走趋势如股票价格其“趋势”本质是累积的随机冲击无法用确定性函数描述。错误识别趋势机制会导致灾难性外推。我处理过一个SaaS公司ARR年度经常性收入数据过去3年呈近似直线增长大家就用线性趋势外推明年。结果新CEO上任后砍掉低毛利产品线ARR增速断崖下跌。问题根源在于原趋势是“产品组合扩张”驱动的而非公司内在增长引擎。正确的趋势分析必须回答“这个趋势是由什么业务动作驱动的该动作在未来是否可持续”——这需要和业务方深度访谈而非只看数据。技术上我采用“机制导向”的趋势识别流程先看对数序列若log(series)呈线性则原序列是指数趋势应建模log(y_t)。再看一阶差分若diff(series)平稳且均值≠0则原序列是线性趋势可用detrend或ARIMA的d1。最后用结构突变检验statsmodels.tsa.regime_switching.markov_regression或ruptures库检测趋势转折点。找到转折点后分段建模比全局拟合更鲁棒。3.7 残差分析不是模型结束的句号而是下一轮迭代的起点残差分析常被当作建模的“收尾工作”但在我这里它是整个流程的“心脏监护仪”。一个健康的残差必须同时满足四个维度统计维度白噪声前述四步检验。时间维度残差在时间轴上无聚集性如连续多天高估用runstest_1samp检验游程。幅度维度残差绝对值与预测值大小无关无异方差画residuals vs fitted散点图应呈均匀云状而非喇叭形。业务维度残差在关键业务时段如促销日、财报日无系统性偏差。我必做分组t检验ttest_ind(residuals[is_promo], residuals[~is_promo])若p0.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 p0.01故需一阶差分”回退到节点2补充诊断4特征工程预处理方案特征矩阵含滞后特征、滚动统计、外生变量特征间相关系数矩阵最大值0.95无多重共线性移除冗余特征或改用PCA5模型拟合特征矩阵训练好的模型对象、训练误差RMSE/MAE训练误差在历史基准范围内如比简单移动平均低15%切换模型族如从ARIMA切到Prophet6残差深度诊断模型对象、测试集残差健康报告四维检验结果、分组偏差表所有四维检验通过且关键业务分组偏差1%返回节点3调整预处理或节点4增强特征7业务验证残差报告、业务场景列表业务方签字确认的验证记录至少3个核心业务场景如大促、新品上市的预测误差业务容忍阈值返回节点5微调模型或增加场景特化训练这条流水线强制把模糊的概念讨论转化为可审计、可追溯的动作。例如“季节性”不再是一个名词而是节点2中必须填写的“周期7来源业务日历验证ACF lag7峰值0.62p0.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, freqD, max_lag50): 全面诊断时间序列固有属性 :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(axaxes[0,0], title原始序列) axes[0,0].axhline(yseries.mean(), colorr, linestyle--, alpha0.7) # ACF/PACF plot_acf(series, axaxes[0,1], lagsmax_lag, alpha0.02) axes[0,1].set_title(ACF图 (alpha0.02)) plot_pacf(series, axaxes[1,0], lagsmax_lag, alpha0.02) axes[1,0].set_title(PACF图 (alpha0.02)) # 季节性分解自动选择周期 if freq D: period 7 elif freq W: period 52 else: period 12 try: decomp seasonal_decompose(series, modeladditive, periodperiod) decomp.trend.plot(axaxes[1,1], titlef趋势分量 (周期{period})) except: axes[1,1].text(0.5, 0.5, 分解失败\n尝试其他周期, hacenter, vacenter) 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, nlagsmax_lag) pacf_vals pacf(series, nlagsmax_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, fs1, nfft1024) # 找功率谱峰值排除f0的直流分量 peaks, _ signal.find_peaks(Pxx[1:], heightnp.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, freqD) # print(diagnosis)这段代码的关键设计点可视化先行强制先看图避免“先入为主”。时序图上的红色均值线能瞬间暴露趋势。双检验绑定ADF和KPSS结果同屏展示结论冲突时一目了然。ACF/PACF置信度升级alpha0.0298%置信比默认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, groupsNone): 残差四维深度诊断 :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, popmean0) 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_dfTrue) 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_varFalse) 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