4.3 特征工程高级技巧 本节摘要:特征工程决定了模型效果的上限,模型本身只是在逼近这个上限。基础篇讲过标准化和独热编码,本节往高级走:特征选择用 SelectFromModel 和递归消除去掉冗余;多项式与交互特征让线性模型也能捕捉非线性;目标编码把高基数类别变量压成数值;主成分分析做降维;再补齐缺失值处理和时间特征这两个实战高频场景。掌握了这些,你就有了把脏数据加工成模型爱吃形态的完整工具箱。 本节导读 阅读完本节,你应当能够: 区分过滤式、包裹式、嵌入式三类特征选择,并说出各自代表方法。 用 SelectFromModel 和 RFECV 做特征选择,理解两者适用场景的差异。 说清多项式特征和交互特征各解决什么问题,以及维度爆炸的风险。 理解目标编码的原理与过拟合风险,知道何时该用。
本节摘要:特征工程决定了模型效果的上限,模型本身只是在逼近这个上限。基础篇讲过标准化和独热编码,本节往高级走:特征选择用 SelectFromModel 和递归消除去掉冗余;多项式与交互特征让线性模型也能捕捉非线性;目标编码把高基数类别变量压成数值;主成分分析做降维;再补齐缺失值处理和时间特征这两个实战高频场景。掌握了这些,你就有了把脏数据加工成模型爱吃形态的完整工具箱。
阅读完本节,你应当能够:
有句流传很广的话——数据和特征决定了机器学习的上限,模型和算法只是在逼近这个上限。翻译成大白话:你费劲调半天的模型,可能还不如把某个特征换一种加工方式带来的提升大。这不是说模型不重要,而是说很多人把时间全花在模型上,忽略了特征这一头。
特征工程做的事,本质是"把人类对问题的理解翻译成模型能读懂的数字"。理解越深,翻译越准,模型越省力。下面按"选、造、编、降、补、挖"六个动作,把高级技巧串起来讲。
特征不是越多越好。冗余特征浪费算力,噪声特征带偏模型,无关特征纯属干扰。特征选择就是挑出真正有用的子集。它分三派:
动手看嵌入式最常用的一例:
from sklearn.feature_selection import SelectFromModel from sklearn.ensemble import RandomForestClassifier rf = RandomForestClassifier(random_state=42).fit(X_train, y_train) sel = SelectFromModel(rf, threshold="median") X_train_sel = sel.fit_transform(X_train, y_train) X_test_sel = sel.transform(X_test)
这四行干的事:先让随机森林算一遍特征重要性,再留下重要性高于中位数的特征,最后用筛选后的数据重训。如果不知道该留几个特征,用 RFECV——它加了一层交叉验证,自动挑出得分最高的特征数量。
💡 关键直觉:把特征选择想成"裁员"。过滤式是按简历初筛,包裹式是让主管一轮轮面,嵌入式是边干活边淘汰。简历筛最快但可能看走眼,一轮轮面最准但最贵。
线性模型又稳又可解释,但它只能表达直线关系,遇到"面积越大越值钱、但地段好时涨得更猛"这种交互,就抓瞎了。多项式特征和交互特征就是给线性模型补这个短板——把原始特征的高次项和乘积项显式造出来,让线性模型在扩展后的空间里也能拟合曲线。
from sklearn.preprocessing import PolynomialFeatures import numpy as np X = np.array([[1, 2], [3, 4]]) poly = PolynomialFeatures(degree=2, include_bias=False) X_poly = poly.fit_transform(X)
原始两列 [x1, x2] 会被扩展成 [x1, x2, x1平方, x1乘x2, x2平方]。线性回归拿到这五列,就等于拥有了一个二次曲面。
代价是维度爆炸。假设原始有 20 个特征,degree 设 2,特征数会从 20 涨到两百多;degree 设 3,直接上千。特征一多,线性模型开始过拟合,训练也变慢。所以多项式特征通常只用在特征数量本来就不多的场景,或者配合正则化一起用。
| 方式 | 生成什么 | 适用 | 风险 |
|---|---|---|---|
| 多项式特征 | 单特征的高次项 | 特征与目标有非线性关系 | 维度爆炸、过拟合 |
| 交互特征 | 多特征的两两乘积 | 特征间有协同效应 | 同上,且解释性下降 |
| interaction_only 模式 | 只保留乘积项 | 只想抓交互,不要高次项 | 可能漏掉非线性主效应 |
类别变量如果取值很多(比如邮编、商品 ID,动辄上千种),独热编码会炸出一千多列稀疏矩阵,树模型嫌慢,线性模型嫌吵。目标编码的思路是:不用 0 和 1 表示类别,而是用"这个类别的目标均值"来替换它。
原理直观:如果一个邮编对应的房子普遍很贵,那这个邮编编码成的数值就应该偏大,模型一眼就能看出它与高房价的关系。但这里藏着个大坑——过拟合。如果某个类别在训练集里只有两三个样本,你用它们的均值去编码,等于把这个类别的稀有信息泄露给了模型,训练集看着很准,测试集就崩。
所以目标编码从来不是"直接算均值",而是要做平滑:把稀有类别的均值往全局均值拉一拉,样本越少,拉得越狠。Scikit-learn 里没有现成的目标编码器,但相关生态提供了带平滑的实现,核心参数就是平滑强度。用它时务必配合交叉验证,编码统计量只能从训练折里算。
⚠️ 常见坑:在全量数据上先算目标均值、再做编码,再切训练测试集。这就是标准的数据泄露——测试集的目标信息被提前缝进了特征里。正确顺序永远是先切分,编码统计量只从训练集算,再应用到测试集。
降维不是偷工减料,而是去掉冗余维度、保留主要信息。主成分分析(PCA)是最常用的线性降维——它找到数据方差最大的几个方向,把数据投影上去。方差最大的方向往往就是信息最多的方向。
from sklearn.decomposition import PCA pca = PCA(n_components=0.95) # 保留 95% 的方差 X_reduced = pca.fit_transform(X_train)
n_components 可以给整数(留几个成分),也可以给浮点(保留多少比例的方差)。给浮点更省心——你不需要事先猜该降到几维,让算法按信息量自动决定。
降维的适用场景很明确:特征维度过高导致训练慢、噪声大,或者特征间高度相关、信息冗余。它的代价是可解释性——降维后的每个成分都是原始特征的线性组合,业务方看不懂"成分三"是什么。所以如果下游要解释模型,降维前先掂量清楚这笔账。
缺失不是无脑填均值。缺的原因不同,处理方式就该不同。完全随机缺失,均值中位数填充就行;非随机缺失——比如收入高的人偏偏不爱填收入——那"缺失"这件事本身就是个信号,应该专门造一列 0/1 特征标记"是否缺失",而不是把洞填平就完事。
from sklearn.impute import SimpleImputer imp = SimpleImputer(strategy="median") # 中位数填充 X_imputed = imp.fit_transform(X_train)
常用策略有均值、中位数、众数、常数。中位数对异常值更稳,众数适合类别变量,常数适合"缺失有特殊含义"的场景。进阶做法是拿其他特征去预测缺失值,但那要额外建模,成本高,只在缺失比例大且重要时用。
时间戳本身模型用不好,但拆开就值钱了。一个 2024-01-15 09:30 能拆出:年、月、日、星期几、第几周、是月初还是月末、是周末还是工作日、距某个基准日过了多少天。这些零件里往往藏着强信号——零售看星期几和节假日,风控看"距上次交易过了多久"。
import pandas as pd df["hour"] = df["ts"].dt.hour df["weekday"] = df["ts"].dt.weekday df["is_weekend"] = df["weekday"].isin([5, 6]).astype(int)
还有一个容易忽略的点:时间特征有周期性。23 点和 0 点只差一小时,但直接给数值,模型看到的是 23 和 0,会误以为差很远。用正弦余弦把小时、月份编码成角度,才能把"23 点挨着 0 点"这个关系告诉模型。
单看每一招都不难,真正的功力在于组合。一份真实数据,往往同时要补缺失、编类别、选特征、造交互。这时就该用管道把流程串起来,让每一步只在训练集上 fit、再一致地 apply 到测试集,从根上杜绝数据泄露。
from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.impute import SimpleImputer from sklearn.linear_model import LogisticRegression pipe = Pipeline([ ("impute", SimpleImputer(strategy="median")), ("scale", StandardScaler()), ("model", LogisticRegression()), ]) pipe.fit(X_train, y_train)
⚠️ 常见坑:把 StandardScaler 的 fit 用到整个数据集(训练加测试一起算均值和方差)。测试集的信息又漏进来了。管道的价值正在于此——它强制"先 fit 训练集、再 transform 测试集",你想漏都难。
💡 关键直觉:特征工程不是一次性做完的动作,而是一条要反复走的流水线。先用最简单的特征跑通基线,再一招一招往上加,每加一招看一次验证集有没有真提升。没提升的招就撤掉——能加会减,才算真的会用。
多项式特征 degree 设多少合适?
没有固定值,2 或 3 是常见起点。特征多时 degree 一高维度就爆,先用交叉验证看验证误差,再决定要不要往上加。多数实际问题里,degree 超过 3 的收益就很有限了。
目标编码和独热编码怎么选?
类别取值少、彼此独立,独热编码就够。类别取值多、且类别与目标有统计关联(比如邮编和房价),目标编码更省维度、更能体现这种关联。但目标编码一定要做平滑、且只能在训练集上算统计量,否则就是数据泄露,这点务必记住。
到这里,我们把"造零件、拼模型、喂数据"三条线都走通了。下一节把这条流水线放到数据量暴涨的现实里——大规模数据处理,看看当内存装不下时,前面学的这些招还能不能照常使。