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

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 章标准化 数值变换的一种

✅ 检查点

  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% 的假象。

🛑 可以停在这里

走神救援

数据和特征决定上限。⭐ 两条心法:①特征工程的价值=你知道多少模型不知道的事(领域知识)②先广后深——快速造30个跑CV,留下有用的10个(你对"哪个有用"的直觉经常是错的),但必须每加一批就跑一次CV记录。缺失值:加一列"是否缺失"常比填充值有用,树模型可直接不管。数值变换:标准化(线性/NN必做)、log1p(长尾)、分箱。类别编码:One-Hot(少类别)、Label只适合树模型目标编码必须OOF+平滑否则泄漏、频次、Embedding。时间:小时用sin/cos,时间差比绝对时间有用,是泄漏重灾区。构造四套路:比值/聚合统计(提分最猛,且 std/nunique/max-over-mean 最常被漏掉)/交叉/多时间窗口造特征提问清单:能相除吗/有时间维度吗/是一对多吗/长尾吗/和群体均值差多少(相对偏离常比绝对值更有预测力——月薪2万在北京和小城市不是一回事)。筛选先看覆盖率,用Permutation Importance,且必须在CV内部做

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

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