交易反欺诈实战:从信用卡盗刷识别到移动支付异常转账
一、交易反欺诈,究竟在解决什么问题?
假设你是一家银行的风险管理人员。系统每秒接收大量交易,其中绝大部分正常,只有极少数可能涉及盗刷、账户接管或资金转移。
如果逐笔人工审核,成本太高;如果只靠固定规则,又容易被不断变化的欺诈手法绕过。
因此,现代交易反欺诈通常采用多层架构:
交易请求
|
v
实时特征提取
|
v
规则引擎 + 机器学习模型
|
v
交易风险评分
|
+--> 低风险:正常放行
|
+--> 中风险:短信验证 / 人脸验证 / 人工审核
|
+--> 高风险:拦截或暂缓交易
|
v
交易结果回流,更新规则与模型
机器学习模型的核心任务,是根据交易金额、交易类型、历史行为等信息,估计一笔交易存在欺诈风险的程度。
例如:
- 用户过去每天只进行几笔小额消费,突然出现一笔金额巨大的异地交易。
- 某个账户短时间内连续转出大量资金。
- 多个账户使用相同设备,资金最终汇集到少数收款账户。
- 一笔交易的金额、时间、交易对象与用户的历史习惯明显不符。
这些异常并不一定意味着欺诈,但值得系统进一步检查。
反欺诈模型输出的是风险信号,而不是自动证明某笔交易违法。 实际决策还要结合业务规则、身份验证结果、审核能力和误判成本。
二、案例一:信用卡盗刷识别
1. 业务背景
第一个案例使用经典的 Credit Card Fraud Detection 数据集。
数据集包含 284,807 笔信用卡交易,其中只有 492 笔被标记为欺诈交易,欺诈率约为 0.173%。
每笔交易包含以下信息:
| 字段 | 含义 |
|---|---|
Time |
相对于数据起始时点的经过时间 |
Amount |
交易金额 |
V1 至 V28 |
经过 PCA 变换的匿名化数值特征 |
Class |
交易标签,0 为正常,1 为欺诈 |
由于原始业务特征被匿名化,无法直接知道 V1 对应的是交易地点、设备还是其他行为。这恰好适合用于学习监督分类的基本流程。
去 Kaggle 的 Credit Card Fraud Detection 数据集页面下载 creditcard.csv。
2. 建模思路
这里使用 XGBoost 作为分类模型。
选择它的原因是,交易风险通常涉及复杂的非线性关系。例如,某个金额本身可能完全正常,但如果同时具有异常的交易特征组合,风险就会显著上升。
XGBoost 通过逐轮训练决策树,学习这些特征之间的组合关系。
本案例的建模流程是:
- 按时间划分训练集、验证集和测试集。
- 使用类别权重缓解欺诈样本稀少的问题。
- 在验证集上选择风险告警阈值。
- 在保留真实欺诈比例的测试集上评估模型。
安装依赖:
pip install xgboost pandas numpy scikit-learn
3. 完整实践代码
import numpy as np
import pandas as pd
from xgboost import XGBClassifier
from sklearn.metrics import (
average_precision_score,
precision_recall_curve,
precision_score,
recall_score,
confusion_matrix
)
# 1. 读取数据,按时间排序
df = pd.read_csv("creditcard.csv")
df = df.sort_values("Time").reset_index(drop=True)
# Time 只用于时间排序,暂不作为模型输入
X = df.drop(columns=["Class", "Time"])
y = df["Class"].astype(int)
# 2. 按时间划分数据集
n = len(df)
i1 = int(n * 0.70)
i2 = int(n * 0.85)
X_train, y_train = X.iloc[:i1], y.iloc[:i1]
X_valid, y_valid = X.iloc[i1:i2], y.iloc[i1:i2]
X_test, y_test = X.iloc[i2:], y.iloc[i2:]
# 3. 计算类别不平衡权重
neg = (y_train == 0).sum()
pos = (y_train == 1).sum()
# 4. 训练 XGBoost
model = XGBClassifier(
n_estimators=300,
max_depth=4,
learning_rate=0.05,
subsample=0.8,
colsample_bytree=0.8,
scale_pos_weight=neg / max(pos, 1),
eval_metric="aucpr",
tree_method="hist",
n_jobs=-1,
random_state=42
)
model.fit(X_train, y_train)
# 5. 在验证集上选择阈值
valid_prob = model.predict_proba(X_valid)[:, 1]
precision, recall, thresholds = precision_recall_curve(
y_valid, valid_prob
)
f1 = (
2 * precision[:-1] * recall[:-1]
/ (precision[:-1] + recall[:-1] + 1e-12)
)
threshold = thresholds[np.argmax(f1)]
# 6. 在测试集上评估
test_prob = model.predict_proba(X_test)[:, 1]
test_pred = (test_prob >= threshold).astype(int)
print("告警阈值:", threshold)
print("Average Precision:",
average_precision_score(y_test, test_prob))
print("Precision:",
precision_score(y_test, test_pred, zero_division=0))
print("Recall:",
recall_score(y_test, test_pred, zero_division=0))
print("Confusion Matrix:")
print(confusion_matrix(y_test, test_pred))
4. 如何理解模型输出?
最重要的不是准确率,而是以下三个指标。
Precision(精确率)
模型判定为欺诈的交易中,真正欺诈的比例。
精确率太低,意味着风控团队需要审核大量正常交易,或者频繁要求正常客户进行额外验证。
Recall(召回率)
全部真实欺诈交易中,被模型识别出来的比例。
召回率太低,意味着大量欺诈交易会漏过系统。
Average Precision(AP)
衡量不同风险阈值下精确率与召回率整体表现的指标。由于欺诈样本极少,AP 通常比单纯的准确率更有参考价值。
这里有一个容易忽略的问题:如果模型将所有交易都预测为正常,准确率仍然可以达到约 99.827%,但它完全没有识别出欺诈交易。
所以,反欺诈模型不能只追求准确率,而应该重点研究欺诈识别能力与误报成本之间的平衡。
另外,代码用验证集 F1 最大化选择阈值,仅作为入门示例。实际业务还需要结合每笔欺诈的预期损失、额外验证成本和审核团队的处理能力来选择阈值。
三、案例二:移动支付中的异常转账识别
1. 业务背景
第二个案例使用 PaySim 数据集。它是一个模拟移动支付交易的数据集,包含约 636 万笔交易,其中 8,213 笔被标记为欺诈,欺诈率约为 0.129%。
数据涉及五类交易:
CASH_IN:资金转入。CASH_OUT:资金转出或提现。DEBIT:借记交易。PAYMENT:支付。TRANSFER:账户间转账。
数据中还包含交易金额、发起方和接收方账户编号、交易前后余额,以及欺诈标签。
与信用卡案例相比,PaySim 更适合研究移动支付场景下的异常资金转移。
去 Kaggle 搜索 Synthetic Financial Datasets for Fraud Detection,下载 PaySim 数据集。
2. 先别急着训练模型:规则基线很重要
反欺诈系统通常不会完全依赖机器学习。
假设一个账户在短时间内连续转出大额资金,这种行为可以通过规则识别。如果规则已经能够覆盖某类明确的风险,就没有必要为了这一类问题专门训练一个复杂模型。
更值得机器学习处理的,是那些难以通过单一条件定义的复杂模式,例如金额、交易类型和交易时间共同构成的异常行为。
在 PaySim 中,可以先建立一个简单的模型,使用交易类型、金额和时间特征预测欺诈风险。
但这里有个重要陷阱:PaySim 是模拟数据,不等同于真实银行的生产数据。
该数据集的欺诈生成机制具有明显的人为规律:大量欺诈交易的金额恰好等于发起方交易前的全部余额。如果模型利用这一规律获得极高分数,也不能据此认定它在真实交易中同样有效。
此外,交易完成后的余额属于事后信息。若模型需要在交易授权前作出判断,就不能使用只有交易完成后才能获得的字段。
3. 实践代码:按时间训练移动支付风险模型
下面的示例刻意不使用交易完成后的余额,也暂时不使用发起方交易前余额,避免直接依赖该模拟数据中容易形成捷径的“余额全部转出”模式。
同时,考虑到数据量较大、欺诈样本稀少,代码只对训练集的正常交易进行下采样,验证集和测试集仍保留原始类别比例。
import numpy as np
import pandas as pd
from xgboost import XGBClassifier
from sklearn.metrics import (
average_precision_score,
precision_recall_curve,
precision_score,
recall_score
)
# 1. 读取必要字段
path = "PS_20174392719_1491204439457_log.csv"
df = pd.read_csv(
path,
usecols=[
"step", "type", "amount",
"oldbalanceOrg", "newbalanceOrig",
"oldbalanceDest", "newbalanceDest",
"isFraud"
]
)
df = df.sort_values("step").reset_index(drop=True)
# 2. 构造交易发生时可用的基础特征
df["hour"] = df["step"] % 24
df["log_amount"] = np.log1p(df["amount"])
df["log_dest_balance"] = np.log1p(df["oldbalanceDest"])
# 暂时不使用:
# newbalanceOrig、newbalanceDest:交易完成后的余额
# oldbalanceOrg:先排除,避免模型直接利用模拟器中的特殊规律
# 真实业务中可将其作为候选特征,但需进行消融验证
X = pd.get_dummies(
df[["type", "hour", "log_amount", "log_dest_balance"]],
columns=["type"],
dtype=np.uint8
)
y = df["isFraud"].astype(int)
# 3. 按时间划分训练、验证和测试集
train_cut = int(df["step"].max() * 0.70)
valid_cut = int(df["step"].max() * 0.85)
train_ids = df.index[df["step"] <= train_cut]
valid_ids = df.index[
(df["step"] > train_cut) &
(df["step"] <= valid_cut)
]
test_ids = df.index[df["step"] > valid_cut]
# 4. 仅对训练集下采样正常交易
train_y = y.loc[train_ids]
pos_ids = train_ids[train_y.to_numpy() == 1]
neg_ids = train_ids[train_y.to_numpy() == 0]
n_neg = min(len(neg_ids), max(100 * len(pos_ids), 20000))
neg_sample = pd.Series(neg_ids).sample(
n=n_neg, random_state=42
).to_numpy()
sample_ids = np.concatenate([
np.asarray(pos_ids), neg_sample
])
# 5. 训练模型
model = XGBClassifier(
n_estimators=250,
max_depth=5,
learning_rate=0.05,
subsample=0.8,
colsample_bytree=0.8,
eval_metric="aucpr",
tree_method="hist",
n_jobs=-1,
random_state=42
)
model.fit(X.loc[sample_ids], y.loc[sample_ids])
# 6. 验证集选择阈值
valid_prob = model.predict_proba(X.loc[valid_ids])[:, 1]
p, r, thresholds = precision_recall_curve(
y.loc[valid_ids], valid_prob
)
f1 = (
2 * p[:-1] * r[:-1]
/ (p[:-1] + r[:-1] + 1e-12)
)
threshold = thresholds[np.argmax(f1)]
# 7. 测试集评估:保持真实欺诈比例
test_prob = model.predict_proba(X.loc[test_ids])[:, 1]
test_pred = (test_prob >= threshold).astype(int)
print("Test AP:",
average_precision_score(y.loc[test_ids], test_prob))
print("Precision:",
precision_score(y.loc[test_ids], test_pred, zero_division=0))
print("Recall:",
recall_score(y.loc[test_ids], test_pred, zero_division=0))
这段代码的重点并不是追求某个预设的模型分数,而是展示如何建立一个更接近真实交易环境的实验。
首先,按照时间划分训练集、验证集和测试集,可以避免模型利用未来数据训练,再去预测过去的交易。
其次,正常交易的下采样仅发生在训练集。测试集必须保留自然欺诈比例,否则评估出来的精确率就无法反映真实交易环境。
最后,排除交易完成后的余额,避免模型使用在交易决策时尚不可获得的信息。
需要注意,下采样会改变训练数据中的欺诈比例,因此模型输出的概率不一定等于真实业务环境中的欺诈概率。实际部署前还需要进行概率校准,并重新评估阈值。
四、从实验模型走向真正的反欺诈系统
前面的案例展示了两个不同场景,但真正的交易反欺诈还需要解决三个工程问题。
1. 从单笔交易扩展到用户行为
单笔交易特征只能回答“这笔交易看起来是否异常”。
很多欺诈行为需要结合用户历史才能发现,例如:
- 最近 5 分钟的交易笔数和累计金额。
- 最近 1 小时新增的收款账户数量。
- 当前交易金额相对于用户历史消费水平的偏离程度。
- 近期设备、IP 地址、地理位置和交易对象是否发生异常变化。
这类特征通常称为行为统计特征或时序特征。
它们的前提是:在预测当前交易时,只使用当前时点及之前已经发生的信息。
2. 从单个账户扩展到资金网络
如果欺诈者通过多个账户转移资金,单笔交易模型可能无法识别整个团伙。
此时可以将账户作为节点、转账作为边,构建交易关系图,进一步识别资金快速汇集、分散转出或多个账户共享设备等风险模式。
不过,图模型是否有效取决于数据中是否真的存在足够的账户关联和历史交易。不能仅仅因为数据包含账户编号,就认为它天然适合图学习。
3. 从模型分数扩展到业务决策
最终,反欺诈系统需要将模型分数转化为实际行动。
例如,风险较低的交易直接放行;中等风险的交易触发额外身份验证;高风险交易进入人工审核或暂缓处理。
模型评估也应进一步转向业务指标:
- 每天能够识别多少欺诈交易?
- 每识别一笔欺诈交易,需要审核多少笔正常交易?
- 被拦截交易中,实际欺诈的金额占多少?
- 模型上线后,欺诈损失是否下降?
- 新型欺诈出现后,模型性能是否快速衰减?
这些指标往往比单独追求 ROC-AUC 或 AP 更接近反欺诈系统的真实价值。
五、总结
交易反欺诈并不是简单地训练一个二分类模型,而是将实时特征、风险规则、机器学习、交易决策和反馈机制结合起来。
信用卡盗刷案例展示了如何利用监督学习识别少量欺诈样本;移动支付案例则进一步说明,时间切分、特征可用性和模拟数据中的特殊规律,都会直接影响实验结论。
实际落地时,最值得记住的是三点:
- 不要被高准确率迷惑。 欺诈样本极少,必须重点关注精确率、召回率、AP 和实际损失。
- 不要让未来信息进入模型。 特征必须在真实交易决策时可用,测试集也应尽可能模拟未来交易。
- 不要把模型分数当作最终目标。 反欺诈系统最终追求的是减少欺诈损失,同时控制误拦截、审核成本和用户体验损失。
只有将这些因素共同纳入设计,离线训练出的模型才有机会成为真正有效的交易反欺诈系统。
- 点赞
- 收藏
- 关注作者
评论(0)