简介:面向社会保险反欺诈场景的 Python 分析与建模项目,内含完整源码、数据集下载指引与说明文档,适合计算机相关专业学生用于毕业设计、课程设计或期末大作业,也适合希望实战机器学习完整流程的学习者。项目以 ipynb 分模块组织,覆盖特征分析、特征衍生、特征选择、特征变换与参数调优五个环节,并以 BuildModel.py 衔接建模;data 目录统一存放训练集、测试集与衍生数据,标签 1 表示欺诈用户、0 表示正常用户,便于直接理解任务与复现实验。压缩包共 12 个文件,以 5 个 ipynb 脚本为核心,另有 py 建模脚本、md 说明文档、tsv/csv 数据文件及 XML 配置等,整体约 15.21MB,目录结构清晰。项目源自大三课程作业,经导师指导并获得 96.5 分的评审成绩,当前已有 75 人学习;下载后可按 README 和模块笔记走通数据读取、特征工程、模型训练与调参全流程,也可在此基础上扩展新的反欺诈规则。
1. 社会保险反欺诈分析到底在解决什么问题
人工智能正从尝鲜工具变成日常帮手,其中一个落地最扎实、 ROI 最清晰的场景就是社会保险反欺诈。社保基金每年因为虚假参保、挂靠缴费、骗领待遇造成的损失是天文数字,而稽核人员有限,不可能逐条翻报表。基于人工智能的反欺诈分析,本质是用异常检测算法从海量缴费记录里把人查不出来的“可疑行为”筛出来,再交给稽核人员做人工确认。这套方案适合有 Python 基础、手头有社保缴费明细或相关业务数据的从业者,也适合拿公开数据做课题的学生。它不追求模型多复杂,核心是把业务规则转成特征,再用无监督算法找出偏离群体规律的那批人。
2. 社保数据特征工程:反欺诈模型的上限在数据准备阶段就定死了
2.1 反欺诈分析需要哪些原始数据?先理清五张表的业务关系
社保反欺诈分析不是拿来一个 CSV 就能跑模型的,第一步是理解业务上到底有哪些表、表之间怎么关联。我经手的项目里,最常见的原始数据至少涉及五张表:人员基本信息表、参保缴费明细表、单位基本信息表、待遇发放表、稽核案件表。人员表给的是身份证号、性别、出生日期、户籍地;缴费表给的是每个参保人每个月在哪个单位、按什么基数缴费、单位和个人各缴多少;单位表解决“这批人是不是同一个单位报上来的”;待遇表管的是领取养老金或者失业金的人是否还在正常缴费;稽核案件表是历史查实的欺诈记录,这是后面做有监督模型的标签来源。
表之间的关联核心是两个键:人员维度用身份证号,单位维度用统一社会信用代码。建模前必须先把这五张表做宽表拼接,以“人 × 月份”为粒度,把一个人在某一个月的缴费记录拉成一行,再把单位属性和历史案件标签左连接上去。这里最常见的问题就是一对多关系导致行数爆炸——一个人在同一个月在多家单位缴费(疑似挂靠),拼接后会出现两行,这本身就是一个反欺诈信号,不能简单去重。
import pandas as pd person = pd.read_csv("person_info.csv", dtype={"id_card": str}) payment = pd.read_csv("payment_detail.csv", dtype={"id_card": str, "company_id": str}) company = pd.read_csv("company_info.csv", dtype={"company_id": str}) case = pd.read_csv("audited_fraud_cases.csv", dtype={"id_card": str}) # 合并人、缴费、单位信息 df = payment.merge(person, on="id_card", how="left") df = df.merge(company, on="company_id", how="left") df = df.merge(case, on="id_card", how="left", indicator="is_fraud") # 同一个人同一个月出现多条缴费记录,标记为疑似挂靠 df["multi_company_flag"] = df.groupby(["id_card", "year", "month"])["company_id"].transform("nunique") > 1 # 构造标签:有稽核案件记录的为欺诈样本 df["is_fraud"] = df["is_fraud"].map({"both": 1, "left_only": 0})这段代码做了三件事:按身份证号关联人员信息;按单位关联企业属性;通过indicator参数知道哪些人出现在稽核案件里,从而构造标签。groupby.transform("nunique")是计算每个人每个月关联了多少家公司,大于 1 直接打成疑似挂靠特征。此处的is_fraud列不要直接喂给模型,它只是给你后续评估用的基准标签,因为稽核案件永远是“查出来才叫案件”,没被查的人不代表干净。
2.2 从缴费行为里挖特征:那些“太正常”的数字反而是重点
社保数据的特征工程和电商用户画像完全不同,医保、养老、失业各险种的缴费行为有很强的政策约束,所以偏离政策的样本本身就是重点。我一般会构造四类特征:基数类、连续性类、单位类、待遇类。
基数类特征的核心是缴费基数与本人工资收入、与社平工资的比例。正常参保人基数通常是社平工资的 60% 到 300% 之间,低于 60% 或者恰好压在 60% 线上,在真实业务里往往是“按最低标准挂靠”的表现。连续性类特征包括断缴次数、补缴次数、参保总月数、连续缴费月数。骗保行为有一个典型画面:平时不缴费,出事了(比如查出大病)再突击补缴几个月,然后立刻申请报销。
单位类特征构造方式是聚合——同一个单位下,参保人数、平均缴费基数、单位参保率,以及单位里“缴费基数恰好等于最低基数”的人数占比。如果一家公司有 200 个员工全是按最低基数缴费,这本身就是一种系统性风险。待遇类特征针对的是“已领待遇同时继续缴费”的套利行为,比如一个人开始领失业金了,同时又在另一家单位正常缴费。
# 计算一个人的断缴次数和补缴次数(以月为粒度) df["gap_flag"] = df.groupby("id_card")["month"].diff().fillna(1) df["gap_flag"] = (df["gap_flag"] > 1).astype(int) df["cum_gap"] = df.groupby("id_card")["gap_flag"].cumsum() # 缴费基数与社平工资的比例 df["base_ratio"] = df["pay_base"] / df["avg_social_wage"] # 单位层面聚合特征 df["comp_min_base_ratio"] = df.groupby("company_id")["pay_base"].transform( lambda x: (x <= df.loc[x.index, "avg_social_wage"] * 0.6).mean() )这里的diff()是核心操作,它在人员分组内计算相邻两个月之间的间隔,如果间隔大于 1 说明中间断缴了。cumsum把断缴次数累加成一个递增序列,这样后面的模型输入就是一个“越到近期越大的累计值”。base_ratio是把缴费基数除以当年社平工资,得到标准化的比例,避免不同年份的绝对金额不可比。单位最低基数占比这个特征用transform而不是agg,是为了返回和原表同样长度的序列,这样可以直接加列,不需要再 merge 一次。
2.3 特征预处理:把金额特征转成比率、偏差和波动率
社保缴费金额动辄几万几十万,直接用原始金额喂模型会导致距离度量完全被金额量纲控制。我在实践里的做法是:对所有金额类特征先做对数变换,再做 z-score 标准化,或者干脆转成比率。比率特征天然具有可比性,是最推荐的方案。
每个人的缴费基数是随时间变化的,正常人通常是缓慢调整(每年调一次),而欺诈行为往往是突变——比如基数从 3000 跳到 20000 再跳回 3000。用滚动窗口的均值和标准差来描述这种波动会非常有效。按人分组后,取缴费基数的 6 个月滚动标准差除以滚动均值,得到变异系数;再取当前缴费基数与个人历史均值的偏差百分比。
# 金额特征对数变换 amount_cols = ["pay_base", "personal_payment", "company_payment"] df[amount_cols] = df[amount_cols].apply(np.log1p) # 个人缴费基数的滚动波动率 df["base_vol"] = df.groupby("id_card")["pay_base"].transform( lambda x: x.rolling(6, min_periods=2).std() / x.rolling(6, min_periods=2).mean() ) # 当前基数相对个人历史均值的偏差 df["base_dev"] = df.groupby("id_card")["pay_base"].transform( lambda x: (x - x.expanding().mean()) / x.expanding().std() )np.log1p是 log(1+x),防止缴费基数为 0 时报错。rolling(6).std() / rolling(6).mean()构造的是 6 个月变异系数,min_periods=2保证只有一两个月缴费记录的人不会算出空值。expanding().mean()是累计均值,代表这个人的“历史正常水平”,当前值减去历史均值再除以历史标准差,就是偏离信号。这里有一个坑:如果一个人只有 1 条缴费记录,expanding().std()是 NaN,模型会直接丢弃这个人,所以在预处理末尾需要对这类样本做填充——用全局均值填充,或者单独打一个“缴费记录过少”的二值特征。
3. 欺诈检测算法选型:无监督做初筛,有监督做定性
3.1 为什么首选孤立森林而不是逻辑回归:正样本太少是常态
社保反欺诈分析里,已确认的欺诈样本占整体参保人的比例通常不到千分之一,拿这种极端不平衡的数据直接训逻辑回归,模型会把所有人都预测成“正常”,因为这样做正确率有 99.9%。孤立森林是这里最合适的起点,它不依赖标签,核心逻辑是用随机切割的方式把“容易被单独切出来”的样本标记为异常。正常人的缴费模式高度相似,要很多刀才能切出来;而骗保行为无论在基数、断缴还是单位关联上都与群体差异巨大,随机切几刀就孤立出来了。
孤立森林的路径长度越短,异常分数越高。这个“路径长度”就是断言的依据——异常点离群太远,所以很快被切出来。在 sklearn 中调用非常直接,但参数选择有讲究。n_estimators我一般设在 200 到 300 之间,太少不稳定,太多训练时间线性增长但收益递减。max_samples设 256 即可,这是论文里验证过的比较稳的采样数,再大不会明显改善精度。contamination是预估的异常比例,它只影响predict的阈值划分,不影响score_samples输出的原始分数。
from sklearn.ensemble import IsolationForest feature_cols = ["base_ratio", "base_vol", "base_dev", "cum_gap", "multi_company_flag", "comp_min_base_ratio"] model_if = IsolationForest( n_estimators=256, max_samples=256, contamination=0.001, random_state=42, n_jobs=-1 ) df["anomaly_score_if"] = model_if.fit_predict(df[feature_cols].fillna(0)) df["anomaly_raw_if"] = model_if.score_samples(df[feature_cols].fillna(0))fit_predict返回的是 1 或 -1,1 代表正常,-1 代表异常。但是真实项目里不要直接用这个二分结果去做稽核名单,因为contamination是拍脑袋预估的,按 0.001 筛出来的可能只有几百人,但里面的假阳性会很高。我一般会取score_samples的原始分数,然后按分数排名取前千分之一或按业务可复核的人数上限来圈名单。fillna(0)用 0 填充缺失值在这类树模型里是安全的,孤立森林不敏感于特征值的大小,只敏感于分割难度。
3.2 孤立森林与局部离群因子组合:一个抓全局,一个抓局部
孤立森林的问题是它会优先抓“全局离群”——那些在所有特征上都偏离群体的人。但社保反欺诈里有一类人非常难抓:他们缴费基数、断缴次数、单位属性全都在正常范围内,唯一的异常是“某个月突然换了一家刚成立的皮包公司,然后下个月又换了一家”。这种局部模式的异常,孤立森林容易忽略。
局部离群因子(LOF)擅长解决这个问题。LOF 比较的是某个样本的局部密度和它 k 个近邻的局部密度,如果这个样本周围比它的邻居稀疏很多,就判定为异常。这两者我认为是互补关系而不是替代关系:孤立森林抓“跟所有人都不一样”的,LOF 抓“跟身边人都不一样”的。实际项目里我会同时跑两个模型,然后取两个异常分数的交集或者加权融合。
from sklearn.neighbors import LocalOutlierFactor model_lof = LocalOutlierFactor( n_neighbors=30, contamination=0.001, novelty=True ) model_lof.fit(df[feature_cols].fillna(0)) df["anomaly_raw_lof"] = model_lof.score_samples(df[feature_cols].fillna(0)) df["anomaly_score_lof"] = model_lof.predict(df[feature_cols].fillna(0))n_neighbors=30是一个常用经验值。这个参数太小,局部性太强,每个样本都觉得自己是异常的;太大会退化成全局密度估计,丢失局部语义。novelty=True很关键,它让模型变成“先拟合再预测”的模式,否则fit_predict和score_samples在训练数据上会有完全不同的定义。两个模型的分数量纲不一样,不能直接相加,需要先各自做排名百分位,再把百分位相加取平均,这样融合才是公平的。
3.3 用半监督思路处理“已确认欺诈”样本的冷启动
新上线反欺诈系统时最尴尬的问题是历史稽核案件样本太少。我见过有人拿 30 条已确认案件去训 XGBoost,然后跑出 0.98 的 AUC,这个数字毫无意义——样本太少,而且稽核案件本身有选择性偏差:稽核人员只会查他们认为可疑的人,所以“已确认欺诈”这个标签永远不是随机抽样出来的结果。
更合理的方式是做半监督冷启动。第一轮用孤立森林和 LOF 的无监督分数,圈出排名前 2000 的疑似样本,交给稽核人员批量复核;复核回来的人如果确实有问题,就作为种子标签;然后用这些种子样本加上大量无标签数据,训练一个半监督模型比如 Label Spreading,或者直接把种子样本丢给 XGBoost 做有监督训练。这套流程的要点是:无监督模型负责发现有价值的疑似对象,有监督模型负责在下一轮把相似模式的样本自动扩出来。
# 取两个模型异常分数的排名百分位 df["if_pct"] = df["anomaly_raw_if"].rank(pct=True) df["lof_pct"] = df["anomaly_raw_lof"].rank(pct=True) df["fusion_score"] = (df["if_pct"] + df["lof_pct"]) / 2 # 圈出待稽核名单,导出供业务人员复核 audit_list = df[df["fusion_score"] >= 0.999].sort_values("fusion_score", ascending=False) audit_list[["id_card", "company_id", "year", "month", "fusion_score"]].to_csv( "audit_list_v1.csv", index=False )rank(pct=True)把原始异常分数转成百分位排名,数值越高代表越异常。两个模型融合后取 0.999 以上的部分,对应大约千分之一的名单量——这个比例是根据稽核人力定的:一个人一天能复核 50 条,10 个人两周刚好能处理完这 2000 条。导出名单时只保留能唯一定位到人的字段,不要给业务人员看一堆漂漂亮亮的特征,他们要的是一个能直接在系统里查到的身份证号。
4. 用 Python 把一套可复现的反欺诈流水线跑起来
4.1 项目目录与核心模块划分
真实项目里代码要能给别人接手,目录结构从一开始就不能乱。我的常用组织方式是按功能拆四个目录加两个入口脚本。data/放原始数据和中间产物,features/放特征工程代码,models/放训练和评估脚本,output/放稽核名单和可视化报表。入口脚本只有两个:pipeline.py负责串起全流程,audit_report.py负责生成给业务方看的名单和 PDF 报告。
这里有个容易被忽略的点:特征工程代码必须单独成模块,不要和建模写在同一个 notebook 里。原因很简单,稽核流程是迭代的——第一轮名单复核完,新的标签会回流到训练集里,你必须只重训练模型而不用重新跑一遍特征工程。另外,所有中间结果统一用 parquet 格式落盘,不要用 CSV,因为特征宽表动辄几百万行,CSV 读写慢且丢类型信息。
project/ ├── data/ │ ├── raw/ # 原始五张表,只读不写 │ ├── processed/ # 宽表、特征表 │ └── labels/ # 稽核复核回流标签 ├── features/ │ ├── build_features.py # 从原始表到特征宽表 │ └── feature_config.py # 特征列名与参数配置 ├── models/ │ ├── train_unsupervised.py │ ├── train_supervised.py │ └── eval_report.py ├── output/ │ └── audit_list_v1.csv └── pipeline.py目录拆分这件事看着简单,但在项目里影响很大。很多人图省事把全部逻辑写在一个 notebook 里,第一轮跑完没问题,第二轮业务方说要加一个“单位参保人数不足 5 人”的规则,你就得从头跑到尾。特征模块独立之后,加规则只需要改feature_config.py里的配置,模型模块完全不用动。
4.2 特征工程代码:从原始缴费表到建模宽表
特征工程这块我要强调一个顺序问题:先做数据质量校验,再做特征构造。社保原始数据里身份证号位数不一致、单位名称重复、缴费基数为负、月份缺失这些是家常便饭。先写一个validate_data()函数把所有校验跑一遍,打印出入参数量和异常量,再进入特征构造流程。不做这一步的人,后面模型结果里混了多少脏数据都不知道。
def validate_data(df): assert df["id_card"].str.len().isin([15, 18]).mean() > 0.95, "身份证号异常比例过高" assert (df["pay_base"] >= 0).all(), "存在负缴费基数" assert df["year"].between(2000, 2025).all(), "年份字段存在越界值" dup_count = df.duplicated(subset=["id_card", "year", "month", "company_id"]).sum() print(f"重复缴费记录: {dup_count} 条") return df def build_feature_table(payment_df, person_df, company_df): df = payment_df.merge(person_df, on="id_card", how="left") df = df.merge(company_df, on="company_id", how="left") df = validate_data(df) # 聚合特征统一输出 df["multi_company_flag"] = df.groupby(["id_card", "year", "month"])["company_id"].transform("nunique") > 1 df["base_ratio"] = df["pay_base"] / df["avg_social_wage"] df["base_vol"] = df.groupby("id_card")["pay_base"].transform( lambda x: x.rolling(6, min_periods=2).std() / x.rolling(6, min_periods=2).mean() ) df["base_dev"] = df.groupby("id_card")["pay_base"].transform( lambda x: (x - x.expanding().mean()) / x.expanding().std() ) return df feature_df = build_feature_table(payment, person, company) feature_df.to_parquet("data/processed/feature_table.parquet")validate_data里的assert会自动截断异常数据比例过高的输入,这是第一道防线。duplicated检查重复缴费记录,输出数量但不停下来,因为重复记录本身可能就是挂靠线索。特征构造函数我故意没有把全部特征都写进去,真实项目里特征会有二十到三十个,但函数结构是固定的——先拼接、再校验、再聚合,最后统一落盘为 parquet。参数的默认值在feature_config.py里统一管理,比如rolling_window=6、contamination=0.001,调参时只改配置文件。
4.3 孤立森林训练与异常分数输出
训练环节的完整流程是:读取特征宽表 → 剔除掉特征全部为空的行 → 训练无监督模型 → 输出异常分数和待稽核名单。这里我踩过一个坑:如果训练数据里混入了大量“参保时间只有一个月”的新用户,孤立森林会把他们都判为异常,因为特征缺失太多导致被随机分割时路径很短。解决方案是在建模前过滤掉缴费记录少于 3 个月的人,或者单独建一个“新参保人群”的模型去分析,不要和存量人群混在一起。
import pandas as pd from sklearn.ensemble import IsolationForest feature_df = pd.read_parquet("data/processed/feature_table.parquet") # 过滤缴费记录过少的样本,避免特征缺失造成的假异常 record_counts = feature_df.groupby("id_card").size() valid_users = record_counts[record_counts >= 3].index model_df = feature_df[feature_df["id_card"].isin(valid_users)].copy() feature_cols = ["multi_company_flag", "base_ratio", "base_vol", "base_dev", "cum_gap", "comp_min_base_ratio"] model_if = IsolationForest( n_estimators=256, max_samples=256, contamination=0.001, random_state=42, n_jobs=-1 ) model_df["anomaly_raw_if"] = model_if.score_samples(model_df[feature_cols].fillna(0)) model_df["if_pct"] = model_df["anomaly_raw_if"].rank(pct=True) # 融合 LOF 分数 from sklearn.neighbors import LocalOutlierFactor model_lof = LocalOutlierFactor(n_neighbors=30, contamination=0.001, novelty=True) model_lof.fit(model_df[feature_cols].fillna(0)) model_df["anomaly_raw_lof"] = model_lof.score_samples(model_df[feature_cols].fillna(0)) model_df["lof_pct"] = model_df["anomaly_raw_lof"].rank(pct=True) model_df["fusion_score"] = (model_df["if_pct"] + model_df["lof_pct"]) / 2 model_df.sort_values("fusion_score", ascending=False).to_parquet( "output/scored_table.parquet", index=False )record_counts[record_counts >= 3]是过滤“缴费月数少于 3 个月”的样本,这里的 3 是按业务经验定的——一次完整的社保缴费周期至少覆盖一个季度,少于 3 个月无法计算波动率特征。score_samples返回的是负分,越接近 0 越异常,所以后面用rank(pct=True)排序时数值越大越异常。合并输出时保留用户维度所有记录,而不是只保留最后一次缴费,因为稽核人员需要看完整的时间序列来判断是否立案。
5. 社会保险反欺诈分析的避坑指南
5.1 身份证号脱敏导致数据无法 JOIN
现象:特征宽表里每个人的缴费记录残缺,大量人在 merge 后变成 NaN。
原因:社保原始数据里身份证号经常被脱敏成****开头,或者长度是 15 位和 18 位混用,直接按字符串相等去关联两张表,匹配率只有 60%。
解决:在validate_data()里先检查身份证号分布,15 位的统一在前面补"19"转成 18 位格式,脱敏数据要么从业务系统申请完整版、要么用姓名加出生日期(身份证第 7 到 14 位)拼接出第二关联键。我的经验是永远不要依赖单一关联键,至少准备两个键做交叉验证,吸烟者数据 JOIN 完成率低于 95% 就直接打回数据源。
5.2 跨年度重复参保在单年数据里看不出异常
现象:模型跑出来的名单里全是当年的断缴、补缴异常,但真正恶性的跨省重复参保一个都找不到。
原因:如果你只拿一年的缴费数据,一个人确实只在一个地方缴费,系统根本看不到他在外省同时参保的事实。
解决:跨年度重复参保检测必须把数据范围扩展到至少 3 年,并且按身份证号做“按年聚合”——一个人在同一年、不同统筹区都有缴费记录,就是重复参保嫌疑。这个特征要单独建,不进无监督模型,直接作为规则前置过滤,因为它的业务定义太明确了,不需要算法去“发现”。
5.3 缴费基数“恰好”是社平工资 60% 的样本不是噪声
现象:你把基数低于社平工资 60% 的全部标成异常,名单里出现大量中小微企业员工,实际复核后大部分是正常参保。
原因:政策上允许按最低基数缴费,很多劳动密集型企业的员工就是按最低标准缴的。这批人不是“欺诈”,只是“合规地低缴”。
解决:把“低于 60%”改成“低于 60% 且同时满足断缴次数高、单位参保人数少”——也就是把单一特征规则改成组合规则再进模型。我见过最有效的做法是单独给出一个“最低基数占比”特征,让模型学习它和断缴、挂靠之间的关系,而不是人工一刀切。
5.4 异常分数排名前 100 里全是“正常”的大企业
现象:孤立森林把知名大型企业排到了名单最前面,业务方一看就觉得模型不可信,项目被叫停。
原因:大企业员工人数多、缴费模式高度整齐划一,导致每个人的缴费基数方差极小,而孤立森林对“低方差群体”天然敏感——他们太像彼此了,反而容易被随机切割路径变短。
解决:这个问题的根源不是算法错了,而是特征空间缺少“单位规模校准”。在大企业工作的正常员工,其缴费模式本来就趋同,需要把这个“群体相似度”减掉。具体做法是按单位分组,把个人的波动率特征减去单位整体的波动率均值,得到“偏离本单位程度”的特征列。这个特征才是区分“正常的一致”和“异常的雷同”的关键。
6. 投产前的验证技巧:用影子模式校准阈值,而不是拍脑袋
模型跑完不是终点,难点在于:你要拿什么证据说服稽核部门说“这 500 个人值得查”。我常用的验证手法是历史回溯——把过去三年已经查实并追回资金的案件全部捞出来,看它们在模型里排到什么位置。如果查到 100 件历史案件,其中 80 件的异常分数排在前 1%,说明模型捕捉到了核心信号;如果历史案件散布在分数各个档位,先不要调阈值,大概率是特征还缺了什么。这是在给模型“做体检”。
阈值校准有一个非常实用的小技巧:把名单按分数从高到低切成 10 等份,业务方抽前 100 条复核,统计每一份的真实阳性率。你会发现通常前两档的命中率在 60% 以上,越往后越低。这时候阈值应设在真实阳性率明显下降的那个拐点前面,而不是程序里写的0.999。其中无常的规律是:第一轮复核的命中率直接决定这个项目还能不能活,所以第一轮宁可少给名单,也要把命中率做高,给业务方信心。
影子模式的逻辑是:先不打断现有稽核流程,每周把模型圈出的 100 条名单和稽核人员实际查的案件做对比,攒三个月数据,统计模型能提前多久预测出案件、覆盖了多少案件。这种评估方式在业务风险上是最稳的——任何 AI 系统在社保场景里都不会被直接信任,只有拿历史战绩说话。我经历过最理想的一次上线,是影子模式运行两个月后,模型提前 3 周预警了一个虚报工伤待遇的团伙案件,稽核处长才松口说“可以进正式流程”。
最后我想提醒一个很多人忽略的细节:模型名单里永远附上“为什么推这个人”——把贡献最大的前三项特征名写在推荐理由里。稽核人员看到“断缴次数超过 12 次、缴费基数一年内变动超过 15 次、关联单位参保人数不足 3 人”这样的描述,会有底气决定是否立案;而单纯丢一个“异常分数 0.997”过去,他们只会觉得你在制造负担。这个习惯帮我换了三次团队都没被业务方骂过,希望你也能用上。
本文还有配套的精品资源,点击获取