特征工程实战:滑动窗口统计、时间衰减因子、比率类特征构建、特征存储

好,咱们进入第四章。这一章,我打算跟你聊聊特征工程里几个特别实在的活儿。

你想想看,风控模型跑得好不好,很多时候不取决于你用多牛的算法,而是特征做得够不够细。我见过太多团队,模型结构花里胡哨,结果特征全是原始字段直接丢进去,效果自然惨不忍睹。

今天咱们就聚焦四个核心点:滑动窗口统计、时间衰减因子、比率类特征,还有特征存储。这些都是我在实际项目中反复打磨过的套路,希望能帮你少走弯路。

4.1 滑动窗口统计:捕捉近期行为模式

滑动窗口,说白了就是看用户最近一段时间内的行为统计。比如最近7天登录次数、最近30天交易金额。

为什么要用滑动窗口?因为用户的信用风险是动态变化的。半年前的逾期记录,跟昨天的逾期记录,意义完全不同。滑动窗口能帮我们捕捉这种「近期行为模式」。

我在项目中遇到过这样一个场景:某个用户历史总交易量很大,但最近一周交易量骤降。如果只看总量特征,模型会认为他是优质用户。但加上滑动窗口特征后,模型立刻发现了异常——这个用户可能已经换了平台,或者账户被盗用了。

实现上,我习惯用Python的rolling函数。给你看个例子:

import pandas as pd

# 假设df是用户交易数据,按用户和时间排序
df['交易金额_7d_sum'] = df.groupby('user_id')['交易金额'].transform(
    lambda x: x.rolling(window=7, min_periods=1).sum()
)

df['交易金额_30d_mean'] = df.groupby('user_id')['交易金额'].transform(
    lambda x: x.rolling(window=30, min_periods=1).mean()
)

df['交易次数_7d_count'] = df.groupby('user_id')['交易金额'].transform(
    lambda x: x.rolling(window=7, min_periods=1).count()
)

这里有个细节要注意:min_periods=1表示窗口内至少要有1个数据点。如果用户刚注册,窗口内数据很少,这个参数能避免出现大量空值。

⚠️ 避坑指南
我曾经犯过一个错:直接用rolling(7).sum(),结果前6天的数据全是NaN。后来模型训练时,这些NaN被填充为0,导致新用户的特征值全部偏低,模型对他们产生了系统性偏差。所以,min_periods这个参数一定要设好。

4.2 时间衰减因子:让近期数据更有话语权

滑动窗口虽然好用,但它有个问题:窗口内的所有数据权重一样。比如7天窗口内,昨天的交易和6天前的交易,对当前风险的指示意义显然不同。

这时候就需要时间衰减因子了。它的核心思想是:越近的数据,权重越大。

我常用的衰减函数是指数衰减:

import numpy as np

def time_decay_weight(days_ago, decay_rate=0.9):
    """指数衰减权重,days_ago越小表示越近"""
    return decay_rate ** days_ago

# 假设df已经按时间排序,且包含days_ago字段
df['衰减权重'] = df['days_ago'].apply(lambda x: time_decay_weight(x, decay_rate=0.9))
df['加权交易金额'] = df['交易金额'] * df['衰减权重']

# 计算加权后的滑动窗口统计
df['加权金额_7d_sum'] = df.groupby('user_id')['加权交易金额'].transform(
    lambda x: x.rolling(window=7, min_periods=1).sum()
)

衰减率decay_rate怎么选?我个人习惯先设0.9,然后根据业务场景微调。如果业务变化很快(比如现金贷),我会用0.8甚至0.7;如果业务相对稳定(比如房贷),0.95可能更合适。

💡 小技巧
你可以把衰减率当作一个超参数,用交叉验证来调优。我在一个项目中试过,把衰减率从0.9调到0.85,模型AUC提升了0.02。别小看这0.02,在风控领域,这可能是几百万的损失差异。

4.3 比率类特征:消除量纲影响

比率类特征,就是把两个相关字段做除法。比如「逾期金额/总借款金额」、「近7天交易金额/近30天交易金额」。

为什么要做比率?因为原始数值受量纲影响太大。同样是借款100万,对月入5万的用户和月入50万的用户,风险含义完全不同。比率类特征能消除这种量纲差异,让模型更关注「相对关系」。

我常用的比率特征包括:

  • 使用率:已用额度 / 总授信额度
  • 逾期率:逾期次数 / 总借款次数
  • 近期活跃度:近7天交易次数 / 近30天交易次数
  • 金额集中度:最大单笔交易金额 / 总交易金额

实现起来很简单:

df['使用率'] = df['已用额度'] / (df['总授信额度'] + 1e-8)  # 加小常数防除零
df['逾期率'] = df['逾期次数'] / (df['总借款次数'] + 1e-8)
df['近期活跃度'] = df['近7天交易次数'] / (df['近30天交易次数'] + 1e-8)
🔑 核心要点
比率类特征的关键在于「分母要有业务意义」。别为了做比率而做比率。比如「交易金额/用户年龄」这种特征,业务解释性很差,模型也很难学到有用信息。

4.4 特征存储:Redis vs Feature Store

特征做完了,存哪儿?这是个很实际的问题。

我经历过两个阶段:

阶段一:Redis 硬扛

早期项目规模不大,我直接用Redis存特征。优点是快,缺点是管理混乱。每个特征一个key,时间长了根本记不清哪个key对应哪个特征。而且特征更新逻辑散落在代码各处,出了问题很难排查。

import redis

r = redis.Redis(host='localhost', port=6379, db=0)

# 存储用户特征
r.hset(f'user:{user_id}:features', '交易金额_7d_sum', 15000.0)
r.hset(f'user:{user_id}:features', '逾期率', 0.05)

# 读取用户特征
features = r.hgetall(f'user:{user_id}:features')

阶段二:Feature Store 规范化

后来项目大了,团队也多了,Redis那套完全撑不住。我们引入了Feature Store(比如Feast、Tecton)。

Feature Store的好处是:

  • 特征注册:每个特征都有元数据,谁建的、什么时候建的、怎么算的,一目了然
  • 在线/离线一致性:训练时用的特征和线上推理时用的特征,保证完全一致
  • 特征复用:不同模型可以共享同一套特征,不用重复计算

给你看个Feast的配置示例:

# feature_view.yaml
feature_view:
  name: user_transaction_features
  entities:
    - user_id
  features:
    - name: 交易金额_7d_sum
      dtype: float64
    - name: 逾期率
      dtype: float64
    - name: 近期活跃度
      dtype: float64
  batch_source:
    type: bigquery
    table_ref: project.dataset.user_features
  online_store:
    type: redis
    host: localhost
    port: 6379

你看,底层存储还是Redis,但上层多了Feature Store这一层管理。说白了,Feature Store就是给Redis加了个「管家」。

⚠️ 避坑指南
我曾经在一个项目里,离线特征和在线特征不一致。离线训练时用了「近7天交易金额」,但线上推理时因为数据延迟,实际用的是「近6天交易金额」。模型上线后效果暴跌。后来用了Feature Store,通过时间戳对齐,才彻底解决了这个问题。

4.5 本章知识体系

为了让你更直观地理解本章内容,我画了张图:

特征工程实战知识体系 特征工程实战 滑动窗口统计 时间衰减因子 比率类特征 特征存储 rolling函数 min_periods参数 sum/mean/count 指数衰减函数 decay_rate调优 加权滑动窗口 使用率/逾期率 近期活跃度 金额集中度 Redis直接存储 Feature Store 在线/离线一致性 核心目标:让特征更准确、更稳定、更可复用 滑动窗口 → 时间衰减 → 比率类 → 特征存储

嗯,这一章的内容就这些。特征工程是个细活,每个环节都值得反复打磨。你在实际项目中遇到什么问题,欢迎随时交流。


公众号:蓝海资料掘金营,微信deep3321