Python实现社会保险反欺诈分析:从数据集到模型落地全流程
简介基于人工智能的社会保险反欺诈分析Python源码与数据集资源源自经导师指导并获得96.5分的大三课程设计适合计算机相关专业学生用于毕业设计、课程设计或项目实战练习。项目采用交互式笔记本模块化开发按多人分工的文件交互形式组织完整覆盖特征分析、特征衍生、特征选择、特征变换、参数调优与模型构建等环节数据集中1代表欺诈用户、0代表正常用户可支撑从数据清洗、特征工程到建模评估的完整流程。资源共12个文件包括5个笔记本、Python脚本、TSV/CSV数据表格与说明文档等压缩包仅15.21MB便于快速下载目前已有74人学习下载。目录结构清晰各模块文件命名与处理流程对应配合说明文档可方便复现实验、理解多人协作开发思路也可迁移到其他保险欺诈识别场景进行二次练习。1. 社会保险反欺诈分析为什么一套 Python 源码 数据集就够起步社保基金里的欺诈行为——挂床住院、虚构诊疗记录、跨院重复开药、冒名就医——一直靠稽核人员人工翻病历和结算单来找。这套基于人工智能的社会保险反欺诈分析方案本质上就是把稽核经验转成特征工程和模型评分让机器先筛一遍疑点单据再交给人复核。你手里那个 python 源码 数据集 zip 包解压后就是一个可以直接跑的实验环境里面有清洗过的脱敏医保结算数据、特征构造脚本和欺诈检测模型适合刚接手“医保大数据”的学生做课程设计也适合机构里想从规则筛查升级到模型评分的技术人员。我按这个方向重新搭过类似流程这篇就把从解压到落地验证的完整路径讲清楚包括参数设置和坑。2. 解压 zip 后的第一件事把原始数据理清再做反欺诈特征的工程化2.1 数据目录结构与字段语义先别碰模型先摸清数据很多人在拿到 zip 后第一反应是直接跑 main.py然后被报错弹回来。常见做法是先把压缩包容错解开看一眼目录结构再动手。下面的代码用 Python 内置库完成解压和基础检查不需要额外依赖import zipfile import os zip_path social_security_fraud_analysis.zip extract_dir ./fraud_project # 先看压缩包内文件列表确认有没有路径穿越风险 with zipfile.ZipFile(zip_path, r) as zf: for info in zf.infolist(): print(f{info.filename} {info.file_size/1024:.1f} KB) zf.extractall(extract_dir) print(解压完成目录, extract_dir) print(顶层文件, os.listdir(extract_dir))这段逻辑的关键是infolist()先行遍历而不是直接extractall()。因为 zip 里如果夹带了../开头的文件名直接解压可能把文件写到目录外这在处理来历不明的数据集时不是玄学是真实风险。file_size能帮你快速判断数据规模——比如某个 CSV 有 300MB那 Pandas 读取时要考虑分块或指定数据类型否则内存直接翻车。解压后你大概率会看到这四类文件原始数据表通常是 CSV 或 Excel、特征工程脚本、模型训练脚本、说明文档。先别急着跑模型用下面的代码把核心表加载出来看字段类型和缺失情况import pandas as pd df_claim pd.read_csv(data/claim_detail.csv, nrows5000) print(df_claim.dtypes) print(df_claim.head(3)) print(缺失率统计) print((df_claim.isnull().sum() / len(df_claim)).sort_values(ascendingFalse))nrows5000是试读防止一次性加载大文件把内存打爆。对于社保反欺诈分析核心字段通常包括人员编号、就诊机构编码、就诊日期、结算金额、基金支付金额、药品明细或诊疗项目编码。字段语义很重要——比如“结算金额”是总费用还是个人自付部分直接决定你后面构造的特征有没有意义。读数据时我一般会顺手把金额字段转成 float、日期字段转成 datetime这能省掉后面大量的 dtype 报错df_claim[claim_date] pd.to_datetime(df_claim[claim_date], errorscoerce) df_claim[total_amount] pd.to_numeric(df_claim[total_amount], errorscoerce)errorscoerce的意思是无法解析的置为NaT或NaN而不是抛异常终止脚本。社保系统导出的数据里经常混入空字符串、Excel 日期序列号这类脏值这一步能让你快速定位脏数据的比例再决定是删除还是填充。2.2 特征工程从就诊、报销、结算流水里构造反欺诈特征社会保险反欺诈分析的核心不在模型而在特征。稽核人员看一份可疑单据时心里在意的无非是几个事这个人有没有在短时间内跑多家医院开同类药报销金额是不是远超同病种平均水平就诊频次有没有异常把这些判断翻译成数值就是特征工程的工作。我一般先做三个维度的聚合特征代码可以直接复用# 按人员就诊机构聚合看跨机构行为 df_person df_claim.groupby(person_id).agg( claim_count(claim_id, count), org_count(org_code, nunique), total_amount(total_amount, sum), avg_amount(total_amount, mean), max_amount(total_amount, max), date_span(claim_date, lambda x: (x.max() - x.min()).days), ).reset_index() # 同一天多机构就诊次数是挂床/跑方子的强信号 df_daily df_claim.groupby([person_id, claim_date])[org_code].nunique().rename(daily_org_cnt) df_person[max_daily_org] df_daily.groupby(person_id).max().values print(df_person.describe())这段逻辑里几个参数值得说明nunique统计去重后的机构数一个人如果 30 天内出现在 8 家医院这个特征会非常刺眼date_span用最大日期减最小日期如果这个人只有一条记录这个值就是 0属于正常max_daily_org单独提炼“单日最多跑了几个机构”这是定点骗保的典型画像。除了聚合特征还要做药品维度的异常。比如“同一个人在同一家机构反复开同一种慢病药”这既可能是合规的长期用药也可能是套取医保基金倒卖。这里需要把药品编码做成序列特征# 构造同一人同一药品的开药次数与平均间隔 df_drug df_claim.groupby([person_id, drug_code]).agg( drug_count(claim_id, count), drug_total(total_amount, sum), drug_avg_interval(claim_date, lambda x: (x.max() - x.min()).days / max(len(x) - 1, 1)), ).reset_index() # 只看开药超过 6 次的对象过滤偶发记录 df_drug_high df_drug[df_drug[drug_count] 6] print(高频开药组合数量, len(df_drug_high))这里的max(len(x) - 1, 1)是为了防止除零——如果只有一条开药记录间隔没有意义给个默认值 1。开药次数阈值 6 是我个人的经验值如果你面对的慢病用药是三个月一疗程这个阈值要相应提高。做反欺诈分析时阈值一定要跟业务对齐否则模型学到的只是“数字异常”而不是“医保异常”。特征工程做完后把所有特征合并成一张宽表保存为 Parquet 格式后面训练模型可以直接读df_feature df_person.merge( df_drug.groupby(person_id)[drug_count].max().rename(max_drug_cnt), onperson_id, howleft ) df_feature.to_parquet(data/features.parquet)保存为 Parquet 而不是 CSV一是压缩率高二是保留数据类型。到这一步你已经从原始流水表里提炼出几十个特征接下来就该跑模型了。2.3 基线模型用孤立森林先把异常个案筛出来在没有真实欺诈标签的情况下孤立森林是最合适的入口。它不依赖标签直接通过“样本之间是否容易被分离”来打分原理上很像审计人员“一眼看出不对劲”的过程。下面是一段可运行的基线代码import pandas as pd from sklearn.ensemble import IsolationForest from sklearn.preprocessing import StandardScaler df pd.read_parquet(data/features.parquet) feature_cols [claim_count, org_count, total_amount, avg_amount, max_amount, date_span, max_daily_org, max_drug_cnt] # 填充缺失标准化 df[feature_cols] df[feature_cols].fillna(0) scaler StandardScaler() X scaler.fit_transform(df[feature_cols]) model IsolationForest( n_estimators200, contamination0.02, random_state42, n_jobs-1 ) df[anomaly_score] model.fit_predict(X) df[anomaly_label] df[anomaly_score].apply(lambda x: 1 if x -1 else 0) print(标记为疑点的样本数, df[anomaly_label].sum()) print(df[df[anomaly_label] 1][feature_cols].describe())几个参数要解释清楚contamination0.02是预设的异常比例也就是你认为全量人群里有 2% 左右存在欺诈嫌疑。这个值不要拍脑袋用历史稽核的阳性率做参考——如果你们过去人工复核的命中率是 5%那就设 0.05。random_state42保证结果可复现这在做对比实验时非常重要。n_jobs-1让模型用满所有 CPU 核社保数据动辄几十万行特征表这个参数能明显压缩训练时间。孤立森林输出的-1表示异常1表示正常。我把anomaly_label映射成 0/1方便后面跟有监督模型的结果做对比。这一步能做到的是把“明显不像正常人的行为模式”挑出来但它的局限也很明显没有标签你无法告诉模型“医院编码 X 骗保高发”“药品 Y 是套保重灾区”这类业务经验。也正是因为这样下面要讲的有监督方案才是生产环境里的主力。3. 用 XGBoost 做有监督欺诈检测标签怎么来参数怎么调3.1 伪标签与规则标签没有真实标签时的常见处理深度学习和树模型在反欺诈场景里最大的拦路虎不是模型能力是标签。社保欺诈的真实标签要到稽核结束、处罚决定书下达之后才有而且样本量通常只有几百到几千条。拿到 zip 源码包时你首先应该确认里面有没有label或is_fraud字段。如果有直接用如果没有常见做法是用规则生成伪标签。下面是用规则打标的代码规则本身可以做得很粗糙但要有业务依据import numpy as np df pd.read_parquet(data/features.parquet) def rule_label(row): # 规则1单日超过3家机构就诊极其可疑 if row[max_daily_org] 3: return 1 # 规则230天内机构数超过5且金额超过同群组中位数3倍 if row[org_count] 5 and row[total_amount] row[total_amount] * 0: return 1 # 规则3开药次数超过15次高度疑似倒卖药品 if row[max_drug_cnt] 15: return 1 return 0 df[pseudo_label] df.apply(rule_label, axis1) print(伪标签正样本比例, df[pseudo_label].mean())这段代码里的规则是有意写糙的row[total_amount] row[total_amount] * 0这条恒真只是为了保留规则结构实际使用时应该替换成“同疾病诊断分组的均值倍数”。你要理解规则打标的本质它是用专家经验把一小批样本标出来给模型一个起步的“师父”。规则太严正样本太少模型学不到东西规则太松噪声太多模型学到的全是规则本身。一个稳妥的比例是正样本占全量的 1%5%低于 1% 需要换规则或降阈值。打完伪标签后立刻做一件事分层采样看标签在各特征上的分布。df.groupby(pseudo_label)[[claim_count, org_count, total_amount]].mean()这一步能直观看出正负样本在特征均值上有没有区分度。如果两组数据的所有特征均值都几乎一样说明规则跟特征无关要么换规则要么回到无监督方案。这不是玄学我见过不少项目在这里自欺欺人——规则打得开心模型一训练 AUC 只有 0.52等于白做。3.2 模型训练与阈值选择代码precision/recall 的平衡拿到伪标签后就可以上 XGBoost 了。我习惯用xgb.XGBClassifier因为它自带缺失值处理而且对表格数据的效果通常优于深度模型。下面的代码包含训练、验证和阈值选择三个环节import xgboost as xgb from sklearn.model_selection import train_test_split from sklearn.metrics import precision_score, recall_score, f1_score feature_cols [claim_count, org_count, total_amount, avg_amount, max_amount, date_span, max_daily_org, max_drug_cnt] X df[feature_cols] y df[pseudo_label] # 按伪标签分层切分保持验证集里正样本比例一致 X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.3, stratifyy, random_state42 ) model xgb.XGBClassifier( n_estimators300, max_depth5, learning_rate0.05, subsample0.8, colsample_bytree0.8, scale_pos_weight(y_train 0).sum() / max((y_train 1).sum(), 1), eval_metricauc, early_stopping_rounds30, random_state42 ) model.fit(X_train, y_train, eval_set[(X_val, y_val)], verboseFalse) y_prob model.predict_proba(X_val)[:, 1]scale_pos_weight是这里最关键的参数它把正样本的权重按负正比例放大缓解类别不平衡。计算式里加了max(...,1)防止除零。early_stopping_rounds30的意思是验证集 AUC 连续 30 轮不提升就提前终止既省时间又防过拟合。模型输出的是概率但业务场景里你要的是一个“是否进入人工复核”的二值判定。阈值不要默认取 0.5要按业务可承受的复核量来倒推# 扫描阈值找 precision/recall 平衡点 best_f1, best_thr 0, 0.5 for thr in np.arange(0.3, 0.9, 0.05): y_pred_bin (y_prob thr).astype(int) p precision_score(y_val, y_pred_bin) r recall_score(y_val, y_pred_bin) f1 f1_score(y_val, y_pred_bin) print(fthr{thr:.2f} precision{p:.3f} recall{r:.3f} f1{f1:.3f}) if f1 best_f1: best_f1, best_thr f1, thr print(最佳阈值, best_thr)这段扫描代码的意义在于把模型输出的连续概率切到可执行的复核名单上。社保反欺诈场景里precision 低意味着浪费稽核人力去查大量无辜参保人recall 低意味着放走真正的骗保。哪个更重要取决于你的复核产能——产能充足就多查阈值调低产能紧张就精准打阈值调高。我见过有人直接拿默认 0.5 上线结果某个月复核名单从 200 条涨到 2000 条稽核团队直接崩溃这就是没做阈值扫描的后果。3.3 落地部署的考虑批次评分与规则引擎的结合训练好的模型不能只在 Jupyter 里跑。真正要上线常见做法是做批次评分每天凌晨把前一天的新增结算数据灌进来跑特征工程产出疑点名单推送给稽核系统。下面是批次评分的骨架代码def predict_fraud_batch(new_data_path, model, scaler, feature_cols): df_new pd.read_csv(new_data_path, parse_dates[claim_date]) # 这里要重复第2.2节的聚合特征逻辑保持一致 # 假设已经得到 df_new_features X_new scaler.transform(df_new[feature_cols]) prob model.predict_proba(X_new)[:, 1] df_new[fraud_prob] prob return df_new # 调用示例 # result predict_fraud_batch(data/new_claims_20250301.csv, model, scaler, feature_cols) # result[result[fraud_prob] best_thr].to_csv(data/review_list.csv, indexFalse)注意这里的scaler必须是用训练数据 fit 过的那个不能在新数据上重新 fit重新 fit 会改变特征分布等于模型每跑一次就换了一把尺子。这个我在生产里踩过输出名单忽多忽少后来查了半天才发现线上代码里对 StandardScaler 做了重新 fit属于典型的“在训练集上很准、上线就翻车”。批次评分还有一个容易被忽略的点聚合特征会用到历史数据。比如“近 30 天就诊机构数”这种特征如果你只取当天的数据来算结果必然偏小。正确做法是把当天的数据拼接进一个历史宽表里再跑聚合逻辑这样特征口径才和训练时一致。这个问题在反欺诈项目里几乎人人会遇到区别只是发现得早晚。4. 社会保险反欺诈分析的常见坑数据、标签与评估的五个教训4.1 现象重复报销记录被当成高欺诈风险解压数据集后跑第一次模型发现大量重复的 claim_id 被标记为异常。原因原始流水表里同一笔报销可能因结算重传出现多行而特征工程里claim_count统计了这些重复行导致“报销次数虚高”。解决加载数据后先做去重再进入特征工程。代码就一步df_claim df_claim.drop_duplicates(subset[claim_id, person_id, claim_date])去重键要按业务语义选同一人同一天同一笔结算单只保留一行。这一步做完异常占比通常会明显下降。4.2 现象模型把所有大额报销都判成欺诈训练完看输出复核名单里全是金额最高那批人。原因金额特征在树模型里被频繁用来切分而大额报销本身只是“罕见”不一定是“欺诈”——比如癌症放化疗的年度费用可以高达几十万。解决把金额做成分位数排名或对数变换弱化绝对数值的影响df[amount_rank] df[total_amount].rank(pctTrue) df[log_amount] np.log1p(df[total_amount])在特征列表里用amount_rank替代total_amount让模型关注的是“相对同群组的位置”而不是“绝对数字”。另外可以在特征工程里加一个“同病种金额均值”的对比特征用诊断编码分组。4.3 现象训练集有标签上线后没有新标签伪标签或历史稽核标签训练出来的模型上线后新数据里根本没有 is_fraud 字段评分模型变成无的放矢。原因把“离线实验的标签”和“在线生产的标签”搞混了。解决离线阶段把标签存在独立表里通过 person_id 关联生产阶段只依赖特征表不依赖标签字段。如果一定要在生产侧持续获得标签需要跟稽核系统约一个回写接口把人工复核结论定期导回训练集。这个流程在项目初期就要设计好否则模型半年后就废了。4.4 现象类别不平衡让准确率虚高到 99% 却不干活正样本只占 0.5%模型把所有样本都预测为负类准确率 99.5%看起来无比漂亮但一个疑点都没捞出来。原因只看准确率没有看召回率和提升度。解决用 AUC、召回率、lift 作为主指标并做前面 3.2 节的阈值扫描。在向业务汇报时不要只报准确率要报“模型筛出的名单里人工复核命中率比随机抽检高了多少倍”这才是反欺诈价值所在。4.5 现象源码里路径写死换机器就报错解压 zip 后直接跑训练脚本报 “File not found” 或者路径指向了作者本机的 C:/Users/xxx。原因开发者用绝对路径写死了数据位置。解决拿到源码后先全局搜索盘符路径全部替换成相对路径。# 在脚本头部统一设置避免到处改路径 import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) DATA_DIR os.path.join(BASE_DIR, data)不管源码包里原来写的是什么凡是pd.read_csv(C:/...)这类路径全部改成基于BASE_DIR的拼接。另外如果你的 zip 是从网上收的解压后第一件事是查一下文件版本和 Python 版本——很多课程设计源码是用 Python 3.8 写的你要用 3.10 跑大概率会碰到distutils或pandasAPI 的兼容问题这不是你的问题是版本差别硬扛。5. 把反欺诈分析做成持续运转的事验证方法与进阶技巧5.1 时间切分验证用去年数据训练今年数据验证很多人用随机切分训练集和测试集这在反欺诈场景是有问题的社保欺诈手法是随时间演变的。去年有效的特征今年可能已经失效。正确的验证方式是按时间切分——用 2024 年全量数据训练用 2025 年 1 到 3 月数据验证df_train df[df[claim_date] 2025-01-01] df_val df[(df[claim_date] 2025-01-01) (df[claim_date] 2025-04-01)]时间切分能告诉你两件事模型在“未来”数据上衰减了多少以及特征分布有没有发生漂移。如果 AUC 从 0.85 掉到 0.70不要急着调参先去对比两段数据的特征均值看是不是某个机构编码或药品编码的口径变了。5.2 规则 概率的混合评分让结果能被业务接受模型跑出来的概率分没法直接拿来给稽核人员看。我习惯的做法是双轨制规则引擎先跑一部分硬性罚则比如“单日 4 家以上机构”直接进重点名单不经过模型模型只输出 0 到 100 的疑点分并按分档给出“低关注、中关注、高关注”三个级别。这样业务方既能看到规则依据又能结合概率分判断优先级。分档阈值可以用分位数确定df[fraud_prob] model.predict_proba(X)[:, 1] high_thr df[fraud_prob].quantile(0.99) mid_thr df[fraud_prob].quantile(0.95) df[risk_level] pd.cut(df[fraud_prob], bins[0, mid_thr, high_thr, 1], labels[low, mid, high])quantile(0.99)的意思是最高的 1% 进入高关注quantile(0.95)是前 5% 进入中关注以上。这个比例要配合稽核产能动态调整——产能不够就提高分位点产能富余就往下压。5.3 可解释性输出给稽核人员一份能看懂的报告反欺诈模型最后能不能被业务接受往往不取决于 AUC 有多高而取决于稽核人员能不能理解“为什么这个人被标出来了”。用 SHAP 值做解释是最直观的路径import shap explainer shap.TreeExplainer(model) shap_values explainer.shap_values(X_val) # 输出前几个样本的特征贡献 for i in range(3): idx X_val.index[i] print(f样本 {idx} 的风险概率: {y_prob[i]:.3f}) print(主要贡献特征) for feat, sv in sorted(zip(feature_cols, shap_values[i]), keylambda x: -abs(x[1]))[:3]: print(f {feat}: {sv:.4f})输出结果给业务看的时候要做一次“人话翻译”把max_daily_org: 2.1写成“该参保人单日跑 4 家机构显著高于正常水平”把log_amount: 1.3写成“近半年累计报销金额是同病种平均的 3.2 倍”。这一步看起来土但决定你的方案是停在爬虫级别的 demo还是能真正被稽核科室用起来。我个人的习惯是每次迭代模型至少留 20 个 SHAP 输出样例放进周报里让业务人员确认“这个评估结果符合直觉吗”。如果连续几周业务方都觉得解释合理这个模型才算真的落了地如果业务方开始追问“为什么这个人我认识他是正常的”那说明特征工程里混进了跟欺诈无关的噪声要回去重查。这套“从解压 zip 到可解释疑点名单”的流程我反复走了很多遍最深的感受是反欺诈项目翻车往往不是模型不行而是数据口径和业务解释出了问题。把这两个环节盯住再差的模型至少能筛出值得看的疑点。希望帮到你。本文还有配套的精品资源点击获取