ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

机器学习如何识别财务欺诈:从数据构造到模型上线全流程

机器学习如何识别财务欺诈:从数据构造到模型上线全流程 简介财务欺诈识别是金融风控的核心难点这份docx资料面向金融机构风控人员与研究者展示如何综合运用随机森林、支持向量机、XGBoost、逻辑回归和神经网络完成欺诈检测。内容从数据清洗、缺失值与异常值处理、特征缩放与编码入手逐步讲解特征工程与训练/验证集划分随后针对每一类模型给出原理说明、关键参数如n_estimators、max_depth、核函数gamma、learning_rate等的配置与调优思路并附上Python代码实现。评估环节覆盖总准确率、一型准确率、二型准确率与AUC同时给出模型集成等风控改进建议。通过对比不同模型的评估结果可掌握各算法在欺诈识别场景下的适用性资源包仅22KB内含1个docx文档便于直接阅读。该资源已有81人学习下载适合希望将机器学习落地到反欺诈实战、且需要代码与参数解释的读者。1. 财务欺诈识别为什么值得用机器学习先看数据长什么样做财务欺诈识别的人第一年踩的坑大多不是模型选错而是拿到的数据根本没法用。我在银行风控和第三方支付项目里做了三年多欺诈检测最大的体会是欺诈方永远在变规则永远追不上。传统 IF-THEN 规则比如“单笔超 5 万且凌晨交易就告警”能挡住上一批团伙但下一批换个渠道、换个时间段规则就哑了。机器学习模型的价值是让系统自己从历史交易里学“哪些组合更像欺诈”而不是靠人肉枚举。这篇笔记要解决三件事怎么构造欺诈识别用的训练数据、哪几种算法值得先试、参数怎么调、上线后怎么评估。内容包括可复现代码和解释适合在信贷审批、支付反欺诈、内部审计场景里做建模或风控策略的工程师。2. 构造欺诈识别训练集标签、特征与数据划分2.1 欺诈标签怎么来从坏账、投诉和人工审核结果里挖财务欺诈识别是典型的监督学习第一步是拿到“哪些样本是欺诈”的标签。常见来源有三个资金损失确认比如银行核实后的盗刷、用户投诉用户说“这笔不是我刷的”、事后人工审核反欺诈团队查出来的可疑交易。注意这三种来源的噪声差别很大。用户投诉有误报人工审核有漏报资金损失确认最干净但最滞后。我一般会把三类来源合并按“是否有确认损失”给高权重确认损失直接标 1投诉和人工审核标记为“嫌疑”样本先不直接当正样本而是做半监督或者 PU learning正样本-未标注样本学习。如果项目时间紧也可以把“嫌疑”当弱正样本但要在评估时单独看这一部分的精确率。import pandas as pd df pd.read_csv(transactions.csv, parse_dates[trans_time]) # 假设有 loss_flag(确认损失)、complaint_flag(用户投诉)、review_flag(人工审核) df[fraud_label] 0 df.loc[df[loss_flag] 1, fraud_label] 1 # 嫌疑样本先不标 1留作半监督候选 df[suspect] ( (df[complaint_flag] 1) | (df[review_flag] 1) ) (df[fraud_label] 0)这段代码的逻辑是先把确认损失的硬标签放进fraud_label投诉和审核结果单独存为suspect字段不污染训练标签。这样做的原因是把误杀正常的用户投诉直接当正样本会让模型学到“投诉过的客户都是坏人”上线后误报率会高到业务方没法接受。参数说明complaint_flag和review_flag都是 0/1 整数列suspect字段后续可以用于半监督的伪标签迭代也可以直接删除不用。如果你没有确认损失标签只有投诉和审核建议把这两列做特征而不是标签至少能保留部分信号。2.2 特征工程金额、频次、时间间隔和商户组合财务欺诈的特征工程核心就一句话把“这笔交易诡异在哪”变成数字。单个交易本身的字段金额、时间、商户类型信息量很小真正有用的是上下文特征——这笔交易和这个人过去的行为差多远。我常用四类特征金额类amount的对数、是否超过该用户历史均值的 n 倍、是否接近整数阈值比如 9999。频次类该用户过去 1 小时、24 小时内的交易笔数。时间间隔类与上一笔交易的时间差欺诈交易往往在短时间内连续发生。商户/渠道组合该商户类型是否首次在该用户的历史中出现。import numpy as np def build_features(df): df df.sort_values([user_id, trans_time]).copy() # 1小时内的交易笔数 df[tx_count_1h] df.groupby(user_id)[trans_id].transform( lambda x: x.rolling(1h, min_periods1).count() ) # 与上一笔交易的时间差秒 df[time_since_last] ( df.groupby(user_id)[trans_time].diff().dt.total_seconds() ) # 金额是否超过该用户均值的 3 倍 user_mean_amt df.groupby(user_id)[amount].transform(mean) df[amount_ratio] df[amount] / (user_mean_amt 1e-6) # 商户类型是否首次出现 df[is_new_mcc] df.groupby(user_id)[mcc].transform( lambda x: x.ne(x.shift()).cumsum() ) return df注意rolling(1h)要求trans_time是 DatetimeIndex 的组内时间窗口所以必须先sort_values并设置时间列。这个特征在支付场景里非常有效团伙盗刷的典型行为是短时间内大量小额试探tx_count_1h会直接拉高。time_since_last是捕获“凌晨突然有一笔间隔很长的交易”这类行为漂移。amount_ratio我用用户均值 1e-6做分母是防止用户没有任何历史交易时除零。is_new_mcc这里用的是“相对于该用户历史是否新出现”但cumsum之后还要判断是否为 1 才是真正的“首次”实际使用时建议改成groupby(user_id)[mcc].transform(lambda x: (x ! x.shift()).cumsum()) 1。2.3 数据划分时间序列切分别随机打乱财务欺诈数据是强时间相关的时间序列随机train_test_split是新手最容易犯的错。随机打乱会让训练集里混入未来的交易模型在测试集上表现虚高上线后立刻打回原形。我一般按时间切分用前 70% 时间段的交易训练后 30% 做验证和测试并且要求验证集最晚时间戳严格小于测试集最晚时间戳。from sklearn.model_selection import TimeSeriesSplit df df.sort_values(trans_time) tscv TimeSeriesSplit(n_splits5) for train_idx, valid_idx in tscv.split(df): train_data df.iloc[train_idx] valid_data df.iloc[valid_idx] # 每个 fold 里 train 都早于 valid assert train_data[trans_time].max() valid_data[trans_time].min()TimeSeriesSplit是 sklearn 自带的时序切分器它会按顺序把数据切成 5 份每轮用前 k 份训练、后 1 份验证保证训练数据永远在验证数据之前。assert那行是保险如果数据没排好序这里会直接报错避免后续模型在泄漏的数据上训练。3. 三种算法实现欺诈识别逻辑回归、随机森林与 XGBoost3.1 为什么先跑逻辑回归可解释性和基线财务场景里模型不只是给分数还要过审计。监管或内审问起来“为什么给这个客户打了高风险分”如果你拿不出解释模型再好也上不了线。逻辑回归是第一选择不是因为它效果最好而是因为它的权重能直接翻译成人话。from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline feature_cols [amount_ratio, tx_count_1h, time_since_last, is_new_mcc] X df[feature_cols].fillna(-1) y df[fraud_label].values pipe_lr Pipeline([ (scaler, StandardScaler()), (lr, LogisticRegression(class_weightbalanced, C1.0, max_iter1000)) ]) pipe_lr.fit(X_train, y_train)这里先做StandardScaler是必要的逻辑回归的 L2 正则对特征尺度敏感金额类特征动辄几百上千不标准化会让正则惩罚扭曲。class_weightbalanced是处理类别不平衡最朴素的方案欺诈样本通常只有 0.5%~2%自动按比例放大正样本的损失权重。C1.0控制正则强度C 越小正则越强。欺诈场景特征数不多时C 设为 0.1~1.0 都合理具体靠验证集调。fillna(-1)是把缺失的时间间隔等特征填成 -1避免把缺失样本整行删除——缺失本身可能是因为“没有上一笔交易”这在行为序列里是有信息的。3.2 随机森林处理非线性特征交互随机森林是我在欺诈项目里最常用的树模型基线。它能捕捉“凌晨 新商户 金额异常”这种特征组合不用像逻辑回归那样手工做交叉特征。代价是模型不可解释需要用 SHAP 值补解释。from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier( n_estimators300, max_depth12, min_samples_leaf20, class_weightbalanced, n_jobs-1, random_state42 ) rf.fit(X_train, y_train)max_depth12和min_samples_leaf20是针对欺诈样本量少做的约束。如果深度太大树会记住个别欺诈样本的模式验证集 AUC 好看但线上泛化差。min_samples_leaf20强制每片叶子至少 20 个样本有效抑制过拟合。n_estimators300是这个规模的折中再涨收益很小训练时间却线性增加。随机森林的class_weightbalanced与逻辑回归同理但要注意如果欺诈率极低低于 1%光靠balanced还不够后面第 5 章会讲到欠采样/过采样的组合用法。运行后看特征重要度amount_ratio和tx_count_1h几乎总是前两名。3.3 XGBoost为什么它常是最终选择如果项目允许上生产依赖XGBoost 基本是我在欺诈识别里的默认主力。它对稀疏特征、缺失值、非线性关系都处理得比随机森林好训练速度也快。关键参数是学习率、树深和正样本权重。import xgboost as xgb model_xgb xgb.XGBClassifier( n_estimators500, learning_rate0.05, max_depth6, min_child_weight5, scale_pos_weight99, # 负样本正样本比按实际数据算 subsample0.8, colsample_bytree0.8, eval_metricaucpr, early_stopping_rounds50, use_label_encoderFalse )scale_pos_weight是 XGBoost 处理类别不平衡的关键参数通常设成“负样本数 / 正样本数”。在我的数据集里欺诈率约 1%所以设 99。注意eval_metricaucpr而不是默认的 AUC欺诈场景正样本极少AUC 容易被大量负样本拉高AUCPR精确率-召回率曲线下面积对正样本更敏感直接优化它更贴近业务目标。early_stopping_rounds50需要在fit时传入验证集不然 sklearn 包装器在旧版本里不生效。use_label_encoderFalse是新版本 xgboost 的必需项否则会报警告。拟合时把早停的验证集按时间顺序放在训练集之后和 2.3 节保持一致。3.4 XGBoost 的早停与验证集写法上面代码里只写了模型参数实际训练时还要配合eval_set才能发挥早停作用。以下是最小可运行的训练循环# 延续 3.3 的 model_xgb eval_set [(X_valid, y_valid)] model_xgb.fit( X_train, y_train, eval_seteval_set, verboseFalse ) best_iteration model_xgb.best_iteration print(fbest iteration at {best_iteration}, train done)best_iteration会记录验证集上 AUCPR 最好的轮次你可以用这个值重设n_estimators避免保留后面过拟合的树。这种“先宽后紧”的调法比 grid search 高效很多——先跑 500 棵看早停点在哪再把n_estimators设为早停点附近最后微调learning_rate和max_depth。早停在财务欺诈里尤其重要欺诈样本少不加早停的 XGBoost 会把最后几十轮学进噪声里测试集分数掉得很快。如果你看到训练集 AUCPR 0.98、测试集 0.82八成就是没早停或者max_depth太大。3.5 三算法对比该选谁、各自边界在哪算法可解释性处理特征交互对不平衡的容忍度训练速度适合场景逻辑回归高权重即解释弱需手工交叉中快审计要求高、特征少随机森林中特征重要度SHAP中中高中特征工程弱、快速基线XGBoost低需 SHAP/树解释强高快需调参大数据、精度优先实际项目里我通常三个全跑逻辑回归当稳定基线随机森林和 XGBoost 对比 AUCPR。如果 XGBoost 比随机森林只高 0.01 个 AUCPR我会选随机森林——部署和维护成本低不需要额外管理 xgboost 的版本依赖。如果高 0.03 以上XGBoost 值得上。4. 欺诈识别模型评估为什么 AUC 会骗人、PR-AUC 才是真相4.1 用 PR-AUC 替代 ROC-AUC财务欺诈数据集正样本比例极低ROC-AUC 的横轴是假阳性率它统计了绝大多数负样本所以哪怕模型把所有正样本都漏掉只要负样本排序排得好AUC 依然可以飙到 0.9。这在实际业务里毫无意义——你要的是“在几千条告警里尽量多抓到真欺诈”。PR-AUC 的横轴是召回率纵轴是精确率正样本少时它才会给出有区分度的分数。from sklearn.metrics import precision_recall_curve, auc precision, recall, _ precision_recall_curve(y_valid, y_pred_proba) pr_auc auc(recall, precision) print(fPR-AUC: {pr_auc:.4f})这代码很短但我在项目里会拿它做所有模型的统一度量。注意auc(recall, precision)是 sklearn 的常见写法横纵轴顺序别写反。如果你想可视化直接plt.plot(recall, precision)就能画 PR 曲线曲线越靠近右上角越好。4.2 阈值不是 0.5是业务方说了算欺诈识别模型输出的概率默认 0.5 阈值完全不适用。因为欺诈率只有 1%模型预测概率普遍在 0.1 以下你设 0.5 会一个欺诈都抓不到。正确做法是让业务方定义“能接受的误报率”再从验证集上反推阈值。import numpy as np from sklearn.metrics import precision_recall_curve precision, recall, thresholds precision_recall_curve(y_valid, y_pred_proba) # 业务要求精确率至少 0.6即告警里有 60% 是真欺诈 target_precision 0.6 valid_idx np.where(precision target_precision)[0] best_threshold thresholds[valid_idx[-1]] print(fbusiness threshold: {best_threshold:.4f})这段逻辑是在 PR 曲线上找到“精确率不低于 0.6”的最后位置对应阈值就是要用到的分数截断点。valid_idx[-1]取的是满足条件里阈值最大的那个保证收益最大化。实际操作中我还会同时打印这个阈值下的召回率和决策台数给业务方三个数阈值、精确率、召回率让他们拍板。4.3 按时间分段评估防止“过拟合历史”欺诈数据的分布一直在漂移。我在项目里会额外做一个滑动时间窗评估把验证集按周切成多段分别计算每周的 PR-AUC 和套用固定阈值后的召回率。如果模型在最近一周的召回率明显低于最早一周说明欺诈模式已经变模型需要重训或加特征。df_valid df.iloc[valid_idx].copy() df_valid[week] df_valid[trans_time].dt.isocalendar().week for week, grp in df_valid.groupby(week): p, r, _ precision_recall_curve(grp[y_true], grp[y_score]) pr_auc_w auc(r, p) print(fweek {week}: PR-AUC{pr_auc_w:.4f}, n{len(grp)})这个分周评估的代码很简单但能很直观地暴露模型退化。如果你发现某周 PR-AUC 从 0.7 掉到 0.2第一反应不是调参而是去查这周出现了什么新型欺诈——通常是没有见过的商户、设备或渠道组合。5. 财务欺诈识别最容易翻车的 6 个坑从数据泄漏到类别不平衡5.1 数据泄漏用了未来信息验证集虚高现象训练时 AUC 0.95测试集 AUC 0.93结果上线第一个月只有 0.6。原因最常见的是把“事后才知道的字段”当特征。例如用“交易是否被拒付”“是否在黑名单里”这类标签类的字段作为入模特征模型在训练时偷看了答案。解决特征集合里只保留交易发生时点可得的数据。我在每次建模前会过一遍特征清单逐个问“这笔交易发生的瞬间这个值拿得到吗”。拿不到的字段全部剔除宁少勿多。另外时序切分必须严格不能用随机切分这一点在 2.3 已经强调过。5.2 类别不平衡处理不当欠采样还是过采样现象用class_weightbalanced后训练集稳定但验证集精确率极低大量正常交易被误杀。原因单纯调类别权重只能缓解权重失衡不能改变特征空间的稀疏性。欺诈样本只有几百条时模型根本无法学到复杂模式。解决对少数类做 SMOTE 过采样或者对多数类做欠采样交替实验。我一般用 SMOTE 时保留原始测试集不动只在训练集上合成样本欠采样则随机抽取多数类使其与少数类比例为 5:1 左右。注意 SMOTE 合成样本容易在特征边界上造出不符合实际的组合加入k_neighbors3减少合成噪声。5.3 时间漂移去年训练好的模型今年不能用现象模型上线三个月后每日告警量翻倍但确认欺诈数没涨。原因欺诈团伙会主动适应规则比如避开“凌晨高频交易”特征把交易时间改到白天或者单笔金额控制在阈值以下。解决建立月度重训机制。我把重训时间线定为每月一次每次用最近 6 个月数据重新训练并用 4.3 节的分周评估监控召回率变化。如果某周召回率跌破设定下限立即触发人工特征排查而不是等月底。5.4 特征分布偏移线上和训练时对不上现象验证集 PR-AUC 0.78线上表现只有 0.5特征重要度最高的字段分布完全变了。原因最常见的是amount的均值漂移——新用户群或新商户接入后交易金额整体变大模型见过的分布被拉偏。解决模型上线时记录每个特征的均值和分位数每天计算线上特征分布与训练分布的距离PSI。PSI 超过 0.2 的特征要报警并且只重建这部分特征不通篇重训。我在项目里用scipy.stats.ks_2samp快速检测简单直接参考如下from scipy.stats import ks_2samp train_amount X_train[amount_ratio] live_amount X_live[amount_ratio] ks_stat, p_value ks_2samp(train_amount, live_amount) if p_value 0.05 and ks_stat 0.1: print(amount_ratio drifted, retrain needed)5.5 忽略业务拒绝域模型分数和规则冲突现象规则已经拦截了 30% 的交易模型告警的另外 20% 和规则重叠业务人力不够只能放过一半。原因模型在特征上学到的规律和规则系统有大量重叠。直接叠加两套系统导致重复拦截召回率没提升误报率翻倍。解决建模阶段就把规则的拦截结果作为一个特征喂给模型rule_blocked0/1让模型学“哪些规则拦截是对的、哪些是错的”。上线时用模型分数对未拦截样本排序规则拦截样本保留规则的历史 ROC。这个做法本质上是把规则当作先验特征而不是平行决策层。5.6 评估只看整体不看时段现象整体 PR-AUC 0.8但每天 0-6 点的交易段召回率只有 0.3。原因欺诈数据在时段上分布极不均匀凌晨的样本模式更复杂整体指标被白天大量正常交易稀释。解决评估报告必须按小时不分段输出 PR-AUC、精确率和召回率。如果某个时段明显劣化单独对该时段数据做特征切片分析。我在项目里普遍会在早 6 点跑一次离线重评估把凌晨样本的召回率做成单独看板防止“整体被平均”带来的错觉。6. 模型上线后的进阶技巧告警优先级排序与自动化重训模型分数上线后真正的功夫在业务落地。我在生产环境里最常用的一招是“风险分层”把用户按分数从高到低分成 4 个区间最高区间走人工电话核实次高区间走短信验证中间区间只记录日志最低区间放行。这样不用提高整体拦截率就能把有限的人力放到最高风险的用户上。另一个值得做的技巧是“分数校准”。财务欺诈模型的输出概率往往偏极端接近 0 或接近 1业务方想看分数稳定的趋势变化会很难。我用 isotonic regression 做单变量校准简单做法是把模型输出的 logit 和真实标签丢进sklearn.isotonic.IsotonicRegression拟合出校准后的概率再按业务阈值切分。自动化重训是欺诈项目的终极形态。我一般搭一个定时任务每月 1 号拉取最近 6 个月已确认标签的交易数据自动跑一遍 3.3 节的 XGBoost 训练脚本用早停法选最优轮次把新模型文件推送到模型服务平台。流程里有几个强制检查点数据量是否小于设定下限、PR-AUC 是否比上一版低超过 0.03、特征分布 PSI 是否超阈值任何一个不满足就发告警并回滚到上一版。最后一条个人教训不要追求验证集上的 0.01 个点。每次调参后我都会问自己这个改动在业务上能多拦下几笔欺诈、多打多少电话、用户投诉会不会涨。模型是手段成本才是账本。希望这篇笔记里的代码和踩坑能帮你少走几条弯路让项目从“能跑”真正走到“能上线、能持续迭代”。祝在你的数据上早日看到稳定的 PR-AUC 曲线。本文还有配套的精品资源点击获取
返回列表