第三章 特征工程基础:数值特征、类别特征、时间特征、特征缩放与编码
各位同学,欢迎来到特征工程这一章。说实话,在反欺诈模型里,特征工程的重要性怎么强调都不过分。我见过太多团队,模型选得再花哨,特征没做好,最后 AUC 就是上不去。反过来说,哪怕你用最简单的逻辑回归,只要特征工程做到位,效果往往能吊打那些花里胡哨的深度模型。
这一章,我们就来聊聊特征工程里最基础、也最核心的几个部分:数值特征、类别特征、时间特征,以及特征缩放和编码。嗯,内容不少,但都是干货。
核心观点:特征工程不是简单的数据搬运,而是把原始数据中隐藏的「信号」放大,把「噪声」过滤掉。反欺诈尤其如此——欺诈行为往往藏在细微的异常里。
3.1 数值特征:别只当它是数字
数值特征看起来最简单,年龄、金额、次数……直接扔进模型不就行了?我以前也这么想,直到被现实狠狠教育了一顿。
数值特征的处理,我个人习惯从三个维度入手:
- 分布形态——数据是正态分布还是长尾分布?
- 异常值——有没有明显的离群点?
- 业务含义——这个数值在业务场景下意味着什么?
3.1.1 对数变换与 Box-Cox 变换
为什么需要变换?你想想看,交易金额这个特征,大部分用户都是几十到几百块,突然来一笔 10 万的交易。模型会怎么想?它会把 10 万当成一个巨大的数值,权重拉得特别高。但实际业务中,10 万可能只是正常的大额消费。
解决办法很简单:取对数。
import numpy as np
import pandas as pd
# 原始交易金额
amounts = np.array([50, 100, 200, 500, 1000, 5000, 100000])
# 对数变换
log_amounts = np.log1p(amounts) # log(1+x) 避免 log(0)
print(log_amounts)
# 输出: [3.93, 4.62, 5.30, 6.22, 6.91, 8.52, 11.51]
你看,100000 和 50 的差距从 2000 倍缩小到了不到 3 倍。模型更容易学习到规律。
我的经验:在反欺诈场景下,金额、次数、时长这类特征,我几乎无脑做对数变换。除非你确定数据本身就是正态分布。
3.1.2 分箱与离散化
有时候,连续数值反而不好用。比如年龄,25 岁和 26 岁对欺诈风险的影响可能差别不大,但 18 岁和 60 岁差别就大了。这时候,分箱是个好选择。
# 年龄分箱
ages = pd.cut(df['age'], bins=[0, 18, 25, 35, 50, 100],
labels=['未成年', '青年', '中青年', '中年', '老年'])
# 或者用等频分箱
from sklearn.preprocessing import KBinsDiscretizer
kbd = KBinsDiscretizer(n_bins=5, encode='ordinal', strategy='quantile')
分箱的好处是:模型不用去学复杂的非线性关系,直接按区间处理就行。坏处是:丢失了区间内的细节信息。怎么取舍?看你的数据量和业务理解。
3.2 类别特征:高基数问题的解法
类别特征在反欺诈里太常见了:设备型号、IP 地址、城市、银行卡发卡行……这些特征往往有几十甚至几百个取值。直接做 One-Hot 编码?维度爆炸,模型直接崩掉。
我曾经在一个项目中,设备型号有 3000 多种取值。如果 One-Hot 编码,特征维度直接多出 3000 列。训练时间从 10 分钟变成 2 小时,效果还没提升。
3.2.1 目标编码(Target Encoding)
目标编码是我在反欺诈场景下最常用的方法。它的思路很简单:用类别对应的目标变量均值来替代原始类别。
# 目标编码示例
import category_encoders as ce
encoder = ce.TargetEncoder(cols=['device_model', 'city'])
df_encoded = encoder.fit_transform(df[['device_model', 'city']], df['is_fraud'])
但这里有个大坑:过拟合。如果某个类别只有一两个样本,它的均值可能完全不准。解决办法是加入平滑项,或者用交叉验证的方式做编码。
避坑指南:我曾经在线上模型里直接用全量数据做目标编码,结果上线后效果暴跌。原因就是过拟合了。后来改用 5 折交叉验证做目标编码,才稳住。记住:目标编码一定要在训练集上做,而且要用交叉验证的方式。
3.2.2 计数编码与频率编码
有时候,类别出现的次数本身就包含信息。比如某个 IP 地址在历史数据中出现了 1000 次,另一个 IP 只出现了 1 次。前者大概率是正常用户,后者可能是新注册的欺诈账号。
# 计数编码
count_map = df['ip_address'].value_counts().to_dict()
df['ip_count'] = df['ip_address'].map(count_map)
# 频率编码
freq_map = df['ip_address'].value_counts(normalize=True).to_dict()
df['ip_freq'] = df['ip_address'].map(freq_map)
这个方法简单粗暴,但效果往往不错。尤其是对于长尾分布的特征,低频类别往往对应着异常行为。
3.3 时间特征:欺诈的「时间密码」
时间特征在反欺诈里是个宝藏。为什么?因为欺诈行为往往有特定的时间模式。比如凌晨 3 点的交易,比如周末的异常活跃,比如注册时间和交易时间间隔极短。
我个人习惯从时间特征里提取以下几类信息:
| 特征类型 | 示例 | 业务含义 |
|---|---|---|
| 周期性特征 | 小时、星期几、月份 | 欺诈是否集中在特定时段 |
| 时间间隔 | 距上次交易时间、距注册时间 | 行为是否过于急促 |
| 统计聚合 | 过去 1 小时交易次数、过去 24 小时金额总和 | 短时间内是否异常活跃 |
| 节假日标记 | 是否周末、是否法定节假日 | 欺诈是否利用节假日放松监管 |
3.3.1 周期性特征的编码
小时这个特征,直接当成数值 0-23 是不对的。为什么?因为 23 点和 0 点其实只差 1 小时,但数值上差了 23。模型会认为它们差异很大。
正确的做法是用正弦和余弦变换:
# 小时的正弦余弦编码
df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24)
df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24)
这样,23 点和 0 点在二维空间里的距离就很近了。同样的方法也适用于星期几、月份等周期性特征。
3.3.2 时间窗口聚合
这是反欺诈里最强大的特征之一。比如,过去 1 小时内这个用户交易了多少次?过去 24 小时内这个 IP 地址关联了多少个不同的账号?
# 过去 1 小时交易次数
df['tx_count_1h'] = df.groupby('user_id')['transaction_time'].transform(
lambda x: x.rolling(window='1h', min_periods=1).count()
)
# 过去 24 小时 IP 关联账号数
df['ip_unique_users_24h'] = df.groupby('ip_address')['user_id'].transform(
lambda x: x.rolling(window='24h', min_periods=1).apply(lambda y: y.nunique())
)
我的经验:时间窗口聚合特征,窗口大小很关键。太短了捕捉不到模式,太长了噪声太多。我一般会试 1 小时、6 小时、24 小时、7 天这几个窗口,然后看哪个对模型提升最大。
3.4 特征缩放:让模型「一视同仁」
特征缩放,说白了就是把不同量纲的特征拉到同一个尺度上。为什么需要?因为像 SVM、KNN、神经网络这些模型,都依赖于距离计算。如果金额是几万,年龄是几十,模型会认为金额更重要——但这可能不是事实。
常用的缩放方法有三种:
| 方法 | 公式 | 适用场景 |
|---|---|---|
| 标准化(Z-score) | (x - mean) / std | 数据近似正态分布 |
| Min-Max 缩放 | (x - min) / (max - min) | 数据有明确边界 |
| 鲁棒缩放 | (x - median) / IQR | 数据有较多异常值 |
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
# 标准化
scaler_std = StandardScaler()
X_std = scaler_std.fit_transform(X)
# Min-Max 缩放
scaler_mm = MinMaxScaler()
X_mm = scaler_mm.fit_transform(X)
# 鲁棒缩放
scaler_robust = RobustScaler()
X_robust = scaler_robust.fit_transform(X)
注意:缩放参数(均值、标准差、最小值、最大值等)一定要在训练集上计算,然后应用到测试集和验证集上。千万不要用全量数据计算,否则会造成数据泄露。
3.5 特征编码:让模型「读懂」数据
特征编码,就是把非数值数据转换成数值数据。除了前面提到的目标编码,还有几种常用的方法:
3.5.1 One-Hot 编码
适用于低基数类别特征(取值少于 10 个)。比如性别、婚姻状况等。
from sklearn.preprocessing import OneHotEncoder
encoder = OneHotEncoder(sparse_output=False)
encoded = encoder.fit_transform(df[['gender', 'marital_status']])
3.5.2 Label Encoding
适用于有序类别特征。比如教育程度:小学 < 中学 < 大学 < 研究生。
from sklearn.preprocessing import LabelEncoder
encoder = LabelEncoder()
df['education_encoded'] = encoder.fit_transform(df['education'])
3.5.3 哈希编码
适用于超高基数特征。比如 IP 地址有几十万种取值,用哈希编码可以压缩到固定维度。
from sklearn.feature_extraction import FeatureHasher
hasher = FeatureHasher(n_features=100, input_type='string')
hashed_features = hasher.transform(df['ip_address'].values.reshape(-1, 1))
总结一下:特征工程没有银弹。每种方法都有它的适用场景。我的建议是:先理解业务,再动手做特征。多试几种方法,用模型效果来验证。反欺诈是一场攻防战,特征工程就是我们最有力的武器。