简介:在大数据时代,机器学习技术广泛用于政务数据挖掘,其中基于梯度提升树的LightGBM以高效训练和类别特征支持成为表格数据建模的主流。真实业务场景中的数据往往体量大、维度杂,且存在严重类别不平衡,如何通过特征工程构建有效指标,并设计模型融合策略,是提升预测效果的关键。这类技术在社会保险、金融风控等领域具有重要应用价值,例如识别违规参保、欺诈报销等行为。以全国社会保险大数据应用创新大赛为背景,文章分享了一套完整的Python实现方案,涵盖数据清洗、特征工程、模型训练与调优等全流程,为处理类似政务表格数据提供了可复用的工程实践参考。 天池这个比赛出来的时候,我正好在折腾社保相关的大数据项目,看到赛题第一反应是:"这不就是拿真实业务场景考人嘛。"全国社会保险大数据应用创新大赛,数据来自社保业务系统的真实脱敏数据,任务是做参保人员的行为分析、风险预测这一类事情。我把自己整套Python源码和数据处理思路整理出来,写给打算参加这类大数据竞赛、或者正在做社保/政务数据挖掘的朋友。
这类比赛和学校的机器学习课设最大的区别在于:数据量大、字段杂、业务含义重。你光靠调参是上不了分的,真正拉开差距的是对业务场景的理解、特征工程的处理,以及对数据质量的把控。这套源码我赛后重新整理过,去掉了所有涉及敏感信息的细节,保留了一套从数据清洗到模型输出的完整链路,可以直接复用到类似结构的表格型数据竞赛里。
1. 赛题理解与整体设计思路
1.1 赛题背景与核心任务拆解
先说赛题本身。全国社会保险大数据应用创新大赛由阿里天池承办,主打"用大数据技术提升社保经办服务能力"。社保业务数据有一个其他数据没有的特点:它是真正意义上的"长尾数据"——绝大多数参保人员行为正常,少数人存在异常模式,比如违规领取待遇、挂靠参保、欺诈报销等。这类问题本质上是一个不平衡分类问题,而且对可解释性有要求,你不能扔出一个黑盒模型就完事,还要能回答"为什么这个人是高风险"。
我拿到的数据分为几类表:参保人员基本信息表、缴费明细表、待遇发放表、就医/报销记录表。数据量大概是百万级用户、上亿条流水记录。目标变量是用户是否存在违规行为(二分类标签)。这个设定在政务数据竞赛里非常典型,核心任务就是基于历史行为数据预测用户风险等级,并尽量给出可解释的风险因子。
1.2 技术选型:为什么用Python + LightGBM这套组合
赛题发布后,很多人在群里问用什么框架。我当时直接拍板用Python,理由很实际:pandas做特征工程太方便了,而且LightGBM和XGBoost都有成熟的Python接口,调参、交叉验证、特征重要性分析一条龙。不要为了炫技上深度学习,表格数据上深度学习除非你有海量数据和强大的调参能力,否则很难干过带正则化的树模型集成。
我的baseline方案是:pandas + LightGBM + XGBoost双模型融合。选择这两个模型的核心原因有三个:
- 原生支持类别特征,省去大量One-Hot编码工作(不过我还是手动做了目标编码,后面细说)
- 对缺失值有内置处理策略,社保数据里缺失值非常多,这个特性很关键
- 训练速度快,支持并行,百万级样本、上千维特征也能在合理时间内跑完
注意:这套方案在"表格型数据+强业务规则"的赛题里几乎永远是最优选择。如果你的数据是图像、文本或者时序信号,那再考虑深度学习,但社保类比赛基本不会出现这些类型。
1.3 源码整体目录结构与模块职责
我的项目源码按照"数据处理→特征工程→建模→预测"四个阶段组织。项目结构如下:
social_security_competition/ ├── config.py # 全局配置:路径、参数、随机种子 ├── data_process/ │ ├── 01_merge_tables.py # 多表关联合并 │ ├── 02_clean_data.py # 缺失值、异常值处理 │ ├── 03_reduce_memory.py # 数据类型降级,节省内存 │ └── 04_split_dataset.py # 训练/验证集划分 ├── feature_engineering/ │ ├── 05_base_features.py # 基础统计特征 │ ├── 06_time_features.py # 时间序列特征 │ ├── 07_aggregate_features.py # 分组聚合特征 │ ├── 08_target_encoding.py # 目标编码 │ └── 09_feature_select.py # 特征筛选 ├── model/ │ ├── 10_train_lgb.py # LightGBM训练 │ ├── 11_train_xgb.py # XGBoost训练 │ ├── 12_blend.py # 模型融合 │ └── 13_threshold_tune.py # 阈值优化 └── predict/ ├── 14_generate_submit.py # 生成提交文件 └── 15_feature_importance.py # 特征重要性分析每个模块都是独立脚本,通过config.py统一管理参数。这样做的好处是:调参时不用翻遍所有代码,只需要改config.py;复现实验时也不会因为改了某段代码导致结果不一致。
2. 数据剖析与预处理:这一步决定了你的上限
2.1 原始数据字段盘点与业务归类
拿到数据的第一件事不是写代码,而是打开数据字典,把每个字段的业务含义搞明白。我把字段分成了几个大类:
| 类别 | 典型字段 | 建模用途 |
|---|---|---|
| 身份属性 | 年龄、性别、户籍地、参保状态 | 基础风险分层 |
| 缴费特征 | 缴费基数、缴费时长、缴费连续性 | 识别挂靠、断缴 |
| 待遇特征 | 领取金额、领取时长、发放频次 | 识别违规领取 |
| 就医行为 | 就诊次数、费用结构、医院等级 | 识别欺诈报销 |
| 时间特征 | 参保时间、最后缴费时间、时间间隔 | 构建时序特征 |
这个归类的意义在于,它决定了你后续特征工程的方向。比如"缴费连续性"这类字段,直接反映用户是否有挂靠参保的嫌疑——正常在职人员缴费是连续的,但挂靠人员往往在某一时段集中补缴。
2.2 数据清洗中的三个关键操作
数据清洗阶段最容易翻车,因为政务数据比互联网数据脏得多。我总结了三个关键操作:
**第一,去重。**社保数据里一个用户可能有多条记录,因为参保单位变更、系统迁移等因素导致重复。我用用户ID + 参保地 + 参保类型作为去重键,保留时间最近的一条。这一步不做,后面所有特征都会失真。
# 示例:去重逻辑 df = df.sort_values('record_time').drop_duplicates( subset=['user_id', 'insure_city', 'insure_type'], keep='last' )**第二,异常值截断。**缴费基数是关键特征,但存在极端值。比如有人缴费基数是0(可能离职停保),有人缴费基数超过社平工资3倍(封顶线)。我采用的策略是:缴费基数为0的单独标记一个is_zero_payment特征,不直接删除;超过上限的用上限截断。
**第三,缺失值分类型处理。**社保数据里缺失值不是随机缺失,它本身包含信息。比如"单位名称"缺失可能意味着灵活就业人员,"待遇发放金额"缺失可能意味着这个人从未领取待遇。我按照缺失比例分了三类:
- 缺失率超过60%的字段:建议删除,或者只保留"是否缺失"标记
- 缺失率20%-60%的字段:用中位数填充,同时保留缺失标记
- 缺失率低于20%的字段:用模型预测或按类别众数填充
这里有一个经验教训:不要迷信填补算法的花活,比如用KNN、MICE这些方法填补。政务数据填补后的真实信息增益很低,而且极度耗时。用一个中位数加一个缺失标记,效果不差,跑得还快。
2.3 大数据量下的pandas内存优化实战
社保数据动辄几千万行,直接读入内存很容易爆。我在项目启动时就写了专门的内存优化函数,核心思路是让pandas自动把能降级的数据类型降下来。
def reduce_mem_usage(df): start_mem = df.memory_usage().sum() / 1024**2 for col in df.columns: col_type = df[col].dtype if col_type != object: c_min = df[col].min() c_max = df[col].max() if str(col_type)[:3] == 'int': if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max: df[col] = df[col].astype(np.int8) elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max: df[col] = df[col].astype(np.int16) elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max: df[col] = df[col].astype(np.int32) else: if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max: df[col] = df[col].astype(np.float16) elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max: df[col] = df[col].astype(np.float32) end_mem = df.memory_usage().sum() / 1024**2 print('Memory decreased from {:.2f} MB to {:.2f} MB'.format(start_mem, end_mem)) return df这个函数在我的数据上把内存占用降低了约60%。注意,千万级的DataFrame一定要在清洗结束后立刻做类型降级,否则后面跑特征工程时内存会频繁触顶。
另外还有一个技巧:如果原始数据文件是CSV,读取时指定dtype参数、提前定义列类型,能大幅减少读取时间和中转内存占用。我习惯先读前1000行推断类型,再手动修正,这样比pandas自动推断快很多。
2.4 训练集与验证集的划分策略
验证集划分是很多参赛者容易忽略的环节。社保数据里有很强的时间特性:违规行为被标记往往有时间滞后,同一时间段内的用户行为模式相似。因此我在划分训练集和验证集时,按时间切分而不是随机切分。
具体做法:按用户最后的“参保时间”排序,前80%的样本做训练集,后20%做验证集。这样做更接近大赛线上评测的逻辑,也能避免随机划分带来的"时间穿越"问题——即用未来的数据预测过去的目标。
还有一个注意点:划分时必须以用户ID为单位,同一个用户的所有记录必须分到同一侧,不能打散。否则会造成信息泄露,线上分数和线下分数差距会非常大。
3. 特征工程核心细节:上分的关键在这里
3.1 时序特征:参保时长、断缴次数与缴费节奏
社保数据天然的时序属性非常强。参保时长、断缴次数、缴费间隔这些特征对风险预测的区分度非常高。正常职工缴费节奏稳定,违规人员的缴费记录往往呈现出"集中补缴、长期断缴"的异常模式。
我构建的时序特征主要包括:
insure_duration_days:从首次参保到数据截止日期的总天数last_payment_gap_days:最后缴费时间到截止日期的间隔天数(断缴越久,异常风险越高)payment_break_count:连续两个缴费记录间隔超过90天,记为一次断缴payment_std_days:所有缴费间隔的标准差,衡量缴费节奏稳定性monthly_payment_ratio:实际缴费月份数除以应缴费月份数,反映缴费完整度
其中payment_std_days这个特征,我在初赛中忽略了,后来加入后线上AUC提升了0.003左右。这说明"节奏稳定度"确实能捕捉到异常缴费行为。
3.2 分组聚合特征:站在业务角度做统计
聚合特征是这类竞赛最核心的上分手段。思路就是:按业务维度分组,对数值字段做统计运算。我常用的分组维度包括user_id、insure_city、insure_type、company_id,统计量包括count、mean、std、max、min、skew等等。
举个例子,按company_id聚合:
company_agg = df.groupby('company_id')['payment_base'].agg( company_payment_mean='mean', company_payment_std='std', company_user_count='count' ).reset_index()这些特征能刻画"你所在的公司整体情况"。比如,一家公司大量用户缴费基数恰好贴着最低标准,这种公司可能就是挂靠公司,所有关联用户的风险都会上升。这类特征在业务解释上也站得住脚。
做聚合特征时有个效率问题:如果分组后要生成大量统计量,建议用agg一次搞定,不要循环多个groupby,否则几千万行数据跑起来非常痛苦。另外要留意skew这类高阶统计量,在数据量大时计算开销不小,我一般只对关键数值字段算mean和std,skew和kurt做选择性使用。
3.3 目标编码与交叉特征的正确姿势
社保数据里的类别特征,比如insure_city、company_id,基数差异巨大。insure_city可能只有几十个取值,但company_id有几万个。直接用LabelEncoder会让树模型产生误解,One-Hot又会让维度爆炸。我的处理方案是:低基数类别特征用One-Hot或原生类别特征,高基数类别特征用目标编码。
目标编码用LightGBM原生的categorical_feature也可以,但目标编码更灵活,可以在编码时加入平滑项。我的实现方式:
def target_encode(train, val, col, target, smooth=20): temp = train.groupby(col)[target].agg(['mean', 'count']) temp['smooth_mean'] = (temp['mean'] * temp['count'] + global_mean * smooth) / (temp['count'] + smooth) val[col + '_te'] = val[col].map(temp['smooth_mean']) train[col + '_te'] = train[col].map(temp['smooth_mean']) return train, val交叉特征这块,我重点做了几组:insure_city + insure_type、company_id + payment_month、age_group + payment_base_band。交叉特征的思路是把两个单独特征组合成新的类别特征,让模型捕捉它们的交互效应。政务数据里这类交互效应非常强,比如"某城市的某类参保人员"往往有特定的风险模式。
经验:目标编码必须在训练集内计算后映射到验证集,绝不能直接用全量数据计算,否则就是典型的数据泄露,会让线下分数虚高很多。
3.4 特征筛选:不要迷信特征数量
我做特征工程做到后面,特征维度到了1000多维。这时候不是越多越好,而是要筛选。我用三个方法结合:
- 特征重要性排名(LightGBM的
feature_importance) - 相关性分析(去除相关系数超过0.9的冗余特征)
- 单特征AUC(删除单特征AUC低于0.5的特征)
最终保留大概400个特征。实际上从600个特征加到1000个特征,线上分数几乎没有变化,但训练时间增加了近一倍。后来我意识到,政务数据竞赛中特征的有效性呈现出"长尾分布"——真正有价值的特征就那几十个,剩下的都是边际贡献极小甚至纯噪声的特征。把精力花在核心特征的理解和打磨上,远比堆砌特征数量有效。
4. 模型构建与调参实战:如何稳定上分
4.1 Baseline方案与评估指标选择
大赛的评估指标用的是AUC(ROC曲线下面积)。这个指标对正负样本比例不敏感,非常适合社保这种类别不平衡的数据。我搭建baseline的时候,先只用了基础统计特征+少量时序特征,用LightGBM默认参数训练,得到AUC约0.72。这个分数作为起点,后续所有工作都是在这个baseline上逐步迭代。
baseline的代码框架如下:
import lightgbm as lgb from sklearn.model_selection import train_test_split X_train, X_val, y_train, y_val = train_test_split( features, target, test_size=0.2, random_state=42 ) lgb_params = { 'objective': 'binary', 'metric': 'auc', 'learning_rate': 0.05, 'num_leaves': 31, 'max_depth': -1, 'feature_fraction': 0.8, 'bagging_fraction': 0.8, 'bagging_freq': 5, 'verbose': -1, 'seed': 42 } d_train = lgb.Dataset(X_train, label=y_train) d_val = lgb.Dataset(X_val, label=y_val) model = lgb.train( lgb_params, d_train, num_boost_round=1000, valid_sets=[d_val], early_stopping_rounds=100, feval=None )4.2 LightGBM关键参数调优思路
调参是这个环节的重头。我按照"先粗后细"的顺序来调,不搞暴力网格搜索。我的调参路线分为四步:
**第一步,调num_leaves和max_depth。**这两个参数控制模型复杂度。num_leaves是LightGBM的核心参数,我从小往大试,观察验证集AUC的变化:8、16、31、63、127。最后发现31效果最好,再大会过拟合。max_depth设成-1让模型自己决定,但配合min_data_in_leaf来防止过深。
第二步,调采样参数。feature_fraction和bagging_fraction可以简单理解成"随机丢特征"和"随机丢样本",作用都是增强泛化能力。我初始设0.8,之后微调。如果验证集AUC明显高于测试集,说明过拟合,就调低这两个值。
第三步,调正则化参数。lambda_l1和lambda_l2的值我通常在0到10之间尝试。社保数据特征多且稀疏,L2正则化对抑制噪声特征作用比较明显。我最后的参数是lambda_l1=0.1, lambda_l2=5。
**第四步,调学习率与轮数。**这部分比较无脑:学习率降到0.01,num_boost_round放大到5000,配合early_stopping_rounds=200。低学习率需要更多轮数,但精度略有提升,而且不容易过拟合。
我最终使用的LightGBM参数表:
| 参数 | 初赛 | 决赛 |
|---|---|---|
| learning_rate | 0.05 | 0.01 |
| num_leaves | 31 | 63 |
| max_depth | -1 | -1 |
| feature_fraction | 0.8 | 0.7 |
| bagging_fraction | 0.8 | 0.7 |
| min_data_in_leaf | 50 | 100 |
| lambda_l1 | 0 | 0.1 |
| lambda_l2 | 1 | 5 |
| num_boost_round | 1000 | 5000 |
初赛到决赛把参数调得更保守(更低的采样率、更强的正则化),是为了在更复杂的全量数据上减少过拟合。实际上决赛的数据量是初赛的3倍左右,同样的模型在更多数据上训练,AUC自然会有提升。
4.3 XGBoost与LightGBM的融合策略
用过双模型的人都知道,融合的收益主要来自模型"看问题的方式不同"。LightGBM基于直方图算法,训练更快,对特征中包含大量离散值的场景很友好;XGBoost基于预排序算法,对缺失值处理更精细。两者融合能互补。
我的融合方式很简单:先分别训练两个模型得到预测概率,然后做加权平均。权重我通过网格搜索确定,范围0到1,步长0.05,以验证集AUC为目标函数。最后最优权重大约是0.7 * LightGBM + 0.3 * XGBoost。
还要注意:两个模型尽量用同一套特征和同一套样本划分,否则融合时会产生不一致的样本分布,导致融合结果不稳定。
4.4 阈值选择与最终提交策略
大赛要求提交的是预测概率,但实际业务里需要给出"是否违规"的判定阈值。我在验证集上计算了不同阈值下的精确率和召回率,画了PR曲线。对于社保欺诈检测场景,我更看重召回率——漏掉一个违规用户比误报一个正常用户代价更大,所以我选择了一个让召回率达到85%左右的阈值。这个决策完全来自业务场景,不是一个纯技术问题。
线上提交时还有一个细节:提交文件必须包含所有测试集用户的预测结果,不能因为某个用户特征缺失就漏掉。我写了个兜底逻辑:特征完全缺失的用户,用全局平均概率填充,保证提交文件全覆盖。
5. 常见问题与排查技巧实录
5.1 数据泄露的经典案例与排查方法
数据泄露是这个比赛里最容易踩的坑。我初赛时遇到过线下AUC 0.86,线上只有0.75的情况,后来排查发现是目标编码泄露。
具体过程是这样的:我用全量数据(包括测试集)计算了目标编码的均值映射,然后在训练集上应用,导致验证集分数虚高。修复方法是只在训练集内部计算映射,再映射到验证集和测试集。这个错误很隐蔽,因为线下验证集表现稳定,你不会意识到泄露了。
排查数据泄露的方法我现在分享几个:
- 检查单特征AUC:如果某个特征的单独AUC超过0.8,大概率泄露了
- 打印特征重要性Top10:如果榜首特征的importance远超其他特征,警惕信息泄露
- 对比验证集和线上分数:线下远高于线上,优先怀疑泄露
5.2 类别不平衡的处理策略
社保数据里违规用户占比极低,大概只有1%左右。如果用原始比例训练,模型会倾向把所有样本预测为负类。我的处理策略不是用class_weight,而是做下采样:从负样本中随机抽取与正样本数量相同的子集,保证训练集正负比例1:1。验证集保持原始比例不变,这样评估分数才反映真实场景。
下采样后,我用早停机制训练模型,并在每个epoch后对验证集做完整评估。这里有一个细节:下采样会导致模型预测概率普遍偏高,因为训练集正样本比例远高于真实场景。因此在实际提交前,我会对预测概率做一次校准,用验证集上的实际正样本比例调整Cutoff。简单来说,就是训练集正样本比例是0.5,真实比例是0.01,那么预测分数要压低,阈值要相应调低。
5.3 内存溢出与运行时间的优化
这个坑几乎每个参赛者都会遇到。数据量大的时候,特征工程做一步炸一次内存,非常崩溃。我的经验是分块处理,结合gc.collect()主动释放内存。
import gc # 每处理完一个大表,主动释放内存 del temp_df gc.collect()另外,特征工程阶段尽量以user_id为粒度做聚合和合并,而不是整个大表操作。具体做法:先把基础表按user_id聚合,生成细粒度的用户特征表,最后再和主表做一次merge。这样特征工程过程中每个步骤的数据量都不会爆炸。
训练时间优化方面,我在特征筛选后把训练集转成LightGBM的Dataset格式,并且开启了feature_pre_filter=True。这个方法能在建树过程中提前过滤无用特征,能减少20%-30%的训练时间。
5.4 代码版本管理与实验记录
很多人参赛时会忽略这个,但实际比赛中这是非常重要的。我每天会记录实验日志,包含:当天的特征列表、模型参数、验证集AUC、提交分数、做了哪些改动。这个习惯让我能清楚地知道每一步上分或掉分的原因。
代码版本管理我用的是Git,每个可提交版本打一个tag。特征工程文件的命名带日期,例如07_aggregate_features_1108.py。这样回滚到历史版本很方便,不至于改崩了不知道怎么恢复。比赛中后期,特征的版本管理比代码本身更重要,因为不同特征组合的上分效果千差万别。
5.5 其他高频报错速查表
最后整理一下我遇到的高频报错和对应解决方法,适合直接收藏:
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
MemoryError | 数据量超出内存 | 类型降级、分块读取、及时del+gc |
ValueError: Input contains NaN | 特征中存在缺失值 | 检查特征管线中的merge是否导致NaN,填充或删除 |
LightGBMError: number of categorical features must be integer | categorical_feature参数传入格式错误 | 改用整数索引而不是字符串列名 |
AUC一直等于0.5 | 正负样本顺序错乱或标签对齐错误 | 检查训练集和标签的索引是否对齐 |
验证集AUC高但线上低 | 数据泄露或验证集划分不随机 | 按时间划分、检查目标编码/填充是否泄露 |
写在最后:一些实在话
赛后复盘这次天池社保大数据比赛,我觉得收获最大的不是AUC从0.72提到0.81这个结果,而是对整个数据竞赛流程的掌控感。第一次跑通全流程时手忙脚乱的,但到决赛阶段,我已经能清楚地判断每个操作对最终分数的边际贡献,知道什么时候该停手,什么时候该继续挖特征。
对于想参加同类竞赛的朋友,我个人的建议是:不要一开始就追求复杂的模型和花哨的算法,先把baseline打通,再逐步迭代。数据竞赛没有银弹,所谓高分方案,都是把数据处理、特征工程、模型调参每个环节做扎实之后自然涌现的结果。这套Python源码的逻辑相信对你有帮助,无论你是准备天池的比赛,还是处理类似的政务表格数据,按这个流程走一遍,方向基本不会错。
如果你在复现这套方案时遇到问题,可以对照我上面整理的报错速查表排查。祝你在数据竞赛里拿到好名次。
本文还有配套的精品资源,点击获取