📑 本页目录(点开跳转)
16 · 特征工程基础
⏱ 28 分钟 | ⭐ 表格数据的胜负手
🎯 一句话
「数据和特征决定了上限,模型只是逼近这个上限。」 在表格数据上,好特征带来的提升通常远大于换模型。
🧭 零、特征工程的两条心法
① 你在做的事:把【模型学不到的知识】喂给它
模型能自己学的:单调关系、简单阈值、(树模型)低阶交互
模型学不到的: 你的领域知识
· "这个字段是身份证号,前 6 位是地区"
· "订单金额除以商品数才是真正有意义的量"
· "凌晨 3 点下单的用户和白天的是两类人"
⭐ 特征工程的价值 = 你知道多少模型不知道的事
② 特征工程的效率法则:先广后深
❌ 精雕细琢 3 个特征,每个花 2 小时
✅ 快速造 30 个特征,跑一遍 CV,留下有用的 10 个 ⭐
理由:你对"哪个特征有用"的直觉【经常是错的】,
让数据告诉你,比你想更快
⚠️ 但"造 30 个特征"必须配一条纪律: 每加一批特征就跑一次 CV 并记录,否则加到第 30 个时你不知道是哪个在起作用(甚至有的在拖后腿)。
🧹 一、数据清洗:先把地基弄干净
缺失值
| 策略 | 什么时候用 |
|---|---|
| 删行 | 缺失极少(<1%) |
| 删列 | 该列缺失 >50% 且没有业务含义 |
| 均值/中位数填充 | 数值特征,用中位数更稳(抗离群值) |
| 众数填充 | 类别特征 |
| 单独当一类 ⭐ | 缺失本身有意义("未填写收入"可能就是信号) |
| 树模型直接不管 ⭐ | LightGBM/XGBoost 原生支持缺失值,且能学到"缺失往哪边走" |
# ⭐ 加一列"是否缺失",常常比填充值本身更有用
df["income_missing"] = df["income"].isna().astype(int)
df["income"] = df["income"].fillna(df["income"].median())
离群值
先问:它是【错误】还是【真实的极端值】?
├─ 错误(年龄=999)→ 修正或删除
└─ 真实(土豪用户)→ 别删!用变换压缩:log1p、分位数裁剪
🔢 二、数值特征的四种变换
| 变换 | 做什么 | 什么时候用 |
|---|---|---|
| 标准化 | (x−μ)/σ | 线性模型、神经网络必做(第 3 章) |
| 归一化 | 缩到 [0,1] | 需要固定范围时 |
| log1p ⭐ | log(1+x) | 长尾分布(金额、播放量、点赞数) |
| 分箱 | 连续值切成区间 | 想让模型学非线性;或减少离群值影响 |
import numpy as np, pandas as pd
df["log_amount"] = np.log1p(df["amount"]) # 长尾必备
df["age_bin"] = pd.qcut(df["age"], q=10, labels=False, duplicates="drop") # 等频分箱
🔗 推荐算法第 7 章讲的分箱→Embedding,就是这个思路在深度模型里的版本。
🏷️ 三、类别特征(最容易出事的地方)
| 编码 | 做法 | 注意 |
|---|---|---|
| One-Hot | 每个取值一列 | 类别少(<10)时用;多了会维度爆炸 |
| Label/序数编码 | 映射成 0,1,2... | ⚠️ 只适合树模型——线性模型会误以为有大小关系 |
| 目标编码 ⭐ | 用该类别的目标均值代替 | 效果好但极易泄漏,见下 |
| 频次编码 | 用出现次数代替 | 简单有效,无泄漏风险 |
| Embedding | 学一个低维向量 | 高基数 + 神经网络(推荐算法第 7 章) |
💀 目标编码的泄漏陷阱
❌ 错误:用全部数据算每个类别的目标均值
→ 每一行都"看到"了自己的标签 → 严重泄漏 → CV 虚高、上线崩
✅ 正确:K 折内部编码(Out-of-Fold)
算第 i 折的编码时,只用其他 K−1 折的数据
+ 平滑:样本少的类别向全局均值收缩
# 带平滑的 OOF 目标编码
import numpy as np
def target_encode_oof(df, col, target, n_splits=5, smooth=20):
from sklearn.model_selection import KFold
global_mean = df[target].mean()
oof = np.full(len(df), np.nan)
for tr_idx, va_idx in KFold(n_splits, shuffle=True, random_state=0).split(df):
stats = df.iloc[tr_idx].groupby(col)[target].agg(["mean", "count"])
# 平滑:count 少时向全局均值靠拢
smoothed = (stats["mean"]*stats["count"] + global_mean*smooth) / (stats["count"]+smooth)
oof[va_idx] = df.iloc[va_idx][col].map(smoothed).fillna(global_mean).values
return oof
🔗 这是第 5 章数据泄漏在特征工程里最常见的形态。 Kaggle 第 1 章有更多目标编码变体。
⏰ 四、时间特征
import numpy as np
df["hour"] = df["ts"].dt.hour
df["dayofweek"] = df["ts"].dt.dayofweek
df["is_weekend"]= df["dayofweek"].isin([5,6]).astype(int)
# ⭐ 周期性特征要用 sin/cos,否则 23点 和 0点 在模型看来差了 23
df["hour_sin"] = np.sin(2*np.pi*df["hour"]/24)
df["hour_cos"] = np.cos(2*np.pi*df["hour"]/24)
# ⭐ 时间差比绝对时间更有用
df["days_since_signup"] = (df["ts"] - df["signup_ts"]).dt.days
⚠️ 时间特征是数据泄漏重灾区:任何"未来才知道"的统计量都不能用(第 5 章)。
🔧 五、构造特征:四个通用套路
① 比值/差值 单价 = 总价/数量 利润率 = 利润/收入
② 聚合统计 ⭐ 每个用户的:订单数、金额均值/最大/标准差、类目数
③ 交叉组合 性别×类目、城市×时段(第7章说的非线性,树模型能自动学一部分)
④ 时间窗口 ⭐ 近1天/7天/30天的行为统计(同时给多个窗口,让模型自己选)
💡 聚合统计是表格竞赛提分最猛的一类特征——把一对多的关系(用户→多条订单)压成一行。
🔨 聚合统计的标准写法(照抄改字段名就能用)
agg = orders.groupby("user_id").agg(
n_orders = ("amount", "count"),
amt_sum = ("amount", "sum"),
amt_mean = ("amount", "mean"),
amt_max = ("amount", "max"),
amt_std = ("amount", "std"), # ⭐ 波动性常被忽略但很有用
n_category = ("category", "nunique"), # ⭐ 多样性
last_ts = ("ts", "max"),
)
# ⭐ 再造二阶特征:比值往往比原始统计量更强
agg["amt_max_over_mean"] = agg["amt_max"] / (agg["amt_mean"] + 1e-9)
agg["days_since_last"] = (NOW - agg["last_ts"]).dt.days
df = df.merge(agg, on="user_id", how="left")
🔑 三个最容易被漏掉但很有信息量的聚合:
std(波动性)、nunique(多样性)、max/mean(集中度)。 大多数人只做 count/sum/mean 就停了。
📐 一个通用的"造特征"提问清单
卡住不知道造什么时,对每个字段问这五个问题:
① 它和别的字段【相除】有意义吗? → 单价、频率、占比
② 它有【时间维度】吗? → 距今多久、近N天统计、趋势
③ 它是【一对多】的吗? → 聚合统计(上面那套)
④ 它的【分布】是长尾的吗? → log1p
⑤ 它和【全局/分组均值】的差是多少? → "这个用户比同城用户高多少"⭐
💡 第 ⑤ 条特别值得记:「相对于某个群体的偏离」常常比绝对值更有预测力。 月薪 2 万在北京很普通,在小城市是高收入——模型看不到这个上下文,除非你造出来。
📊 六、特征筛选
| 方法 | 说明 |
|---|---|
| 覆盖率 | 缺失 >95% 的直接删 ⭐ 先做这个 |
| 方差过滤 | 几乎是常数的列没信息 |
| 相关性 | 两列相关系数 >0.95,删一个 |
| 模型重要性 | GBDT 的 importance(⚠️ 有第 4 章说的三个坑) |
| Permutation Importance ⭐ | 打乱某列看性能掉多少,比自带 importance 可靠 |
| 消融实验 | 去掉后 CV 掉多少 —— 最可靠但最慢 |
⚠️ 特征筛选必须放进 Pipeline / 在 CV 内部做——否则就是第 5 章那个 76.5% 的假象。
🔗 和站内其他章的关系
| 相关的地方 | 这里的对应 |
|---|---|
| Kaggle 第 1 章数据策略与特征工程 | 本章的竞赛实战加强版 |
| 推荐算法第 7 章特征工程 | 本章 + 推荐场景特有的(行为序列、实时统计) |
| 第 5 章数据泄漏 | 目标编码、时间特征是最常见的泄漏源 |
| 第 3 章标准化 | 数值变换的一种 |
✅ 检查点
- 特征工程的价值来自哪里?为什么说"先广后深"?配套的纪律是什么?
- 缺失值有哪几种处理策略?为什么"加一列是否缺失"常常有用?
- Label 编码为什么只适合树模型?
- 目标编码怎么防泄漏?
- 为什么时间的小时特征要用 sin/cos?
- 聚合统计里最容易被漏掉的三个是什么?
- 「造特征提问清单」的第 ⑤ 条是什么?为什么它特别有用?
- 为什么特征筛选必须在 CV 内部做?
👀 答案
- 价值 = 你知道多少模型不知道的事(领域知识)。"先广后深"是因为你对"哪个特征有用"的直觉经常是错的,快速造 30 个跑 CV 比精雕细琢 3 个更快找到有用的。纪律:每加一批就跑一次 CV 并记录,否则不知道是哪个在起作用(甚至有的在拖后腿)。
- 删行/删列/统计量填充/单独当一类/树模型直接不管。因为"缺失"本身可能携带信息(没填收入的可能是特定人群),填充会抹掉这个信号。
- Label 编码把类别映射成 0,1,2...,线性模型会误以为"2 比 1 大"存在数值关系;树模型只做大小比较切分,不受影响。
- K 折内部编码(OOF):算某一折的编码时只用其他折的数据;再加平滑,让样本少的类别向全局均值收缩。
- 直接用 0-23 的话,23 点和 0 点在数值上差 23,但实际只差 1 小时。sin/cos 编码保留了周期性。
std(波动性)、nunique(多样性)、max/mean(集中度)。大多数人只做 count/sum/mean 就停了。- 「相对于某个群体的偏离」(这个用户比同城用户高多少)。因为绝对值缺少上下文——月薪 2 万在北京很普通、在小城市是高收入,模型看不到这个上下文除非你造出来。
- 因为在全量数据上筛选会让特征选择看到验证集的标签 → 数据泄漏 → 就是第 5 章那个纯噪声数据跑出 76.5% 的假象。
🛑 可以停在这里
⚡ 走神救援
数据和特征决定上限。⭐ 两条心法:①特征工程的价值=你知道多少模型不知道的事(领域知识)②先广后深——快速造30个跑CV,留下有用的10个(你对"哪个有用"的直觉经常是错的),但必须每加一批就跑一次CV记录。缺失值:加一列"是否缺失"常比填充值有用,树模型可直接不管。数值变换:标准化(线性/NN必做)、log1p(长尾)、分箱。类别编码:One-Hot(少类别)、Label只适合树模型、目标编码必须OOF+平滑否则泄漏、频次、Embedding。时间:小时用sin/cos,时间差比绝对时间有用,是泄漏重灾区。构造四套路:比值/聚合统计(提分最猛,且 std/nunique/max-over-mean 最常被漏掉)/交叉/多时间窗口。造特征提问清单:能相除吗/有时间维度吗/是一对多吗/长尾吗/和群体均值差多少(相对偏离常比绝对值更有预测力——月薪2万在北京和小城市不是一回事)。筛选先看覆盖率,用Permutation Importance,且必须在CV内部做。
下一节 👉 17-实战项目.md