ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

健康险保费预测实战:从数据到模型完整指南

健康险保费预测实战:从数据到模型完整指南 简介这份资源面向具备一定机器学习基础、希望以真实业务数据完成回归建模全流程的学习者与数据从业者围绕个人健康保险费用预测这一经典场景展开。包内共22个文件以20个Python源代码为主另附1份csv数据集与1份说明文档压缩包约58KB代码与数据分离便于直接运行与二次修改。内容覆盖探索性数据分析、统计检验、特征工程与模型调优等环节涉及线性回归、岭回归、Lasso、弹性网络、多项式特征、支持向量回归、随机森林、XGBoost与决策树等多种算法并配合标准化、独热编码、互信息筛选、交叉验证与网格搜索等处理手段同时使用方差膨胀因子、Box-Cox变换、偏度与峰度等指标辅助诊断。目前已有77人学习适合作为课程设计、竞赛练手或面试项目的参考范例帮助读者理解从数据清洗到模型评估的完整链路并积累可复用的调参排错经验。1. 从一份 54 KB 的保单数据说起健康险保费到底能不能被算准手里拿到一份个人健康保险费用数据集54.32 KB字段不多行数也不算大第一反应往往是「这么点数据能跑出什么模型」。但真正做过保险定价的人会告诉你保费预测这件事的难点从来不在数据量而在特征之间的耦合关系年龄、BMI、吸烟与否、地区、子女数这几个变量彼此纠缠线性模型经常在吸烟人群上给出离谱的低估。这个标题指向的正是这样一类实战任务——用一份结构清晰的健康险保单数据从零跑通「读数据、看分布、建特征、训模型、评误差」的完整链路配套 20 个源代码文件把每一步拆开。它适合两类人一类是想找一个干净数据集练手回归建模的新手另一类是想看看保险场景下特征工程该怎么做的从业者。我下面讲的路径就是围绕这份数据最常见的落地方式展开。2. 先搞清楚这份数据能回答什么字段、分布与建模目标2.1 字段含义与预测目标的确定健康险费用数据集通常包含 age、sex、bmi、children、smoker、region、charges 这几列前六列是特征最后一列 charges 是年度医疗费用也就是我们要预测的目标。这里有个容易被忽略的点charges 不是保费本身而是理赔支出保险公司用它来反推定价。所以建模目标严格说是「预测个人年度医疗支出」而不是「预测保费」。这个区别决定了你后面评估模型时该看什么指标——绝对误差MAE比相对误差更贴近业务因为定价部门关心的是「平均每人差多少钱」。先做一次字段体检确认没有缺失和异常import pandas as pd import numpy as np df pd.read_csv(insurance.csv) print(df.shape) # 行数、列数 print(df.dtypes) # 每列类型object 列需要编码 print(df.isnull().sum()) # 缺失值统计 print(df.describe()) # 数值列分布重点看 charges 的偏度 print(df[charges].skew()) # 偏度大于 1 说明右偏严重这段代码的作用是先建立数据的基本印象。shape告诉你样本规模dtypes决定哪些列要做编码isnull排查脏数据describe和skew则直接暴露 charges 的分布形态。参数上没什么可调的但要注意如果skew()返回值超过 1说明费用分布被少数高额理赔拉长了右尾后面建模时要么对目标做对数变换要么选对异常值不敏感的模型。2.2 三个必须提前看的分布特征在动手建模前有三张图值得先画出来它们会直接改变你的特征处理策略。第一张是 charges 的直方图。健康险费用几乎必然右偏大部分人花费不高少数人花费极高。如果你不做处理直接上线性回归模型会被高额样本带偏导致对普通人群的预测偏高。第二张是 smoker 与 charges 的分组箱线图。吸烟人群的费用分布和 non-smoker 几乎是两个世界这个变量大概率是全场最强的单一特征。如果分组后差异巨大你就该考虑给它做交互项而不是当成普通类别丢进去。第三张是 bmi 与 charges 的散点图按 smoker 着色。很多版本的数据里BMI 超过 30 且吸烟的人群会出现一个明显的费用跃升这就是典型的交互效应。import matplotlib.pyplot as plt import seaborn as sns fig, axes plt.subplots(1, 3, figsize(18, 5)) sns.histplot(df[charges], kdeTrue, axaxes[0]) sns.boxplot(xsmoker, ycharges, datadf, axaxes[1]) sns.scatterplot(xbmi, ycharges, huesmoker, datadf, axaxes[2]) plt.tight_layout() plt.show()逻辑说明第一张图判断是否需要 log 变换目标第二张图确认 smoker 的区分度第三张图寻找交互项线索。参数上huesmoker是关键它让散点按吸烟状态分色交互效应一眼可见。如果散点图里两类颜色明显分层且斜率不同就说明 bmi 和 smoker 存在交互值得手动构造bmi_smoker bmi * (smoker yes)这样的特征。3. 特征工程把 6 列原始字段变成模型能吃的输入3.1 类别编码与数值标准化原始数据里 sex、smoker、region 都是字符串模型读不了。最常见的做法是 one-hot 编码但要注意 region 有四个取值one-hot 后会多出四列如果样本量小容易稀释信息。我的习惯是二值类别sex、smoker用 0/1 映射多值类别region用 one-hot 并加drop_firstTrue避免共线性。from sklearn.preprocessing import StandardScaler df[sex] df[sex].map({male: 1, female: 0}) df[smoker] df[smoker].map({yes: 1, no: 0}) df pd.get_dummies(df, columns[region], drop_firstTrue) num_cols [age, bmi, children] scaler StandardScaler() df[num_cols] scaler.fit_transform(df[num_cols])逻辑说明先做二值映射再做 one-hot最后对连续变量标准化。drop_firstTrue是防止虚拟变量陷阱让 region 的四个取值用三列表示。标准化只对 age、bmi、children 做因为 charges 是目标不能动而 sex、smoker 已经是 0/1 不需要再缩放。参数上StandardScaler默认按列减均值除标准差如果你的模型是树模型随机森林、XGBoost标准化其实可以跳过但线性模型和神经网络必须做。3.2 构造交互项让模型看见 BMI 与吸烟的叠加效应前面散点图如果确认了交互效应就该手动加特征。树模型理论上能自己学出交互但在样本量不大时手动给一个往往收敛更快、效果更稳。df[bmi_smoker] df[bmi] * df[smoker] df[age_smoker] df[age] * df[smoker] df[bmi_age] df[bmi] * df[age]逻辑说明bmi_smoker捕捉「肥胖且吸烟」的高风险组合age_smoker捕捉「年长且吸烟」的叠加bmi_age则是两个连续变量的乘积。这三个特征在保险定价里都有业务含义不是瞎凑。参数上没什么可调的但要注意加了交互项后原始特征最好保留让模型自己决定权重不要直接替换。3.3 目标变量要不要取对数如果前面skew()显示 charges 严重右偏可以试一版对目标取对数再训练预测时再指数还原。df[log_charges] np.log1p(df[charges])逻辑说明log1p是 log(1x)避免 charges 为 0 时报错。取对数后分布更接近正态线性模型的残差会更规整。但要注意取对数后评估指标不能直接和原始尺度比需要还原后再算 MAE。我的经验是线性模型取对数收益明显树模型收益有限可以两版都跑用交叉验证的 MAE 决定。4. 建模与评估从线性回归到梯度提升的完整对比4.1 切分数据与建立基线任何建模都该先有一个基线否则你不知道复杂模型到底带来了多少提升。健康险费用预测里线性回归就是最合适的基线。from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error, r2_score X df.drop(columns[charges, log_charges]) y df[charges] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) lr LinearRegression() lr.fit(X_train, y_train) pred_lr lr.predict(X_test) print(MAE:, mean_absolute_error(y_test, pred_lr)) print(R2:, r2_score(y_test, pred_lr))逻辑说明train_test_split按 8:2 切分random_state42保证可复现。线性回归直接拟合输出 MAE 和 R2。参数上test_size可以调成 0.15 或 0.25 看稳定性但 0.2 是最通用的选择。MAE 的单位是金额直接反映平均预测偏差R2 反映解释方差比例但保险数据里 R2 通常不会特别高0.7 以上就算不错。4.2 随机森林与梯度提升的对比线性模型跑通后换树模型看提升。随机森林稳梯度提升如 GradientBoostingRegressor 或 XGBoost通常更强但需要调参。from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor rf RandomForestRegressor(n_estimators300, max_depth6, random_state42) rf.fit(X_train, y_train) pred_rf rf.predict(X_test) gb GradientBoostingRegressor(n_estimators300, learning_rate0.05, max_depth3, random_state42) gb.fit(X_train, y_train) pred_gb gb.predict(X_test) for name, pred in [(RF, pred_rf), (GB, pred_gb)]: print(name, MAE:, mean_absolute_error(y_test, pred), R2:, r2_score(y_test, pred))逻辑说明随机森林用 300 棵树、最大深度 6 控制过拟合梯度提升用 300 棵树、学习率 0.05、深度 3这是比较保守的配置。参数上n_estimators越大越稳但越慢max_depth是防过拟合的关键learning_rate和n_estimators要配合调——学习率小就需要更多树。如果 MAE 比线性回归还差多半是树太深过拟合了先把max_depth降到 3 试试。4.3 特征重要性验证你的交互项有没有用树模型训练完可以直接看特征重要性这是验证特征工程是否有效的直接手段。import pandas as pd importance pd.Series(gb.feature_importances_, indexX.columns) print(importance.sort_values(ascendingFalse))逻辑说明把feature_importances_和列名对应起来排序看哪些特征贡献大。如果bmi_smoker排进前五说明交互项构造成功如果region的几列全在末尾说明地区信息在这份数据里几乎没用可以考虑删掉简化模型。参数上没什么可调的但要注意树模型的重要性对高基数类别有偏好one-hot 出来的列会分散重要性解读时要合并看。5. 避坑与排查这份数据里最容易翻车的 4 个地方5.1 现象模型在测试集上 MAE 很低但预测值出现负数原因线性回归没有约束输出范围遇到极端特征组合时会外推出负的费用。健康险费用不可能为负这是典型的模型与业务约束脱节。解决对预测结果做下界截断pred np.clip(pred, 0, None)或者改用对数目标训练还原后天然为正。更彻底的做法是换用对输出有正约束的模型比如 Gamma 回归但实现成本高截断是最实用的后悔药。5.2 现象加了 region 的 one-hot 后模型反而变差原因region 有四个取值one-hot 后增加三列如果样本量本来就不大这三列会稀释有效信息还可能引入噪声。有些版本的数据里地区差异本身就很弱。解决先看特征重要性如果 region 相关列全部垫底直接删掉。判断标准是删掉后交叉验证 MAE 没有明显上升就说明它没用。不要因为「字段里有」就硬塞进模型。5.3 现象训练集 R2 很高测试集 R2 掉一大截原因过拟合。常见于树模型深度太大、叶子节点样本太少或者特征里混入了和目标强相关的泄漏变量。解决先把max_depth降到 3 到 5min_samples_leaf提到 5 以上再用交叉验证代替单次切分看多折之间的方差。如果方差很大说明数据划分敏感样本量可能不够考虑用 K 折交叉验证的均值作为最终指标。5.4 现象smoker 编码后模型效果没变化原因编码映射写反了或者没生效。比如map({yes: 1, no: 0})如果数据里实际是 Yes/No 大写映射会全部变成 NaN模型直接忽略这列。解决编码后立刻print(df[smoker].unique())确认只剩 0 和 1。这个检查花不了几秒但能省掉半天排查。血泪经验类别映射后一定要验证唯一值别信自己的记忆。6. 把模型用起来交叉验证调参与一个可复用的评估习惯单次切分的评估结果波动很大尤其是这份数据只有一千多行。我一般会用 5 折交叉验证把 MAE 的均值和标准差都打出来标准差比均值更能说明模型稳不稳。from sklearn.model_selection import cross_val_score from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler pipe Pipeline([ (scaler, StandardScaler()), (model, GradientBoostingRegressor(n_estimators300, learning_rate0.05, max_depth3, random_state42)) ]) scores cross_val_score(pipe, X, y, scoringneg_mean_absolute_error, cv5) print(MAE per fold:, -scores) print(Mean MAE:, -scores.mean(), Std:, scores.std())逻辑说明用 Pipeline 把标准化和模型串起来避免交叉验证时标准化在折外数据上泄漏。scoringneg_mean_absolute_error是因为 sklearn 的交叉验证默认越大越好MAE 要取负。cv5是五折。参数上折数可以调到 10但 5 折在千行级数据上是精度和耗时的平衡点。看结果时如果某一折的 MAE 明显高于其他折说明数据里有影响很大的异常样本值得单独拎出来看。调参时我习惯先粗后细先固定学习率 0.05把n_estimators从 100 试到 500找到 MAE 不再下降的点再在这个点上微调max_depth和min_samples_leaf。不要一上来就网格搜索所有参数组合爆炸且容易过拟合验证集。最后说个我自己的习惯每次跑完模型我都会把测试集里预测误差最大的十条样本单独打印出来看它们的特征组合有什么共性。十次里有八次能发现数据问题或者新的特征线索比盯着总体 MAE 有用得多。这份数据不大但把这条链路走通、把每个坑踩一遍比拿一个现成的高分模型更有价值。希望帮到你。本文还有配套的精品资源点击获取
返回列表