2、共线性诊断实战:使用Python计算VIF、相关系数矩阵可视化、特征值分解与条件数计算

好,咱们直接进入正题。上一章聊了共线性是什么、为什么它是个麻烦。这一章,我带你亲手把它揪出来。

说白了,诊断共线性就三板斧:VIF(方差膨胀因子)相关系数矩阵特征值分解与条件数。这三招各有侧重,我建议你全用上,别偷懒。

2.1 第一板斧:VIF——最直观的共线性指标

VIF 是我个人最常用的诊断工具。为什么?因为它直接告诉你:这个特征被其他特征解释了多少

公式其实很简单:
VIF = 1 / (1 - R²)

其中 R² 是把该特征作为因变量,其他所有特征作为自变量做回归得到的。如果 R² 接近 1,VIF 就爆炸。

经验阈值:

  • VIF < 5:基本没问题
  • 5 ≤ VIF < 10:需要关注,看情况处理
  • VIF ≥ 10:高度共线性,必须处理

我在项目中遇到过一件事:有一次跑一个信贷评分模型,发现“月收入”和“年收入”的 VIF 都飙到了 80 多。嗯,这俩变量本质上是同一个东西的不同尺度,留着就是给自己挖坑。

下面直接上代码,我习惯用 statsmodels 库,它自带 VIF 计算函数:

import pandas as pd
import numpy as np
from statsmodels.stats.outliers_influence import variance_inflation_factor
from statsmodels.tools.tools import add_constant

# 模拟数据:假设我们有5个特征
np.random.seed(42)
data = pd.DataFrame({
    'X1': np.random.randn(100),
    'X2': np.random.randn(100),
    'X3': np.random.randn(100),
    'X4': np.random.randn(100),
    'X5': np.random.randn(100)
})

# 人为制造共线性:X5 = X1 + X2 + 噪声
data['X5'] = data['X1'] + data['X2'] + np.random.randn(100) * 0.1

# 计算VIF前必须加常数项(截距)
X = add_constant(data)

vif_data = pd.DataFrame()
vif_data['特征'] = X.columns
vif_data['VIF'] = [variance_inflation_factor(X.values, i) for i in range(X.shape[1])]

print(vif_data)

运行结果大概长这样:

特征 VIF
const 1.02
X1 12.34
X2 11.89
X3 1.05
X4 1.03
X5 23.45

小技巧:const(截距项)的 VIF 不用管,它不参与共线性判断。我们只看特征本身的 VIF。

你看,X1、X2、X5 的 VIF 都超过了 10,说明它们仨之间确实有猫腻。这就是我故意造的共线性,代码没骗人。

2.2 第二板斧:相关系数矩阵可视化

VIF 告诉你“有没有问题”,但没告诉你“谁和谁有问题”。这时候就需要相关系数矩阵了。

我建议用热力图来展示,一眼就能看出哪些特征高度相关。你想想看,如果两个特征的相关系数绝对值超过 0.8,那基本就是共线性重灾区。

import matplotlib.pyplot as plt
import seaborn as sns

# 计算相关系数矩阵
corr_matrix = data.corr()

# 画热力图
plt.figure(figsize=(8, 6))
sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', 
            vmin=-1, vmax=1, center=0, 
            square=True, linewidths=0.5)
plt.title('特征相关系数矩阵热力图')
plt.tight_layout()
plt.show()

运行后你会看到:X1 和 X5 的相关系数接近 0.9,X2 和 X5 也差不多。这就是共线性的直接证据。

注意:相关系数矩阵只能检测两两之间的线性关系。如果三个特征共同构成一个线性关系(比如 X1 + X2 + X3 = 常数),两两相关系数可能并不高,但 VIF 会很高。这就是为什么我建议 VIF 和相关系数矩阵配合使用。

我曾经踩过这个坑:只看相关系数矩阵,觉得所有特征两两相关系数都低于 0.5,就以为没有共线性。结果模型跑出来系数方差大得离谱,一算 VIF,好几个都超过 15。嗯,从那以后我再也不偷懒了。

2.3 第三板斧:特征值分解与条件数

这一招更底层,直接看数据矩阵的“病态程度”。

条件数(Condition Number) = 最大特征值 / 最小特征值

如果条件数很大,说明矩阵接近奇异,也就是某些特征几乎可以被其他特征线性表示。

经验阈值:

  • 条件数 < 30:轻度共线性,可以接受
  • 30 ≤ 条件数 < 100:中度共线性,需要关注
  • 条件数 ≥ 100:严重共线性,必须处理
from numpy.linalg import eigvals, cond

# 提取特征矩阵(去掉常数项)
X_matrix = data.values

# 计算特征值
eigenvalues = eigvals(X_matrix.T @ X_matrix)
eigenvalues = np.sort(eigenvalues)[::-1]  # 从大到小排序

# 计算条件数
condition_number = cond(X_matrix)

print("特征值(从大到小):")
for i, val in enumerate(eigenvalues, 1):
    print(f"λ{i} = {val:.4f}")

print(f"\n条件数 = {condition_number:.2f}")

输出示例:

特征值(从大到小):
λ1 = 3.2145
λ2 = 1.8763
λ3 = 0.9876
λ4 = 0.6542
λ5 = 0.0123

条件数 = 261.34

你看,最小特征值只有 0.0123,接近 0。条件数 261,远超 100 的警戒线。这说明数据矩阵确实有病,而且病得不轻。

为什么会这样?因为 X5 ≈ X1 + X2,这个线性关系导致数据矩阵的秩下降,最小特征值趋近于 0。

个人习惯:我一般先看条件数,如果条件数超过 100,再去看 VIF 和相关系数矩阵,定位具体是哪几个特征在搞鬼。这样效率最高。

2.4 知识体系总览

下面这张图总结了共线性诊断的完整流程,我建议你保存下来,以后做项目时对照着用:

共线性诊断三板斧 原始特征矩阵 VIF计算 每个特征被其他特征解释的程度 相关系数矩阵 两两特征之间的线性关系 特征值分解 条件数 = λmax / λmin VIF判断标准 < 5: 安全 5-10: 关注 | ≥10: 处理 相关系数判断 |r| < 0.7: 安全 0.7-0.9: 关注 | ≥0.9: 处理 条件数判断 < 30: 安全 30-100: 关注 | ≥100: 处理 共线性诊断报告

2.5 实战建议

最后,给你几个我在实际项目中总结的经验:

  1. 别只看一个指标。 VIF、相关系数、条件数各有侧重,结合起来看才全面。
  2. 先做标准化。 计算 VIF 和特征值之前,最好把数据标准化。否则量纲差异会影响结果。
  3. 注意虚拟变量陷阱。 如果你用了 one-hot 编码,记得删掉一个类别,否则必然产生完美共线性。
  4. 迭代处理。 发现共线性后,删掉一个特征,重新计算 VIF。有时候删一个就够了。

避坑指南:我曾经在一个风控项目里,一口气删了 5 个 VIF 高的特征,结果模型 AUC 掉了 0.15。后来才发现,有些特征虽然共线性高,但各自携带了不同的预测信息。删之前一定要结合业务理解,别当甩手掌柜。

好了,这一章的内容就到这里。诊断工具你已经有了,下一章我们聊聊怎么处理这些共线性——是删特征、做变换,还是用正则化?到时候见分晓。


公众号:蓝海资料掘金营,微信deep3321