返回金融系统设计
高频与风险科学 · 章节

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/ΔtO(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三层过滤