尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Python+机器学习:社会保险反欺诈分析实战与避坑指南

发布时间:2026/9/28 15:45:34

资讯中心
01
ARTICLE

Python+机器学习:社会保险反欺诈分析实战与避坑指南

Python+机器学习:社会保险反欺诈分析实战与避坑指南
简介面向计算机相关专业学生的「基于人工智能的社会保险反欺诈分析」Python 项目资源以课程作业/毕设形式呈现聚焦保险场景中欺诈用户的识别与建模采用 ipynb 模块化开发覆盖特征分析、特征构造、特征选择、特征变换、参数调优到模型构建的完整流程。资源共 12 个文件压缩包 15.21MB包含 5 个 ipynb 分析脚本、2 个 Markdown 说明文档、Python 建模脚本及配套数据文件等目录结构清晰便于对照学习。已有 74 人学习下载。数据集标签中 1 代表欺诈用户、0 代表正常用户data 目录下集中存放训练与测试表格以及派生、选择、变换后的特征文件可直接用于模型训练与预测。项目代码均经测试运行成功评审分达 96.5 分适合正在做毕设、课程设计或期末大作业的学生作为实战参考也可用于快速搭建同类反欺诈分析流程。1. 社会保险反欺诈分析为什么值得用人工智能落地守住社保基金的钱袋子一直是稽核部门头疼的事。人工抽查能覆盖的案件比例很有限固定规则也只能抓重复报销这类明显欺诈遇到跨院骗保、虚构诊疗、团伙集中刷卡就无能为力。基于人工智能的社会保险反欺诈分析通常就是拿一份 Python 源码加数据集用机器学习把参保人、报销流水、就诊机构串起来输出嫌疑名单和风险特征。对正在做毕业设计、准备数据竞赛或者刚转风控数据分析的工程师来说这是一套非常完整的落地练习。但这个项目有一个反直觉的起点拿到 zip 压缩包第一步不是急着跑模型而是先认清数据里到底有没有“欺诈”标签、字段粒度是什么、表之间怎么关联否则后面全是白算。2. 先想清楚再做社保反欺诈是分类问题还是异常检测问题2.1 标签从哪来有监督欺诈检测与无监督异常检测的选型带不带 label决定整套技术路线。常见公开风控数据集会把欺诈标记放在最后一列取值 0 或 1看起来很清楚但社保场景要追问一句这个标签是“稽核确认的欺诈”还是“疑似被投诉的对象”我见过不止一份数据集把“可疑”和“确认”混在同一个字段里正样本噪声很大模型学出来的是“报销金额高”而不是“行为异常”。如果没有标签异常检测模型就得登场。IsolationForest 用随机切割把孤立点筛出来LOF 看局部密度偏离程度都能在完全没有 y 的情况下给出可疑程度排序。这类模型适合线索发现不适合直接当处罚依据——离群点里有很多只是异地就医、急诊、长期慢病高额报销必须靠人工复核再沉淀新标签。更常见的做法是先跑无监督圈出一批高嫌疑样本把复核后的确认和排除结果补回数据集到下一轮它就变成了有监督问题。这也是为什么很多基于人工智能的社会保险反欺诈项目源码里会同时放分类和异常检测两套脚本两套路线覆盖的正是数据集质量不一样的两种情况。2.2 拿到 zip 源码包后先确认数据字段而不是先跑模型拿到 zip 后我一般不先解压跑模型而是先做数据体检。大数据集装在 zip 里解压后可能是 csv、xlsx也可能是多张表组成的数据库导出文件名未必叫 train.csv。必须确认几件事有没有 README 或字段字典label 字段在不在表的粒度是“参保人”还是“报销单据”如果是多张表关联主键是什么。import pandas as pd # 先只读前5行避免大文件一次性打满内存 df pd.read_csv(social_security_fraud.csv, nrows5) print(df.columns.tolist()) print(df.dtypes) # 字段确认无误后再全量读入 full pd.read_csv(social_security_fraud.csv) print(full.shape) print(full[label].value_counts())nrows 参数只需要读前几行用来快速看清字段名和类型。确认有 label 后再全量读入解压后的大 csv 常常有几百兆一上来就读全量会浪费大量时间。如果文件是多 sheet 的 Excel用pd.ExcelFile(data.xlsx).sheet_names列出所有表再逐个读取不要指望源码里写死的文件名一定存在。字段确认的优先级上member_id、claim_date、claim_amount这三列几乎决定了后面能不能构造有效特征。id 是纯编码别把它丢进模型当数值用日期要确认格式是%Y%m%d还是带时分秒金额要确认单位是元还是分很多数据集用“分”存整型量纲差三四个数量级后面做特征时会误导模型。2.3 最小可行模型用逻辑回归建立基线在把特征工程做复杂之前先让逻辑回归跑通一条最短路径。它快、可解释还能帮你验证“数据里到底有没有信号”。代码放在这里特征先不做复杂加工只用数据类型转换后的原始字段。from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report # 特征列排除掉 label 和 member_id身份编码不参与建模 feat_cols [c for c in df.columns if c not in (label, member_id)] X pd.get_dummies(df[feat_cols], drop_firstTrue) y df[label] X_tr, X_te, y_tr, y_te train_test_split( X, y, test_size0.3, stratifyy, random_state42 ) clf LogisticRegression(max_iter2000, class_weightbalanced) clf.fit(X_tr, y_tr) print(classification_report(y_te, clf.predict(X_te)))pd.get_dummies会把地区、诊断类型这类类别字段展开成 0/1drop_firstTrue防止产生冗余列。test_size0.3留出三成做验证stratifyy保证正负样本在划分后比例不变否则欺诈样本只有 5% 时有可能全被分到测试集。class_weightbalanced让算法按样本量的反比放大少数类权重这是类别不平衡时最快见效的一招。跑完看报告里的 recall如果欺诈样本召回率还在 0.1 以下说明特征里没有信号或者正样本质量太差这时候该回头清洗数据而不是换复杂模型。逻辑回归虽然只是 baseline但它的系数还能直接告诉你哪些特征和欺诈正相关后面做解释性分析时也用得上。3. 用 Python 做社保反欺诈特征工程原始字段到模型输入3.1 清洗社保数据缺失值、异常值、日期字段打开真实数据会发现不少脏数据身份证列里混着“无”或出生日期缺位报销日期有的是字符串YYYYMMDD有的是日期时间格式报销金额出现负值年龄字段出现 0 岁或 130 岁。清洗不能删得太狠也不能直接填中位数就完事每一步都要有目的。import pandas as pd # 日期解析失败置为 NaT而不是抛异常中断 df[claim_date] pd.to_datetime(df[claim_date], errorscoerce) df[age] pd.to_numeric(df[age], errorscoerce) # 负金额压到0年龄限制到有效区间 df[claim_amount] df[claim_amount].clip(lower0) df[age] df[age].clip(0, 100) # 数值列用中位数填充类别列单独标记缺失 num_cols [claim_amount, age] for c in num_cols: df[c] df[c].fillna(df[c].median()) cat_cols [region, hospital_type] for c in cat_cols: df[c] df[c].fillna(缺失)pd.to_datetime加errorscoerce解析不了的日期会变成 NaT 而不是直接报错这样后续可以统一处理坏日期。clip(lower0)只把负金额压到 0保留负数位置信息同时避免负值把聚合特征拉偏。年龄中位数填充比均值稳健不受 130 岁这种极端值拉动。类别列单独填“缺失”让模型有机会学到“信息缺失本身可能与欺诈有关”——这是风控里容易被忽略的信号。3.2 构造反欺诈特征频次、金额比、机构关联特征工程主要围绕“频次、金额、关联”三个角度展开。欺诈行为会留下统计痕迹短时间内多次报销、总金额显著高于同类人群、频繁更换就诊医院、多个参保人共用同一家机构。用member_id做 groupby把这些聚合特征算出来再拼回原表。grp df.groupby(member_id) feat pd.DataFrame({ claim_cnt: grp[claim_id].count(), claim_total: grp[claim_amount].sum(), claim_avg: grp[claim_amount].mean(), claim_max: grp[claim_amount].max(), hospital_cnt: grp[hospital_id].nunique(), doctor_cnt: grp[doctor_id].nunique(), day_span: grp[claim_date].max() - grp[claim_date].min(), }) feat[claim_freq] feat[claim_cnt] / (feat[day_span].dt.days 1) feat[hospital_avg_amount] feat[claim_total] / (feat[hospital_cnt] 1) df df.merge(feat, left_onmember_id, right_indexTrue)nunique()统计的是去重个数用来捕捉“跨医院、跨医生”这类行为。hospital_avg_amount算的是每家医院的平均消费它会拉高那些疯狂去不同医院开药再报销的用户。注意day_span是 Timedelta 类型要先.dt.days转成天数再参与除法分母一律加 1防止除零。这样一条报销记录就从“单笔是否异常”升级成“这个人整体是否异常”。还可以继续做交叉特征把hospital_cnt除以claim_cnt得到“每次报销更换机构的频率”这往往是团伙骗保的典型模式。特征不在多而在是否对应业务上的欺诈手法很多项目堆了几百个特征最后 SHAP 解释时发现排前面的还是这几个聚合量。3.3 类别不平衡处理过采样与权重社保反欺诈数据普遍严重失衡欺诈样本占比常常低于 5%。如果直接训练模型会学着学着就变成“全都预测为正常”accuracy 看起来很高实际一条欺诈都捞不出来。两种常用处理方式调整 class_weight或者用 SMOTE 过采样。from sklearn.model_selection import train_test_split from imblearn.over_sampling import SMOTE X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, stratifyy, random_state42 ) sm SMOTE(random_state42, k_neighbors3) X_train_sm, y_train_sm sm.fit_resample(X_train, y_train) print(y_train.value_counts()) print(y_train_sm.value_counts())k_neighbors默认是 5这里特意改成 3正样本本来就少近邻空间稀疏取太多近邻容易把样本插值到远离实际分布的怪异位置合成出业务上不可能出现的画像。这条铁律要记住SMOTE 只能作用于训练集验证集和测试集必须保持原始分布。如果对测试集也做合成评估出来的召回率就是假的到真实验证时直接翻车。不想引入imblearn依赖的话class_weightbalanced是最低成本的替代效果通常差不到哪去只是可控粒度不如 SMOTE。4. XGBoost 训练与调参社保反欺诈模型的 3 个必调参数4.1 先跑通 XGBoost 最小训练脚本特征准备好之后XGBoost 就成了主力模型。它在表格数据上精度高、对缺失值有原生处理能力Python 源码里也最常出现。第一个版本别追求极限参数先把脚本跑通。import xgboost as xgb model xgb.XGBClassifier( n_estimators300, max_depth5, learning_rate0.05, subsample0.8, colsample_bytree0.8, scale_pos_weight(y_train 0).sum() / (y_train 1).sum(), eval_metricaucpr, use_label_encoderFalse, verbosity0, ) model.fit(X_train, y_train, eval_set[(X_test, y_test)], verboseFalse)scale_pos_weight直接按负样本与正样本的比值传入这一步比后面所有调参都重要。eval_metricaucpr要刻意选它而不是默认的auc因为在极不平衡数据上 AUC 会被大量正常样本拉高看起来 0.95 很漂亮实际欺诈样本召回率可能只有 0.2。use_label_encoderFalse只是消除新版 xgboost 的警告不影响结果。4.2 三个必调参数scale_pos_weight、max_depth、learning_rate调参的顺序比参数本身重要。先把正负样本权重拉到位再管树结构最后收紧学习率。按这个顺序调方向上基本不会乱参数作用调参方向scale_pos_weight提升少数类权重召回率不足就加大误报太多就减小max_depth控制模型复杂度从 5 起步验证集掉点说明太深learning_rate控制每棵树贡献幅度调小后 n_estimators 要成倍增加scale_pos_weight理论值等于负样本数除以正样本数但实际直接照搬常常不够。如果模型把欺诈样本判定为正常的比例高就把值往 2 到 5 倍方向拉如果验证集上正常样本被误杀太多就往回降。社保数据里的欺诈模式并不复杂max_depth通常 5 到 7 就够。树太深会强行记下个别极端欺诈样本训练集 aucpr 拉到接近 1验证集却直线下滑这就是典型的过拟合。learning_rate和n_estimators要绑在一起看从 0.3 降到 0.05 后n_estimators必须从 100 提到 300 以上否则模型还没学够就停了。4.3 用早停确定树数量让验证集说了算固定n_estimators300只是一种启动方式更稳的是用早停让算法自己决定到底要多少棵树。model.fit( X_train, y_train, eval_set[(X_test, y_test)], early_stopping_rounds50, verboseFalse, ) print(best_iteration:, model.best_iteration)early_stopping_rounds50的含义是每训练一棵树后在验证集上算一次 aucpr连续 50 轮没有提升就停止best_iteration会记录最优树数量。这样比固定棵数更省时间也更不容易过拟合。跑完脚本的最后一步是把预测概率保存下来而不是只存 0/1 类别。风控业务最终需要的是排序名单predict_proba输出的是连续嫌疑度后面设置人工复核阈值全靠它只存类别等于把信息扔了一半。5. 避坑社保反欺诈项目最常见的 5 个翻车点5.1 现象精确率 0.9 以上业务复核却一无所获模型报告上 precision 很高模型选出的名单交到稽核手里查一圈下来都是金额高的正常报销甚至包含大量长期慢病患者。原因出在标签口径和评估方式上。欺诈样本占比太低时模型只要保守到几乎不报欺诈把所有正常样本预测对precision 也能很高。再加上数据集里那个 label 可能写的是“可疑”而不是“稽核确认”模型学的根本不是反欺诈。解决评估从单看 precision 改成“precision recall 复核确认率”三件套。选阈值时不要用 0.5按业务每天能复核的名单量取 TopN。比如每天只能查 100 单就把预测概率最高的前 100 条导出来查完之后统计确认了多少条这个“确认率”才是模型真正的业务价值。5.2 现象验证集表现很好换时间段数据就崩社保报销有明显的季节性和政策效应年初集中报销、医保目录调整、某类药品集中采购都会让报销频次和金额分布变化。模型崩掉的更深层原因是数据划分方式错了。直接按“记录”随机切分同一个参保人的多笔报销会被同时分进训练集和验证集模型学到的可能是“这个人的 ID 很怪”而不是“这种行为很怪”验证分数自然虚高。解决按参保人分组或者按时间切分。我习惯用训练集前 11 个月、验证集最后 1 个月模拟真实上线的样本外效果。用 GroupKFold 也一样保证同一个 member_id 的所有记录只出现在一个折里。from sklearn.model_selection import GroupKFold gkf GroupKFold(n_splits5) for tr_idx, va_idx in gkf.split(X, y, groupsdf[member_id]): # 每一折里的人都与另一折完全隔离 X_tr_fold, X_va_fold X.iloc[tr_idx], X.iloc[va_idx] y_tr_fold, y_va_fold y.iloc[tr_idx], y.iloc[va_idx]groups参数必须传参保人 ID不能让同一人的记录跨集合。如果数据里有时间字段直接按月份切是更贴近业务的做法能同时避开“时序泄漏”和“同人泄漏”两个坑。5.3 现象源码里读的文件名和你的 zip 对不上一跑就 KeyErrorPython 源码包里的脚本往往针对某个特定数据集写死了字段名有的是member_id有的是SERIAL_NO还有的是中文表头“身份证号”。你下载的数据集字段一换脚本直接报 KeyError。另一个常见问题是编码zip 里解压出的 csv 可能是 GBK 编码pd.read_csv默认按 UTF-8 读直接 UnicodeDecodeError。解决不要硬改模型代码加一个字段映射层把外部数据统一成脚本内部的标准名。COLUMN_ALIASES { 身份证: member_id, 就诊日期: claim_date, 报销金额: claim_amount, 医院编码: hospital_id, 诊断编码: diagnosis_code, } df df.rename(columnsCOLUMN_ALIASES) # 读 GBK 编码的 csv # df pd.read_csv(data.csv, encodinggbk)这份映射表可以单独放在配置里。脚本内部从此只认member_id、claim_date这些标准字段换下一份数据集时只改映射不动模型代码。这段逻辑还能救你以后接入公司内部数据社保系统的字段名只会比公开数据集更乱映射层是标配。5.4 现象预测结果全是 0一条欺诈都没捞到模型训练完predict(X_test)返回的全是 0欺诈样本一条都没召回。原因是默认阈值 0.5 对极不平衡数据根本不适配少数类的后验概率往往集中在 0.1 以下0.5 这个门槛把一切都挡在门外。很多人这时候只盯着 accuracy 看觉得模型“工作正常”实际上模型已经退化成“永远说正常”的复读机。解决不看predict看predict_proba把概率排名输出给业务用业务产能定阈值。proba model.predict_proba(X_test)[:, 1] rank pd.DataFrame({ member_id: X_test.index, fraud_prob: proba, }).sort_values(fraud_prob, ascendingFalse) # 假设每天最多人工复核 100 条就取概率最高的 100 条 rank.head(100).to_csv(manual_review_list.csv, indexFalse)业务侧关心的是“先查谁”不是模型自己拍脑袋定 0 或 1。概率 0.5 只是 sklearn 的默认约定在欺诈检测里几乎从不直接使用。把“选多少人复核”这个决策留给业务模型负责把最可疑的排在前面这样双方都不会被阈值绑架。5.5 现象模型上线一个月后效果明显下滑模型刚上线时确认率还不错跑了一个月后名单质量越来越差。原因是社保数据存在概念漂移医保目录调整、季节性流行病高发、某段时间集中办理报销都会改变报销金额和频次的分布。更隐蔽的是标签滞后——训练集里的正样本反映的是半年前被确认的欺诈手法欺诈分子早换套路了模型还在用旧模式抓人。解决上线时把特征均值、正样本比例、模型概率分位数存成一份基线快照每周重算特征分布。如果报销金额均值上升超过 30%或模型输出概率的整体分布明显右移就触发重训。同时建一个“新欺诈种子库”人工复核确认的案件以周为周期回填训练集让模型始终跟得上最新手法。静态模型在这个场景撑不过三个月监控和回流才是能持续用的保障。6. 验证与进阶用小样本跑通后再谈上线先别急着追求 AUC 涨零点几个点把“按人分组 时间切分 概率排名 人工复核回流”这条链路跑通比调参重要得多。验证时我会同时看三个数欺诈样本召回率、TopN 名单确认率、特征分布偏移量。召回率衡量模型有没有力气确认率衡量业务认不认可特征偏移量决定模型还能用多久。解释模型这一步用 SHAP 比树模型自带的feature_importance更直观。feature_importance只告诉你哪个特征被用得频繁SHAP 能告诉你特征怎么影响预测方向。import shap explainer shap.TreeExplainer(model) shap_values explainer.shap_values(X_test) shap.summary_plot(shap_values, X_test)summary_plot 的纵轴是特征横轴是 SHAP 值右侧颜色越红代表特征值越高。如果发现“报销总金额”的 SHAP 值最大说明模型在靠金额判断这不是真正的反欺诈信号需要继续用频次、机构关联特征去压制它让模型把注意力放在行为模式上。我现在的习惯是任何反欺诈模型都留一个“低置信度抽查桶”也就是概率排在中间的那批样本也抽 2% 给业务复核。这样做有三个好处——防概率校准偏移、持续积累难例、在模型改烂之前及时发现。用 Python 源码加数据集做社保反欺诈分析真正的难点从来不是训练那一行代码而是从拿到 zip 那一刻起就保持怀疑字段对不对标签准不准划分严不严。把这三个问题盯住了模型自然能在稽核场景里站住脚。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。