1. 项目缘起:为什么我们总在“杀”特征?
做机器学习项目,尤其是数据挖掘和建模,你肯定遇到过这种情况:辛辛苦苦从业务里扒拉出几百个特征,满怀信心地扔进模型,结果训练时间长得离谱,模型效果却提升有限,甚至还不如只用几十个特征的时候。更让人头疼的是,模型变得难以解释,你搞不清楚到底是哪个特征在真正起作用。
这背后,十有八九是“冗余特征”在作祟。冗余特征,顾名思义,就是那些提供的信息跟其他特征高度重叠、甚至一模一样的特征。比如,在一个房价预测模型里,你同时引入了“房屋建筑面积(平方米)”和“房屋使用面积(平方米)”。这两个特征虽然名字不同,但高度相关,它们所携带的关于房屋大小的信息是重复的。模型在训练时,会为这两个高度相关的特征都分配权重,这不仅浪费了计算资源,更严重的是,它可能导致模型权重变得不稳定(统计学上称为“多重共线性”),让模型在遇到新数据时表现得很差,也就是我们常说的“过拟合”。
所以,特征选择,特别是消除冗余特征,是建模前至关重要的一步。它不是为了炫技,而是为了构建一个更健壮、更高效、更可解释的模型。今天,我们不谈那些复杂的嵌入式或包装式特征选择方法,就从最基础、最直观,也最容易被忽视的“相关性分析”说起。这个方法简单到用Excel都能做,但用好了,能帮你避开建模路上80%的坑。
2. 相关性分析:不只是看个数字那么简单
提到相关性分析,很多人第一反应就是计算皮尔逊相关系数(Pearson Correlation Coefficient),得到一个介于-1到1之间的数字,然后根据经验阈值(比如0.8或0.9)来判断是否相关。这个思路没错,但太粗糙了,很容易掉坑里。
2.1 理解三种核心的相关性系数
首先,你得知道面对不同类型的数据,该用哪种“尺子”去量。
皮尔逊相关系数:这是最常用的,但它衡量的是两个连续变量之间的线性相关程度。它的前提假设是数据服从正态分布,且关系是线性的。如果你的两个特征之间的关系是曲线(比如先增后减),皮尔逊系数可能会很低,误导你认为它们不相关。计算时,它用的是协方差除以各自标准差的乘积,公式是ρ = cov(X, Y) / (σ_X * σ_Y)。在Python里,用pandas的.corr()方法默认计算的就是它。
斯皮尔曼等级相关系数:这就是热搜词里的“spearman相关性分析”。它衡量的是两个变量之间的单调关系(即一个变量增大,另一个变量也倾向于增大或减小,不一定是直线)。它不关心具体数值,只关心排名顺序。因此,它对异常值不敏感,也适用于不服从正态分布的数据或等级数据。当你怀疑特征间可能存在非线性但趋势一致的关系时,或者数据中有很多异常值时,斯皮尔曼是更好的选择。在pandas中,调用.corr(method='spearman')即可。
肯德尔等级相关系数:和斯皮尔曼类似,也是基于等级(排序)的非参数相关度量。它在数据量小、或者有很多相同等级(Tie)的时候更稳健。但在机器学习特征筛选中,斯皮尔曼更常见。
怎么选?我的经验是:对于大多数数值型特征,先画个散点图看看。如果点大致沿着一条直线分布,用皮尔逊;如果呈单调的曲线,或者点比较散乱但趋势明显,用斯皮尔曼。拿不准的时候,可以两个都算一下,如果结果差异很大,那就要深入探究数据分布了。
2.2 相关系数矩阵:你的第一张特征“关系网”
实操的第一步,永远是先计算所有特征两两之间的相关系数,生成一个矩阵,并可视化。在Python中,这非常简单:
import pandas as pd import numpy as np import seaborn as sns import matplotlib.pyplot as plt # 假设 df 是你的DataFrame,列是特征 corr_matrix = df.corr(method='pearson') # 或 'spearman' # 可视化 plt.figure(figsize=(12, 10)) sns.heatmap(corr_matrix, annot=True, fmt='.2f', cmap='coolwarm', square=True, cbar_kws={"shrink": .8}) plt.title('Feature Correlation Matrix') plt.show()这张热图是你的“藏宝图”。颜色越红(接近1),表示正相关性越强;越蓝(接近-1),表示负相关性越强。对角线永远是1(自己和自己相关)。你的目标,就是寻找那些非对角线上的深红色或深蓝色方块。
注意:热图的
annot=True会把数值显示在格子里,特征多的时候会显得很乱,可以先关掉(annot=False)看整体色块分布,再对高相关区域进行局部放大查看具体数值。
3. 设定阈值与筛选:艺术与科学的结合
看到相关系数矩阵后,下一个问题就是:多高的相关才算“冗余”?0.7?0.8?0.9?网上有很多经验值,但我告诉你,没有放之四海而皆准的黄金阈值。这需要结合你的业务知识、模型特性和后续步骤来综合判断。
3.1 阈值的动态决策逻辑
业务层面:有些特征,即使数学上高度相关,业务上也可能代表不同含义,需要保留。比如,“用户最近一次登录时间”和“用户本周登录天数”,在预测用户流失时都可能有价值,虽然它们相关。这时,你需要基于业务逻辑决定保留哪一个,或者都保留但意识到它们共线性的风险。
模型层面:
- 线性模型(如线性回归、逻辑回归):对多重共线性非常敏感,会导致系数估计不准、标准误增大。建议设置较严格的阈值,如
|corr| > 0.8或0.85时,就必须处理。 - 树模型(如随机森林、XGBoost):对特征间的相关性不敏感,因为这些模型是单变量分裂的。阈值可以放宽一些,比如
|corr| > 0.9。消除高度冗余的特征主要为了提升训练效率和模型简洁度。 - 深度学习模型:通常能自动学习特征间的交互,但过多的冗余特征同样会增加不必要的计算复杂度和过拟合风险。阈值可参考树模型,或结合特征重要性(如果可用)来判断。
- 线性模型(如线性回归、逻辑回归):对多重共线性非常敏感,会导致系数估计不准、标准误增大。建议设置较严格的阈值,如
经验法则:一个常用的起步策略是,设定一个阈值(例如0.9),然后观察特征簇。在热图中,你会看到几组特征彼此之间都高度相关,形成一个“簇”。我们的目标是在每个簇里,保留一个最具代表性(业务意义明确、或与其他簇特征相关性较低)的特征,剔除簇内其他特征。
3.2 自动化筛选的代码实现
手动看热图选特征,在特征多的时候不现实。下面是一个基于相关性的自动化特征筛选函数,它采用“保留高相关特征组中其中一个”的策略:
def remove_redundant_features_by_correlation(df, threshold=0.9): """ 根据相关系数阈值,移除冗余特征。 策略:对于一组高度相关的特征,只保留其中第一个(按列顺序),移除组内其他特征。 参数: df: pandas DataFrame,仅包含特征列。 threshold: 相关系数绝对值阈值,高于此值视为高度相关。 返回: selected_features: 筛选后的特征列表。 """ # 计算相关系数矩阵 corr_matrix = df.corr().abs() # 取绝对值,关心相关性强弱而非方向 # 取上三角矩阵(不包括对角线),避免重复比较 upper_tri = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(bool)) # 找出相关系数大于阈值的特征对 to_drop = [column for column in upper_tri.columns if any(upper_tri[column] > threshold)] print(f"基于阈值 {threshold},建议移除的特征数量: {len(to_drop)}") print(f"建议移除的特征: {to_drop}") # 保留的特征 selected_features = [col for col in df.columns if col not in to_drop] print(f"保留的特征数量: {len(selected_features)}") return selected_features # 使用示例 # 假设 X 是你的特征DataFrame features_to_keep = remove_redundant_features_by_correlation(X, threshold=0.85) X_filtered = X[features_to_keep]这个函数提供了一个基线方法。但它有个明显的缺点:它只是按列的顺序(DataFrame的列顺序)保留第一个特征,这个选择可能不是最优的。更高级的做法是,在每个高相关组内,计算每个特征与组外所有特征的平均相关性,保留那个与组外特征平均相关性最低的(即最具独特信息的),或者结合业务知识来选择。
4. 超越简单相关:陷阱与进阶策略
仅仅依靠两两相关系数来消除冗余,可能会遇到一些陷阱,需要更精细的工具和策略。
4.1 陷阱一:多重共线性与VIF
两两相关只能发现“一对一”的冗余。但现实中,更常见的是“多对一”的冗余,即一个特征可以由其他多个特征线性组合而成。这就是经典的多重共线性问题。检测它,需要用方差膨胀因子。
VIF衡量的是一个特征,由于与其他特征相关,其回归系数的方差被放大了多少倍。经验上:
- VIF = 1:表示该特征与其他特征无关。
- 1 < VIF <= 5:中度相关,通常可以接受。
- VIF > 5 或 10:存在严重多重共线性,需要考虑处理。
计算VIF通常需要借助statsmodels库:
from statsmodels.stats.outliers_influence import variance_inflation_factor import pandas as pd def calculate_vif(X): """ 计算特征DataFrame的VIF值。 注意:需要给数据添加常数项(截距)。 """ X_with_const = pd.DataFrame(X).copy() # 确保没有无穷大或缺失值 X_with_const = X_with_const.replace([np.inf, -np.inf], np.nan).dropna() vif_data = pd.DataFrame() vif_data["feature"] = X_with_const.columns vif_data["VIF"] = [variance_inflation_factor(X_with_const.values, i) for i in range(X_with_const.shape[1])] return vif_data # 使用 vif_df = calculate_vif(X_filtered) # 用之前相关性筛选后的数据 print(vif_df.sort_values(by='VIF', ascending=False))处理高VIF特征是一个迭代过程:移除VIF最高的特征,重新计算剩余特征的VIF,直到所有特征的VIF都低于阈值(如5)。这个过程可以和前面的相关性筛选结合。
4.2 陷阱二:非线性关系与互信息
皮尔逊和斯皮尔曼捕捉的是单调关系。但如果两个特征之间存在复杂的非线性关系(比如圆形、正弦波),这些系数都会接近0,让你误以为它们独立。然而,在机器学习中,这种非线性关系可能被模型(如神经网络、带核函数的SVM)所利用,盲目剔除可能会损失信息。
这时,可以引入互信息。互信息衡量的是两个变量共享的信息量,对关系形式没有假设,能捕捉任何类型(线性或非线性)的统计依赖。scikit-learn提供了计算函数:
from sklearn.feature_selection import mutual_info_regression, mutual_info_classif # 回归问题用 mutual_info_regression,分类问题用 mutual_info_classif # 假设 X 是特征,y 是连续型目标变量 mi_scores = mutual_info_regression(X, y) mi_series = pd.Series(mi_scores, index=X.columns).sort_values(ascending=False) # 你可以画出来看看 mi_series.plot.bar(figsize=(10, 6)) plt.ylabel('Mutual Information Score') plt.title('Feature Importance based on Mutual Information')互信息可以帮助你从“预测目标”的角度理解特征的重要性。一个与目标变量互信息高,但与其他特征也高度互信息的特征,可能是一个强预测因子,但也可能携带了大量冗余信息。你可以结合相关性/互信息矩阵,做出更明智的取舍。
4.3 策略整合:一个实战工作流
在实际项目中,我通常采用一个分层的工作流来处理冗余特征:
第一层:业务清洗。基于领域知识,直接移除明显无意义或重复的业务指标(如同时存在“销售额”和“销售额(万元)”)。
第二层:高相关过滤。计算斯皮尔曼相关系数矩阵(对数据分布假设更宽松),设定一个较高的阈值(如0.95),使用改进的筛选策略(如在每个高相关簇中,保留与目标变量相关性最高或互信息最大的那个),进行初步剔除。
第三层:多重共线性诊断。对剩余特征计算VIF,迭代移除VIF值最高的特征,直到所有特征VIF<10。
第四层:模型导向精筛。将处理后的特征送入一个简单的基线模型(如Lasso回归或带特征重要性的随机森林)。Lasso的系数收缩可以自动将一些冗余特征的系数压到0。树模型的特征重要性可以帮你确认哪些特征在预测中真正有用。结合这些结果,进行最终的手动调整。
5. 相关性分析在机器学习流程中的定位
看了热搜词,很多人关心“机器学习应用流程”。消除冗余特征,属于特征工程的核心环节,通常发生在数据清洗和探索性数据分析之后,在模型训练之前。一个简化的流程如下:
数据收集 -> 数据清洗(处理缺失值、异常值)-> 探索性数据分析(EDA,包括相关性分析、分布可视化)-> 特征工程(构造新特征、转换特征、特征选择)-> 模型训练与评估 -> 模型部署
相关性分析是EDA和特征选择之间的桥梁。它不仅能指导我们删除冗余特征,还能带来其他好处:
- 发现潜在关系:高相关可能暗示着有趣的业务逻辑,值得深入分析。
- 辅助特征构造:如果两个特征A和B都与目标相关,但彼此不相关,那么构造一个交互特征(如A*B)可能会带来效果提升。
- 降低维度灾难风险:对于样本量有限的数据集,减少特征数量能有效缓解过拟合。
最后,记住一点:特征选择没有银弹。相关性分析是一个强大而基础的起点,但它必须与业务理解、模型特性和其他统计工具(如VIF、互信息)结合使用。每次做完特征筛选,一定要在验证集或通过交叉验证评估模型性能的变化。有时候,扔掉一个“冗余”特征,可能会损失掉模型捕捉复杂模式的微弱能力,这需要反复的实验和权衡。
我个人的习惯是,建立一个特征筛选的“流水线”,将相关性阈值、VIF阈值作为参数,方便快速实验不同严格度下的模型表现。毕竟,我们的终极目标不是得到一个最“干净”的特征集,而是得到一个最“好用”的模型。