Python 实战指南:如何绘制残差图并深入分析回归模型

在数据科学和机器学习的实战项目中,仅仅建立一个回归模型并得到一个预测分数往往是不够的。作为经验丰富的开发者,我们都知道,理解模型的“错误”发生在哪里,比单纯知道它“正确”了多少更为关键。这就是残差图大显身手的地方。

在本文中,我们将深入探讨如何在 Python 中使用多种强大的库来创建和解读残差图。我们将不仅局限于画出图表,还会深入探讨残差图背后的统计学原理,分享如何通过它来检测线性回归的假设是否成立,以及如何发现数据中隐藏的模式。

无论你是使用 Seaborn 进行快速探索,还是利用 Statsmodels 进行详尽的统计诊断,亦或是使用 Scikit-learn 进行底层的自定义控制,我们都将一一覆盖。让我们开始这段技术探索之旅吧。

什么是残差图?为什么我们需要它?

在开始编写代码之前,让我们先统一一下概念。简单来说,残差就是实际观测值与模型预测值之间的差值($Residual = Actual – Predicted$)。

当我们把这些残差相对于自变量(或预测值)绘制出来时,就得到了残差图。它是回归诊断中最直观的工具之一。一个表现良好的线性回归模型,其残差图应该显示出数据点随机分布在 0 刻度线的上下。这意味着误差是随机的,模型已经成功捕捉到了数据中的主要规律。

反之,如果我们在图中看到了明显的曲线、漏斗形状或聚集的簇,那就是模型在向我们“诉苦”:也许线性假设并不适用,或者数据的方差存在异方差性。学会倾听这些视觉信号,能让我们避免发布一个表面上看起来不错,但实际上存在严重缺陷的模型。

方法一:使用 Seaborn 进行快速可视化

Seaborn 是我们进行探索性数据分析(EDA)时的首选工具,它的 residplot() 函数让绘制残差图变得异常简单。这种方法非常适合在项目初期快速检查模型的拟合情况。

让我们先看一个基础的例子。假设我们正在分析头围与大脑重量之间的关系(数据集使用 headbrain3.csv)。

基础实现

在这个场景中,我们将直接将原始数据传递给 Seaborn。Seaborn 会在后台默默地帮我们计算线性回归,然后画出残差。

import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

# 读取数据
data = pd.read_csv(‘headbrain3.csv‘)

# 设置绘图风格,让图表更专业
sns.set(style="whitegrid")

# 创建图形
plt.figure(figsize=(10, 6))

# x: 自变量 (特征)
# y: 因变量 (目标)
# lowess=True: 局部加权散点平滑,帮助我们观察残差的非线性趋势
sns.residplot(x=‘Head_size‘, y=‘Brain_weight‘, data=data, lowess=True, color="g")

plt.title("残差图:头围 vs 大脑重量", fontsize=15)
plt.xlabel("头围", fontsize=12)
plt.ylabel("残差", fontsize=12)
# 添加一条水平参考线
plt.axhline(y=0, color=‘blue‘, linestyle=‘--‘, linewidth=2)
plt.show()

代码解析与实用见解

在这个例子中,我们不仅调用了 residplot,还做了一些专业的优化:

  • INLINECODE4687fa21: 这个函数的核心作用是计算 INLINECODE28fed1d3 对 x 的线性回归,并绘制残差。如果数据点随机分布在 0 线周围,说明线性模型是合适的。
  • lowess=True: 这是一个非常实用的技巧。LOWESS(局部加权回归)拟合了一条平滑曲线。如果这条平滑曲线不是一条直线,而是弯曲的,这就强烈暗示我们的数据中存在非线性关系,线性模型可能遗漏了某些信息。
  • plt.axhline(...): 我们手动添加了 y=0 的参考线。残差图的核心就是看数据点是否围绕这条线上下对称波动。

最佳实践:Seaborn 结合 Scikit-learn

有时候,我们不想让 Seaborn 帮我们拟合模型,而是想用我们已经训练好的 Scikit-learn 模型的预测结果来画图。这给了我们更大的控制权。让我们看一个更进阶的例子。

import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression

# 准备数据
df = pd.read_csv(‘headbrain3.csv‘)
X = df[[‘Head_size‘]]  # 特征必须是二维的
y = df[‘Brain_weight‘]

# 1. 使用 Scikit-learn 训练模型
model = LinearRegression()
model.fit(X, y)

# 2. 获取预测值
predictions = model.predict(X)

# 3. 计算残差
residuals = y - predictions

# 4. 使用 Seaborn 绘制基于计算结果的残差图
plt.figure(figsize=(10, 6))
# 这里我们传入 X 和计算出的 residuals
sns.scatterplot(x=X[‘Head_size‘], y=residuals, hue=residuals, palette="coolwarm", legend=False)

# 添加 LOWESS 线来辅助观察趋势
sns.residplot(x=X[‘Head_size‘], y=y, lowess=True, scatter=False, color="red")

plt.title("基于 Sklearn 预测的残差分布", fontsize=15)
plt.xlabel("头围", fontsize=12)
plt.ylabel("残差", fontsize=12)
plt.axhline(y=0, color=‘black‘, linestyle=‘--‘)
plt.show()

通过这种方法,我们将机器学习工作流与可视化紧密结合,确保了绘图的数据与模型评估的数据完全一致。

方法二:使用 Statsmodels 进行深度诊断

如果说 Seaborn 是“快速手绘板”,那么 INLINECODEee60fec9 就是“专业级的工作台”。当我们需要一份完整的“成绩单”时,Statsmodels 的 INLINECODE0a38596e 是不二之选。

这个方法之所以强大,是因为它在一个图中同时展示了:

  • Y 与 X 的拟合图:看看回归线是否贴合数据。
  • 残差图:查看误差分布。
  • 分位数图 (QQ Plot):检查残差是否符合正态分布(这对线性回归的统计推断至关重要)。

完整代码实现

import numpy as np  
import pandas as pd 
import matplotlib.pyplot as plt  
import statsmodels.api as sm  
from statsmodels.formula.api import ols 

# 读取数据
df = pd.read_csv(‘headbrain3.csv‘)  

# 1. 拟合模型
# 使用公式接口,类似于 R 语言。 ‘Brain_weight ~ Head_size‘ 表示我们要预测 Brain_weight
lm = ols(‘Brain_weight ~ Head_size‘, data=df).fit()  

# 打印模型统计摘要
print("模型统计摘要:")
print(lm.summary())  

# 2. 绘制诊断图
# 这将生成一个包含 4 个子图的大图
fig = plt.figure(figsize=(14, 8))  
fig = sm.graphics.plot_regress_exog(lm, ‘Head_size‘, fig=fig)

# 调整子图布局,防止标题重叠
plt.tight_layout()
plt.show()

深入解析统计摘要和图表

当你运行 INLINECODEbbddac78 时,你不仅看到了 R-squared(拟合优度),还看到了 p 值。如果 p 值小于 0.05,说明变量 Headsize 对 Brain_weight 有显著的统计影响。

而在生成的图表中,请重点关注 Partial Regression Plot (偏回归图)CCPR Plot (Component-Component plus Residual)

  • CCPR 图展示了单个变量(Head_size)对结果的贡献加上残差。如果这里的点不在一条直线上,说明该变量与结果的关系可能不是线性的,或者需要加入多项式特征。

这种诊断方法在学术论文或需要严谨统计证明的项目中是必不可少的。

方法三:手动计算残差(完全掌控)

虽然现成的库很方便,但作为开发者,从零开始手动实现一遍能让我们更好地理解底层原理。这种方法最灵活,我们可以随意添加自定义的逻辑,比如根据残差的大小给点上色,或者添加置信区间。

让我们使用 Scikit-learn 从头构建整个流程。

import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression

# 加载数据
df = pd.read_csv(‘headbrain3.csv‘)

# 定义特征 X 和目标 y
# 注意:sklearn 通常期望 X 是二维的 
X = df[[‘Head_size‘]] 
y = df[‘Brain_weight‘]

# 1. 初始化并训练模型
lr = LinearRegression()
lr.fit(X, y)

# 2. 预测
y_pred = lr.predict(X)

# 3. 手动计算残差: 实际值 - 预测值
residuals = y - y_pred

# 4. 可视化
plt.figure(figsize=(10, 6))

# 绘制散点图
plt.scatter(X, residuals, alpha=0.6, edgecolors=‘k‘, color=‘purple‘)

# 添加一条红色的水平虚线作为基准
plt.axhline(y=0, color=‘r‘, linestyle=‘--‘, linewidth=2)

# 增加图表的可读性
plt.title("手动计算的残差图", fontsize=15)
plt.xlabel(‘头围‘, fontsize=12)
plt.ylabel(‘残差‘, fontsize=12)
plt.grid(True, linestyle=‘:‘, alpha=0.6)

# 标注离群点:找出残差绝对值最大的点
threshold = 2 * np.std(residuals)
outliers = np.abs(residuals) > threshold
if np.any(outliers):
    plt.scatter(X[outliers], residuals[outliers], color=‘orange‘, s=100, label=‘离群点‘)
    plt.legend()

plt.show()

代码亮点:离群点检测

在上面的代码中,我们添加了一个额外的逻辑:离群点检测

threshold = 2 * np.std(residuals)
outliers = np.abs(residuals) > threshold

这是一个非常实用的技巧。我们将残差超过标准差两倍的数据点标记为离群点。在可视化的图表中,你可以清晰地看到哪些数据点模型没有很好地拟合。在实际业务中,这些点可能代表数据录入错误,或者特殊的客户群体,值得你去深入挖掘。

方法四:处理截距 —— Statsmodels 与 Sklearn 的结合

在处理线性回归时,截距(Intercept,即 $y = ax + b$ 中的 $b$)是一个让很多人困惑的概念。Scikit-learn 的 INLINECODE521118d6 默认会自动拟合截距(INLINECODE0143728c),而 Statsmodels 通常需要显式地添加一列常数(值为1的列)来作为截距项。

如果我们需要在两个库之间切换,或者需要手动控制是否包含截距,理解 add_constant 就非常重要。

显式添加截距的实战场景

有时候,如果我们不给数据添加截距项,回归线会被强制穿过原点 $(0,0)$。这在很多物理意义上是不合理的。下面的代码展示了如何显式地处理这个问题,确保我们的模型更加稳健。

import pandas as pd
import matplotlib.pyplot as plt
import statsmodels.api as sm
from sklearn.linear_model import LinearRegression

df = pd.read_csv(‘headbrain3.csv‘)
X = df[[‘Head_size‘]]
y = df[‘Brain_weight‘]

# 关键步骤:使用 statsmodels 添加常数项(截距)
# 这将在 X 中增加一列,所有值都为 1
X_with_const = sm.add_constant(X) 

# 此时,sklearn 训练的是 y = w1*const + w2*Head_size
# 由于 const 全是 1,w1 就是截距
lr = LinearRegression(fit_intercept=False) # 注意:因为数据里已经有 const 列了,这里设为 False
lr.fit(X_with_const, y)

# 预测
y_pred = lr.predict(X_with_const)

# 计算残差
res = y - y_pred

# 绘图
plt.figure(figsize=(10, 6))
plt.scatter(X, res, color="teal", label="残差")
plt.axhline(y=0, color=‘r‘, linestyle=‘--‘)

# 美化图表
plt.title("显式包含截距项的残差分析", fontsize=14)
plt.xlabel(‘头围‘, fontsize=12)
plt.ylabel(‘残差‘, fontsize=12)
plt.legend()
plt.show()

print(f"模型截距: {lr.coef_[0]:.2f}")
print(f"回归系数: {lr.coef_[1]:.2f}")

为什么这一步很重要?

通过 INLINECODEa9d115c6,我们将特征矩阵从 $[x1, x2, …]$ 变成了 $[1, x1, x_2, …]$。这使得线性代数的计算能够自动求解出截距参数。在处理广义线性模型或对比不同库的结果时,统一数据结构是避免错误的关键。

常见问题与解决方案

在绘制和分析残差图时,你可能会遇到以下几种“不完美”的情况,这里是我们的诊断和修复建议:

1. 发现 U 型或倒 U 型曲线

  • 现象:残差图呈现出明显的抛物线形状。
  • 诊断:这说明数据中存在非线性关系,线性模型无法捕捉。
  • 解决方案:尝试在模型中加入多项式特征(如 $X^2$),或者使用非线性模型(如随机森林、支持向量机)。

2. 发现漏斗形状

  • 现象:随着 x 轴数值的增加,残差的扩散范围越来越大(像喇叭口一样)。
  • 诊断:这被称为异方差性。即数据的波动性不稳定。
  • 解决方案:可以对目标变量 $y$ 进行对数变换,或者使用加权最小二乘法。

3. 所有点都紧密围绕 0 线,但 R方 很低

  • 现象:残差很小,但模型解释力很差。
  • 诊断:可能发生了过拟合,或者特征选择有误。
  • 解决方案:检查训练集和测试集的表现,考虑引入更多相关特征或进行正则化(Lasso/Ridge)。

总结

通过这篇文章,我们从多个维度——从便捷的 Seaborn 到严谨的 Statsmodels,再到底层的 Scikit-learn 手动实现——全面掌握了在 Python 中创建残差图的技能。

残差图不仅仅是几张图表,它是我们与模型对话的窗口。通过观察残差是否随机分布,我们可以自信地判断模型的假设是否成立,或者是否需要调整特征、修正算法。掌握这些可视化和分析技巧,将使你在数据建模的道路上更加专业和稳健。

现在,当你拿到一个新的回归数据集时,尝试按照我们今天讨论的方法,先不要急着看准确率,而是先画一张残差图,听听数据想告诉你什么。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。如需转载,请注明文章出处豆丁博客和来源网址。https://shluqu.cn/23852.html
点赞
0.00 平均评分 (0% 分数) - 0