第三章 特征工程基础:数值特征、类别特征、时间特征、特征缩放与编码

各位同学,欢迎来到特征工程这一章。说实话,在反欺诈模型里,特征工程的重要性怎么强调都不过分。我见过太多团队,模型选得再花哨,特征没做好,最后 AUC 就是上不去。反过来说,哪怕你用最简单的逻辑回归,只要特征工程做到位,效果往往能吊打那些花里胡哨的深度模型。

这一章,我们就来聊聊特征工程里最基础、也最核心的几个部分:数值特征、类别特征、时间特征,以及特征缩放和编码。嗯,内容不少,但都是干货。

核心观点:特征工程不是简单的数据搬运,而是把原始数据中隐藏的「信号」放大,把「噪声」过滤掉。反欺诈尤其如此——欺诈行为往往藏在细微的异常里。

3.1 数值特征:别只当它是数字

数值特征看起来最简单,年龄、金额、次数……直接扔进模型不就行了?我以前也这么想,直到被现实狠狠教育了一顿。

数值特征的处理,我个人习惯从三个维度入手:

  1. 分布形态——数据是正态分布还是长尾分布?
  2. 异常值——有没有明显的离群点?
  3. 业务含义——这个数值在业务场景下意味着什么?

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))

总结一下:特征工程没有银弹。每种方法都有它的适用场景。我的建议是:先理解业务,再动手做特征。多试几种方法,用模型效果来验证。反欺诈是一场攻防战,特征工程就是我们最有力的武器。

特征工程知识体系 特征工程 数值特征 类别特征 时间特征 缩放与编码 对数变换 分箱离散化 异常值处理 目标编码 计数/频率编码 One-Hot编码 周期性编码 时间窗口聚合 节假日标记 标准化 Min-Max缩放 鲁棒缩放 哈希编码 特征工程是反欺诈模型的核心,好的特征能让模型事半功倍

专注资料整理