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 知识体系总览
下面这张图总结了共线性诊断的完整流程,我建议你保存下来,以后做项目时对照着用:
2.5 实战建议
最后,给你几个我在实际项目中总结的经验:
- 别只看一个指标。 VIF、相关系数、条件数各有侧重,结合起来看才全面。
- 先做标准化。 计算 VIF 和特征值之前,最好把数据标准化。否则量纲差异会影响结果。
- 注意虚拟变量陷阱。 如果你用了 one-hot 编码,记得删掉一个类别,否则必然产生完美共线性。
- 迭代处理。 发现共线性后,删掉一个特征,重新计算 VIF。有时候删一个就够了。
避坑指南:我曾经在一个风控项目里,一口气删了 5 个 VIF 高的特征,结果模型 AUC 掉了 0.15。后来才发现,有些特征虽然共线性高,但各自携带了不同的预测信息。删之前一定要结合业务理解,别当甩手掌柜。
好了,这一章的内容就到这里。诊断工具你已经有了,下一章我们聊聊怎么处理这些共线性——是删特征、做变换,还是用正则化?到时候见分晓。
公众号:蓝海资料掘金营,微信deep3321