特征工程实战:从原始数据到有效特征的完整流程

系统讲解特征工程全流程:数据清洗(缺失值/异常值/重复)、特征构造(数值/类别/文本/时间/组合特征)、编码技巧(独热/目标编码/嵌入)、特征选择(过滤/包裹/嵌入法),以及特征工程在真实业务数据上的完整实战与防泄漏纪律。

引言

「模型决定上限,特征决定下限」——同一份数据、同一个算法,特征工程做得好坏可以拉开成倍的差距。很多「调参大师」在竞赛里拼不过「特征专家」,原因就在这。

特征工程是把原始数据加工成模型能高效学习的输入:清洗脏数据、构造新特征、编码类别、挑选有效特征。本文按生产流程四步走:数据清洗 → 特征构造 → 编码 → 特征选择,每一步都有可运行代码与决策表。最后重点讲防泄漏——这是特征工程里最容易犯、最致命、也最隐蔽的错误。

前置:[[ml]] 专题的环境与 pandas 基础(https://plumephp.com/ml-python-environment-setup/)。相关深度文章见 [[ai-ml]] 专题。


目录


1. 特征工程全景:它在模型链路里的位置

1.1 完整链路

原始数据 → 清洗 → 特征构造 → 编码 → 特征选择 → 训练
              ↑ 特征工程集中发力 ↑

特征工程的产出是训练特征矩阵 X,直接影响模型的可学性。

1.2 四大任务

任务目标典型手段
清洗去掉脏数据缺失/异常/重复处理
构造造出更有信息量的特征组合、聚合、分箱
编码把非数值变成数值独热、目标编码
选择只留有效特征过滤/包裹/嵌入

2. 数据清洗:缺失、异常与重复

2.1 缺失值处理决策

import pandas as pd

df = pd.read_csv('sales.csv')
print(df.isnull().sum())          # 看缺失分布

# 方案矩阵:
# 少量缺失数值列 → 均值/中位数填充
df['age'] = df['age'].fillna(df['age'].median())

# 类别列 → 众数或"未知"占位
df['city'] = df['city'].fillna('未知')

# 关键字段缺失 → 删行
df = df.dropna(subset=['order_id'])

2.2 缺失占比过高直接弃列

missing_ratio = df.isnull().mean()
drop_cols = missing_ratio[missing_ratio > 0.6].index
df = df.drop(columns=drop_cols)
print("弃列:", list(drop_cols))

2.3 异常值识别(IQR 法)

Q1 = df['amount'].quantile(0.25)
Q3 = df['amount'].quantile(0.75)
IQR = Q3 - Q1
low  = Q1 - 1.5 * IQR
high = Q3 + 1.5 * IQR

outliers = df[(df['amount'] < low) | (df['amount'] > high)]
print("异常值数量:", len(outliers))

2.4 重复行

print("重复行:", df.duplicated().sum())
df = df.drop_duplicates()

3. 特征构造:数值、类别、时间与组合

3.1 数值特征:组合与分箱

# 组合特征(比值往往比原始值更有信息)
df['客单价']   = df['消费额'] / df['消费次数']
df['月均消费'] = df['消费额'] / (df['注册月数'] + 1)

# 分箱:连续值转区间
df['年龄段'] = pd.cut(df['age'], bins=[0, 18, 30, 45, 60, 100],
                     labels=['<18','18-30','30-45','45-60','60+'])

3.2 时间特征:拆出周期性

df['下单日期'] = pd.to_datetime(df['下单日期'])

df['年']   = df['下单日期'].dt.year
df['月']   = df['下单日期'].dt.month
df['星期'] = df['下单日期'].dt.dayofweek        # 0=周一
df['小时'] = df['下单日期'].dt.hour
df['是否周末'] = (df['星期'] >= 5).astype(int)

3.3 聚合特征:跨多行提炼

# 每个用户的统计特征
agg = df.groupby('user_id').agg(
    order_count = ('order_id', 'count'),
    total_amount = ('amount', 'sum'),
    avg_amount = ('amount', 'mean'),
).reset_index()

df_user = df.merge(agg, on='user_id')

3.4 组合特征:交互项

# 显式交互(线性模型尤其需要)
df['消费额_x_频次'] = df['消费额'] * df['消费次数']

构造特征的原则:从业务逻辑出发。问「这个场景里什么因素真正驱动目标」,而不是盲目造一堆组合。


4. 编码技巧:从独热到目标编码

4.1 低基类别:独热编码

from sklearn.preprocessing import OneHotEncoder

df = pd.get_dummies(df, columns=['城市'], prefix='城市')
# 城市 → 城市_北京, 城市_上海, 城市_深圳 ...

4.2 独热的代价

基数值大时(城市 100 个),独热会制造 100 列稀疏特征,训练慢且过拟合风险高。此时考虑目标编码。

4.3 高基类别:目标编码

用目标均值作为类别特征:

# 简单的目标编码(训练集)
city_target = df.groupby('城市')['是否复购'].mean()

# ⚠️ 必须加平滑,防止小样本类别的均值过拟合
smooth = 10
global_mean = df['是否复购'].mean()
df['城市_编码'] = df['城市'].map(
    lambda c: (city_target[c]*df['城市'].eq(c).sum() + global_mean*smooth)
              / (df['城市'].eq(c).sum() + smooth)
)

4.4 时序/序号:直接数值化

# 有序类别直接映射
df['等级'] = df['等级'].map({'普通':0, '银卡':1, '金卡':2, '铂金':3})

4.5 编码选择速查

类别基数建议
≤10独热
10-100独热 or 目标编码
>100目标编码 / 嵌入
有序类别直接数值映射

5. 特征选择:过滤、包裹与嵌入法

5.1 为什么需要选择

特征太多:训练慢、过拟合、可解释性差。三种思路:

方法思路例子
过滤法独立评估每个特征方差、相关性、卡方
包裹法用模型评估子集递归特征消除 RFE
嵌入法训练中自动挑Lasso、树模型重要性

5.2 过滤法:方差与相关性

# 方差极低(几乎不变)的特征无信息
from sklearn.feature_selection import VarianceThreshold

selector = VarianceThreshold(threshold=0.01)
X_reduced = selector.fit_transform(X)

# 与目标高度相关 → 保留;两特征间高度相关 → 去冗余
corr = pd.DataFrame(X).corrwith(y).abs().sort_values(ascending=False)

5.3 嵌入法:树模型特征重要性

from sklearn.ensemble import RandomForestClassifier

rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X, y)

importance = pd.Series(rf.feature_importances_, index=X.columns)
keep = importance[importance > 0.02].index.sort_values()
print("保留特征:", list(keep))

5.4 包裹法:RFE

from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression

rfe = RFE(LogisticRegression(max_iter=1000), n_features_to_select=20)
rfe.fit(X, y)
print("选中的特征:", X.columns[rfe.support_])

6. 文本与图像特征:非结构化数据初探

6.1 文本:TF-IDF 向量化

from sklearn.feature_extraction.text import TfidfVectorizer

tfidf = TfidfVectorizer(max_features=1000, stop_words='english')
X_text = tfidf.fit_transform(df['评论'])

6.2 图像:预训练模型提取

# 用预训练 CNN 的倒数第二层做特征(迁移学习)
from torchvision.models import resnet18
import torchvision.transforms as T

model = resnet18(pretrained=True)
model.fc = torch.nn.Identity()      # 去掉分类头,输出特征向量

6.3 结论

非结构化数据的「特征工程」正在被表示学习(预训练模型)取代:与其手工造特征,不如用模型学到的高层表示。手工特征工程的主战场仍在结构化表格数据。


7. 防泄漏:特征工程最致命的错误

7.1 什么是泄漏

特征里混入了「只有知道答案才知道的信息」,让训练指标虚高、上线暴跌。典型三例:

# ❌ 泄漏1:用全量数据归一化/填充
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)      # fit 用到了测试集统计量

# 正确:fit 只用训练集
scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s = scaler.transform(X_test)
# ❌ 泄漏2:目标编码用全量目标均值
city_target = df.groupby('城市')['是否复购'].mean()   # 用到 y 了

# 正确:在训练集内算,测试集用训练集映射
city_target = df_train.groupby('城市')['是否复购'].mean()
# ❌ 泄漏3:时序问题用了未来信息
# 训练某天的预测,特征里却包含当天之后的统计量

7.2 泄漏检测信号

信号含义
训练准确率 >99%高度怀疑泄漏
测试远低于训练或过拟合、或泄漏
特征名带未来语义检查时序边界

7.3 防泄漏纪律

  1. 一切 fit 只在训练集。
  2. 特征工程的统计量都从训练集算。
  3. 时序数据按时间切分,禁止穿越。

8. 实战:一个端到端特征工程案例

8.1 任务:预测用户是否复购

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score

# 1. 加载
df = pd.read_csv('users.csv')

# 2. 清洗
df['age'] = df['age'].fillna(df['age'].median())
df = df.dropna(subset=['user_id'])
df = df.drop_duplicates()

# 3. 构造
df['注册月数'] = (pd.Timestamp.now() - pd.to_datetime(df['注册日期'])).dt.days / 30
df['客单价'] = df['消费额'] / (df['消费次数'] + 1)
df['是否周末下单'] = (pd.to_datetime(df['最近下单日期']).dt.dayofweek >= 5).astype(int)

# 4. 编码
df = pd.get_dummies(df, columns=['城市'], prefix='城市')

# 5. 选择特征
features = ['age','消费额','消费次数','客单价','注册月数',
            '是否周末下单'] + [c for c in df if c.startswith('城市_')]
X = df[features]
y = df['是否复购']

# 6. 切分 + 标准化(防泄漏)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42)
scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s = scaler.transform(X_test)

# 7. 训练 + 评估
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X_train_s, y_train)
print("AUC:", roc_auc_score(y_test, rf.predict_proba(X_test_s)[:,1]).round(4))

9. 总结:特征工程的最佳实践清单

9.1 完整 checklist

□ 缺失值:占比/列重要性决定 填充/删除
□ 异常值:IQR/业务规则 处理
□ 重复:去重
□ 构造:业务驱动的组合/聚合/分箱
□ 编码:按基数选 独热/目标/映射
□ 选择:过滤+嵌入 组合
□ 防泄漏:所有 fit 只在训练集

9.2 黄金三条

原则说明
业务优先特征构造从业务逻辑出发
简洁优先特征宁少勿滥
防泄漏优先错一次,全部白做

9.3 一句话心法

特征工程 = 把「业务知识」翻译成「模型语言」,而防泄漏是这条翻译链上的安全底线。


延伸阅读

  • https://plumephp.com/ml-supervised-regression/ — 特征工程服务于的模型训练
  • https://plumephp.com/ml-unsupervised-clustering/ — 特征工程与聚类/降维协作
  • https://plumephp.com/ml-model-evaluation/ — 特征有效性最终要靠评估验证
  • [[ai-ml]] 专题的特征工程深度文章
  • scikit-learn 特征工程指南

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「ml」更多文章

  1. 集成学习实战:Bagging、随机森林、梯度提升与 Stacking
  2. 迁移学习实战:预训练模型、特征提取与微调全流程
  3. 计算机视觉入门实战:图像处理与 CNN 图像分类