高频与风险科学 · 章节
07 - 实时特征工程
06-hft-risk-science/07-feature-engineering.md
07 - 实时特征工程
定位:给有10年金融经验的架构师/PM讲清楚"风控模型的弹药库是怎么建的" 目标:理解特征类型全景、滑动窗口实现、EWMA、特征选择、特征一致性、Feature Store架构 前提:你理解基本的统计概念(均值/方差/分布),了解风控引擎的整体架构
一、核心概念与直觉
1.1 特征工程是什么?
一句话定义:把原始数据变成模型能理解的"语言"。
原始数据:
用户A在14:32:15发起了一笔1500元的转账到某个新账户
模型看到的特征:
- 交易金额: 1500
- 是否工作时间: 1
- 近5分钟交易次数: 3 ← 频率特征
- 近1小时交易总额: 4500 ← 金额聚合特征
- 交易额偏离30天均值: 2.1σ ← 偏离特征
- 收款方是否首次: 1 ← 关系特征
- 收款方注册天数: 2 ← 对手方特征
- 设备指纹匹配: 0 ← 设备特征
模型根据这些特征打出风险评分:0.87(高风险)
1.2 为什么特征工程是核心?
业界共识:
"模型效果的80%取决于特征工程,20%取决于算法选择"
原因:
1. 垃圾进,垃圾出(Garbage In, Garbage Out)
再好的XGBoost也无法从无关特征中学到有用的模式
2. 好特征 > 好算法
用好特征的逻辑回归 往往优于 用差特征的深度学习
3. 特征决定了模型的"天花板"
算法只是在逼近这个天花板
在风控中:
特征工程的质量直接决定了"能不能在欺诈发生前拦截"
一个好的速度特征(近5分钟交易频率)可能比10个静态特征更有用
1.3 实时特征的挑战
离线特征:
用Spark从Hive表里跑SQL,算过去30天的统计量
可以用几个小时慢慢算
结果存到数据库里供模型使用
实时特征:
交易来了 → 必须在10毫秒内算出"近5分钟交易次数"
不能去数据库里COUNT(*)——太慢了
必须维护一个实时更新的计数器
挑战矩阵:
┌────────────┬─────────────┬──────────────┐
│ 维度 │ 离线特征 │ 实时特征 │
├────────────┼─────────────┼──────────────┤
│ 计算时间 │ 小时级 │ 毫秒级 │
│ 数据完整性 │ 完整 │ 可能不完整 │
│ 计算框架 │ Spark/Hive │ Flink/自研 │
│ 存储 │ Hive/S3 │ Redis/内存 │
│ 精确性 │ 精确 │ 可能近似 │
│ 回溯 │ 容易 │ 困难 │
└────────────┴─────────────┴──────────────┘
二、特征类型全景
2.1 特征分类体系
特征工程知识树
│
├── 基础统计特征
│ ├── 频率特征(计数)
│ ├── 金额特征(求和/均值/最大/最小)
│ ├── 偏离特征(Z-Score)
│ └── 比率特征(条件概率)
│
├── 时序特征
│ ├── 速度特征(一阶差分)
│ ├── 加速度特征(二阶差分)
│ ├── EWMA特征(指数加权)
│ └── 趋势特征(斜率/周期)
│
├── 信息论特征
│ ├── 熵特征(分布多样性)
│ └── 互信息特征(变量间依赖)
│
├── 图特征
│ ├── 度中心性
│ ├── PageRank
│ └── 社区归属
│
└── 业务衍生特征
├── 时段特征(工作时间/深夜/节假日)
├── 地理特征(常用城市/IP距离)
├── 设备特征(设备指纹/新设备)
└── 对手方特征(新收款方/高风险商户)
2.2 频率特征
定义:在给定时间窗口内的事件发生次数。
数学定义:
Count(user, window) = |{event : event.user = user ∧ event.time ∈ window}|
实现要点:
- 维护每个用户的事件时间戳列表
- 新事件到来时:+1,过期事件移出:-1
- 多粒度窗口:同时计算1分钟/5分钟/1小时/1天
应用场景:
近5分钟交易次数 > 10 → 可能是自动化攻击
近1小时登录失败次数 > 5 → 可能是暴力破解
近24小时转账次数 > 50 → 可能是刷单/洗钱
代码示意:
// 环形缓冲区实现(固定大小,O(1)更新)
class FrequencyCounter {
private timestamps: number[] = []; // 时间戳队列
private windowMs: number; // 窗口大小(毫秒)
add(ts: number): void {
this.timestamps.push(ts);
this.evict(ts);
}
count(now: number): number {
this.evict(now);
return this.timestamps.length;
}
private evict(now: number): void {
const cutoff = now - this.windowMs;
while (this.timestamps.length > 0 && this.timestamps[0] < cutoff) {
this.timestamps.shift();
}
}
}
2.3 金额特征
定义:在给定时间窗口内的金额统计量(求和/均值/最大值/最小值)。
数学定义:
Sum(user, window) = Σ amount_i, event_i ∈ window
Avg(user, window) = Sum / Count
Max(user, window) = max(amount_i), event_i ∈ window
Min(user, window) = min(amount_i), event_i ∈ window
实现要点:
Sum和Count可以增量更新(加入新值 + 减去过期值)
Max和Min不能简单增量更新(移除的可能是最大值)
→ 用有序集合(SortedSet)或分段最大值
应用场景:
近1小时交易总额 > 50000 → 大额交易检测
单笔金额 > 日均交易额的5倍 → 异常大额
近24小时最大单笔 vs 历史最大单笔 → 行为突变
多粒度聚合技巧:
5分钟窗口 = 5个1分钟桶之和
1小时窗口 = 12个5分钟桶之和
→ 粗粒度窗口复用细粒度桶,减少重复计算
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│b1│b2│b3│b4│b5│b6│b7│b8│b9│b0│b1│b2│ ← 1分钟桶
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
├────5分钟────┤ ├────5分钟────┤
├──────────────1小时──────────────────┤
2.4 偏离特征(Z-Score)
定义:当前值偏离历史均值的程度,用标准差衡量。
数学定义:
Z-Score = (x - μ) / σ
x = 当前交易金额
μ = 该用户过去30天的日均交易额
σ = 该用户过去30天交易额的标准差
直觉:
Z = 0 → 当前值等于历史平均
Z = 1 → 当前值比历史平均高1个标准差
Z = 3 → 当前值比历史平均高3个标准差(非常异常)
Z = -2 → 当前值比历史平均低2个标准差
应用场景:
交易额Z-Score > 3 → 异常大额,需要核查
登录频率Z-Score > 2.5 → 异常活跃,可能被盗号
实现要点:
需要维护每个用户的均值μ和标准差σ
可以用EWMA近似(见后续章节)
也可以离线计算后存入Redis
// EWMA方式维护均值和方差
class ZScoreFeature {
private ewmaMean: number = 0;
private ewmaVar: number = 0;
private alpha: number = 0.05; // 等效窗口 ≈ 39天
private initialized: boolean = false;
update(x: number): number {
if (!this.initialized) {
this.ewmaMean = x;
this.ewmaVar = 0;
this.initialized = true;
return 0;
}
const diff = x - this.ewmaMean;
this.ewmaMean = this.alpha * x + (1 - this.alpha) * this.ewmaMean;
this.ewmaVar = (1 - this.alpha) * (this.ewmaVar + this.alpha * diff * diff);
const sigma = Math.sqrt(this.ewmaVar);
return sigma > 0 ? (x - this.ewmaMean) / sigma : 0;
}
}
2.5 速度特征(一阶差分)
定义:某个指标随时间的变化率。
数学定义:
Velocity = Δf / Δt = (f(t) - f(t-Δt)) / Δt
实际应用:
交易频率变化率 = (近5分钟交易次数 - 前5分钟交易次数) / 5分钟
例如:
14:30-14:35: 2次交易
14:35-14:40: 8次交易
速度 = (8 - 2) / 5 = 1.2 次/分钟
→ 交易频率突然增加了6倍
应用场景:
频率突增 → 可能是自动化攻击
金额突增 → 可能是被盗后快速转移资金
地理位置变化速度 > 物理可能 → "不可能旅行"检测
例:5分钟前在北京登录,现在从纽约登录 → 不可能
2.6 加速度特征(二阶差分)
定义:变化率的变化率——检测突变的开始。
数学定义:
Acceleration = Δ²f / Δt² = (v(t) - v(t-Δt)) / Δt
v(t) = 速度(一阶差分)
直觉:
速度特征告诉你"变化了多少"
加速度特征告诉你"变化的速度是否在加快"
应用场景:
频率加速度突然变大 → 攻击刚刚开始(可以更早拦截)
时间序列:
t=1: count=2, v=0, a=0 → 正常
t=2: count=3, v=1, a=1 → 轻微增加
t=3: count=6, v=3, a=2 → 加速增加 ⚠️
t=4: count=15, v=9, a=6 → 急剧加速 🚨
速度在t=3才告警,但加速度在t=3就能发现趋势
2.7 加权特征(EWMA)
定义:指数加权移动平均,近期数据权重更高。(详见下一章节)
公式:S_t = α·x_t + (1-α)·S_{t-1}
应用:
用EWMA维护用户行为基线
偏离EWMA基线 → 行为异常
与普通均值的对比:
普通均值:所有历史数据等权 → 反应迟钝
EWMA:近期权重高 → 能跟踪行为变化
2.8 熵特征
定义:衡量分布的不确定性/多样性。
数学定义(Shannon信息熵):
H(X) = -Σ p_i × log₂(p_i)
p_i = 第i个类别的概率(频率)
性质:
分布越均匀 → 熵越大
分布越集中 → 熵越小
只有一个类别 → 熵 = 0
在风控中的应用:
用户交易金额分布熵:
正常用户:交易金额多样(100, 500, 1200, 3000...) → 高熵
洗钱嫌疑:交易金额固定(9999, 9999, 9999, 9999...) → 低熵
用户交易时段分布熵:
正常用户:交易分散在工作时间 → 中等熵
异常用户:所有交易集中在凌晨3点 → 低熵
计算示例:
用户过去30天交易金额分布:
0-100元: 5次 (10%)
100-500元: 15次 (30%)
500-1000元: 15次 (30%)
1000-5000元: 10次 (20%)
5000+元: 5次 (10%)
H = -(0.1×log₂0.1 + 0.3×log₂0.3 + 0.3×log₂0.3 + 0.2×log₂0.2 + 0.1×log₂0.1)
= -(0.1×(-3.32) + 0.3×(-1.74) + 0.3×(-1.74) + 0.2×(-2.32) + 0.1×(-3.32))
= 0.332 + 0.522 + 0.522 + 0.464 + 0.332
= 2.17 bits
最大熵 = log₂5 = 2.32 bits(5个分箱,均匀分布时)
归一化熵 = 2.17 / 2.32 = 0.94 → 分布比较均匀(正常)
2.9 比率特征
定义:满足某条件的事件占总事件的比例。
数学定义:
Ratio = Count(condition=true) / Count(all)
常用比率特征:
夜间交易占比 = 22:00-06:00交易数 / 总交易数
境外交易占比 = 境外交易数 / 总交易数
新收款方占比 = 向新收款方转账数 / 总转账数
大额交易占比 = 金额>10000的交易数 / 总交易数
失败交易占比 = 失败交易数 / 总交易数
应用场景:
夜间交易占比突然从5%上升到80% → 行为异常
新收款方占比 > 90% → 可能是盗号后快速转移
失败交易占比 > 50% → 可能是在试探
2.10 图特征
定义:基于资金转账网络的图结构特征。
核心概念:
节点 = 用户/账户
边 = 转账关系
权重 = 转账金额/次数
常用图特征:
1. 度中心性(Degree Centrality)
入度 = 有多少人给我转账
出度 = 我给多少人转账
正常用户:入度和出度相对平衡
资金归集账户:高入度、低出度
资金分散账户:低入度、高出度
2. PageRank
考虑"谁给你转账"的重要性
被重要账户转账的账户 → PageRank更高
应用:识别核心洗钱节点
3. 社区发现(Community Detection)
Louvain算法:找到紧密关联的账户集群
同一个社区内的账户可能是同一个人/团伙控制
应用:女巫检测、团伙欺诈识别
4. 资金链路径
A→B→C→D→A 形成环路 → 资金回流(自转/洗钱嫌疑)
A→B₁,B₂,...B₁₀₀ → 资金分散(拆分嫌疑)
B₁,B₂,...B₁₀₀→C → 资金归集
图特征的挑战:
× 计算复杂度高(大规模图)
× 实时计算困难 → 通常离线计算后存储
× 需要维护完整的转账图谱
2.11 特征类型完整对照表
| 类别 | 特征名 | 计算方法 | 数学基础 | 时间复杂度 | 应用场景 |
|---|---|---|---|---|---|
| 频率 | 近5分钟交易次数 | 滑动窗口计数 | 计数统计 | O(1)增量 | 频繁交易检测 |
| 金额 | 近1小时交易总额 | 滑动窗口求和 | 累加 | O(1)增量 | 大额交易检测 |
| 均值 | 近30天日均交易额 | 移动平均 | 算术平均 | O(1)EWMA | 行为基线 |
| 加权 | EWMA交易额 | 指数加权 | EWMA(λ) | O(1) | 近期行为加权 |
| 偏离 | 交易额偏离度 | Z-Score | (x-μ)/σ | O(1) | 异常金额检测 |
| 速度 | 交易频率变化率 | 一阶差分 | Δf/Δt | O(1) | 突增检测 |
| 加速度 | 频率变化的变化率 | 二阶差分 | Δ²f/Δt² | O(1) | 突变检测 |
| 熵 | 交易金额分布熵 | 信息熵 | -Σp·log(p) | O(K)桶数 | 模式变化检测 |
| 比率 | 夜间交易占比 | 条件计数/总计数 | 条件概率 | O(1)增量 | 异常时段检测 |
| 图 | 资金网络度中心性 | PageRank/度 | 图论 | O(V+E) | 团伙关联 |
三、滑动窗口详解
3.1 固定窗口(Tumbling Window)
┌────────┐┌────────┐┌────────┐┌────────┐
│ 窗口1 ││ 窗口2 ││ 窗口3 ││ 窗口4 │
│ 00-05 ││ 05-10 ││ 10-15 ││ 15-20 │ ← 5分钟窗口
└────────┘└────────┘└────────┘└────────┘
特点:
窗口之间不重叠
每5分钟产出一次统计值
实现:
维护一个计数器/累加器
每5分钟:输出结果 → 重置计数器
优点:
实现极简——一个变量就行
内存消耗最小
缺点:
边界效应——如果一批欺诈交易跨越两个窗口,
每个窗口看到的都是一半,可能都不会触发告警
│ 窗口1: 2次 │ 窗口2: 3次 │ ← 每个窗口看起来正常
│ 5次欺诈交易横跨两个窗口 ← 但总数其实异常
3.2 滑动窗口(Sliding Window)
时间轴:→→→→→→→→→→→→→→→→→→→→→
├─────5min─────┤ t=10:05
├─────5min─────┤ t=10:06
├─────5min─────┤ t=10:07
特点:
窗口随时间连续滑动
每个时刻都有一个"最近5分钟"的统计值
没有边界效应
实现方案:
方案一:维护事件列表
数据结构:时间戳有序列表 / 环形缓冲区
新事件到来:加入列表尾部
查询时:移除过期事件,返回统计值
优点:精确
缺点:内存随事件数增长
方案二:分桶聚合
将时间轴分成小桶(如10秒一个桶)
每个桶存储聚合值(计数/求和)
5分钟窗口 = 最近30个10秒桶之和
优点:内存固定(只需要30个桶)
缺点:精度降低到桶粒度
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐...
│b₁│b₂│b₃│b₄│b₅│b₆│b₇│b₈│b₉│b₀│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
← ← ← 5分钟窗口 = 30个桶 → → →
新桶加入右端,旧桶从左端移除
3.3 会话窗口(Session Window)
时间轴:──●──●●──●──────────────●●●──●──────
Session 1 │ gap > 30min │ Session 2
├──●──●●──●──┤ ├──●●●──●──┤
特点:
按用户活动间隔自动分割窗口
超过gap阈值(如30分钟无活动)→ 开始新会话
应用场景:
用户行为分析:一次登录session内的操作模式
会话级别特征:session内交易次数/总金额/持续时间
正常用户:session时长15分钟,做3-5笔交易
异常用户:session时长2小时,做100笔交易(自动化脚本)
3.4 滑动窗口的高效实现
加法器窗口(O(1)更新)
对于可增量更新的聚合函数(SUM, COUNT, AVG):
维护:
sum = 当前窗口内的总和
count = 当前窗口内的计数
更新规则:
新事件进入窗口:sum += new_value; count++
旧事件离开窗口:sum -= old_value; count--
→ 每次更新O(1),不需要重新扫描窗口内所有数据
注意:
MAX/MIN不能用加法器——移除的可能是最大值
解决方案:
1. 单调队列(Deque):O(1)更新,但复杂
2. 分桶MAX/MIN:维护每个桶的MAX/MIN,窗口MAX = max(各桶MAX)
3. 容忍近似:只在需要时重新扫描(惰性评估)
分桶聚合策略
多粒度窗口复用:
细粒度桶 → 粗粒度窗口
1分钟桶 → 5分钟窗口(5个桶)→ 1小时窗口(12个5分钟窗口 = 60个桶)
→ 1天窗口(用另一层粗粒度桶)
存储结构(Redis示例):
Key: user:{uid}:1min:{bucket_id}
Value: {count: 5, sum: 3200, max: 1500, min: 100}
TTL: 70分钟(1小时窗口 + 余量)
Key: user:{uid}:1hour:{bucket_id}
Value: {count: 120, sum: 56000, max: 5000, min: 50}
TTL: 25小时(1天窗口 + 余量)
查询5分钟窗口:
GET user:{uid}:1min:{current-4} 到 user:{uid}:1min:{current}
求和即可
内存优化:
1分钟桶保留60个(1小时)→ 每用户 60 × ~50bytes = 3KB
1小时桶保留24个(1天)→ 每用户 24 × ~50bytes = 1.2KB
1天桶保留30个(1月)→ 每用户 30 × ~50bytes = 1.5KB
每用户总计 ≈ 6KB
1000万用户 → 60GB → Redis集群可以承载
多粒度窗口同时维护
实时事件流处理:
收到一笔交易(user=A, amount=1500, time=14:32:15)
同时更新:
✓ 1分钟桶 [14:32] += 1500
✓ 5分钟窗口 [14:28-14:32] 重算(5个1分钟桶求和)
✓ 1小时窗口 [13:33-14:32] 重算(12个5分钟窗口求和)
✓ 或者用EWMA直接增量更新(不需要桶)
计算结果:
feature_1: count_5min = 3
feature_2: sum_1hour = 4500
feature_3: avg_30day = 680(EWMA近似)
feature_4: zscore = (1500 - 680) / 450 = 1.82
feature_5: velocity_5min = (3 - 1) / 5 = 0.4
四、EWMA(指数加权移动平均)
4.1 公式推导
EWMA递推公式:
S_t = α·x_t + (1-α)·S_{t-1}
其中:
S_t = 时刻t的EWMA值
x_t = 时刻t的观测值
α = 平滑因子(0 < α < 1)
展开递推:
S_t = α·x_t + (1-α)·[α·x_{t-1} + (1-α)·S_{t-2}]
= α·x_t + α(1-α)·x_{t-1} + (1-α)²·S_{t-2}
= α·x_t + α(1-α)·x_{t-1} + α(1-α)²·x_{t-2} + ...
= α · Σ_{k=0}^{∞} (1-α)^k · x_{t-k}
第k个历史值的权重 = α·(1-α)^k
权重之和 = α · Σ(1-α)^k = α / (1-(1-α)) = 1 ✓(权重和为1)
4.2 α的选择
α大(如0.3):
近期权重高 → 反应快但噪声大
等效窗口 ≈ 5个数据点
α小(如0.01):
历史权重高 → 反应慢但稳定
等效窗口 ≈ 199个数据点
等效窗口长度公式:
N_eff = 2/α - 1
α = 0.3 → N_eff ≈ 5.7(约6天)
α = 0.1 → N_eff ≈ 19(约3周)
α = 0.05 → N_eff ≈ 39(约2个月)
α = 0.01 → N_eff ≈ 199(约10个月)
选择建议:
短期行为基线(近1周) → α ≈ 0.2-0.3
中期行为基线(近1月) → α ≈ 0.05-0.1
长期行为基线(近半年) → α ≈ 0.01-0.02
4.3 EWMA在风控中的应用
应用一:行为基线建模
维护每个用户的EWMA交易额:
baseline_t = 0.05 × current_amount + 0.95 × baseline_{t-1}
偏离检测:
deviation = |current_amount - baseline| / baseline
if deviation > 3: alert("交易额偏离基线超过3倍")
应用二:EWMA方差(用于Z-Score)
维护均值和方差两个EWMA值:
mean_t = α·x_t + (1-α)·mean_{t-1}
var_t = (1-α)·(var_{t-1} + α·(x_t - mean_{t-1})²)
Z-Score = (x_t - mean_t) / √var_t
应用三:EWMA频率
每隔固定时间(如1分钟)统计该分钟内的交易次数count_t
freq_ewma_t = α·count_t + (1-α)·freq_ewma_{t-1}
如果当前分钟交易次数远超freq_ewma → 频率异常
EWMA的核心优势:
✓ 只需要存储一个值S_{t-1}(不需要历史序列)
✓ O(1)时间更新
✓ 自动对近期数据加权
✓ 不需要清除过期数据(自然衰减)
五、特征选择方法
5.1 为什么要做特征选择?
原始特征可能有几百甚至上千个
并非所有特征都有用——很多是冗余的或噪声
特征太多的问题:
1. 过拟合风险增加(维度灾难)
2. 模型训练和推理变慢
3. 可解释性降低
4. 维护成本高(每个特征都需要计算和存储)
目标:选出最有区分力的一小组特征
5.2 过滤法(Filter)
IV值(Information Value)
IV是评分卡场景最常用的特征选择指标
原理:
将特征分箱后,计算每个箱的WOE(Weight of Evidence)
IV = Σ (Good_pct_i - Bad_pct_i) × WOE_i
WOE_i = ln(Good_pct_i / Bad_pct_i)
IV值解读:
IV < 0.02 → 无预测能力
0.02 ≤ IV < 0.1 → 弱预测能力
0.1 ≤ IV < 0.3 → 中等预测能力 ✓
0.3 ≤ IV < 0.5 → 强预测能力 ✓
IV ≥ 0.5 → 可疑(可能有特征穿越)
计算示例:
特征"近30天交易次数"分箱结果:
分箱 Good数 Bad数 Good% Bad% WOE (G%-B%)×WOE
0-5 5000 200 50.0% 20.0% 0.916 0.275
6-20 3000 300 30.0% 30.0% 0.000 0.000
21-50 1500 300 15.0% 30.0% -0.693 -0.104
51+ 500 200 5.0% 20.0% -1.386 -0.208
IV = 0.275 + 0.000 + (-0.104) + (-0.208) = 0.275 - 0.312 = ...
(简化演示,实际IV一定是非负的,因为(a-b)×ln(a/b)≥0)
相关系数/互信息
相关系数(Pearson):
衡量线性关系强度
|r| > 0.7 的两个特征通常需要去掉一个(冗余)
互信息(Mutual Information):
MI(X; Y) = Σ p(x,y) × log(p(x,y) / (p(x)·p(y)))
衡量X和Y之间的非线性依赖
MI = 0 → X和Y独立
MI大 → X和Y有很强的依赖关系
优点:能捕捉非线性关系(Pearson只看线性)
缺点:计算复杂,需要估计联合分布
方差过滤:
方差极小的特征(如99%的值都是0)→ 无区分度 → 直接删除
5.3 包裹法(Wrapper)
RFE(递归特征消除):
1. 用全部特征训练模型
2. 根据特征重要性排序
3. 删除最不重要的k个特征
4. 重新训练
5. 重复直到达到目标特征数
优点:考虑了特征间的交互
缺点:计算量大(每轮都要重新训练)
前向选择(Forward Selection):
从0个特征开始
每轮加入一个使模型效果提升最大的特征
直到效果不再显著提升
后向消除(Backward Elimination):
从全部特征开始
每轮删除一个对效果影响最小的特征
直到效果明显下降
5.4 嵌入法(Embedded)
L1正则化(Lasso):
在损失函数中加入 λ·Σ|w_i|(L1范数)
效果:自动将无用特征的系数压缩为0
→ 天然的特征选择
直觉:L1正则化的几何解释
等高线与菱形约束的交点在坐标轴上
→ 某些w_i精确为0
树模型的Feature Importance:
XGBoost/LightGBM自带特征重要性:
- Gain:该特征带来的增益总和
- Cover:该特征覆盖的样本数
- Frequency:该特征被使用的次数
推荐用Gain排序(最有代表性)
SHAP值排序:
用SHAP计算每个特征的平均|SHAP|值
排序后选择Top K个特征
优点:理论基础最严谨
缺点:计算量大(SHAP本身就慢)
5.5 实际操作建议
推荐流程:
1. 方差过滤 → 删除零方差/极低方差特征
2. IV值筛选 → 保留IV > 0.02的特征
3. 相关性去重 → 两个特征|r| > 0.7只保留IV更高的
4. 训练XGBoost → 看Feature Importance
5. 用SHAP值做最终排序
6. 选Top 20-50个特征
7. 用这些特征训练最终模型
特征数量经验值:
评分卡(逻辑回归):10-20个特征
树模型(XGBoost):30-100个特征
深度学习:可以用更多特征(但需要足够的数据)
六、特征一致性(训练-推理一致性)
6.1 特征穿越(Feature Leakage)
定义:训练时使用了推理时拿不到的信息
这是特征工程中最致命的错误——会导致模型在训练集上表现完美,
但在生产环境中完全失效。
经典案例:
案例1:目标变量泄露
特征:"是否还款逾期" → 预测"是否违约"
AUC = 0.99!但这是废话——逾期就是违约的定义
上线后:发放贷款时还不知道会不会逾期 → 该特征不存在 → 模型失效
案例2:未来信息泄露
特征:"该笔交易最终是否被确认为欺诈"
训练时有标签 → 可以构造
推理时交易刚发生 → 不知道是不是欺诈 → 无法使用
案例3:时间穿越
用3月的数据训练,特征用到了4月的市场均值
训练时看起来很好(因为看到了"未来")
上线后预测3月的交易时,4月数据还不存在
案例4:群体信息泄露
特征:"该商户的历史欺诈率"
训练时用全量数据计算 → 包含了当前样本的标签
正确做法:用t时刻之前的数据计算t时刻的特征
防范方法
1. 严格的时间切割
特征计算只用 < event_time 的数据
标签确定只用 > event_time 的数据
时间线:
←───── 特征窗口 ─────→│event│←── 标签观察期 ──→
2. 特征字典管理
每个特征记录:
- 计算逻辑
- 数据来源
- 时间约束(可用时间点)
- 是否包含目标变量相关信息
3. 特征审核流程
新特征上线前必须review:
✓ 推理时是否可获取?
✓ 是否使用了未来信息?
✓ 是否间接包含标签信息?
4. 异常检测
如果某个特征的IV值 > 0.5 → 高度可疑,很可能有泄露
6.2 训练推理偏移(Training-Serving Skew)
定义:同一个特征在训练时和推理时的计算结果不一致
常见原因:
原因1:不同的计算引擎
训练时:Spark SQL → SELECT AVG(amount) FROM transactions WHERE ...
推理时:Flink流式 → 用EWMA近似计算平均值
Spark精确计算 vs Flink近似计算 → 数值不同
原因2:不同的数据源
训练时:用的是T+1清洗后的数据(干净)
推理时:用的是实时流数据(可能有脏数据/延迟数据)
原因3:时间窗口不一致
训练时:"近30天"是精确的30天
推理时:"近30天"可能因为数据延迟变成28天
原因4:空值处理不一致
训练时:Spark把NULL填成0
推理时:Redis返回nil,代码处理为NaN
影响:
轻微偏移:模型效果略有下降(可能不被注意)
严重偏移:模型输出完全错误,线上事故
实际案例:
某银行的反欺诈模型,线下AUC=0.92,线上AUC=0.75
排查发现:离线特征用的是"T-1日的账户余额"(日终批量)
在线特征用的是"实时余额"(包含当天变动)
→ 特征含义不同 → 模型效果下降
6.3 Feature Store架构
Feature Store是解决训练推理一致性的核心基础设施
设计目标:
1. 统一特征定义(一处定义,到处使用)
2. 统一特征计算(训练和推理用同样的逻辑)
3. 特征复用(不同模型共享特征)
4. 特征监控(分布漂移、缺失率、延迟)
架构图:
┌──────────────────────────────────────────────────┐
│ Feature Store │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ Feature Registry(特征注册中心) │ │
│ │ 特征名 | 类型 | 计算逻辑 | 所有者 | 版本 │ │
│ └────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────┐ ┌──────────────────────┐ │
│ │ 离线特征计算 │ │ 在线特征计算 │ │
│ │ │ │ │ │
│ │ Spark/Hive │ │ Flink/自研 │ │
│ │ 批量计算 │ │ 流式计算 │ │
│ │ T+1更新 │ │ 实时更新 │ │
│ │ ↓ │ │ ↓ │ │
│ │ HBase/S3 │ │ Redis/内存 │ │
│ │ (离线存储) │ │ (在线存储) │ │
│ └─────────────────┘ └──────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ Feature Serving API │ │
│ │ GET /features?user=A&features=f1,f2,f3 │ │
│ │ → {f1: 3, f2: 4500, f3: 1.82} │ │
│ └────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
↑ 供模型推理使用
↑ 供模型训练使用(通过时间戳参数回溯)
特征注册中心示例
特征注册条目:
{
"feature_name": "txn_count_5min",
"description": "用户近5分钟交易次数",
"data_type": "INT",
"computation": {
"offline": "SELECT COUNT(*) FROM txn WHERE user_id = ? AND txn_time > ? - INTERVAL 5 MINUTE",
"online": "SLIDING_WINDOW_COUNT(event_stream, user_id, 300s)"
},
"storage": {
"offline": "hbase:feature_store:txn_count_5min",
"online": "redis:user:{uid}:txn_count_5min"
},
"freshness": "real-time",
"ttl": "10 minutes",
"owner": "risk-team",
"version": "2.1",
"created": "2026-01-15",
"consumers": ["fraud_model_v3", "aml_model_v2"]
}
特征回放验证
验证离线特征和在线特征的一致性:
方法:Feature Replay(特征回放)
1. 记录在线特征服务的请求和返回值(包含时间戳)
2. 用离线引擎对同一时间戳重新计算特征
3. 对比两者的差异
差异指标:
精确匹配率 = 完全相同的占比(目标 > 99%)
平均误差率 = |online - offline| / offline(目标 < 1%)
最大误差率(不应超过5%)
如果差异超过阈值 → 排查计算逻辑不一致
定期回放频率:每周一次
→ 保证在线特征没有因为代码变更而悄悄偏移
七、工程落地要点
7.1 Flink实时特征计算架构
整体架构:
┌─────────┐ ┌──────────┐ ┌────────────┐ ┌───────┐
│ 事件源 │────→│ Kafka │────→│ Flink Job │────→│ Redis │
│ (交易/ │ │ (事件流) │ │ (特征计算) │ │ (特征 │
│ 登录/ │ │ │ │ │ │ 存储)│
│ 行为) │ │ │ │ ┌────────┐ │ │ │
└─────────┘ └──────────┘ │ │窗口聚合│ │ └───────┘
│ │EWMA │ │
│ │Z-Score │ │
│ │频率/金额│ │
│ └────────┘ │
└────────────┘
Flink计算特征的优势:
✓ 原生支持滑动窗口/固定窗口/会话窗口
✓ 精确一次语义(Exactly-Once)
✓ 高吞吐低延迟
✓ 支持事件时间处理(解决乱序问题)
Flink特征计算Job示意:
// 伪代码
eventStream
.keyBy(event -> event.userId)
.window(SlidingEventTimeWindows.of(Minutes(5), Seconds(10)))
.aggregate(new CountAndSumAggregator())
.map(result -> {
// 输出到Redis
redis.set("user:" + result.userId + ":count_5min", result.count);
redis.set("user:" + result.userId + ":sum_5min", result.sum);
});
7.2 Redis特征存储设计
Key结构设计:
方案一:扁平化Key
user:{uid}:count_5min → 3
user:{uid}:sum_1hour → 4500
user:{uid}:zscore_amount → 1.82
优点:GET单个特征最快
缺点:获取多个特征需要MGET
方案二:Hash结构
Key: user:{uid}:features
Fields: count_5min=3, sum_1hour=4500, zscore=1.82
优点:HGETALL一次获取所有特征
缺点:不能对单个Field设TTL
方案三:混合
高频更新特征(实时窗口)→ 扁平化Key + 短TTL
低频更新特征(日级别)→ Hash结构 + 长TTL
TTL策略:
实时特征(5分钟窗口):TTL = 10分钟
小时特征(1小时窗口):TTL = 2小时
日级特征(30天窗口):TTL = 48小时
离线特征(EWMA基线):TTL = 7天
淘汰策略:
maxmemory-policy = volatile-ttl(优先淘汰TTL最短的Key)
确保离线特征(长TTL)不会被实时特征(短TTL)挤掉
容量规划:
每用户 20个实时特征 × 50bytes = 1KB
活跃用户 1000万 → 10GB
加上Hash结构和过期Key → 预留30GB
Redis集群:3主3从,每主12GB
7.3 特征监控
监控三个维度:
1. 分布漂移(Feature Drift)
用PSI(Population Stability Index)监控:
PSI = Σ (A_i - E_i) × ln(A_i / E_i)
A = 当前分布, E = 基线分布(训练时)
PSI < 0.1 → 稳定
PSI > 0.25 → 需要告警(特征分布显著变化)
可能原因:
上游数据源变更
业务规则变化
季节性因素
数据bug
2. 缺失率
null_rate = COUNT(NULL) / COUNT(*)
正常特征缺失率 < 1%
缺失率突然上升 → 可能是数据源断连
处理策略:
缺失率 < 5% → 用默认值填充
缺失率 > 5% → 告警 + 降级(不使用该特征)
缺失率 > 50% → 紧急告警 + 模型降级
3. 计算延迟
latency = feature_ready_time - event_time
P99延迟监控:
实时特征P99延迟 < 10ms → 正常
实时特征P99延迟 > 50ms → 告警
实时特征P99延迟 > 200ms → 可能导致风控超时
监控看板示例:
┌─────────────────────────────────────────────┐
│ Feature Monitoring Dashboard │
├─────────────────────────────────────────────┤
│ Feature: txn_count_5min │
│ PSI: 0.08 (✅ Stable) │
│ Null Rate: 0.3% (✅ Normal) │
│ P99 Latency: 5ms (✅ Fast) │
│ Mean: 2.3 (vs baseline 2.1) │
│ Std: 3.1 (vs baseline 2.8) │
├─────────────────────────────────────────────┤
│ Feature: user_ewma_amount │
│ PSI: 0.31 (⚠️ Drift Detected!) │
│ Null Rate: 12.5% (⚠️ High Missing!) │
│ P99 Latency: 3ms (✅ Fast) │
│ Mean: 850 (vs baseline 680) ← 偏离 │
└─────────────────────────────────────────────┘
7.4 与风控引擎的集成
完整链路:
事件 → Kafka → Flink(特征计算) → Redis(特征存储)
↓
风控引擎(规则+模型)
↓
决策(通过/拦截/人工审核)
风控引擎调用特征的流程:
1. 收到交易事件
2. 从Redis批量获取该用户的特征向量
MGET user:{uid}:count_5min user:{uid}:sum_1hour ...
3. 将特征输入规则引擎(简单规则判断)
4. 将特征输入ML模型(评分)
5. 综合规则和模型结果做最终决策
总延迟预算:50ms
特征获取:5ms(Redis本地机房)
规则执行:2ms
模型推理:10ms
网络开销:5ms
余量:28ms
关键设计原则:
1. 特征获取要批量(一次MGET比多次GET快)
2. 特征缺失要有降级策略(用默认值而非报错)
3. 特征计算和模型推理可以并行(pipeline)
4. 关键路径不能有外部依赖(Redis必须在同机房)
八、常见误区与面试重点
8.1 常见误区
误区1:"特征越多越好"
✗ 冗余特征增加过拟合风险和计算成本
✓ 精选20-50个高质量特征 > 500个低质量特征
误区2:"线下效果好就能上线"
✗ 特征穿越导致线下虚高
✗ 训练推理偏移导致线上退化
✓ 必须验证特征一致性
误区3:"EWMA可以替代滑动窗口"
✗ EWMA是近似——权重指数衰减,不是精确的窗口截断
✓ 对精度要求高的场景(如交易计数)还是要用精确窗口
✓ 对基线建模(行为均值)用EWMA很合适
误区4:"实时特征一定比离线特征好"
✗ 实时特征有噪声、延迟、数据不完整问题
✓ 最佳实践:实时特征 + 离线特征互补
实时:近5分钟交易次数(捕捉正在发生的攻击)
离线:近30天日均交易额(稳定的行为基线)
误区5:"Feature Store是大厂才需要的"
✗ 只要你有2个以上模型共享特征,就需要Feature Store
✓ 哪怕是简单的"特征字典 + Redis + 一致性校验"也算Feature Store
误区6:"图特征可以实时计算"
✗ 大规模图的PageRank/社区发现需要分钟到小时级计算
✓ 图特征通常离线计算,缓存到Redis供在线使用
✓ 增量图更新(新边加入时局部更新)是研究热点
8.2 面试高频问题
Q: 什么是特征穿越?举个例子。
A: 训练时用了推理时不可用的信息。例如用"该笔交易最终是否被标记为欺诈"
来预测该交易是否欺诈——训练时AUC完美,上线后无法获取该特征。
Q: 如何保证训练和推理的特征一致性?
A: Feature Store统一特征定义和计算逻辑;特征回放验证
(用在线记录的特征值与离线重算的值对比);
定期PSI监控特征分布变化。
Q: EWMA和滑动窗口的区别?各自适用场景?
A: EWMA用指数衰减权重,只需存储一个状态值,适合行为基线建模。
滑动窗口精确截断,需要存储窗口内所有数据/分桶,适合精确计数。
实际中两者互补使用。
Q: 如何处理实时特征的延迟?
A: 分桶聚合减少计算量;Redis同机房部署减少网络延迟;
批量获取(MGET)替代多次GET;预计算+缓存热点特征;
设置降级策略(特征超时时用默认值)。
Q: 如何做特征选择?
A: 分三步:(1)方差/IV过滤低质量特征;(2)相关性去重;
(3)用XGBoost Feature Importance或SHAP排序选Top K。
评分卡场景10-20个特征,树模型30-100个。
九、延伸阅读
经典教材
- Zheng & Casari - Feature Engineering for Machine Learning — 特征工程入门最佳
- Kuhn & Johnson - Feature Engineering and Selection — 特征选择的系统化方法
- Chip Huyen - Designing Machine Learning Systems — Feature Store和MLOps实战
论文
- Flink Window Semantics: Apache Flink 官方文档 — 窗口类型的权威解释
- Feast: A Feature Store for Machine Learning (2020) — 开源Feature Store设计
- Uber Michelangelo (2017) — Uber ML平台中的特征工程实践
在线资源
- Featureform/Feast/Tecton — 三大开源/商业Feature Store
- Flink官方文档 — 流式计算和窗口操作
- RiskMetrics Technical Document — EWMA的工业标准参考
开源工具
- Apache Flink — 流式特征计算
- Feast — 开源Feature Store
- Redis — 在线特征存储
- SHAP — 特征重要性分析
十、与已有笔记的关联
关联笔记矩阵:
本笔记主题 已有笔记 关联点
──────────────────────────────────────────────────────────
滑动窗口/Flink → 03-risk-engine 风控引擎的实时计算模块
EWMA → 06-market-risk VaR中的EWMA波动率估计
Z-Score → 05-fraud-detection 异常检测的基础方法
Feature Store → Arch Day 80+ 微服务架构中的数据平台
特征选择(IV/WOE) → 04-credit-risk-modeling 评分卡特征筛选
图特征 → 05-fraud-detection 团伙欺诈的图分析
关键要点回顾:
→ 特征穿越是最致命的错误(面试必考)
→ EWMA的O(1)更新特性使其成为实时基线建模的首选
→ Feature Store的核心价值是训练推理一致性
→ 滑动窗口的分桶聚合技巧是工程面试的加分项
→ 特征选择用IV/相关性/SHAP三层过滤