2. 数据准备:交易数据的特征工程(时间、金额、地点、设备指纹等)
好,咱们进入实战的第二关。上一章我们把原始交易日志捞出来了,但说实话,那些日志就像一堆没洗的菜,直接下锅肯定不行。这一章,咱们得好好「洗菜切菜」——做特征工程。
我个人习惯把特征工程看作「翻译官」的工作。把机器看不懂的原始字段,翻译成模型能理解的数字语言。你想想看,模型它不认识「北京」和「上海」,它只认 0 和 1。所以,这一步做得好不好,直接决定了模型的天花板。
2.1 时间特征:从时间戳里挖出「行为密码」
时间戳是最容易被忽视的金矿。很多新手直接把它当字符串扔掉,太可惜了。我在项目中遇到过,光靠时间特征就能干掉 30% 的明显盗刷。
我们一般从三个维度拆解:
- 周期性特征:交易发生在几点?周几?是月初还是月末?
- 间隔特征:距离上一笔交易过了多久?距离注册时间过了多久?
- 行为模式特征:用户通常在这个点交易吗?
举个例子,一个用户平时都在晚上 8-10 点网购,突然凌晨 3 点在境外刷了一笔。这个「凌晨 3 点」就是强信号。怎么提取?看代码:
import pandas as pd
import numpy as np
# 假设 df 包含 'trans_time' 列,格式为 datetime
df['hour'] = df['trans_time'].dt.hour
df['day_of_week'] = df['trans_time'].dt.dayofweek # 0=周一
df['is_weekend'] = df['day_of_week'].isin([5, 6]).astype(int)
df['is_night'] = ((df['hour'] >= 23) | (df['hour'] <= 5)).astype(int)
# 计算距离上一笔交易的时间间隔(秒)
df = df.sort_values(['user_id', 'trans_time'])
df['time_since_last_txn'] = df.groupby('user_id')['trans_time'].diff().dt.total_seconds()
df['time_since_last_txn'] = df['time_since_last_txn'].fillna(999999) # 首笔交易填充大值
2.2 金额特征:不只是看数字大小
金额特征,说白了就是看「钱对不对」。但光看绝对值是不够的。一个富哥刷 10 万可能正常,一个学生党刷 10 万就是异常。
我一般会构建三类金额特征:
- 统计偏离特征:这笔金额偏离用户历史均值多少个标准差?
- 金额取整特征:金额是不是整数?比如 10000.00 比 9999.87 更可疑。
- 金额与余额关系:交易后余额是否接近 0?
这里有个坑,我曾经踩过。统计偏离特征如果直接用全局均值,效果很差。一定要按用户分组计算。看代码:
# 按用户计算历史均值和标准差
user_stats = df.groupby('user_id')['amount'].agg(['mean', 'std']).rename(
columns={'mean': 'user_amount_mean', 'std': 'user_amount_std'}
)
df = df.merge(user_stats, on='user_id', how='left')
# 计算 Z-score:当前金额偏离用户均值的程度
df['amount_zscore'] = (df['amount'] - df['user_amount_mean']) / (df['user_amount_std'] + 1e-8)
# 金额取整检测:判断金额是否为整数
df['is_round_amount'] = (df['amount'] == np.floor(df['amount'])).astype(int)
# 大额交易标记(超过用户均值 3 倍)
df['is_large_txn'] = (df['amount'] > df['user_amount_mean'] * 3).astype(int)
2.3 地点特征:距离与异常地理位置的博弈
地点特征,核心就两件事:距离和风险区域。
距离特征很好理解。用户上一笔在北京,10 分钟后在纽约又刷了一笔。物理上不可能,这就是强异常。我们通常计算交易地点与常用地点的球面距离。
风险区域呢?有些 IP 地址或城市本身就是高风险区,比如某些诈骗高发地。我们可以构建一个「历史黑名单」特征。
# 假设有经纬度数据
from math import radians, sin, cos, sqrt, asin
def haversine(lon1, lat1, lon2, lat2):
"""计算两点间球面距离(单位:公里)"""
lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])
dlon = lon2 - lon1
dlat = lat2 - lat1
a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
c = 2 * asin(sqrt(a))
r = 6371 # 地球半径
return c * r
# 计算与上一笔交易的距离
df = df.sort_values(['user_id', 'trans_time'])
df['prev_lon'] = df.groupby('user_id')['lon'].shift(1)
df['prev_lat'] = df.groupby('user_id')['lat'].shift(1)
df['distance_km'] = df.apply(
lambda row: haversine(row['prev_lon'], row['prev_lat'], row['lon'], row['lat'])
if pd.notna(row['prev_lon']) else 0,
axis=1
)
# 标记高风险城市(示例)
high_risk_cities = ['CityX', 'CityY']
df['is_high_risk_city'] = df['city'].isin(high_risk_cities).astype(int)
2.4 设备指纹特征:识别「隐身」的坏人
设备指纹,这是反欺诈的杀手锏。坏人可以换账号、换 IP,但很难换设备。设备指纹通常包括:设备 ID、操作系统、浏览器版本、屏幕分辨率、时区、语言设置等。
我常用的特征思路:
- 设备聚集性:一个设备关联了多少个用户?如果超过 3 个,大概率是团伙作案。
- 设备历史风险:这个设备之前有没有触发过风控规则?
- 设备信息一致性:比如操作系统是 iOS,但浏览器是 Chrome Android,明显矛盾。
# 设备关联用户数
device_user_count = df.groupby('device_id')['user_id'].nunique().reset_index()
device_user_count.columns = ['device_id', 'device_user_count']
df = df.merge(device_user_count, on='device_id', how='left')
# 设备历史风险标记(假设有历史黑名单)
blacklist_devices = ['dev_12345', 'dev_67890']
df['is_blacklist_device'] = df['device_id'].isin(blacklist_devices).astype(int)
# 设备信息一致性检测:操作系统与浏览器是否匹配
def check_os_browser_consistency(row):
os = row['os']
browser = row['browser']
if os == 'iOS' and browser not in ['Safari', 'Chrome iOS']:
return 1 # 不一致
if os == 'Android' and browser not in ['Chrome', 'Firefox', 'Samsung Internet']:
return 1
return 0
df['os_browser_mismatch'] = df.apply(check_os_browser_consistency, axis=1)
2.5 特征工程全景图
说了这么多,咱们用一张图把整个特征工程体系串起来。这样你脑子里就有个框架了。
2.6 特征工程避坑指南
最后,分享几个我这些年踩过的坑,希望能帮你省点时间:
坑1:特征膨胀——别一股脑把所有特征都扔进去。我见过有人做了 500 个特征,结果模型过拟合得一塌糊涂。记住:特征不是越多越好,有用才重要。
坑2:缺失值处理——设备指纹经常缺失。我曾经直接填充 -1,结果模型学出了一个「缺失就是异常」的假规律。后来我改用「是否缺失」作为一个独立特征,效果好了很多。
坑3:时间穿越——这个前面提过,但值得再说一遍。计算用户历史统计量时,一定要用「截止到当前时间」的数据。用 pandas 的 expanding 或 shift 操作可以避免。
嗯,特征工程这部分内容确实不少,但它是整个模型的地基。地基打牢了,后面盖楼才稳。你把这些特征构建好,模型就已经成功了一半。
好了,这一章就到这里。下一章我们会把这些特征喂给模型,看看它们到底能发挥多大威力。