保险用户评估模型实战:XGBoost分类与特征工程全解析
简介面向Kaggle经典赛题Prudential保险用户评估的XGBoost分类项目完整覆盖数据清洗、特征构造与筛选、模型训练调参及可解释性分析等环节体现了针对高维结构化数据的完整建模思路。XGBoost在该类保险评估任务中训练效率高、泛化能力强配套的特征工程与模型解释内容能够帮助理解各变量对预测结果的影响。项目适合高校计算机相关专业学生作为毕业设计、课程设计或课程作业参考也可供科研和工程人员在项目初期快速搭建保险评估原型、验证算法路线。压缩包共190个文件大小约2.58MB以34个Python脚本为核心实现另含JSON配置、HTML/JS页面、图片素材及说明文档等辅助内容便于环境搭建、结果可视化和二次开发。代码经过多环境严格测试运行稳定并配有详细设计文档若在配置环境或复现过程中遇到问题还可获得远程指导。目前已有44人浏览学习适合需要完整赛题参考或实践XGBoost分类建模流程的读者。1. 保险用户评估模型不是回归XGBoost分类与特征工程先讲清保险用户评估模型Kaggle竞赛在很多人的印象里是XGBoost的默认科目但这个数据集真正的考点是目标变量——一个 1 到 8 的有序评分。直接用回归拟合验证分数看起来也不差但竞赛评估口径完全对不上交上去分数会很尴尬。这套资源把特征工程和模型解释单独拎出来做了训练代码反而是最薄的一部分。它解决的是三个具体问题高缺失、高偏度特征怎么处理XGBoost 分类参数怎么定以及 SHAP 解释结果怎么对齐业务口径。适合正在做毕业设计、课程设计或课程作业的学生直接复现也适合初阶从业者拿来当特征工程的标准模板省去从零搭框架的重复劳动。2. 特征工程实战缺失率、偏度与频次编码怎么落到代码里2.1 响应变量的分布先画图再谈建模拿到这类保险评估数据第一步不是急着跑模型而是先看目标变量的分布。这个竞赛的预测目标是 1-8 的有序评分看起来是个回归任务但实际提交按分类口径评估所以先确认每个分数档的样本量能避免后面很多误判。我一般会先画一张分布图确认类别是不是均衡。常见的现象是中间档4、5、6样本量极大两端1、8很少这种分布下模型天然偏向预测中间档最后的业务结论会失真。import pandas as pd import matplotlib.pyplot as plt data pd.read_csv(train.csv) # Response 是目标列分数范围 1-8 value_counts data[Response].value_counts().sort_index() plt.figure(figsize(8, 4)) value_counts.plot(kindbar, color#5B9BD5) plt.title(Response Distribution) plt.ylabel(count) plt.show()这段代码里value_counts()统计每个分数出现的次数sort_index()确保横轴按 1 到 8 的自然顺序排列不然画出来的图顺序会乱。如果发现两端类别占比明显偏低后面切分训练集和验证集时就要用分层采样避免验证集里缺少极端类别的样本。from sklearn.model_selection import train_test_split X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, stratifyy, random_state42 )stratifyy的意思是按 y 的类别比例切分保证训练集和验证集里每个分数档的比例大致一致。这一点在有序分类任务里尤其重要因为 1 档和 8 档样本本来就少不做分层的话验证集里可能只有两三个 8 档样本评估指标波动会非常大。2.2 缺失与偏度两个阈值决定处理策略这个竞赛数据集的特点是列数多、缺失多、偏度大特征工程的重头戏就在这段。常见做法是先看缺失率再算偏度。很多人喜欢直接删除缺失率超过 50% 的列这种一刀切的做法在保险数据里会丢掉一些强特征。保险问卷里有些字段只有极少数人填写填写了就说明用户情况特殊反而能直接拉高或拉低风险分。import numpy as np # 统计缺失率 missing_ratio data.isnull().mean().sort_values(ascendingFalse) high_missing missing_ratio[missing_ratio 0.3].index.tolist() print(缺失率超过 30% 的列, high_missing) # 统计偏度 skewness data.select_dtypes(include[np.number]).skew().sort_values(ascendingFalse) high_skew skewness[skewness.abs() 0.75].index.tolist() print(高偏度特征, high_skew) # 对高偏度数值特征做 log1p 变换 for col in high_skew: if col in data.columns and data[col].dtype ! object: data[col _log] np.log1p(data[col].clip(lower0))skew()计算每列的偏度0.75 是我常用的阈值超过这个值分布明显不对称。clip(lower0)先把负值截断到 0避免log1p产生 NaN。这里有个细节XGBoost 对单调变换其实不敏感理论上 log 变换不影响树模型的最终分裂效果但在数据层面做 log 能让极端离群值的权重降下来树的分裂点不会总是集中在大值区间训练会更稳定。缺失值本身不用急着填。xgb.DMatrix原生支持缺失值训练时会自动学习缺失方向的默认分支这是树模型处理缺失的一大优势。你需要做的只是在构造 DMatrix 时保留 NaN不要用 0 或均值去填。用 sklearn 接口时要注意 Pandas 的 Nullable 类型偶尔会报类型错误转成 float 再喂进去就行。2.3 类别特征编码频次编码与基数判断数据集里有一批类别特征比如产品族、就业状态这类字段。类别基数差别很大有的只有两三个取值有的几十个取值。基数小的 one-hot 没问题基数大的用 one-hot 会把特征矩阵撑得很宽而且树模型在 one-hot 后的稀疏特征上分裂效率并不高。我一般用频次编码用每个类别出现的次数替换原值。# 对所有 object 类型的列做频次编码 for col in data.select_dtypes(include[object]).columns: freq_map data[col].value_counts() data[col _freq] data[col].map(freq_map)value_counts()统计每个类别的出现次数map()再把这个频次映射回每一行。这样做的直觉是出现次数多的类别代表常见客群频次本身就携带了群体规模信息。低频类别会被压缩到很小的数值但频次相近的不同类别容易被压成几乎相同的数值丢失区分度所以在编码前我会先做一次稀有类别合并。for col in data.select_dtypes(include[object]).columns: counts data[col].value_counts() rare_cats counts[counts 50].index.tolist() data[col] data[col].apply(lambda x: other if x in rare_cats else x)把出现次数少于 50 的类别统一替换成other再跑频次编码。这样低频类别不再各自为政合并成一个整体后反而能稳定贡献信息。阈值 50 是我在几万行样本量下的常用值如果你的样本量翻倍这个阈值可以按比例上调。3. XGBoost参数与早停从基线到稳定模型的调参次序3.1 参数表与选型理由调参最忌讳一上来就随机搜索先把每个参数的含义和合理区间写清楚调起来才有方向。这张表基本覆盖了这类保险评估任务的核心参数。参数作用常见取值调参方向learning_rate每棵树贡献的权重0.01 ~ 0.1越小越稳但需要更多树max_depth树的深度4 ~ 6样本量不大时别拉太高min_child_weight叶子节点最小样本权重和3 ~ 8调大防止过小叶子subsample行采样比例0.7 ~ 0.9小数据集可降到 0.7colsample_bytree列采样比例0.6 ~ 0.8增加特征多样性num_class类别数8对应 1-8 评分的类别数这类保险数据通常只有几万行max_depth拉到 7 以上很容易记住噪声min_child_weight我会从 5 开始调让每个叶子节点至少有足够的样本权重分裂出来的分支不会太碎。subsample和colsample_bytree是防止过拟合的主力尤其是特征列数多的时候列采样让每棵树看到不同的特征组合整体泛化更稳。调参次序我习惯这样走先固定learning_rate0.03、max_depth5把min_child_weight从 3 到 8 拉一遍确定最优值后再动subsample和colsample_bytree最后才考虑要不要把learning_rate降得更低。前两步的搜索步长可以大一点后两步收敛后再做小范围精调。3.2 训练脚本早停、评估指标与特征重要性训练部分核心代码不长但每个参数都有讲究。import xgboost as xgb from sklearn.model_selection import train_test_split X data.drop(columns[Response]) # 目标列从 1-8 对齐到 0-7匹配 softprob 的类别索引 y data[Response] - 1 X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) dtrain xgb.DMatrix(X_train, labely_train) dval xgb.DMatrix(X_val, labely_val) params { objective: multi:softprob, num_class: 8, eval_metric: mlogloss, learning_rate: 0.03, max_depth: 5, min_child_weight: 5, subsample: 0.8, colsample_bytree: 0.7, seed: 42, } watchlist [(dtrain, train), (dval, valid)] bst xgb.train( params, dtrain, num_boost_round2000, evalswatchlist, early_stopping_rounds50, verbose_eval100, ) # 用 gain 衡量特征重要性 importance bst.get_score(importance_typegain) sorted_imp sorted(importance.items(), keylambda x: x[1], reverseTrue) for fea, gain in sorted_imp[:20]: print(f{fea}: {gain:.2f})这里刻意用了multi:softprob而不是multi:softmax。两者的区别在于输出softmax直接输出预测类别softprob输出的是每个样本属于 8 个类别的概率矩阵后面做 SHAP 解释和业务分档都需要概率而不是硬分类结果。eval_metricmlogloss配合多分类任务watchlist同时监控训练集和验证集的损失early_stopping_rounds50表示验证损失连续 50 轮不下降就停止并自动回滚到最佳迭代数。verbose_eval100是每 100 轮打印一次日志免得刷屏。特征重要性用gain而不是默认的weight。weight统计的是特征被分裂的次数次数多不代表贡献大gain统计的是该特征参与分裂时带来的平均增益更能反映真实贡献。打印 top20 之后你会发现有些分列次数很高的特征根本不在 gain 排序前列这种就是靠频繁分裂但没有实质区分度的特征。3.3 特征筛选的反直觉结论拿到 gain 排序后最常见的操作是取 topN 重训模型但结果往往是验证分数不升反降。原因是相关性高的特征之间重要性会被分散到多个列上单独删掉任意一个它的信息会转移到其他特征模型不一定变差也不一定变好。所以我不直接按重要性排序删特征而是做「删除-重训」对比。from sklearn.metrics import log_loss base_pred bst.predict(dval, iteration_range(0, bst.best_iteration 1)) base_loss log_loss(y_val, base_pred, labelslist(range(8))) # 删除最末位特征后重训对比 loss 变化 for fea in reversed(sorted_imp): X_drop X.drop(columns[fea]) # 用相同参数重新训练比较验证 loss # 若 loss 几乎不变说明该特征冗余 # 若 loss 明显上升停止删除这个循环很机械但对列数较多的保险数据有效。每次删除 gain 排序最末的特征重训并记录验证 loss直到 loss 出现明显上升就能确定冗余特征的边界。这一步同时能减小后续 SHAP 解释时的计算量和内存压力一举两得。4. 避坑记录复现这套资源时的五个常见事故4.1 压缩包里混入无关文件先分清主次再动手现象解压后文件列表里除了 Python 脚本和数据文件还混着ds1302.c、UART.c、DHT11.c、main.c、key.c这些单片机驱动代码甚至还有一个tset.Debug.cspy.bat批处理文件一看就是嵌入式工程的东西和保险评估模型完全不搭边。原因资源在整理打包时把多个项目的目录混在了一起或者原作者的某个工作目录里有旧工程残留压缩时一并拉了进去。这和项目本身的质量无关纯粹是打包不规范。解决忽略所有.c、.h、.bat文件在目录里找train.csv、xgboost_train.py、shap_explain.py这类和机器学习直接相关的文件即可。如果压缩包里有 README 或设计文档优先按文档里的运行说明来找入口别在无关文件上浪费时间。4.2 XGBoost版本升级导致接口崩溃现象运行训练脚本时报AttributeError: XGBClassifier object has no attribute use_label_encoder或者用 sklearn 封装接口训练时突然卡死、参数传进去没反应。原因网上流传的很多代码是基于旧版 XGBoost 写的新版本废弃了部分参数接口行为也变了。最典型的是use_label_encoder这个参数旧代码里不写会告警新代码里写了反而报错。解决如果代码用的是XGBClassifier封装接口构造时显式传参关闭 label encoderXGBClassifier(use_label_encoderFalse, eval_metricmlogloss)。不想纠结版本就直接改用xgb.train原生接口也就是第 3.2 节的写法参数和行为更稳定。再不行就固定版本pip install xgboost1.6.2配合竞赛代码的兼容性一般没问题。4.3 标签列混进特征矩阵验证分数高得离谱现象训练过程一切正常验证分数好到不真实但自己拿一份新数据测试时结果一塌糊涂像是模型完全没学到规律。原因数据处理阶段没有把目标列从特征矩阵里剔除。尤其是做特征派生时用了全列操作Response列也被 map 成了数值后面 drop 的时候列名列表没写全残留在了训练矩阵里。XGBoost 会直接利用这一列做分裂验证时相当于变相「偷看答案」。解决在构造训练集之前加一行断言assert Response not in X.columns, 目标列泄漏进特征矩阵之后每次派生新特征都从列名集合里显式排除目标列不要只依赖drop的列名列表兜底。这个小习惯能防住绝大多数标签泄漏事故。4.4 SHAP与XGBoost版本冲突解释脚本跑不起来现象shap.TreeExplainer(bst)直接报TypeError或AttributeError但模型本身可以正常预测能输出结果一到解释环节就崩。原因SHAP 的 TreeExplainer 对不同版本的 XGBoost booster 对象兼容性不一致新版本改了内部节点结构旧版 SHAP 解析不了。解决先用原生接口加载模型文件再传给 TreeExplainerbst xgb.Booster() bst.load_model(model.json) explainer shap.TreeExplainer(bst)pip install xgboost1.6.2 shap0.42.1这两个版本组合在常见竞赛代码里匹配度较高。如果还报错优先看异常栈里是xgboost的包还是shap的包分别固定版本后再试。4.5 早停轮数设太小模型没长大就停了现象训练日志里显示best iteration在第 15 轮early stopping在第 65 轮触发验证分数平平无奇加树也救不回来。原因learning_rate设得很小比如 0.01时收敛速度慢需要上千轮才能充分训练而early_stopping_rounds只设了 20验证分数稍微震荡一下就触发了停止条件模型还没长大就被叫停了。解决把早停放宽到 50~100先把learning_rate提到 0.03 再观察曲线。如果还是很快触发早停说明特征工程阶段没做出区分度重点回看 2.2 节里那些高缺失列是不是被直接删掉了。早停参数不是越小越省时间它应该配合学习率一起看。注意训练日志里的best iteration和最终num_boost_round是两个概念best iteration是验证分数最优的那一轮早停只保证它能被找到不保证它足够好。5. 模型解释用SHAP拆开特征贡献的黑匣子5.1 SHAP值特征贡献的拆解方式模型训练完最常见的追问是「哪些特征最重要」。树模型自带特征重要性但它只回答「这个特征被用了多少次、带来了多少增益」不回答「它把预测推向了哪个方向」。SHAP 解决的是后一个问题。import shap # bst 是第 3 节训练好的 booster explainer shap.TreeExplainer(bst) shap_values explainer.shap_values(X_val) # 画全局特征重要性 shap.summary_plot(shap_values, X_val, max_display20)shap_values在多分类任务里是一个三维数组维度是(样本数, 特征数, 类别数)summary_plot会自动聚合所有类别的贡献。横轴代表 SHAP 值正值把预测推向更高的分数档负值推向更低的分数档。max_display20控制图里显示的特征数量默认 10 个太少了这类保险数据集特征列多20 个才能看出全貌。这里有个反直觉的点SHAP 排序靠前的特征不一定是数值上最大的特征而是对分类边界影响最大的特征。比如某个字段只有少数人填写一旦填写就决定用户被判定到高分档它的 SHAP 分布会呈现明显的两极分化比一个匀速影响所有样本的特征更值得关注。5.2 把SHAP结果对齐到业务口径模型输出是技术指标但在毕业设计答辩或课程作业评分时真正扣分的是「你知不知道这个特征为什么重要」。把 SHAP 均值排序和业务含义做成一张对照表是常见的交付方式。特征示例SHAP均值贡献业务解读Employment_Info0.62就业状态稳定性对风险评估影响最大Medical_History0.47医疗历史评分直接拉高或拉低风险档位Product_Info0.35不同产品族对应不同风险客群Ins_Age0.28年龄段的非线性影响明显这张表里的特征名是示例实际以你自己跑出来的排序为准。重点是「每个特征名带上业务含义」这一步——SHAP 均值只回答影响大小不回答为什么这需要你结合保险业务场景补上解释。比如Employment_Info贡献高是因为就业稳定性决定用户是否有持续缴费能力缺失或异常值往往是风险用户的信号。5.3 解释脚本里容易忽略的两个参数一个是max_display上面已经提过默认 10 个特征太少。另一个是shap.force_plot它只适合单个样本的局部解释不要拿它在几百个样本上跑图会糊成一团看不出任何信息。局部解释选 3-5 个有代表性的样本就够了比如预测分数最高和最低的样本各挑一个能看到同一批特征在不同方向上的拉动。如果数据量大shap_values计算也会很慢可以先对验证集抽样比如随机抽 500 行再算结论基本不会变。6. 把预测结果映射回业务三档一个一致性验证技巧竞赛指标通常关注 1-8 档的精确匹配或误差但实际业务沟通里很少有人关心精确到第几档更多是说「低风险、中风险、高风险」。这时候可以做一个后处理映射把模型输出的 8 类概率加权求和成一个期望分数再按阈值切成三档然后用这个三档结果做一致性验证。# preds 形如 [n_samples, 8]来自 softprob 输出 expected_score (preds * np.arange(1, 9).reshape(1, -1)).sum(axis1) def risk_level(score): if score 3.5: return low elif score 6.0: return medium else: return high y_pred_level [risk_level(s) for s in expected_score]np.arange(1, 9)生成 1 到 8 的分数向量reshape(1, -1)把它变成(1, 8)的形状这样和概率矩阵做乘法时每一行的 8 个概率分别乘以对应的分数档再求和就得到该样本的期望分数。阈值 3.5 和 6.0 不是拍脑袋写的我是按训练集上原始标签的中位数和上四分位数标定的。你也可以换成np.percentile(y_train, [50, 75])来自动确定避免人工选阈值引入偏差。这个映射最大的用途不是预测而是验证。把原始标签也切成三档和预测三档做混淆矩阵对比看误判主要集中在相邻档还是跨档。如果大量跨档误判说明模型在边界样本上不稳定优先去查是不是有特征在 2.2 节说的高缺失列表里。我有一次复现类似竞赛项目做完这个一致性检查后发现所有跨档误判样本的Employment_Info都是缺失值并不是模型能力不够而是缺失方向的分支学偏了。从那以后我每次拿到有序分类任务都强制走一遍「标签分布 → 基线 → 早停 → SHAP → 三档映射」的流程这套流程把这类项目在我手里的交付时间缩短了将近一半。希望帮到你。本文还有配套的精品资源点击获取