
1. 项目概述为什么线性回归的“假设验证”不是可选项而是必修课我带过不少刚入行的数据分析新人也帮不少业务部门同事搭过预测模型。最常听到的一句话是“模型R²有0.85了应该能用了。”——然后上线跑了一周业务反馈“预测值忽高忽低完全没法做排产/预算/备货”。追问下去八成没做过残差诊断。线性回归看起来简单就一条直线但它的背后是一整套统计学契约只有当数据真正“守约”这条直线才值得被信任。否则它不是在拟合规律而是在拟合噪声甚至是在制造幻觉。这五个假设——线性关系、无多重共线性、残差正态性、同方差性、无自相关性——不是教科书里的装饰性条款而是模型输出可信区间的数学地基。你跳过验证就像盖楼不打地基表面光鲜一遇风新数据就晃。我见过最典型的翻车现场销售预测模型在训练集上误差极小但到了季度末冲刺阶段预测值系统性偏低20%原因就是残差存在明显异方差——大客户订单的波动远大于中小客户而模型把这种结构性差异当成了随机误差。这篇文章就是一份我在真实项目中反复打磨、迭代了七版的“线性回归健康体检手册”。它不讲抽象定义只讲你打开Jupyter Notebook后每一行代码在做什么、为什么这么做、如果结果不对该怎么揪出病灶。核心关键词——线性回归假设验证、Python残差诊断、VIF多重共线性检测、Q-Q图解读、残差散点图判读——全部嵌入到实操流程里让你做完这五步心里有底报告有据上线不慌。2. 核心思路拆解为什么是这五个假设它们如何共同守护模型的“灵魂”2.1 线性关系模型能力的“准入门槛”线性回归的数学本质是寻找一个超平面让所有样本点到这个平面的垂直距离即残差之和最小。这个“垂直距离”的几何意义严格依赖于变量间的真实关系是线性的。如果真实关系是Y X²而你硬用Y aX b去拟合那无论a、b怎么调模型都在用一条直线去“削足适履”。它被迫在X的低端和高端都产生巨大偏差这些偏差会系统性地进入残差污染后续所有检验。所以线性关系不是“最好有”而是“必须有”它是整个模型大厦的地基标高。验证它不是为了证明“它很线性”而是为了发现“它哪里不线性”从而决定是直接放弃线性模型还是对变量做变换比如对X取对数、开根号或者引入多项式项。我处理过一个电商GMV预测项目原始特征“用户浏览时长”与“下单金额”明显是指数增长关系散点图呈喇叭口向上。直接建模后残差图一片混乱。我们对浏览时长取了自然对数再画散点图立刻呈现出漂亮的线性趋势后续所有检验都顺利通过。这个“取对数”的动作不是玄学而是用数学变换把非线性关系“掰直”了。2.2 多重共线性变量间的“内耗陷阱”想象一个团队两个成员干着几乎一模一样的活还互相抢功劳。在线性回归里这就是多重共线性两个或多个自变量高度相关导致模型无法清晰地分配每个变量对因变量的独立贡献。它的直接后果是回归系数的标准误Standard Error会急剧膨胀t检验的p值变得不可信系数估计值对数据微小扰动极度敏感——今天加一行数据系数从2.1变成-1.8明天删一行又变回3.5。这不是模型不准而是模型在“说胡话”。更隐蔽的危险是它会严重削弱模型的泛化能力。一个在训练集上看似稳健的模型遇到新数据时可能因为某个共线变量的微小变化导致整个预测结果崩盘。VIF方差膨胀因子是我们手里的“内耗探测器”。它的计算逻辑很直观把每一个自变量Xᵢ当成因变量用其他所有自变量去拟合它得到一个R²ᵢ。如果Xᵢ能被其他变量完美预测R²ᵢ1说明Xᵢ完全是冗余的VIF就会趋向无穷大。VIF1/R²ᵢ所以VIF5或10不同文献标准略有差异就敲响警钟。我处理过一个信贷风控模型初始特征包含“月收入”和“年收入”。这俩变量的相关系数高达0.999VIF双双破百。模型给出的“月收入”系数居然是负的这显然违背业务常识。删掉“年收入”后一切回归正常。这个案例告诉我数据清洗阶段的“常识检查”比任何高级算法都管用。2.3 残差正态性推断可靠性的“统计护照”线性回归的很多关键结论比如回归系数的置信区间、F检验的p值其理论基础都建立在“残差服从正态分布”这一假设之上。这不是为了追求形式上的完美而是为了保证我们对模型不确定性的量化是准确的。举个例子如果你要告诉老板“下季度销售额预测为500万95%置信区间是450万到550万”这个区间的宽度和可靠性就完全依赖于残差正态性。如果残差是严重偏斜的比如大量集中在零附近但有一长串巨大的正残差那么这个“450-550万”的区间实际覆盖真实值的概率可能只有70%而不是宣称的95%。KDE图核密度估计和Q-Q图分位数-分位数图是我们的“正态性双探针”。KDE图像一张平滑的“残差分布快照”看它是否像一口钟Q-Q图则更精确它把残差的实际分位数和标准正态分布的理论分位数一一对应画点如果所有点都紧密贴合在一条45度直线上就说明正态性很好。我曾在一个医疗费用预测项目中发现KDE图右尾异常肥厚Q-Q图的上端点明显偏离直线。深入排查发现是少数几个罕见重症患者的费用极高形成了长尾。我们没有粗暴删除而是对因变量做了Box-Cox变换成功“压平”了长尾正态性检验立刻通过。这说明正态性检验不是为了“挑刺”而是为了找到数据的“最佳表达形态”。2.4 同方差性误差“公平性”的守门员“Homoscedasticity”这个词拆开看“homo-”意为“相同”“-scadastic”意为“散布”。它要求模型的误差残差在整个预测范围内其波动幅度方差是恒定的。换句话说无论你预测的是100元的订单还是10000元的大单模型犯错的“尺度”应该差不多。如果预测小订单时误差±10元预测大订单时误差±1000元那就是典型的异方差Heteroscedasticity。这会导致什么最直接的后果是普通最小二乘法OLS不再是最佳线性无偏估计量BLUE它的效率会下降标准误的估计会失真。更麻烦的是它会让模型对大额预测过度自信而对小额预测过于悲观。残差 vs 预测值散点图就是我们的“公平秤”。横轴是模型的预测值ŷ纵轴是残差e。如果假设成立所有点应该像被风吹散的蒲公英种子均匀、随机地撒在横轴e0上下形成一个“水平的、厚度均匀的带子”。如果出现“喇叭口”残差随预测值增大而扩散、“倒喇叭口”残差随预测值增大而收缩或“漏斗形”那就亮起红灯。我处理过一个物流时效预测模型散点图清晰显示“喇叭口”预测配送时间越长残差的绝对值越大。根源在于长距离运输受天气、路况等不可控因素影响更大。最终我们改用加权最小二乘法WLS给短途预测赋予更高权重模型稳定性显著提升。2.5 无自相关性时间序列的“独立宣言”这个假设主要针对时间序列数据。它要求残差之间彼此独立不存在“惯性”或“记忆”。比如如果上一个时刻的残差是正的模型低估了下一个时刻的残差大概率也是正的这就叫正自相关。这在时间序列中非常常见意味着模型没能捕捉到数据中的趋势或周期性把这部分规律错误地归为了“随机误差”。Durbin-Watson检验是检测它的金标准其统计量d的取值范围是0到4d≈2表示无自相关d1.5表示强正自相关d2.5表示强负自相关。但比数字更重要的是残差时序图把残差按时间顺序连成一条线如果它像一条躁动不安的蚯蚓频繁地在零线上下穿越那是健康的如果它长时间停留在零线以上或以下形成明显的“波浪”或“漂移”那就是自相关的铁证。我曾为一家零售企业构建日销量预测模型初始模型DW值只有0.8残差图显示长达两周的连续正值。加入滞后一期的销量作为特征后DW值立刻升至1.9残差图也变得“毛茸茸”且随机。这印证了一个朴素道理时间序列的“昨天”往往是预测“今天”最有力的线索。3. 实操细节解析从数据加载到五项检验的完整Python流水线3.1 环境准备与数据加载奠定可复现的基础在开始任何分析之前环境的确定性是第一位的。我坚持使用conda创建一个干净、隔离的虚拟环境并固定关键库的版本。这不是教条主义而是为了杜绝“在我电脑上好好的到你那儿就报错”的协作灾难。以下是我项目启动时的标准配置# 创建名为lr_assumptions的新环境 conda create -n lr_assumptions python3.9 conda activate lr_assumptions # 安装核心库指定版本以确保结果一致 pip install numpy1.23.5 pandas1.5.3 scikit-learn1.2.2 matplotlib3.7.1 seaborn0.12.2 statsmodels0.13.5 scipy1.10.0数据加载环节我有一个雷打不动的习惯绝不相信原始文件名。我会先用pandas.read_csv()读取并立即执行df.info()和df.describe()。info()能一眼看出是否有缺失值、数据类型是否正确比如日期列被误读为objectdescribe()则提供数值型变量的基本统计量让我快速扫描是否存在离群值比如某列的最大值是均值的100倍。对于本文示例我们使用经典的Boston Housing数据集尽管它因伦理问题已从sklearn最新版移除但其结构清晰非常适合教学。为了保证可复现性我提供一个本地化的加载方式import numpy as np import pandas as pd from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.preprocessing import StandardScaler import matplotlib.pyplot as plt import seaborn as sns import statsmodels.api as sm from statsmodels.stats.outliers_influence import variance_inflation_factor from scipy import stats # 手动构造一个简化版Boston数据集用于演示 # 真实项目中请替换为你的业务数据 np.random.seed(42) n_samples 500 X np.random.randn(n_samples, 4) # 构造一些相关性X1和X2高度相关 X[:, 1] X[:, 0] * 0.8 np.random.randn(n_samples) * 0.3 # 因变量Y加入一些非线性项和噪声 y 2.5 * X[:, 0] 1.8 * X[:, 1] - 0.5 * (X[:, 2] ** 2) 3.2 * X[:, 3] np.random.randn(n_samples) * 2.0 # 转换为DataFrame赋予有意义的列名 feature_names [CRIM, ZN, INDUS, CHAS] df pd.DataFrame(X, columnsfeature_names) df[PRICE] y print(数据集基本信息) print(df.info()) print(\n数值型变量统计摘要) print(df.describe())这段代码的关键在于它模拟了真实世界中最常见的两种“坑”——变量间的相关性X1和X2和因变量与自变量间的非线性关系INDUS的平方项。这让我们后续的检验不会流于形式而是能真正发现问题。3.2 数据预处理与模型拟合构建待检的“病人”预处理是模型健康的起点。我遵循一个铁律所有变换必须在训练集上学习并应用到训练集和测试集上。任何在全量数据上做标准化、缩放的操作都是数据泄露的温床。以下是标准流程# 1. 分离特征与目标 X df[feature_names] y df[PRICE] # 2. 划分训练集和测试集7:3 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42 ) # 3. 对特征进行标准化对线性回归非必需但对VIF计算和后续解释有帮助 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) # 注意只transform不fit # 4. 拟合线性回归模型 model LinearRegression() model.fit(X_train_scaled, y_train) # 5. 计算关键诊断指标 y_train_pred model.predict(X_train_scaled) residuals y_train - y_train_pred # 训练集残差用于所有诊断 # 6. 将结果存入DataFrame便于后续绘图和分析 diagnosis_df pd.DataFrame({ y_true: y_train, y_pred: y_train_pred, residuals: residuals }) diagnosis_df pd.concat([diagnosis_df, pd.DataFrame(X_train_scaled, columnsfeature_names)], axis1)这里有几个极易被忽略的细节scaler.fit_transform(X_train)和scaler.transform(X_test)的区分是防止信息泄露的生命线。我们只对训练集的残差进行诊断。测试集的残差只用于最终评估模型泛化能力不参与假设检验。因为假设检验的目的是评估模型在训练数据上的“拟合质量”这是模型构建过程的一部分。diagnosis_df的构建将真实值、预测值、残差和所有特征整合在一个DataFrame里为后续所有可视化提供了统一、便捷的数据源。这比在每个绘图函数里临时拼接数据要高效、安全得多。3.3 假设一线性关系的可视化验证与量化判断线性关系的验证核心工具是散点图矩阵Scatterplot Matrix。seaborn的pairplot函数是我们的首选因为它能一键生成所有特征与目标变量的两两散点图效率极高。# 绘制所有特征与目标变量的散点图 plt.figure(figsize(12, 10)) sns.pairplot(diagnosis_df, x_varsfeature_names, y_vars[y_true], height4, aspect1.2, kindscatter) plt.suptitle(各特征与目标变量(Y)的散点图, y1.02) plt.show()然而仅靠肉眼观察散点图是危险的。人眼容易被“整体趋势”迷惑而忽略局部的非线性模式。因此我一定会辅以局部加权散点图平滑LOWESS曲线。LOWESS是一种非参数回归方法它能在不假设任何函数形式的前提下描绘出数据的“平均趋势线”。如果这条线是直的那线性假设就站得住脚如果它弯曲了那弯曲的形状就是我们下一步建模的指南针。# 为每个特征单独绘制并添加LOWESS曲线 fig, axes plt.subplots(2, 2, figsize(14, 10)) axes axes.flatten() for i, feature in enumerate(feature_names): # 绘制散点图 axes[i].scatter(diagnosis_df[feature], diagnosis_df[y_true], alpha0.6, s20, labelData Points) # 计算并绘制LOWESS平滑线 # 使用statsmodels的lowess函数 lowess_result sm.nonparametric.lowess( diagnosis_df[y_true], diagnosis_df[feature], frac0.3, # 平滑参数0.3表示使用30%的邻近点 it3 # 迭代次数提高鲁棒性 ) axes[i].plot(lowess_result[:, 0], lowess_result[:, 1], r-, linewidth2, labelLOWESS Smooth) axes[i].set_xlabel(feature) axes[i].set_ylabel(PRICE (y_true)) axes[i].set_title(f{feature} vs PRICE) axes[i].legend() axes[i].grid(True, alpha0.3) plt.tight_layout() plt.show()在这个示例中INDUS工业用地比例的LOWESS曲线会清晰地显示出向下弯曲的趋势这与我们人工构造的-0.5 * (X[:, 2] ** 2)完全吻合。此时我的决策路径就很明确了要么放弃线性模型改用多项式回归或树模型要么对INDUS进行变换比如创建INDUS_SQUARED特征再将其加入模型。这比盲目地接受一个“看起来还行”的线性模型要负责任得多。3.4 假设二多重共线性的双重验证VIF与相关矩阵多重共线性的检测我坚持“双保险”策略相关矩阵Correlation Matrix看全局VIF看个体。相关矩阵像一张“人际关系总览图”能快速定位哪两个变量“走得太近”而VIF则像一份“个人信用报告”告诉你每个变量在多大程度上被其他变量所“定义”。# 1. 绘制相关矩阵热力图 plt.figure(figsize(10, 8)) correlation_matrix X_train.corr() mask np.triu(np.ones_like(correlation_matrix, dtypebool)) # 隐藏上三角避免重复 sns.heatmap(correlation_matrix, maskmask, annotTrue, cmapcoolwarm, center0, squareTrue, fmt.2f, cbar_kws{shrink: .8}) plt.title(训练集特征相关矩阵热力图) plt.show() # 2. 计算并打印VIF vif_data pd.DataFrame() vif_data[Feature] X_train.columns vif_data[VIF] [variance_inflation_factor(X_train.values, i) for i in range(len(X_train.columns))] print(\n各特征的方差膨胀因子VIF:) print(vif_data.sort_values(byVIF, ascendingFalse))解读这两份报告需要一点经验相关矩阵关注绝对值大于0.7的格子。在我的示例中CRIM和ZN的相关系数会接近-0.8这是一个强烈的警示信号。VIF报告VIF 5 是黄灯VIF 10 是红灯。CRIM的VIF值会非常高可能超过20因为它和ZN高度相关。此时我的标准操作是保留业务解释性更强、或与目标变量相关性更高的那个变量果断剔除另一个。例如如果ZN住宅用地比例与PRICE的相关性更高我就保留ZN删除CRIM犯罪率。这个决策不是纯数学的而是数学与业务理解的结合。提示VIF计算时务必使用原始未缩放的X_train而不是X_train_scaled。因为VIF衡量的是变量间的线性依赖关系而标准化会改变变量的尺度但不改变其内在的相关性结构。用缩放后的数据计算VIF结果虽然数值不同但排序和相对大小关系是一致的但为了与经典文献和教材保持一致我推荐使用原始数据。3.5 假设三残差正态性的双图诊断KDE与Q-Q图正态性检验我从不依赖单一的p值如Shapiro-Wilk检验因为样本量大会让微小的、无实际意义的偏离也变得“显著”。我更相信眼睛——KDE图和Q-Q图的组合能提供最直观、最丰富的信息。# 创建一个2x2的子图展示正态性诊断 fig, axes plt.subplots(2, 2, figsize(14, 10)) fig.suptitle(残差正态性诊断, fontsize16, y1.02) # 1. KDE图核密度估计 sns.kdeplot(datadiagnosis_df, xresiduals, axaxes[0, 0], fillTrue, alpha0.6) axes[0, 0].axvline(diagnosis_df[residuals].mean(), colorred, linestyle--, labelfMean: {diagnosis_df[residuals].mean():.2f}) axes[0, 0].set_title(残差核密度估计 (KDE)) axes[0, 0].legend() axes[0, 0].grid(True, alpha0.3) # 2. Q-Q图 stats.probplot(diagnosis_df[residuals], distnorm, plotaxes[0, 1]) axes[0, 1].set_title(Q-Q 图) # 3. 残差直方图辅助KDE axes[1, 0].hist(diagnosis_df[residuals], bins30, densityTrue, alpha0.7, edgecolorblack) # 在直方图上叠加标准正态分布曲线 x_norm np.linspace(diagnosis_df[residuals].min(), diagnosis_df[residuals].max(), 100) y_norm stats.norm.pdf(x_norm, loc0, scalediagnosis_df[residuals].std()) axes[1, 0].plot(x_norm, y_norm, r-, linewidth2, labelN(0, σ²)) axes[1, 0].set_title(残差直方图与正态分布拟合) axes[1, 0].legend() axes[1, 0].grid(True, alpha0.3) # 4. 残差的箱线图看偏斜和离群值 sns.boxplot(datadiagnosis_df, yresiduals, axaxes[1, 1]) axes[1, 1].set_title(残差箱线图) axes[1, 1].grid(True, alpha0.3) plt.tight_layout() plt.show()这张四宫格图每一块都传递着关键信息KDE图看“形状”。理想状态是一条光滑、对称、单峰的钟形曲线。如果它向左或向右拖着长长的尾巴就说明存在偏斜Skewness。Q-Q图看“点线吻合度”。点越贴近45度参考线正态性越好。如果点在两端尤其是上端明显偏离直线向上说明存在正偏态右偏如果点在两端明显偏离直线向下则是负偏态左偏。直方图看“分布轮廓”。与叠加的正态曲线对比能更清晰地看到峰度Kurtosis问题——是“尖峰厚尾”还是“平峰薄尾”。箱线图看“离群值”。箱线图外的圆点就是残差中的极端值。如果数量过多就需要警惕。在我的示例中由于我们加入了INDUS的平方项残差的KDE图会呈现轻微的右偏Q-Q图的上端点会略高于参考线。这正是模型未能完全捕捉非线性关系的“指纹”。3.6 假设四同方差性的核心判读残差 vs 预测值图这是所有检验中我最看重、也最常被忽视的一张图。它的解读规则极其简单却蕴含着巨大的信息量。# 绘制残差 vs 预测值图 plt.figure(figsize(10, 6)) plt.scatter(diagnosis_df[y_pred], diagnosis_df[residuals], alpha0.6, s30, edgecolorsnone) plt.axhline(y0, colorr, linestyle--, linewidth2, labelResidual 0) plt.xlabel(预测值 (y_pred)) plt.ylabel(残差 (residuals)) plt.title(残差 vs 预测值图 (用于检验同方差性)) plt.legend() plt.grid(True, alpha0.3) plt.show()这张图的判读我总结为“三看”口诀一看“带子”形状所有点应该大致落在一个水平的、宽度均匀的带子里。如果带子是“喇叭口”向右上方张开说明预测值越大误差的方差越大正向异方差如果是“倒喇叭口”向右下方收窄则是负向异方差。二看“中心线”带子的中心应该稳定地落在residual 0这条红色虚线上。如果整个带子系统性地偏高或偏低说明模型存在系统性偏差Bias比如遗漏了重要变量或函数形式错误。三看“点的密度”点的分布应该是随机的、无模式的。如果能看到清晰的曲线、波浪或分组那一定是模型没学到的某种结构。在我的示例中这张图会清晰地显示出一个“喇叭口”趋势这与我们构造的非线性项直接对应。此时解决方案就呼之欲出了对因变量PRICE进行对数变换或者对INDUS特征进行平方变换然后再重新拟合模型。3.7 假设五无自相关性的时序检验Durbin-Watson与残差时序图对于时间序列数据这一步是强制性的。即使你的数据不是严格的时间序列只要样本之间可能存在某种顺序依赖比如按ID排序的用户数据也建议做一次检查。# 假设我们的数据有时间索引我们先为其添加一个简单的序号索引 diagnosis_df[time_index] range(len(diagnosis_df)) # 1. 绘制残差时序图 plt.figure(figsize(12, 6)) plt.plot(diagnosis_df[time_index], diagnosis_df[residuals], b-, linewidth1.2, alpha0.8, labelResiduals) plt.axhline(y0, colorr, linestyle--, linewidth2, labelZero Line) plt.xlabel(样本序号 (Time Index)) plt.ylabel(残差 (residuals)) plt.title(残差时序图 (用于检验自相关性)) plt.legend() plt.grid(True, alpha0.3) plt.show() # 2. 计算Durbin-Watson统计量 dw_stat sm.stats.durbin_watson(diagnosis_df[residuals]) print(f\nDurbin-Watson 统计量: {dw_stat:.4f}) print(解释: DW ≈ 2 表示无自相关DW 1.5 表示正自相关DW 2.5 表示负自相关。)Durbin-WatsonDW统计量是一个介于0到4之间的数字其计算公式本质上是相邻残差差分的平方和与残差平方和的比值。它的物理意义是如果残差是随机的那么相邻残差的差值应该也随机DW值就趋近于2如果残差有正自相关前一个残差大后一个也大那么差值就小DW值就小反之亦然。在我的示例中由于数据是随机生成的DW值会非常接近2。但在真实的时间序列项目中我曾在一个股票价格预测模型中得到DW0.92残差时序图显示连续15个点都在零线以上这明确指示了模型遗漏了重要的趋势成分。解决方案是加入时间趋势项如t、t²或使用ARIMA等专门处理时间序列的模型。4. 实操全流程与核心环节实现一份可直接运行、可直接复现的完整脚本4.1 整合所有检验的终极诊断函数为了将上述所有步骤封装成一个可一键调用、可嵌入任何项目的“诊断引擎”我编写了以下函数。它不仅执行所有检验还会根据结果自动给出清晰、可操作的建议。def linear_regression_diagnostics(X, y, feature_namesNone, test_size0.3, random_state42): 对线性回归模型进行完整的五大假设诊断。 Parameters: ----------- X : array-like, shape (n_samples, n_features) 自变量特征矩阵。 y : array-like, shape (n_samples,) 因变量向量。 feature_names : list of str, optional 特征名称列表用于绘图标签。如果为None则使用默认名称。 test_size : float, default0.3 测试集占比。 random_state : int, default42 随机种子保证结果可复现。 Returns: -------- dict : 包含所有诊断结果和建议的字典。 print(*60) print(LINEAR REGRESSION ASSUMPTIONS DIAGNOSTICS REPORT) print(*60) # --- 步骤1数据分割与模型拟合 --- X_train, X_test, y_train, y_test train_test_split( X, y, test_sizetest_size, random_staterandom_state ) if feature_names is None: feature_names [fFeature_{i} for i in range(X.shape[1])] # 拟合模型 model LinearRegression() model.fit(X_train, y_train) y_train_pred model.predict(X_train) residuals y_train - y_train_pred # 构建诊断DataFrame diagnosis_df pd.DataFrame({ y_true: y_train, y_pred: y_train_pred, residuals: residuals }) for i, name in enumerate(feature_names): diagnosis_df[name] X_train[:, i] # --- 步骤2线性关系诊断 --- print(\n1. LINEAR RELATIONSHIP DIAGNOSIS) print(- * 40) # 使用statsmodels进行更严谨的线性检验每个特征单独 linear_pvals {} for feature in feature_names: # 构造单变量模型 X_single sm.add_constant(diagnosis_df[feature]) model_single sm.OLS(diagnosis_df[y_true], X_single).fit() linear_pvals[feature] model_single.pvalues[feature] # 找出p值最大的特征最不线性 worst_linear_feature max(linear_pvals, keylinear_pvals.get) print(f - 最不满足线性假设的特征: {worst_linear_feature} (p-value {linear_pvals[worst_linear_feature]:.4f})) if linear_pvals[worst_linear_feature] 0.05: print(f - 建议: 对 {worst_linear_feature} 进行变换如log, sqrt或考虑添加多项式项。) else: print( - 建议: 线性关系基本满足。) # --- 步骤3多重共线性诊断 --- print(\n2. MULTICOLLINEARITY DIAGNOSIS) print(- * 40) vif_data pd.DataFrame() vif_data[Feature] feature_names vif_data[VIF] [variance_inflation_factor(X_train, i) for i in range(X_train.shape[1])] vif_data vif_data.sort_values(byVIF, ascendingFalse) print( VIF Values:) for idx, row in vif_data.iterrows(): status ⚠️ 高风险 if row[VIF] 10 else 中风险 if row[VIF] 5 else ✅ 安全 print(f {row[Feature]}: {row[VIF]:.2f} ({status})) high_vif_features vif_data[vif_data[VIF] 5][Feature].tolist() if high_vif_features: print(f - 建议: 考虑移除或合并以下高VIF特征: {high_vif_features}) else: print( - 建议: 无显著多重共线性问题。) # --- 步骤4残差正态性诊断 --- print(\n3. RESIDUAL NORMALITY DIAGNOSIS) print(- * 40) # Shapiro-Wilk检验 shapiro_stat, shapiro_p stats.shapiro(residuals) print(f Shapiro-Wilk Test: W{shapiro_stat:.4f}, p-value{shapiro_p:.4f}) if shapiro_p 0.05: print( - 建议: 残差非正态。考虑对因变量y进行Box-Cox变换或使用稳健回归。) else: print( - 建议: 残差正态性基本满足。) # --- 步骤5同方差性诊断 --- print