📊 特征工程 · 表格/机器学习

表格数据 · 特征工程与数据处理全流程

清洗只是把数据弄「干净」,特征工程才是把数据变「好用」。本页聚焦 特征构造、类别编码、数值缩放、特征选择、降维、多项式交互 等「处理与变换」环节,用 10 行样本带你走完从原始字段到可建模特征矩阵的完整八步。

样本 10 行 × 7 列 特征工程 6 大环节 5 个交互 Demo 不重复质检/清洗内容

① 特征构造(Feature Construction)

特征构造是从已有字段衍生新特征的过程,是特征工程里最吃业务理解的一步。原始表里往往只有「注册时间」「收入」「家庭人数」这种裸字段,但模型真正关心的是「注册多久了」「人均收入多少」「是不是周末注册的」这类更贴近业务语义的衍生量。三类常见构造思路:

📅
时间拆解
从 register_date 衍生年/月/日/星期/是否周末/注册时长(天)。周期性特征对行为预测极有用。
➗
比值/组合
收入 ÷ 家庭人数 = 人均收入;价格 × 数量 = 总额;点击 ÷ 曝光 = CTR。组合往往比单字段更有信息量。
🔬
业务沉淀
把领域知识变成列:「是否高消费时段」「用户分级」「近 7 日活跃天数」——这些是模型学不到的先验。
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']

🧪交互演练:一键衍生 6 个新特征

点击按钮,从 register_date / income / family_size 衍生 6 个新特征,表格从 4 列扩展到 10 列。

紫色底色 = 新衍生列。注意 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 列变化

同一列 city,4 种方法产出完全不同的列数与语义。高基数时 One-Hot 列数 = 类别数,而 Target/Frequency 始终是 1 列——这是高基数场景避免维度爆炸的关键。

③ 数值特征缩放与变换

年龄几十、收入几万——量纲差三个数量级,不缩放的话收入会「淹没」年龄的作用。距离类模型(KNN)、梯度下降模型(神经网络、逻辑回归、SVM)对量纲敏感,必须缩放;而树模型(随机森林、XGBoost)按阈值分裂,对量纲不敏感,一般不需要缩放。

⚖️ 缩放方法对比
方法 结果 适用
MinMax压到 [0,1]神经网络/KNN;怕异常值
Standard均值0、方差1线性模型/SVM 默认
Robust中位数0、IQR标尺有离群点时稳健
Log/Box-Cox压长尾为正态收入/点击量等偏态
🧠 一句话决策
• 神经网络/KNN/SVM → MinMax 或 Standard
• 线性/逻辑回归 → Standard
• 数据有离群点 → Robust
• 长尾偏态分布 → 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']])

🧪交互演练:拖动 3 个 income 值,对比 4 种缩放结果

income₁ = 6800
income₂ = 12000
income₃ = 20000

注意 Log 变换把大值「压」得更狠(非线性),而 MinMax 把数据严格压进 [0,1]。Standard 输出有正有负(以均值为 0),Robust 不受极端值影响。

④ 特征选择(Feature Selection)

特征不是越多越好——冗余、噪声特征会让模型过拟合 + 训练变慢 + 解释性变差。特征选择就是挑出「真正有用的」那一批,三大流派:

🔍
过滤法(Filter)
不训练模型,按统计量筛:方差阈值、相关系数、互信息。速度快,与模型无关。
🎁
包装法(Wrapper)
反复训练模型评估子集:RFE 递归消除。效果好但计算贵,特征多时慎用。
🧬
嵌入法(Embedded)
训练时自带选择:L1 Lasso 把弱特征系数压到 0、树模型给 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))

🧪交互演练:按互信息排序,剔除最弱 3 个特征

点击按钮,按互信息得分排序并剔除最低 3 个特征(保留 Top 5)。

⑤ 降维(Dimensionality Reduction)

特征选择是「挑列」,降维是「把多列压缩成更少的新列」——通过线性/非线性投影,保留信息的同时大幅降维度。注意:降维后的特征不再有原始语义,是「主成分」「隐变量」。

📐
PCA
线性降维,找方差最大的方向。可保留 95% 方差。前提:数值已缩放,不适用稀疏 One-Hot。
🌐
t-SNE / UMAP
非线性,保留局部结构,仅用于可视化。t-SNE 无 transform,不可用于生产推理。
🧊
TruncatedSVD
稀疏矩阵专用(如 TF-IDF、One-Hot 后的大稀疏表),不要求中心化。
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 方法,无法对新数据推理

🧪交互演练:3D → 2D PCA 投影示意

左侧是 3 维原始空间(8 个样本点)。PCA 会找到方差最大的方向,把数据投影到低维平面。
原始 3 维空间
PCA 降到 2 维

PCA 找到方差最大的两个方向(PC1、PC2)作为新坐标轴,8 个 3D 点被投影成 2D 点。信息有损失但保留了主要结构——这就是「降维」的本质。

⑥ 多项式与交互特征

线性模型(逻辑回归、线性回归)只能学线性关系,但现实里特征间常有交互:年龄和收入的「组合效应」、两个基因的「协同作用」。PolynomialFeatures 自动生成平方项、交互项,给线性模型补上非线性能力——但维度会指数级膨胀,必须配合特征选择。

📦 degree=2 时 [a, b] 的展开(2 列 → 5 列):
[a, b] → [a, b, a², a×b, b²]
interaction_only=True 时只保留 [a, b, a×b](去掉 a²、b² 单项的平方)
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+ 时维度爆炸,必须配合特征选择
💡 关键判断:多项式特征对线性模型提升最大(逻辑回归、线性回归);树模型本身就能学交互,加多项式收益小还可能过拟合。degree ≥ 3 时维度会指数膨胀(3 特征 degree=3 → 34 列),务必跟一步特征选择。

⑦ 常见坑与最佳实践

特征工程里 80% 的「玄学问题」都源于这五条。记住它们,能少走几个月弯路:

① 特征工程必须在 train/test 划分之后,且只用训练集 fit
缩放器、Target Encoding、PCA 的统计量(均值、方差、目标均值)只能用训练集算,再 transform 测试集。用全量 fit = 测试集信息泄漏 = 指标虚高 = 上线翻车。
② Target / 频率编码要在交叉验证内做
Target Encoding 直接用全量 label 均值会把答案写进特征。正确做法:在 K 折交叉验证里,用其余折的 label 均值编码当前折,避免泄漏。
③ PCA 前必须先缩放
PCA 找「方差最大」的方向,量纲大的特征方差天然大,不缩放就等于 PCA 只围着那个大特征转。先 StandardScaler 再 PCA 是铁律。
④ 高基数类别优先 Target / Frequency,而非 One-Hot
邮编 1000 种、商品 ID 上万——One-Hot 会让列数爆炸到无法训练。Target Encoding(1 列)或 Frequency Encoding(1 列)是这类场景的标准答案。
⑤ 多项式特征维度爆炸,需配合特征选择
degree=2 还好,degree=3 时 10 个特征能膨胀到 286 列。生成后立刻用 Lasso 或互信息筛掉噪声项,否则过拟合 + 训练龟速。

⑧ 下载特征工程结果 CSV

点击下载一份可直接喂给模型的特征工程结果表——10 行 × 11 列,包含:MinMax 缩放后的 age、Standard 缩放后的 income、衍生特征(注册时长/是否周末/人均收入)、4 列 city One-Hot、family_size、label。用 pd.read_csv() 即可直接加载训练。

行数 10 列数 11 含:衍生特征 + One-Hot + 缩放数值 BOM + 引号转义,Excel/pandas 无乱码

文件名 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。