特征工程实战:滑动窗口统计、时间衰减因子、比率类特征构建、特征存储
好,咱们进入第四章。这一章,我打算跟你聊聊特征工程里几个特别实在的活儿。
你想想看,风控模型跑得好不好,很多时候不取决于你用多牛的算法,而是特征做得够不够细。我见过太多团队,模型结构花里胡哨,结果特征全是原始字段直接丢进去,效果自然惨不忍睹。
今天咱们就聚焦四个核心点:滑动窗口统计、时间衰减因子、比率类特征,还有特征存储。这些都是我在实际项目中反复打磨过的套路,希望能帮你少走弯路。
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 本章知识体系
为了让你更直观地理解本章内容,我画了张图:
嗯,这一章的内容就这些。特征工程是个细活,每个环节都值得反复打磨。你在实际项目中遇到什么问题,欢迎随时交流。
公众号:蓝海资料掘金营,微信deep3321