4. 高级特征工程:聚合特征、滑动窗口特征、图特征、特征重要性筛选

特征工程做得好,模型就成功了一半。这话在反欺诈领域尤其适用。

我见过太多团队,模型结构搞得花里胡哨,结果特征没处理好,上线就被打穿。说白了,欺诈分子比你更懂你的模型——他们每天都在研究怎么绕过规则。

这一章,我们聊聊真正能打的几个高级特征工程方法。都是我在实战中反复验证过的。

4.1 聚合特征:把零散行为变成画像

单个交易记录能看出什么?几乎什么都看不出来。但如果你把某个用户过去24小时的所有交易聚合起来,画像就清晰了。

聚合特征的核心思路:把多条记录压缩成一个统计量

常用聚合函数

  • 计数类:交易次数、失败次数、不同IP数
  • 统计类:金额均值、标准差、最大值、最小值
  • 比率类:失败率、大额交易占比、夜间交易占比
  • 时序类:最近一次交易距现在的时间、交易间隔均值

我个人习惯用 groupby + agg 一步到位。举个例子:

import pandas as pd

# 假设 df 包含字段:user_id, trans_time, amount, status, ip_address
# 按用户聚合过去7天的行为
features = df.groupby('user_id').agg({
    'trans_time': ['count', lambda x: (pd.Timestamp.now() - x.max()).seconds / 3600],
    'amount': ['mean', 'std', 'max'],
    'status': lambda x: (x == 'fail').mean(),
    'ip_address': 'nunique'
})

features.columns = ['trans_cnt', 'hours_since_last', 
                    'amt_mean', 'amt_std', 'amt_max',
                    'fail_rate', 'unique_ip_cnt']

避坑指南:我曾经在聚合窗口上吃过亏。窗口太短(比如1小时),特征稀疏;窗口太长(比如30天),用户行为已经变了。建议先做分布分析,再定窗口。我一般从3天、7天、30天三个粒度开始试。

4.2 滑动窗口特征:捕捉行为变化趋势

聚合特征只能告诉你「过去怎么样」,但欺诈行为往往是突然变化的。这时候就需要滑动窗口特征。

你想想看,一个正常用户每天消费3-5笔,突然某天刷了50笔——这种突变,聚合特征根本抓不住,但滑动窗口可以。

# 按用户和时间排序后,计算滑动窗口特征
df = df.sort_values(['user_id', 'trans_time'])

# 过去1小时内的交易次数(窗口大小=1小时)
df['trans_cnt_1h'] = df.groupby('user_id')['trans_time'].transform(
    lambda x: x.rolling('1h', min_periods=1).count()
)

# 过去3小时内的金额均值
df['amt_mean_3h'] = df.groupby('user_id')['amount'].transform(
    lambda x: x.rolling('3h', min_periods=1).mean()
)

# 当前交易金额与过去1小时均值的比值(突变检测)
df['amt_ratio_to_1h_mean'] = df['amount'] / (df['amt_mean_1h'] + 1e-8)

嗯,这里要注意:窗口大小和业务场景强相关。做支付反欺诈,我常用1小时、6小时、24小时。做信贷申请反欺诈,窗口要拉到7天、30天。

数据泄露警告:滑动窗口一定要用 shift() 或设置 closed='left',确保只用历史数据,不能看到未来。我见过有人忘记 shift,结果模型在测试集上 AUC 0.99,上线直接崩了。

4.3 图特征:挖掘关系网络中的隐藏信号

单个用户的行为再异常,也可能只是巧合。但如果你发现一群用户共享同一个设备、同一个IP、同一个收货地址——那基本可以确定是团伙欺诈。

图特征就是干这个的。它把用户、设备、IP、手机号等实体看作节点,把交互关系看作边,然后从图结构中提取特征。

import networkx as nx

# 构建用户-设备二部图
G = nx.Graph()
for _, row in df.iterrows():
    G.add_edge(f"user_{row['user_id']}", f"device_{row['device_id']}")

# 提取图特征
# 1. 节点的度(连接了多少个实体)
df['degree'] = df['user_id'].apply(
    lambda uid: G.degree(f"user_{uid}")
)

# 2. 聚类系数(邻居之间的紧密程度)
df['clustering'] = df['user_id'].apply(
    lambda uid: nx.clustering(G, f"user_{uid}")
)

# 3. PageRank(节点的重要性)
pagerank = nx.pagerank(G)
df['pagerank'] = df['user_id'].apply(
    lambda uid: pagerank.get(f"user_{uid}", 0)
)

实战中效果最好的图特征

  • 共享设备数:一个设备关联了多少用户?超过5个基本就是黑产设备
  • 二度关联风险:你的好友的好友是黑名单,你也有风险
  • 社区大小:在同一个连通分量中的节点数,越大越可疑
  • Jaccard相似度:两个用户共享的IP/设备比例

我记得有一次做电商反欺诈,单纯用行为特征 AUC 只有 0.82。加上图特征后,直接飙到 0.94。为什么?因为欺诈团伙的设备复用率极高,这个信号太强了。

4.4 特征重要性筛选:别让噪声毁了模型

特征不是越多越好。我见过有人一口气造了500个特征,结果模型训练三天三夜,效果还不如50个精选特征。

为什么会这样?因为冗余特征会引入噪声,还会导致过拟合

我常用的筛选方法有四种:

方法 原理 适用场景 我的经验
基于模型的特征重要性 LightGBM/XGBoost 内置的 feature_importance 所有场景,首选 用 gain 比用 split 更稳定
Permutation Importance 打乱某列数据,看模型效果下降多少 模型无关,可解释性强 计算量大,但结果最可靠
相关性分析 计算特征与目标变量的相关系数 快速初筛 只能发现线性关系,非线性会漏掉
递归特征消除 (RFE) 逐步删除最不重要的特征 特征数较少时 特征超过100个就别用了,太慢
# 基于 LightGBM 的特征重要性筛选
import lightgbm as lgb

model = lgb.LGBMClassifier(n_estimators=100, max_depth=5)
model.fit(X_train, y_train)

# 获取特征重要性(按 gain 排序)
importance = pd.DataFrame({
    'feature': X_train.columns,
    'importance': model.booster_.feature_importance(importance_type='gain')
}).sort_values('importance', ascending=False)

# 保留累计重要性达到95%的特征
importance['cumsum'] = importance['importance'].cumsum()
importance['cumsum_ratio'] = importance['cumsum'] / importance['importance'].sum()
selected_features = importance[importance['cumsum_ratio'] <= 0.95]['feature'].tolist()

print(f"原始特征数: {len(X_train.columns)}")
print(f"筛选后特征数: {len(selected_features)}")

我的筛选策略:先用 LightGBM 跑一轮,保留 top 50 特征。然后做 Permutation Importance 验证,剔除那些「看起来重要但实际没用」的特征。最后人工检查一遍——有些特征虽然统计上不显著,但业务上意义重大,我会手动保留。

4.5 本章小结

高级特征工程的核心就四个字:信息压缩

  • 聚合特征把多条记录压缩成画像
  • 滑动窗口特征捕捉行为的变化趋势
  • 图特征挖掘关系网络中的隐藏信号
  • 特征重要性筛选帮你剔除噪声

这些方法单独用效果有限,组合起来才能发挥威力。我个人习惯的流程是:先做聚合特征,再叠加滑动窗口,然后构建图特征,最后用特征重要性筛选收尾。

记住:好的特征工程,比好的模型更重要。模型选错了可以换,特征选错了,数据就白费了。

蓝海数据掘金营,专注资料整理