清洗只是把数据弄「干净」,特征工程才是把数据变「好用」。本页聚焦 特征构造、类别编码、数值缩放、特征选择、降维、多项式交互 等「处理与变换」环节,用 10 行样本带你走完从原始字段到可建模特征矩阵的完整八步。
特征构造是从已有字段衍生新特征的过程,是特征工程里最吃业务理解的一步。原始表里往往只有「注册时间」「收入」「家庭人数」这种裸字段,但模型真正关心的是「注册多久了」「人均收入多少」「是不是周末注册的」这类更贴近业务语义的衍生量。三类常见构造思路:
register_date 衍生年/月/日/星期/是否周末/注册时长(天)。周期性特征对行为预测极有用。import pandas as pd # ① 从 register_date 衍生时间特征 df['register_date'] = pd.to_datetime(df['register_date']) df['register_year'] = df['register_date'].dt.year # 年 df['register_month'] = df['register_date'].dt.month # 月 df['register_dow'] = df['register_date'].dt.dayofweek # 星期几(0=周一) df['is_weekend'] = df['register_dow'].isin([5, 6]).astype(int) # 是否周末 REF = pd.to_datetime('2024-03-15') df['register_tenure_days'] = (REF - df['register_date']).dt.days # 注册时长(天) # ② 从 income + family_size 衍生人均收入 df['income_per_capita'] = df['income'] / df['family_size'] # ③ 从 price × quantity 衍生总额(电商场景) df['total_amount'] = df['price'] * df['quantity']
紫色底色 = 新衍生列。注意 is_weekend、register_tenure_days 这类特征原始表里根本没有,但对「周末注册用户更活跃」「老用户留存更高」这类业务假设极其关键。
模型只懂数字,类别字段(城市、学历、邮编)必须编码成数值。但「怎么编」大有讲究——选错方法轻则丢信息,重则数据泄漏。四种主流方法,按类别基数和是否有序来选:
| 方法 | 适用场景 | 核心坑 |
|---|---|---|
| One-Hot | 无序类别、基数低(<10) | 高基数 → 维度爆炸 + 稀疏 |
| Label / Ordinal | 有序类别(学历:高中<本科<硕士) | 无序类别用会引入虚假序关系 |
| Target Encoding | 高基数(邮编、城市) | 用全量算 = 泄漏 label,必须交叉验证 |
| Frequency Encoding | 类别频次有区分度时 | 同频不同类会被赋同值 |
import pandas as pd from sklearn.preprocessing import LabelEncoder from category_encoders import TargetEncoder # pip install category_encoders # ① One-Hot:无序类别(城市),基数低时首选 df = pd.get_dummies(df, columns=['city'], prefix='city') # 缺点:类别数 = 列数,高基数(如邮编)会维度爆炸 # ② Ordinal Encoding:有序类别(学历:高中<本科<硕士) edu_map = {'高中': 0, '本科': 1, '硕士': 2, '博士': 3} df['education'] = df['education'].map(edu_map) # 注意:无序类别用 Label 会引入虚假序关系,线性模型慎用 # ③ Target Encoding:高基数类别,按目标均值编码 te = TargetEncoder(cols=['city']) df['city_target'] = te.fit_transform(df['city'], df['label']) # 必须在交叉验证内计算,否则会泄漏 label → 过拟合 # ④ Frequency Encoding:按类别出现频次编码 freq = df['city'].value_counts(normalize=True) df['city_freq'] = df['city'].map(freq)
同一列 city,4 种方法产出完全不同的列数与语义。高基数时 One-Hot 列数 = 类别数,而 Target/Frequency 始终是 1 列——这是高基数场景避免维度爆炸的关键。
年龄几十、收入几万——量纲差三个数量级,不缩放的话收入会「淹没」年龄的作用。距离类模型(KNN)、梯度下降模型(神经网络、逻辑回归、SVM)对量纲敏感,必须缩放;而树模型(随机森林、XGBoost)按阈值分裂,对量纲不敏感,一般不需要缩放。
| 方法 | 结果 | 适用 |
|---|---|---|
| MinMax | 压到 [0,1] | 神经网络/KNN;怕异常值 |
| Standard | 均值0、方差1 | 线性模型/SVM 默认 |
| Robust | 中位数0、IQR标尺 | 有离群点时稳健 |
| Log/Box-Cox | 压长尾为正态 | 收入/点击量等偏态 |
from sklearn.preprocessing import MinMaxScaler, StandardScaler, RobustScaler from sklearn.preprocessing import PowerTransformer, KBinsDiscretizer import numpy as np # ① MinMax:压缩到 [0,1],神经网络 / KNN / 距离类首选 mm = MinMaxScaler() df[['age', 'income']] = mm.fit_transform(df[['age', 'income']]) # ② Standard:均值 0、方差 1,线性模型 / SVM 首选 ss = StandardScaler() df[['age', 'income']] = ss.fit_transform(df[['age', 'income']]) # ③ Robust:用中位数和 IQR,对离群点稳健 rs = RobustScaler() df[['income']] = rs.fit_transform(df[['income']]) # ④ Log 变换:长尾分布(收入、点击量)压成正态 df['income_log'] = np.log1p(df['income']) # log(1+x) 防 0 # ⑤ Box-Cox:自动找最优幂变换(要求值 > 0) pt = PowerTransformer(method='box-cox') df['income_bc'] = pt.fit_transform(df[['income']]) # ⑥ 分箱离散化:连续值 → 有序分桶 kbd = KBinsDiscretizer(n_bins=5, encode='ordinal', strategy='quantile') df['age_bin'] = kbd.fit_transform(df[['age']])
注意 Log 变换把大值「压」得更狠(非线性),而 MinMax 把数据严格压进 [0,1]。Standard 输出有正有负(以均值为 0),Robust 不受极端值影响。
特征不是越多越好——冗余、噪声特征会让模型过拟合 + 训练变慢 + 解释性变差。特征选择就是挑出「真正有用的」那一批,三大流派:
feature_importances_。from sklearn.feature_selection import (VarianceThreshold, SelectKBest, mutual_info_classif, RFE, SelectFromModel) from sklearn.linear_model import Lasso, LogisticRegression from sklearn.ensemble import RandomForestClassifier X, y = df.drop(columns=['label']), df['label'] # ① 过滤法 - 方差阈值:删除方差过小的常量特征 vt = VarianceThreshold(threshold=0.01) X_vt = vt.fit_transform(X) # ② 过滤法 - 互信息:按与 label 的相关性选 Top K skb = SelectKBest(mutual_info_classif, k=5) X_kbest = skb.fit_transform(X, y) # ③ 包装法 - RFE 递归消除:反复训练 + 剔除最弱特征 rfe = RFE(estimator=LogisticRegression(), n_features_to_select=5) X_rfe = rfe.fit_transform(X, y) # ④ 嵌入法 - L1 Lasso:L1 正则自动把弱特征系数压到 0 sfm = SelectFromModel(Lasso(alpha=0.01)) X_lasso = sfm.fit_transform(X, y) # ⑤ 嵌入法 - 树模型重要性 rf = RandomForestClassifier().fit(X, y) imp = pd.Series(rf.feature_importances_, index=X.columns) print(imp.sort_values(ascending=False))
特征选择是「挑列」,降维是「把多列压缩成更少的新列」——通过线性/非线性投影,保留信息的同时大幅降维度。注意:降维后的特征不再有原始语义,是「主成分」「隐变量」。
from sklearn.decomposition import PCA, TruncatedSVD from sklearn.manifold import TSNE # ① PCA:线性降维,保留 95% 方差 pca = PCA(n_components=0.95) # 自动选主成分个数 X_pca = pca.fit_transform(X) print('保留方差比:', pca.explained_variance_ratio_.sum()) print('降维后:', X_pca.shape) # (n, k),k 远小于原维度 # ② TruncatedSVD:稀疏矩阵(如 One-Hot 后)专用 svd = TruncatedSVD(n_components=10) X_svd = svd.fit_transform(X_sparse) # ③ t-SNE:非线性,仅可视化,不可生产推理 X_2d = TSNE(n_components=2, perplexity=30).fit_transform(X) # ⚠️ t-SNE 没有 transform 方法,无法对新数据推理
PCA 找到方差最大的两个方向(PC1、PC2)作为新坐标轴,8 个 3D 点被投影成 2D 点。信息有损失但保留了主要结构——这就是「降维」的本质。
线性模型(逻辑回归、线性回归)只能学线性关系,但现实里特征间常有交互:年龄和收入的「组合效应」、两个基因的「协同作用」。PolynomialFeatures 自动生成平方项、交互项,给线性模型补上非线性能力——但维度会指数级膨胀,必须配合特征选择。
from sklearn.preprocessing import PolynomialFeatures # 生成二次项 + 交互项:[a, b] → [1, a, b, a², a*b, b²] poly = PolynomialFeatures(degree=2, interaction_only=False, include_bias=False) X_poly = poly.fit_transform(X[['age', 'income']]) print('扩展后维度:', X_poly.shape) # 2 列 → 5 列 # interaction_only=True:只保留交互项 a*b,不要 a²、b² poly_i = PolynomialFeatures(degree=2, interaction_only=True, include_bias=False) X_inter = poly_i.fit_transform(X[['age', 'income']]) # → [a, b, a*b] # 适用:线性模型补非线性能力(逻辑回归拟合非线性边界) # 风险:degree=3+ 时维度爆炸,必须配合特征选择
特征工程里 80% 的「玄学问题」都源于这五条。记住它们,能少走几个月弯路:
点击下载一份可直接喂给模型的特征工程结果表——10 行 × 11 列,包含:MinMax 缩放后的 age、Standard 缩放后的 income、衍生特征(注册时长/是否周末/人均收入)、4 列 city One-Hot、family_size、label。用 pd.read_csv() 即可直接加载训练。
文件名 table_feature_engineered.csv。列说明:age_mm(MinMax)、income_std(Standard)、register_tenure_days、is_weekend、income_per_capita、family_size、city_Beijing / city_Shanghai / city_Shenzhen / city_Chengdu(One-Hot)、label。