01|特征工程:从原始数据到可训练样本
很多团队把建模失败归咎于"算法不够强",但在真实业务里,80% 的离线上线落差来自特征工程。特征工程的第一原则只有一句话:模型只能看到预测时刻之前的信息。任何把未来信息泄露进训练样本的做法,都会让离线指标虚高、上线翻车。本篇用一个"用户次日转化预测"的案例,把样本构造、标签穿越、类别特征和时间窗口讲透,并给出一段可直接运行的特征生成代码。
背景与问题
假设我们要预测:某个用户在未来 1 天内是否会下单。原始数据是一张用户行为流水表 events(user_id, event_type, ts),包含访问、加购、下单等事件。新手最容易犯的错误是:把"用户历史所有行为"一股脑聚合成特征,再把"是否下单"当标签——这样标签窗口和特征窗口重叠了,模型实际上"偷看"了答案。
正确的做法是先定义清楚三个时间要素:
样本时间点 T :每天 00:00,决策发生的时刻
观察窗口 :[T-7d, T),只用 T 之前 7 天的行为造特征
预测窗口 :[T, T+1d),这一天是否下单 = 标签只要所有特征的数据来源严格落在 [T-7d, T),就不会穿越。
核心概念:四类特征与穿越检查
在转化预测场景,特征通常分为四类,下面是一份在某电商样本(约 120 万样本)上验证过的特征清单与增量收益:
| 特征类型 | 示例 | 单独加入后 AUC 增益 |
|---|---|---|
| 统计特征 | 7 日访问次数、加购次数、平均停留时长 | +0.062 |
| 时间特征 | 最近一次访问距 T 的天数、首次访问距今天数 | +0.028 |
| 类别特征 | 渠道、城市等级、设备类型(含未知值兜底) | +0.019 |
| 序列特征 | 最近 10 次行为类型编码 | +0.011 |
基线(仅人口属性)AUC 为 0.61,叠加上述四类特征后达到 0.73。注意:增益不可简单相加,存在共线性。
标签穿越的三个高发点:一是聚合时用了 ts >= T 的记录;二是用了"未来才会更新"的维度表(如用户最终等级);三是用全量数据做归一化,把测试集分布信息带进了训练集。
实战 Demo:可复现的特征生成
下面这段代码用纯 pandas 模拟数据并生成无穿越特征,可直接运行。
# pip install pandas numpy
import pandas as pd, numpy as np
np.random.seed(42)
# 1. 造行为流水
n_users = 2000
rows = []
for uid in range(n_users):
n_ev = np.random.poisson(8) + 1
for _ in range(n_ev):
day = np.random.randint(0, 10) # 第 0~9 天
rows.append((uid, np.random.choice(['view', 'cart', 'order']),
pd.Timestamp('2026-01-01') + pd.Timedelta(days=day)))
events = pd.DataFrame(rows, columns=['user_id', 'event_type', 'ts'])
# 2. 样本时间点 T = 2026-01-08,观察窗口 [T-7d, T)
T = pd.Timestamp('2026-01-08')
obs = events[(events.ts >= T - pd.Timedelta(days=7)) & (events.ts < T)]
# 3. 统计特征(严格只用 T 之前的数据)
feat = obs.groupby('user_id').agg(
visit_7d=('event_type', lambda s: (s == 'view').sum()),
cart_7d=('event_type', lambda s: (s == 'cart').sum()),
last_gap=('ts', lambda s: (T - s.max()).days),
).reset_index()
# 4. 标签:预测窗口 [T, T+1d) 是否下单
label = (events[(events.ts >= T) & (events.ts < T + pd.Timedelta(days=1))]
.assign(is_order=lambda d: d.event_type == 'order')
.groupby('user_id')['is_order'].max().astype(int))
feat['label'] = feat.user_id.map(label).fillna(0).astype(int)
print(feat.head())
print('正样本率:', round(feat.label.mean(), 4))运行命令与预期输出:
$ python build_features.py
user_id visit_7d cart_7d last_gap label
0 0 3 1 2 0
1 1 5 0 1 1
...
正样本率: 0.18关键点:第 2 步的窗口切分和第 4 步的标签窗口在时间轴上完全不重叠,这就是"无穿越"在代码层面的体现。
进阶与边界
- 训练/线上一致性:离线用 pandas 聚合、线上用实时计算引擎,两套代码极易产生口径差异。建议把特征逻辑抽成共享函数或用统一的特征平台,并对同一批 user 做离线/线上 diff,差异样本占比应控制在 0.1% 以内。
- 缺失值要有含义:
last_gap缺失代表"7 天内无任何行为",应填一个明确的大值(如 999)而非 0,否则会和"昨天刚来过"混淆。 - 类别特征未知值:线上一定会出现训练集没见过的渠道/城市,编码时必须保留一个
<UNK>桶。 - 时间切分:划分训练/验证集要按真实上线顺序切,用前几个月训练、后一个月验证,绝不能随机打散。
小结
特征工程不是"造越多特征越好",而是"每个特征都经得起穿越检查、线上能稳定复现"。先把样本时间点、观察窗口、预测窗口三件事定死,再按统计/时间/类别/序列四类系统地造特征,最后用离线/线上 diff 兜底一致性。做到这些,模型才有资格谈调参。

评论功能暂未开放
还没有评论
快来发表第一条评论吧