财务欺诈识别:机器学习模型、特征工程与算法选型实战
发布时间:2026/10/2 19:51:53来源:尧图网络
简介一套面向金融风控与数据分析场景的财务欺诈识别项目资料针对希望用机器学习提升风控能力的金融机构、研究人员及技术从业者。资源聚焦随机森林、支持向量机、XGBoost、逻辑回归与神经网络等主流算法的完整落地流程从数据收集、缺失值处理、异常值清洗、特征缩放与分类变量编码等预处理环节到模型选择、关键参数配置与调优思路均有清晰讲解。同时将注意力放在评估环节对总准确率、一型准确率、二型准确率与AUC等指标给出了分析建议并探讨了模型集成策略以增强识别效果。内容包含可用于直接参考的Python代码片段与训练示例适合有一定编程基础、想系统掌握财务欺诈建模方法的中高级使用者。资源包共1个docx文档体积约22KB文档结构完整当前已有81人浏览学习。整体而言这份资料为从数据工程到模型部署视角理解财务欺诈识别提供了一条较为完整的参考路径。1. 财务欺诈识别为什么必须从规则引擎换成机器学习模型财务欺诈识别是典型的“少数类问题”一万笔交易里可能只有三五笔有问题而这几笔恰恰能造成巨大的资金损失。过去大多数财务团队依赖人工经验写规则比如“单笔金额超过XX万触发复核”“短时间内多笔转账触发告警”。规则引擎在欺诈手法固定时很有效但欺诈手段是动态演变的规则需要不停打补丁而且规则的覆盖率和误报率永远在打架。把机器学习模型引入财务欺诈识别核心价值不是“更智能”的口号而是让模型自己从历史数据里学出哪些组合特征指向欺诈而不是靠人一条条试。这是我这篇文章想讲清楚的财务欺诈识别到底怎么落地多种算法各自的适用边界在哪代码怎么写才不是玩具。本文面向的是有 Python 基础、想在企业财务或风控场景真正跑通一版欺诈识别模型的从业者。你不会看到数学推导堆砌只会看到能照着改、照着调的数据处理流程、算法选型和评估方法。2. 数据准备欺诈识别模型八成的成败在这里2.1 标签构造难的不是模型是你怎么定义“欺诈”做欺诈识别第一个要面对的问题是标签从哪来。公开数据集里 Fraud 那一列是现成的但企业内部真实场景里“欺诈”是一个事后确认的结果而且确认周期很长。常见的做法是用“事后坏账”或“稽核确认的异常交易”作为标签来源具体有三种构造方式。第一种是直接使用业务侧已经定性的记录比如反欺诈部门手工标记过的案例。这种方式标签质量高但数量少而且存在确认偏差——能被人工发现并标记的往往只是欺诈里比较“显眼”的那部分。第二种是用规则引擎的历史告警结果作为代理标签告警且被复核确认的算正样本。这种方式样本量足但会把规则本身的错误继承给模型。第三种是半监督思路先不急着定义正负样本用全部交易数据做无监督异常检测把异常分高的那批交给业务复核复核结果再回填成标签。我一般建议企业从第二种起步规则告警结果加人工复核确认跑通一轮之后再逐步用模型结果反哺规则。标签质量直接影响后面所有环节如果你发现模型离线指标一直上不去先回去看标签而不是急着换算法。2.2 特征体系把原始流水变成模型能吃的表格欺诈识别的特征要从三个视角去构造缺一不可。第一个是交易本身金额、时间、渠道、对手方类型、地理位置、设备指纹。第二个是聚合窗口特征这是欺诈识别最有价值的部分同一账户过去 24 小时交易笔数、过去 7 天累计金额、与对手方的历史交互次数、金额的均值与标准差偏离度。第三个是账户画像特征开户时长、历史交易活跃度、过去 90 天最大单笔金额。窗口特征工程有一个关键参数窗口长度。短窗口适合捕捉“短时间内高频小额试探再大额转出”这类行为长窗口适合捕捉“账户长期休眠突然活跃”这类模式。常见配置是一组多窗口并行1 小时、24 小时、7 天每个窗口都算 count、sum、mean、std、max 五类统计量。特征数量会迅速膨胀这个阶段不用太担心后面用特征筛选或者模型内置正则去控制。下面给一段生成窗口特征的参考代码核心是利用 pandas 的 groupby 和 shift避免用到未来数据。import pandas as pd def build_window_features(df, group_colaccount_id, time_coltrans_time, amount_colamount, windows[1, 24, 168]): 生成时间窗口聚合特征 df: 交易流水必须已经按 account_id trans_time 排好序 windows: 单位是小时[1, 24, 168] 分别对应 1小时内、24小时内、7天内 df df.sort_values([group_col, time_col]).reset_index(dropTrue) for w in windows: # 每个窗口向前累计但不包含当前这笔避免标签泄漏 g df.groupby(group_col)[amount_col].apply( lambda x: x.shift(1).rolling(windoww, min_periods1).sum() ) df[famt_sum_{w}h] g.values return df这段代码里有三个点需要特意解释。shift(1) 是必须的它把上一笔交易的数据推到了当前行这样当前笔的特征里不包含自己更不能包含发生在自己之后的交易。rolling(windoww, min_periods1) 表示窗口不足 w 个样本时用已有样本计算避免开户初期数据量少导致特征为空。单位用的是小时而不是“笔数”因为交易频率在不同账户之间差异巨大用固定笔数窗口对高频账户和低频账户会产生完全不对等的统计意义。代码里用 apply 会比直接用 groupby.transform 慢一些但胜在写法直观、不容易出错数据量在几百万行以内可以接受。特征构造完成后要做一次 leakage 检查随机抽 100 条样本手动看特征值是否依赖了该笔交易之后才产生的信息。这个步骤不能省后面会在避坑章节展开。2.3 数据切分时序数据别用随机切分财务交易流水是强时序数据欺诈模式会随时间演化。如果用随机切分把同一天的交易同时放进训练集和测试集模型会“偷看”到未来的信息分布测试指标会虚高。我见过一个真实案例随机切分下 AUC 0.93 的模型换成按时间切分后 AUC 直接掉到 0.85这才是真实水平。正确做法是按时间截断取前 70% 时间段做训练中间 15% 做验证最后 15% 做测试。验证集不是用来挑模型的是用来调超参和选阈值的测试集必须放在时间轴的末端完全模拟“用过去预测未来”的真实场景。这里还有一个容易被忽略的细节做过采样或 SMOTE 之后训练集和验证集的分布要各自独立处理不能先对全量做过采样再切分否则验证集里会混入由训练样本插值生成的“假数据”评估结果等于自欺欺人。先切分再只对训练集做增强。3. 三种主流算法实现孤立森林、XGBoost、自编码器3.1 无监督路线孤立森林适合“冷启动”多数企业第一次做欺诈识别时没有可靠标签这时候最靠谱的起手式是孤立森林。它的核心思想不是“描述正常样本长什么样”而是“异常样本更容易被孤立”。在随机决策树中异常点通常只需要很少几次切分就能从正常点里分出来路径长度明显更短。这个特点让它在欺诈场景很好用因为欺诈本质上就是“少数且不同的样本”。孤立森林不需要标签只需要特征矩阵。下面是一段完整的训练与预测代码。from sklearn.ensemble import IsolationForest def train_isolation_forest(features, contamination0.01, n_estimators200): features: 已经做完特征工程和标准化后的 DataFrame 或 ndarray contamination: 预期异常比例欺诈场景一般设 0.005~0.02 n_estimators: 树的数量并不是越大越好200 足够 model IsolationForest( n_estimatorsn_estimators, max_samplesauto, contaminationcontamination, random_state42, n_jobs-1 ) model.fit(features) # 返回两个分数异常分数越小越异常和决策函数值 anomaly_scores model.score_samples(features) decision_scores model.decision_function(features) return model, anomaly_scores, decision_scores参数选择上有几个常见的坑。contamination 是最敏感的参数它决定模型认为数据里有多少比例的异常。设小了会漏报设大了会淹没有效告警。如果没有标注数据建议用业务侧给的历史欺诈率估计值而不是默认的 0.1。n_estimators 在这个模型里不是越大越好超过 300 之后性能提升很有限反而拖慢推理速度。max_samples 控制每棵树的抽样量默认 auto 即 min(256, n_samples)对大数据集足够。n_jobs-1 在多核机器上能明显加速但如果你在 Windows 上跑-1 有时会引发进程相关问题改成具体核数更稳。孤立森林输出的原始分数并不是概率它的数值范围没有固定含义。实际使用时先对分数做排序取分数最低的 1% 作为疑似欺诈名单交给业务复核。这一步很重要因为无监督模型没有“标准答案”只有排在前面的样本才有复核价值。3.2 有监督路线XGBoost 配合代价敏感训练当标签积累到几千条正样本之后XGBoost 是欺诈识别的主流选择。树模型能自动处理特征之间的非线性关系对缺失值有内置策略训练速度快解释性也相对可控。用它做欺诈识别关键在于把“代价不对称”写进训练过程。欺诈识别里漏掉一笔欺诈的代价可能是误报一笔正常交易的几十倍甚至上百倍因此不能简单地用 0/1 损失去优化。XGBoost 提供了 scale_pos_weight 参数可以直接在损失层面给正样本加权这是最简单也最常用的方式。另一种方式是手动设置 sample_weight在训练时给正样本一个固定权重。两者效果接近scale_pos_weight 更方便。import xgboost as xgb from sklearn.model_selection import train_test_split def train_xgb_fraud(X_train, y_train, X_val, y_val, pos_weight30): 代价敏感训练的 XGBoost 分类器 pos_weight: 正样本权重等于 负样本数/正样本数 的估计值或业务成本比 dtrain xgb.DMatrix(X_train, labely_train) dval xgb.DMatrix(X_val, labely_val) params { objective: binary:logistic, eval_metric: aucpr, # 用 PR AUC 而不是 AUC scale_pos_weight: pos_weight, max_depth: 5, eta: 0.05, subsample: 0.8, colsample_bytree: 0.8, min_child_weight: 3, nthread: 8, } model xgb.train( params, dtrain, num_boost_round500, evals[(dval, val)], early_stopping_rounds30, verbose_evalFalse ) return model这段代码的关键点有几个。eval_metric 用的是 aucpr 而不是默认的 auc原因是欺诈样本极度不均衡PR 曲线比 ROC 曲线更能反映模型对少数类的排序能力。max_depth 设 5 而不是默认的 6欺诈数据里特征维度不高但噪声不小浅一点的树泛化更好。eta 从默认的 0.3 降到 0.05配合 early_stopping_rounds30让模型在验证集上不再提升时及时停下。scale_pos_weight 的取值可以参考负正样本比但更合理的做法是用业务成本比去调整如果漏一笔欺诈损失是误报一笔的 30 倍就设 30。我一般会用样本比做初始值然后在验证集上对 PR 曲线做一次网格搜索选让 Recall1% 最高的那个权重。XGBoost 训练完成之后还有一个必须做的步骤看特征重要性但不要只看默认的 gain要看 cover 和 frequency 三个指标一起。欺诈场景经常出现一个高 gain 特征是某个聚合操作偶然产生的单独跑一遍特征重要性能帮你发现这类问题下文避坑章节会专门讲。3.3 深度学习基线自编码器的重建误差就是异常分在有标签场景神经网络可以直接做分类但欺诈识别里更常见也更稳妥的深度学习方法是用无监督的自编码器做异常检测。训练时只用正常样本让网络学会把正常样本压缩再还原。推理时把一个样本输入网络如果重建误差很大说明这个样本不符合网络学到的“正常模式”值得怀疑。这个思路和孤立森林互补它能捕捉到特征之间的复杂交互但训练时间更长对数据标准化更敏感。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Dropout def build_autoencoder(input_dim, encoding_dim16): 一个简单的全连接自编码器 input_dim: 特征维度 encoding_dim: 隐层维度通常设为 input_dim 的 1/4 到 1/2 model Sequential([ Dense(64, activationrelu, input_shape(input_dim,)), Dropout(0.2), Dense(encoding_dim, activationrelu), Dense(32, activationrelu), Dense(input_dim, activationlinear) ]) model.compile(optimizeradam, lossmse) return model def detect_anomaly(model, X_normal, X_test, threshold_percentile99): 用正常样本重建误差分布的分位数定阈值 # 只在正常样本上训练 model.fit(X_normal, X_normal, epochs30, batch_size256, verbose0, validation_split0.1) # 计算正常样本的重建误差 normal_recon model.predict(X_normal) normal_error ((normal_recon - X_normal) ** 2).mean(axis1) # 用 99 分位数作为阈值 threshold np.percentile(normal_error, threshold_percentile) # 计算测试样本的重建误差 test_recon model.predict(X_test) test_error ((test_recon - X_test) ** 2).mean(axis1) return test_error, threshold注意这里的几个细节。编码器的隐层维度 encoding_dim 不是越小越好太小会让模型学不到正常样本的多样模式正常样本的重建误差普遍偏大欺诈样本与正常样本的误差拉不开差距。Dropout 在这里不是为了防止过拟合而是防止自编码器“死记硬背”地学会恒等映射加了 Dropout 之后模型被迫学到更鲁棒的压缩表示。训练时只用正常样本这是核心假设。如果训练集里混入了一部分未标记的欺诈样本重建误差的分布会被污染阈值也会失真。自编码器做欺诈识别很适合作为无监督路线的第二道防线与孤立森林做集成两个模型都给出异常分最后取加权。常见的权重配比是孤立森林 0.6 加自编码器 0.4具体权重按业务复核通过率来调。深度学习算法在这一步的价值不是替代树模型而是在树模型的盲区里找补比如那些在单个特征维度上看起来正常、但特征组合上明显偏离历史模式的交易。4. 评估不是看准确率阈值、代价矩阵与业务验证4.1 为什么 ROC 曲线在欺诈识别里会骗人财务欺诈数据里的正样本占比通常不到 1%甚至低于千分之一。在这个前提下ROC 曲线的表现会被负样本主导模型即使把所有正样本都预测成低概率AUC 也可能很高因为负样本数量足够大把整体排序拉住了。这就是为什么欺诈识别领域更看重 PR 曲线——精确率和召回率的权衡直接反映少数类上模型的表现。举一个真实对比某次实验中随机切分下 ROC-AUC 是 0.94看起来非常好但同一模型在时间切分验证集上 PR-AUC 只有 0.18。原因就是负样本基数太大ROC 曲线被“稀释”了。因此做欺诈识别评估第一原则是以 PR-AUC、RecallTop-K% 为主要指标ROC-AUC 只能做参照。下面这张表可以反映出两类指标在不同场景的差异。指标计算方式对欺诈识别的参考价值弱点Accuracy正确样本数 / 总样本数基本无参考价值全部预测为正常也能拿到 99% 以上ROC-AUC从正样本中随机抽一个排序高于随机负样本的概率仅作参照负样本占比高时虚高PR-AUCPR 曲线下面积核心指标受正样本绝对数量影响不能跨数据集比较RecallTop1%得分最高的 1% 样本中欺诈样本占比业务可直接用依赖人工复核确认标签4.2 代价矩阵与阈值搜索欺诈识别的业务决策不是“是不是欺诈”的二选一而是“要不要复核”的成本问题。一次误报消耗的是人工审核资源一次漏报消耗的是真金白银。把这个问题量化成代价矩阵再在这个矩阵上选阈值比单纯用 Youden Index 找 ROC 曲线上的平衡点更贴合业务。下表是一个简化代价矩阵。实际正常实际欺诈预测正常无成本预测正常损失 500 元漏报预测欺诈复核成本 10 元预测欺诈复核成本 10 元命中在这个矩阵下每个阈值对应的业务代价 漏报数 × 500 误报数 × 10。目标是在验证集上找到让总代价最小的阈值而不是让 F1 最大的阈值。这个逻辑新手常忽略总想着把 F1 调到最高实际上 F1 最高的阈值往往偏向误报更多因为它在公式里给了精确率很大的权重但业务里精确率的代价可能只占漏报代价的几十分之一。import numpy as np def find_best_threshold(y_val, pred_proba, cost_fp10, cost_fn500): 遍历阈值找到总代价最小的切分点 y_val: 验证集真实标签 (0/1) pred_proba: 模型输出的欺诈概率 cost_fp: 一次误报的复核成本 cost_fn: 一次漏报的损失金额 thresholds np.linspace(0.01, 0.99, 100) best_threshold 0.5 best_cost float(inf) for t in thresholds: pred (pred_proba t).astype(int) fp ((pred 1) (y_val 0)).sum() fn ((pred 0) (y_val 1)).sum() cost fp * cost_fp fn * cost_fn if cost best_cost: best_cost cost best_threshold t return best_threshold, best_cost这段代码的逻辑很简单遍历从 0.01 到 0.99 的 100 个候选阈值对每个阈值算一次业务总代价取最小值的那个阈值作为线上使用的分割点。cost_fp 和 cost_fn 这两个参数是业务输入不是模型参数需要跟财务或风控部门确认。如果后端系统一次复核只能处理有限数量的告警还可以加一个约束条件阈值不仅要求代价最小还要求 top-K 样本量不超出人工复核产能。这时候把 best_threshold 的筛选逻辑改成先过滤掉超过产能上限的候选再在剩余候选中选代价最小的。4.3 业务验证离线指标只是起点模型离线表现再好不上线跑一遍人工复核闭环都算没落地。常见的做法是做一次影子测试模型对实时或近线交易打分但暂时不阻断任何交易只把得分前 1% 的样本送给风控复核团队人工判断。跑两周左右统计复核确认率。确认率能反映模型在真实数据上的表现与离线评估的差距也能暴露标签延迟问题——有些欺诈案件要过一两个月才会暴露两周的确认率只是下限。我见过一个项目离线 Recall1% 达到 40%影子测试时确认率只有 15%一查原因是离线评估时把复核标记当成了标准答案但标记自身的误判率就有 50%。业务验证就是要尽早暴露这类系统性偏差。5. 五个必踩的坑从现象到原因再到解决5.1 特征里混进了未来信息模型指标好得不真实现象离线 AUC 高得离谱达到 0.98 以上换时间切分也一样高。但上线后告警质量很差复核确认率远低于测试集。原因特征工程时把“当前交易之后才产生的信息”卷进了特征。最典型的例子是用事后确认的欺诈标签去构造行为特征比如“该账户是否被冻结”“该笔交易是否触发过反洗钱复核”这些信息在交易发生时根本不存在。另一个隐蔽的来源是窗口特征没有 shift把当前这笔交易自己的聚合值算进了特征。解决对每个特征做一次时间对齐检查。具体做法是写一个脚本随机抽 100 个样本打印交易发生时间和特征值逐条人工比对特征依赖的数据是否在该时间点之前可用。窗口特征统一用 shift(1) 消除自包含涉及外部标签的字段一律不允许进特征矩阵。5.2 过采样之后再做数据切分验证集被污染现象验证集 precision 很高模型上线后表现明显变差。原因先对全量做了 SMOTE 或随机过采样再造训练集/测试集切分。SMOTE 会在正常样本和欺诈样本之间生成插值样本这些合成样本如果落在测试集或验证集里相当于模型“提前见过”了这些数据点的邻域评估结果必然乐观。这是欺诈识别里最容易犯的顺序错误很多老手也会翻车。解决先切分再只对训练集做过采样。验证集和测试集必须保持真实分布。如果用了 SMOTE还要注意对验证集不做任何合成操作。另一个补充做法是在训练集内部再做一次时间嵌套切分防止过采样造成的时序泄漏。5.3 阈值只看 F1 最优点没算业务代价现象模型调得不错但上线后风控团队抱怨“日常工作量太大”每天数百条告警里有效比例不足 5%。原因把模型最优阈值理解为 F1 最大的点忽略了欺诈场景里误报和漏报的成本不对称。F1 把精确率和召回率等同看待但如果一次漏报损失是误报复核成本的 50 倍最优阈值应该明显偏向提高召回率哪怕精确率降到 20%。解决用上一章的代价矩阵搜索阈值cost_fp 和 cost_fn 由业务确认。上线初期先设一个偏保守的高阈值按产能逐步下调每次下调都要同步统计确认率变化。阈值不是一次定死的它需要随季节、活动周期和欺诈趋势一起调整。5.4 孤立森林的 contamination 拍脑袋设现象告警名单里异常分数最低的一批样本大部分是业务异常而非欺诈比如测试账号、内部调拨、大客户集中采购。原因contamination 被设成 0.1 甚至更高模型把 10% 的样本都当成了潜在异常。实际业务欺诈率可能只有 0.5%模型被迫把正常样本中的“边缘品种”也当异常识别出来这类边缘样本大多是规则触碰者不是欺诈者。解决先不做任何设定用模型跑一版统计 score_samples 的分布找分数分布的拐点。再结合业务侧的历史欺诈率估计值两个数相互印证后确定 contamination。这个参数值得多花时间调它直接决定了告警量的基线。5.5 规则引擎和模型打分各干各的没有形成合力现象模型已经上线但业务仍在按旧规则处理告警模型分数只是多了一个展示字段没有实际改变复核优先级。原因技术团队把模型交付给业务方之后没有设计一个“规则 模型”的融合决策流程。业务方不信任黑匣子不敢直接按模型分排队。解决做一版简单的决策矩阵规则命中的样本按模型分排序规则未命中的样本里模型分排进前 0.5% 的也进复核队列。用这个矩阵跑一个月统计两个来源的确认率和挽回金额用数据说服业务方逐步把模型分提到主决策位。模型不是要替代规则而是给规则排序、给规则找漏。6. 模型上线后的三件事监控、解释与重训触发模型上线只是中场不是终点。欺诈模型最怕的就是“上线即巅峰半年变成废铁”。我见过太多项目模型上线时 Recall1% 做到 35%三个月后业务反馈告警质量直线下降一查才发现交易结构已经变了模型还在用旧参数硬扛。第一件事是监控特征漂移。每天对线上交易的特征分布和训练分布做 PSIPopulation Stability IndexPSI 超过 0.2 的特征要标红超过 0.25 就要考虑特征失效了。不需要每个特征都盯重点关注交易金额均值、地域分布、渠道占比这些业务含义清晰的特征。PSI 监控不复杂写个定时任务就能做但它的价值在于能提前预警“模型开始失效”而不是等业务骂完才反查。第二件事是对模型做解释。虽然树模型比神经网络好解释但欺诈识别需要的解释不是“哪个特征最重要”而是“这一笔为什么被标成高风险”。用 SHAP 值对每笔告警输出贡献度排序发给复核人员作为参考线索能显著提高复核效率。复核人员不再从头开始翻流水而是直接看 SHAP 里排在最前面的几个特征对应的真实值判断是否合理。这个过程也会暴露模型的逻辑漏洞比如某个特征被赋予过高权重但它在业务上其实是个弱信号。第三件事是设定重训触发条件。不要按固定周期重训而是设三个触发开关监控 PSI 超标的特征数超过 5 个、确认率连续一周低于离线阈值的 60%、业务侧反馈新型欺诈手法。三个开关任意一个触发就启动数据回捞和新一轮训练。重训时保留上一版模型在验证集上的表现作为基准防止新版模型“调参调出幻觉”。我早期做欺诈识别时吃过大亏上线后只盯着离线指标结果业务侧确认率掉了一半都没察觉直到第二个月核损报告出来才反应过来。后来把监控、解释、重训三件事做成固定流程模型的保鲜期从两个月拉长到了七八个月。财务欺诈识别这件事算法只占三成剩下的七成是数据治理、阈值调优、监控反馈这些不起眼但决定成败的活而这些是能靠流程和代码一步步做扎实的希望这些经验能帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网