🏠 总目录📚 本教程 16 · 特征工程基础 ← →
📑 本页目录(点开跳转)

16 / 先运行,再对照

先问预测那一刻,
这个信息拿得到吗?

好特征不仅要有用,还必须在真实预测时可获得;预处理也只能在训练数据上拟合。

先抓住一个具体结果

过去 7 天,与未来 7 天

预测明天是否流失,可以使用预测时点之前 7 天的行为。把之后 7 天的行为填进特征,即使分数大涨,也是偷看未来。

接下来,按需要选一项

你现在想看什么?

这些入口是选择,不是必须按顺序完成的任务。

可以停在这里

好特征不仅要有用,还必须在真实预测时可获得;预处理也只能在训练数据上拟合。

需要更多细节时,继续看完整正文 →
继续探索

原有正文、图解和例子都在下方。按需跳转,不必一次读完。

16 · 特征工程基础

⏱ 28 分钟 | ⭐ 表格数据的胜负手


🎯 一句话

「数据和特征决定了上限,模型只是逼近这个上限。」 在表格数据上,好特征带来的提升通常远大于换模型。


🧭 零、特征工程的两条心法

操作步骤

  1. 你在做的事:把【模型学不到的知识】喂给它
  2. 模型能自己学的:单调关系、简单阈值、(树模型)低阶交互
  3. 模型学不到的: 你的领域知识
  4. · "这个字段是身份证号,前 6 位是地区"
  5. · "订单金额除以商品数才是真正有意义的量"
  6. · "凌晨 3 点下单的用户和白天的是两类人"
  7. ⭐ 特征工程的价值 = 你知道多少模型不知道的事

操作步骤

  1. 特征工程的效率法则:先广后深
  2. ❌ 精雕细琢 3 个特征,每个花 2 小时
  3. ✅ 快速造 30 个特征,跑一遍 CV,留下有用的 10 个 ⭐
  4. 理由:你对"哪个特征有用"的直觉【经常是错的】,
  5. 让数据告诉你,比你想更快

⚠️ 但"造 30 个特征"必须配一条纪律: 每加一批特征就跑一次 CV 并记录,否则加到第 30 个时你不知道是哪个在起作用(甚至有的在拖后腿)。


🧹 一、数据清洗:先把地基弄干净

缺失值

策略 什么时候用
删行 缺失极少(<1%)
删列 该列缺失 >50% 且没有业务含义
均值/中位数填充 数值特征,用中位数更稳(抗离群值)
众数填充 类别特征
单独当一类 ⭐ 缺失本身有意义("未填写收入"可能就是信号)
树模型直接不管 ⭐ LightGBM/XGBoost 原生支持缺失值,且能学到"缺失往哪边走"
# 🧩 骨架:`df` 来自你自己的代码,这一段只看写法
# ⭐ 加一列"是否缺失",常常比填充值本身更有用
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 章有更多目标编码变体。


⏰ 四、时间特征

# 🧩 骨架:`df` 来自你自己的代码,这一段只看写法
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 章)。


🔧 五、构造特征:四个通用套路

操作步骤

  1. 比值/差值 单价 = 总价/数量 利润率 = 利润/收入
  2. 聚合统计 ⭐ 每个用户的:订单数、金额均值/最大/标准差、类目数
  3. 交叉组合 性别×类目、城市×时段(第7章说的非线性,树模型能自动学一部分)
  4. 时间窗口 ⭐ 近1天/7天/30天的行为统计(同时给多个窗口,让模型自己选)

💡 聚合统计是表格竞赛提分最猛的一类特征——把一对多的关系(用户→多条订单)压成一行。

🔨 聚合统计的标准写法(照抄改字段名就能用)

# 🧩 骨架:`orders` 来自你自己的代码,这一段只看写法
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 章标准化 数值变换的一种

✅ 检查点

  1. 特征工程的价值来自哪里?为什么说"先广后深"?配套的纪律是什么?
  2. 缺失值有哪几种处理策略?为什么"加一列是否缺失"常常有用?
  3. Label 编码为什么只适合树模型?
  4. 目标编码怎么防泄漏?
  5. 为什么时间的小时特征要用 sin/cos?
  6. 聚合统计里最容易被漏掉的三个是什么?
  7. 「造特征提问清单」的第 ⑤ 条是什么?为什么它特别有用?
  8. 为什么特征筛选必须在 CV 内部做?
👀 答案
  1. 价值 = 你知道多少模型不知道的事(领域知识)。"先广后深"是因为你对"哪个特征有用"的直觉经常是错的,快速造 30 个跑 CV 比精雕细琢 3 个更快找到有用的。纪律:每加一批就跑一次 CV 并记录,否则不知道是哪个在起作用(甚至有的在拖后腿)。
  2. 删行/删列/统计量填充/单独当一类/树模型直接不管。因为"缺失"本身可能携带信息(没填收入的可能是特定人群),填充会抹掉这个信号。
  3. Label 编码把类别映射成 0,1,2...,线性模型会误以为"2 比 1 大"存在数值关系;树模型只做大小比较切分,不受影响。
  4. K 折内部编码(OOF):算某一折的编码时只用其他折的数据;再加平滑,让样本少的类别向全局均值收缩。
  5. 直接用 0-23 的话,23 点和 0 点在数值上差 23,但实际只差 1 小时。sin/cos 编码保留了周期性。
  6. std(波动性)、nunique(多样性)、max/mean(集中度)。大多数人只做 count/sum/mean 就停了。
  7. 「相对于某个群体的偏离」(这个用户比同城用户高多少)。因为绝对值缺少上下文——月薪 2 万在北京很普通、在小城市是高收入,模型看不到这个上下文除非你造出来。
  8. 因为在全量数据上筛选会让特征选择看到验证集的标签 → 数据泄漏 → 就是第 5 章那个纯噪声数据跑出 76.5% 的假象。

🛑 可以停在这里

⚡ 走神救援

先记住这几件事

  • 先明确预测发生的时点,再判断每个特征是否真的可用。
  • 数值、类别、缺失和时间特征分别处理;编码与缩放只在训练部分拟合。
  • 聚合与目标编码容易泄漏,必须配合正确的划分与交叉验证。
  • 一次添加一组特征,用验证结果判断价值,并保留可复现的处理流程。

下一节 👉 17-实战项目.md

打卡记录保存在你的浏览器里,首页能看到总进度