ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

保险反欺诈预测实战:数据清洗、特征工程与LightGBM调优

保险反欺诈预测实战:数据清洗、特征工程与LightGBM调优 简介面向机器学习入门与进阶人群这是一套基于Python的保险反欺诈预测实战资源。针对保险欺诈识别中数据不平衡、特征复杂等痛点资源完整覆盖数据预处理、特征工程、模型对比、参数调优与可解释性分析环节采用逻辑回归、决策树及LightGBM、XGBoost、CatBoost等集成模型并给出融合策略与LIME/SHAP解释方案可直接参考用于构建反欺诈预测流程。压缩包共9个文件包含5个Python脚本、3个CSV数据集和1个说明文档整体仅73KB小巧但结构清晰脚本覆盖单模型训练与多模型融合CSV提供训练集、测试集与提交样例TXT为项目说明。已有45人学习适合希望系统掌握保险反欺诈建模全流程的Python数据分析与机器学习学习者。1. 保险反欺诈预测的实战项目最难的部分不在模型假设你面前摆着几万条车险理赔记录字段有几十个欺诈标签只占百分之三你要用 Python 建一个模型把坏赔案挑出来。大多数人在这个场景里犯的第一个错就是急着调模型结果 LightGBM 跑完 AUC 停在 0.72上线后理赔部门反馈误报太多。这类反欺诈预测实战项目的真实瓶颈往往在数据标签的干净程度、特征是否泄漏、以及阈值有没有贴合赔付金额来定。标题里给出源代码和数据集解决的是起步门槛不用再为缺数据发愁也不用手撸整个工程骨架。但你拿到的是一套能不能复现、能不能迁移到自家数据上的方案。适合理工科转行的数据分析师、准备风控岗位面试的求职者以及保险公司里需要自己搭评分模型的开发者。别急着把代码跑完先学会拆数据。2. 反欺诈数据集怎么组织先看懂理赔表的字段和标签2.1 理赔数据集的核心表结构主表、明细表和关联表的区别保险反欺诈预测的数据集形态上和 iris 那种单表完全不同。理赔事件天然是多表关联的一张主表记录赔案本身一张客户表记录被保人一张赔款明细表记录每笔赔付科目还可能有一张维修厂或医院维度的机构表。你从网上下载的不少教学数据集要么把多表打平成单表要么只给你主表。拿到手第一件事就是找回这些表的关联关系。一张可用的车险理赔主表至少需要以下字段组字段组字段名示例含义对反欺诈的作用赔案标识claim_id, policy_id赔案号、保单号去重、关联其他表的键时间字段report_time, accident_time报案时间、出险时间计算报案延迟金额字段claim_amount, paid_amount, estimated_amount索赔金额、赔付金额、定损预估金额偏离、扩损判断主体字段customer_id, driver_license, plate_no被保人、驾驶员、车牌频次统计、关联分析事故信息accident_address, accident_type, injury_count出险地点、事故类型、受伤人数地点风险、人伤欺诈标签字段is_fraud, fraud_type是否欺诈、欺诈类型建模目标我一般会先做一个字段清单标注每个字段的来源系统再决定要不要把多表合并成宽表。合并之前要确认 join 的键没有一对多问题否则一条赔案被明细表拆成多行标签也会跟着重复训练时评估指标会虚高。2.2 公开数据集和模拟生成数据的取舍别信欺诈率 20% 的教程数据真实保险公司的确认欺诈率车险通常在 3% 到 8% 之间健康险略高一点但也很难超过 10%。很多用来教学的保险反欺诈数据集为了让模型效果漂亮会把欺诈率调到 20% 甚至 30%跑出来的准确率 99%上线后却完全不可用。这个比例的失真意味着误报成本被严重低估。常见做法是拿这种数据集来练整体流程但不要拿它的基线指标当作真实可行的业绩预期。如果你想验证模型在真实比例下的表现有两条路。第一条是找脱敏后的真实业务数据但涉及客户隐私拿到的可能性不大。第二条是自己构造模拟理赔数据像生成 kitti 数据集那样按目录组织好 raw 数据和标注文件。模拟数据丢失了真实分布的边际特征但至少可以控制欺诈率和标签噪声水平。我一般用蒙特卡洛方式模拟先定欺诈率 5%再按业务规则生成欺诈样本的金额偏移和报案延迟特征。这样既练了代码流程也能测试模型在不同欺诈率下的稳定性。像某类 po 数据集提供的是地理 POI 类别可以拼到 accident_address 上做地点风险特征但需要先把地址解析成行政区划和 POI 类型两块否则模型只能学到地址字符串的噪声。这个环节没有捷径跟用后处理工具查 kitti 数据集的下载地址一样下载教程只告诉你文件在哪不告诉你文件之间的坐标对应关系怎么验证。2.3 用 pandas 体检数据的五个必查项拿到数据集的第一件事不是建模是体检。我一般会先跑下面这段确认数据不是看似可用实则满身洞import pandas as pd df pd.read_csv(claims.csv, parse_dates[report_time, accident_time]) print(shape:, df.shape) print(target:, df[is_fraud].value_counts(normalizeTrue)) missing df.isnull().mean().sort_values(ascendingFalse) print(missing[missing 0.05]) dup df.duplicated(subset[claim_id], keepFalse).sum() print(dup claim_id:, dup) print(time range:, df[accident_time].min(), df[accident_time].max())这段代码里read_csv 的 parse_dates 参数把两列时间字符串直接解析成 datetime 类型省掉后面自己 to_datetime 的麻烦。value_counts(normalizeTrue) 返回的是各标签占比而不是绝对数量这样一眼就能看出样本不平衡的级别。isnull().mean() 按列计算缺失率我只关心缺失超过 5% 的列缺失过高的列比如事故责任认定书编号不应该进特征而应该单独建一个 has_recognized 的布尔标志。duplicated 里的 keepFalse 把重复 claim_id 的所有行都标 True而不是默认只标后续行方便看到重复赔案的全貌。还有一个必查项藏在代码外时间截断。训练集里要剔除 accident_time 距今不足 90 天的样本因为欺诈认定有滞后性最近三个月的赔案即使实际上欺诈标签大概率还是 0直接进训练会制造噪声。2.4 标签噪声比类别不平衡更致命怎么处理欺诈数据集的标签噪声几乎不可避免能确认的欺诈只是被发现的欺诈隐藏的欺诈样本被打成了负样本模型学出来的其实是“易被发现的欺诈模式”。处理思路是把训练数据限定到已结案满 90 天且无复议记录的赔案上再根据原始字段构造一个辅助标签。例如同一被保险人 30 天内报案超过 3 次且理赔金额逐次上升先置一个 suspicious 标记不直接当真值而是作为半监督的候选集。这个候选集可以用来做伪标注也可以单独分析但不要直接混进训练集。3. 特征工程把理赔明细变成模型能识别的欺诈痕迹3.1 金额类特征比值和偏离度比原始金额更有用不要把 claim_amount、paid_amount 裸着丢给模型。金额的绝对值受车型、地区、物价影响太大一辆 10 万的车和一辆 50 万的车同样维修费 2 万欺诈含义完全不同。做法是构造三类金额特征金额间比值、费用偏离度、历史对比。df[claim_to_est_ratio] df[claim_amount] / (df[estimated_amount] 1) df[paid_to_claim_ratio] df[paid_amount] / (df[claim_amount] 1) df[damage_intensity] df[claim_amount] / (df[vehicle_value] 1)代码里的三个特征分别刻画索赔金额相对定损金额的放大程度实际赔付相对索赔的折扣程度损失强度即索赔金额占车辆价值的比例。分母加 1 是为了避免除零车险里 estimated_amount 偶尔录入为 0。这三个特征在欺诈样本上通常出现双峰要么异常高比如金额比达到 3 以上要么是维修费接近车辆现值明显没有修复价值。如果你想做更细的金额偏离可以用按车型分组计算均值和标准差后转 z-score。车型在数据集里通常是品牌加型号的文本需要先做编码。用一句话总结金额特征要比“多少”而不是“是什么”。3.2 时间特征报案延迟是最稳定的强特征车险欺诈里“倒签单”是常见手法事故真实发生在几天前无证驾驶或酒驾后驾驶员先把车修好再补报一个案发时间。这类欺诈在时间字段上的表现就是报案延迟异常高。所以时间特征的第一优先级是报案延迟df[report_delay_hours] (df[report_time] - df[accident_time]).dt.total_seconds() / 3600 df[is_night_report] df[report_time].dt.hour.isin([22, 23, 0, 1, 2, 3, 4, 5]).astype(int) df[is_holiday_accident] df[accident_time].dt.dayofweek.isin([5, 6]).astype(int)report_delay_hours 的单位是小时欺诈样本往往集中在几个区间小于 0时间倒挂表明录入错误或故意篡改、大于 72 小时、以及等于整 24 的倍数。夜间报案单独用一个 0/1 特征因为深夜出险后立刻报案的比例本身低与延迟、金额特征交叉后有辨识度。节假日我在这里用 dayofweek 判断周末严格做法是接一个节假日表因为春节、国庆期间的出险规律跟普通周末不同。注意一个坑report_time 和 accident_time 在数据清洗时必须统一时区否则会出现小时级别的时间偏移。如果原始数据集只精确到日期那 report_delay_hours 的粒度就退化成天数不要硬算小时。3.3 频次与历史画像滑动窗口里的重复模式欺诈往往有聚集性同一被保人短期内多次出险同一辆车的理赔金额逐次抬升同一维修厂频繁出现在多个赔案里。频次特征就是把这种聚集性量化成数值。我常用的是 90 天滚动窗口而不是从数据集起点累计到现在因为滚动窗口更贴近业务侧的观察习惯。df df.sort_values([customer_id, accident_time]) df[cust_claim_cnt_90d] df.groupby(customer_id)[claim_id].transform( lambda x: x.rolling(90, min_periods1).count() )关键在 transform 和 rolling 的组合groupby 先按客户分组rolling(90) 表示窗口是 90 天min_periods1 保证组内第一天也有值。这里的时间窗口是日历天不是行数窗口因为理赔事件不是均匀产生的。使用该特征前必须确认数据严格按 accident_time 排序并在计算后把当前赔案自身从窗口里去掉做法是算完减 1否则会出现轻微的数据泄漏。同类特征可以换主体做车牌号、驾驶员手机号、维修厂编号。需要强调的是按 claim_id 去重后再做频次统计。如果主表 join 过明细表claim_id 会重复groupby 统计出来的是赔款科目的次数不是报案次数这属于实战里频率很高的低级错误。3.4 关联特征团伙欺诈的一阶线索与网络分析的边界在图结构上做欺诈挖掘是反欺诈领域的老话题。真正落地时我发现直接上图神经网络在多数业务初期是过度设计因为标签量不够模型学不出稳定的团伙模式。性价比比较高的做法是用一阶关联特征当前赔案的驾驶员手机号是否出现在其他赔案的报案手机号集合里当前赔案的维修厂过去 90 天关联了多少笔赔案。shop_agg df.groupby(shop_id).agg( shop_claim_cnt(claim_id, count), shop_avg_amount(claim_amount, mean), ).reset_index() df df.merge(shop_agg, onshop_id, howleft)这段代码按维修厂聚合出赔案量和平均理赔金额再 merge 回主表。merge 时注意 howleft保证主表的每一行都保留如果写成 howinner会把没有维修厂记录的赔案直接丢掉样本量悄悄少一批。关联特征一旦加到模型里就要同步做交叉验证防止同一维修厂的数据同时出现在训练集和验证集造成分数虚高。关于团伙挖掘社区里的思路是用连通分量或标签传播做群体划分层次聚类 python 写起来直观但数据量上万节点后效率很差我一般只在小团伙分析里用。4. 建模用 LightGBM 和 scale_pos_weight 处理不平衡预测4.1 数据切分欺诈预测不能用随机抽样做反欺诈预测时间顺序不可忽略。随机切分会把同一个客户、同一辆车的不同赔案拆进训练集和测试集模型等于见了答案再考试。正确做法是按 accident_time 排序用前 80% 的时间窗口做训练后 20% 做验证。df df.sort_values(accident_time).reset_index(dropTrue) train_size int(len(df) * 0.8) train df.iloc[:train_size] valid df.iloc[train_size:]这种切分方式会带来一个现实问题欺诈率在验证期可能与训练期不同因为欺诈手法随季节或理赔政策变化。你需要打印两段样本的 is_fraud 均值如果差异超过 1.5 倍就说明业务场景不是平稳的。针对这种情况训练时可以做加权但不要轻易用时间序列交叉验证因为理赔事件不是等间隔的收益率序列直接用时序交叉验证的滑窗逻辑很别忸。4.2 基线模型逻辑回归的职责是校准而非拿名次给每个反欺诈项目定基线时我都会先跑一个逻辑回归。目标不是靠它拿最好的效果而是用它做的事确认特征方向和业务认知一致。比如报案延迟特征系数为正、历史频次特征系数为正说明特征的单调性方向没问题。逻辑回归在 sklearn 里跑from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression feature_cols [claim_to_est_ratio, report_delay_hours, cust_claim_cnt_90d] pipe Pipeline([ (scaler, StandardScaler()), (lr, LogisticRegression(class_weightbalanced, max_iter1000)), ]) pipe.fit(train[feature_cols], train[is_fraud])这里的 class_weightbalanced 让逻辑回归按类别频率自动加权负样本多时自动调低负样本权重。之所以要套 StandardScaler是因为逻辑回归的系数和特征尺度直接相关不归一化时报案延迟的系数会被金额比值、频次这些大数值特征挤压。跑完逻辑回归后把系数打印出来和业务常识对照一遍比直接看模型准确率更有价值。4.3 LightGBM 的参数设定和 scale_pos_weight 计算主模型我一般直接选 LightGBM原因很实际支持类别特征、训练快、自带早停表格数据上的表现不会明显输给深度学习。反欺诈预测中LightGBM 的关键参数是这几项参数推荐值作用scale_pos_weight负样本数 / 正样本数提升少数类权重需再微调num_leaves31 或 64控制树的复杂度不宜过大learning_rate0.02 到 0.05先用小学习率配合早停subsample0.8行采样防过拟合colsample_bytree0.8列采样给特征多样性n_estimators1000 到 5000配合早停不用手动调死scale_pos_weight 的初始值直接按类别比例算然后在这个值附近做网格搜索常用范围是初始值的 0.5 倍到 3 倍。还有个参数 is_unbalance早期版本很多教程推荐设成 True我一般不用它因为它内部处理逻辑不透明不如显式给 scale_pos_weight方便你理解模型到底在做什么。LightGBM 先算初始权重再小幅调比一上来就设很大更能稳定收敛。import lightgbm as lgb neg, pos train[is_fraud].value_counts() init_weight neg / pos model lgb.LGBMClassifier( scale_pos_weightinit_weight, num_leaves31, learning_rate0.03, n_estimators2000, subsample0.8, colsample_bytree0.8, ) model.fit( train[feature_cols], train[is_fraud], eval_set[(valid[feature_cols], valid[is_fraud])], callbacks[lgb.early_stopping(100), lgb.log_evaluation(200)], )注意 fit 里的 eval_set 和 callbacksearly_stopping(100) 表示验证集指标连续 100 轮不提升就停log_evaluation(200) 表示每 200 轮打印一次训练日志避免刷屏。这两个回调是 sklearn 接口和原生接口通用度较高的写法。early_stopping 用的验证集就是 4.1 中按时间切出的 valid不能是随机切出的子集否则模型会以“泄漏到的未来信息”决定早停时机上线后性能会打折。4.4 评估指标AUC、KS 和掉进准确率陷阱反欺诈预测里准确率没有意义。欺诈率只有 5% 的数据集把所有样本判成正常准确率就有 95%。评估至少要同时看 AUC 和 KS前者衡量排序能力后者衡量区分度都不依赖具体阈值。AUC 0.75 到 0.85 在业务中是常见区间超过 0.95 反而要警惕标签泄漏。我在每个项目上会固定打印一批指标from sklearn.metrics import roc_auc_score, precision_recall_curve proba model.predict_proba(valid[feature_cols])[:, 1] auc roc_auc_score(valid[is_fraud], proba) print(fauc: {auc:.4f}) precision, recall, thresholds precision_recall_curve(valid[is_fraud], proba) pr_gain precision / recall.clip(min1e-6)precision_recall_curve 返回的 thresholds 可以用来观察不同阈值下的精确率和召回率。反欺诈场景更看重“在精确率可以接受的前提下召回尽量多”比如精确率定在 40%看召回率能到多少。如果直接用 predict 函数就等于让 sklearn 默认用 0.5 当阈值这在欺诈率极低时完全不合理。5. 阈值调优与上线验证按理赔金额加权决定拦截线5.1 用理赔金额成本函数替代固定阈值模型输出的概率只是一个排序分真正决定业务收益的是你在哪个分数上拉拦截线。常见做法是固定精确率阈值比如得分前 5% 进人工复核但这样会忽略赔案金额差异。一笔 20 万的全损案和一笔 3000 元的小额案误放的代价完全不同。我一般用成本函数来选阈值import numpy as np def select_threshold_by_amount(proba, amount, y_true, false_positive_alpha2000): thresholds np.arange(0.1, 0.9, 0.05) best_th 0.5 best_cost np.inf for t in thresholds: pred (proba t).astype(int) miss_cost ((pred 0) (y_true 1)) * amount fp_cost ((pred 1) (y_true 0)) * false_positive_alpha total_cost miss_cost.sum() fp_cost.sum() if total_cost best_cost: best_cost total_cost best_th t return best_th, best_cost函数里 miss_cost 按赔案实际理赔金额计算漏掉一笔欺诈案损失的是整笔赔付额。fp_cost 是误拦截的成本人工复核每单的劳务成本固定所以用常数 false_positive_alpha 表示我习惯取 2000 元。如果业务方给出更细的数据比如按赔案类型分设置不同的复核成本就把标量替换成数组。最终选出来的 best_th 往往会明显低于 0.5在 0.2 到 0.4 之间不要因此觉得模型错——这个阈值只是代表业务愿意承担多少误报去换回更多的拦截金额。5.2 用验证期回测决定是否上线阈值定好后要在验证集上按时间窗逐月回测每个月拦截多少案、拦截的案子里欺诈率多少、漏掉的欺诈案涉及金额多少。对比只看得分前 5% 的规则看成本函数方法是否每个月份都稳定占优。如果某个月出现明显倒退原因大概率是该月欺诈模式变了当前特征没覆盖到这时候调阈值没有意义回去补特征。LightGBM 的特征重要性打印可以辅助定位但重要性排名只能说明“用了多少”不能说明“为什么有效”我一般把它当作找数据问题的起点而不是结论。5.3 模型保存与阈值配置分离把调好的模型和阈值分开处理模型用 joblib 或 pickle 序列化保存阈值单独存成 YAML 配置。每个迭代周期只改配置文件不重新训练模型变更可审计回滚也快。这一条是团队协作落地的基础做法尤其在理赔审核系统对接时业务方会频繁提出“这周想压一压拦截量”把阈值留在 YAML 里改一行配置重新加载即可。本文还有配套的精品资源点击获取
返回列表