简介面向机器学习与数据分析学习者这套个人健康保险费用预测实战包围绕个人健康保险费用数据集完成从EDA探索、统计检验、特征工程到回归建模的完整流程。压缩包约58KB共22个文件含20个Python源代码脚本、1份CSV原始数据和1个readme说明文档代码覆盖线性回归、岭回归、Lasso、ElasticNet、随机森林、决策树、XGBoost、SVR等主流回归模型并集成StandardScaler、PolynomialFeatures、OneHotEncoder、互信息特征选择等方法适用于系统学习回归任务与评估指标。所有源码手工整理、无语法错误可直接运行复现还包含VIF共线性诊断、Box-Cox变换、Bokeh交互可视化等进阶内容整个流程清晰可参照调试。目前已有77人浏览学习适合准备机器学习项目实践或Kaggle风格练习的读者。1. 个人健康保险费用预测一个 54 KB 的 ZIP 能把回归实战学到什么程度如果你刚学机器学习最容易遇到的尴尬是数据集太大、训练太慢、代码太长一晚上根本跑不完。而这个标题里的数据集反着来——整个 ZIP 压缩包只有 54.32 KB里面装着 20 个源代码文件数据量小到任何一台普通笔记本都能秒开。但数据小不代表问题简单个人健康保险费用的分布严重右偏少数高额理赔样本会把线性模型带偏这个坑我在第一次复现时就踩过。它适合三类人想完整走一遍结构化数据回归全流程的初学者、需要课程设计或简历项目的学生、以及想快速验证自己特征工程和调参思路的从业者。项目核心就一句话用不到 20 列的健康信息预测一个人的年度保险费用。2. 先看数据再动手保险费用数据集的字段语义与分布体检拿到 ZIP 包第一步不是建模而是把数据读懂。你要是连字段含义都没确认就开始跑模型后面所有结论都是空中楼阁。这节我把这个数据集最常见的字段过一遍再用几行 Pandas 完成最基础的“体检”。2.1 字段逐个过哪些是数值、哪些是类别、哪些最容易踩坑这个数据集通常包含 age、sex、bmi、children、smoker、region 和 charges 这几个字段charges 是我们要预测的目标值。下面这张表是按建模视角整理的字段语义会对后面的编码和特征选择起决定作用。字段类型典型取值建模建议age连续数值18-64可直接用也可分箱后使用sex二分类male / femaleLabelEncoder 即可bmi连续数值15-53 左右先检查极端值别急着删行children离散计数0-5可当数值也可当有序类别smoker二分类yes / noLabelEncoder是保费最强影响因素region名义类别4 个地区必须 OneHot不能直接 LabelEncodercharges连续数值右偏分布回归目标考虑 log 变换最容易踩坑的是 charges 的分布形态。正常情况下它的中位数远小于均值90 分位和 99 分位之间差距非常大这意味着少数高额理赔样本几乎主导了整体误差。如果你直接拿原始值做 MSE 评估模型会拼命去拟合那几个极端样本普通人的费用反而不准。另一个容易翻车的是 region四个地区是并列关系不是大小关系编码方式错了线性模型的系数解读就直接崩掉。2.2 用 Pandas 做缺失值与分布初检三行代码看清全局拿到数据之后我一般的习惯是先跑一段最基础的探查代码内容就三件事看形状、看类型、看分布。这段代码不涉及任何建模但能帮你省下后面至少两个小时的排查时间。import pandas as pd import numpy as np df pd.read_csv(data/insurance.csv) print(shape:, df.shape) print(df.dtypes) print(df.describe(percentiles[0.25, 0.5, 0.75, 0.9, 0.99]).T) print(缺失值总数:, df.isnull().sum().sum())逻辑说明df.describe()默认只给 25/50/75 分位我额外加了 0.9 和 0.99目的是专门观察 charges 的长尾程度。如果 99 分位是 75 分位的几倍甚至十几倍说明目标值右偏严重后面建模必须考虑对目标做变换。df.isnull().sum().sum()直接汇总全部缺失值这个数据集通常没有缺失但养成检查习惯不会错。参数说明percentiles接收一个列表可以按需加 0.01、0.95 之类的分位点df.describe().T转置是为了让字段名变成行索引、统计量变成列输出更符合阅读习惯。如果你发现 bmi 存在 50 以上的值先别判定是异常值结合业务去想——极端肥胖人群的 bmi 确实可能到 50这在保险场景里是真实样本要保留而不是删除。3. 特征工程让 BMI、年龄、吸烟这些原始特征变成模型能消化的东西这个数据集的原始特征很少如果不做特征工程模型上限非常有限。但特征工程也不是越多越好关键是让每个特征都有明确的业务含义。我一般会从三个方向入手类别变量编码、连续变量分箱、构造交互项。3.1 类别变量编码为什么吸烟用 LabelEncoder、地区用 OneHot很多初学者拿到类别变量就统一 LabelEncoder这在二分类场景下没问题但遇到多分类名义变量就会埋雷。sex 和 smoker 都只有两个值LabelEncoder 编码成 0/1 不会产生顺序假象而 region 有四个取值如果把它映射成 0/1/2/3模型会默认地区之间存在大小关系这完全不符合业务逻辑。from sklearn.preprocessing import LabelEncoder import pandas as pd df_enc df.copy() le LabelEncoder() df_enc[sex] le.fit_transform(df_enc[sex]) df_enc[smoker] le.fit_transform(df_enc[smoker]) region_encoded pd.get_dummies(df_enc[region], prefixregion) df_enc pd.concat( [df_enc.drop(columns[region]), region_encoded], axis1 ) print(df_enc.head())逻辑说明fit_transform会先学习类别到数值的映射再完成转换返回的是 numpy 数组pd.get_dummies把 region 展开成多个 0/1 列prefix参数给新列加上region_前缀避免列名冲突。pd.concat默认按索引横向拼接这里用axis1明确指定。参数说明如果用决策树或 XGBoost 这类树模型region 用 LabelEncoder 影响不大因为树模型能自行寻找切分点但线性回归和神经网络必须 OneHot。我给的建议是统一走 OneHot代码量差不了几行却能保证你在任何一种模型上都安全。如果你担心 OneHot 后维度爆炸——这个字段只有四个地区最多多出三列完全没有心理负担。3.2 连续变量做分箱、再加交互项年龄×吸烟为什么值得做年龄对保费的影响不是线性的20 岁的年轻人和 30 岁的年轻人费用差距不大但 50 岁之后保费会加速上涨。直接扔年龄给线性模型模型只能拟合出一条直线无法表达这种分段关系。常见做法是分箱把年龄切成几段后让模型对每一段单独学一个系数。import pandas as pd df_enc[age_bin] pd.cut( df_enc[age], bins[0, 30, 45, 60, 100], labels[0, 1, 2, 3] ).astype(int) df_enc[age_smoker] df_enc[age] * df_enc[smoker] df_enc[bmi_obese] (df_enc[bmi] 30).astype(int) print(df_enc[[age, age_bin, smoker, age_smoker, bmi, bmi_obese]].head())逻辑说明pd.cut按bins把连续值切段labels给每段起名.astype(int)把类别型标签转成整数供模型消费。age_smoker是年龄和吸烟标记的乘积用来捕捉“吸烟者的保费随年龄增长更快”这个交互效应。bmi_obese是把连续 bmi 变成临床意义的肥胖标记世界卫生组织的标准是 bmi 大于等于 30 为肥胖。参数说明bins[0, 30, 45, 60, 100]这个切法不是固定的你可以根据数据分布调整比如把 18-25 单独切一段。分箱数量太细会让树模型过拟合太粗又丢失信息我的经验是 4 到 5 段对这个小数据集最稳。这里有个小提醒如果你对同一份数据既保留原始 age 又保留 age_bin会引入共线性我一般会二选一后面的建模章节我会解释具体取舍。4. 建模与调参从线性基线上手到树模型的完整预测链条特征准备好之后进入建模环节。这一章我会按两条路线走先用线性回归做基线再用 XGBoost 做增强最后用一组统一的评估指标对比。中间穿插我在训练这个小数据集时踩过的坑和参数调整思路。4.1 基线模型线性回归与 Ridge先摸清天花板线性回归的最大价值不是拿最终名次而是给你一个“最差也能到多少分”的地板参考。如果线性模型就能做到 R² 0.75那树模型做不到 0.85 就说明你的特征或调参有问题而不是数据不行。这个数据集大概一千多行单次划分验证就够了但后面我会补充交叉验证的必要性。from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score import numpy as np X df_enc.drop(columns[charges, age]) y df_enc[charges] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) pipe Pipeline([ (scaler, StandardScaler()), (lr, LinearRegression()) ]) pipe.fit(X_train, y_train) y_pred pipe.predict(X_test) print(RMSE:, np.sqrt(mean_squared_error(y_test, y_pred))) print(MAE:, mean_absolute_error(y_test, y_pred)) print(R2:, r2_score(y_test, y_pred)) print(预测值范围:, y_pred.min(), y_pred.max())逻辑说明Pipeline把标准化和线性回归串成一个整体fit时会先对 X_train 做标准化再训练模型测试集在predict时会自动用训练集上学习到的均值和方差做转换不会引入数据泄漏。这里我drop掉了原始age只保留age_bin避免两个特征表达同一信息造成冗余。参数说明test_size0.2表示拿 20% 数据做验证random_state42固定随机种子保证结果可复现。打印y_pred.min()是有意的——如果你发现预测值出现负数那就说明原始目标值的右偏已经让线性模型失衡了下一步要么做目标变换要么换树模型。我在第一次跑的时候预测最小值是 -2200 左右费用不可能为负这个现象就是模型假设和数据分布不匹配的直接证据。4.2 树模型上场XGBoost 必调的 4 个参数线性模型把地板摸清楚之后换 XGBoost 上场提升上限。XGBoost 的参数非常多但这个数据集很小真正需要调的也就四个树的数量、最大深度、学习率和采样比例。其他参数用默认值不会差到哪里去。import xgboost as xgb model xgb.XGBRegressor( n_estimators500, max_depth4, learning_rate0.05, subsample0.8, colsample_bytree0.8, random_state42 ) model.fit( X_train, y_train, eval_set[(X_test, y_test)], verboseFalse ) y_pred_xgb model.predict(X_test) print(XGB RMSE:, np.sqrt(mean_squared_error(y_test, y_pred_xgb))) print(XGB MAE:, mean_absolute_error(y_test, y_pred_xgb)) print(XGB R2:, r2_score(y_test, y_pred_xgb))逻辑说明n_estimators是树的数量early_stopping_rounds必须在fit里传它会监控eval_set上的误差连续 20 轮没有改善就提前停止避免白白训练 500 棵树还过拟合。verboseFalse是关掉训练过程的日志输出让终端干净一点如果你想看每轮分数改成verboseTrue即可。参数说明max_depth4对小数据集是偏保守的设置深度太深容易把训练集背下来learning_rate0.05配n_estimators500是“小步慢跑”的组合比大步长更稳subsample0.8和colsample_bytree0.8分别是行采样和列采样加上这两项能让树之间更有差异减少过拟合。如果你发现训练集 R2 远高于测试集优先把max_depth降到 3或者把subsample降到 0.6。4.3 评估口径R²、MAE、RMSE 在费用预测里的偏向以及为什么要跑交叉验证很多新手只看 R²这是个很危险的习惯。R² 描述的是模型对目标方差的解释比例它会被极端值强烈影响。在费用预测这种右偏目标上一个对普通人群预测很准、对高额理赔完全失灵的黑匣子模型R² 依然可能很好看。因此我一般会把三个指标一起打印再按业务口径解释。from sklearn.model_selection import cross_val_score def evaluate(y_true, y_pred, name): rmse np.sqrt(mean_squared_error(y_true, y_pred)) mae mean_absolute_error(y_true, y_pred) r2 r2_score(y_true, y_pred) print(f{name}: RMSE{rmse:.2f}, MAE{mae:.2f}, R2{r2:.4f}) evaluate(y_test, y_pred, Linear) evaluate(y_test, y_pred_xgb, XGBoost) cv_scores cross_val_score(model, X_train, y_train, cv5, scoringr2) print(XGB 5折CV R2:, cv_scores.mean(), /-, cv_scores.std())逻辑说明cross_val_score会把训练集切成 5 份轮流拿 4 份训练、1 份验证最后返回 5 个 R² 分数。单次划分的结果可能因为随机种子差异产生明显波动小数据集尤其明显交叉验证能给出一个更稳的估计。scoringr2指定评估指标如果要改成 MAE就传scoringneg_mean_absolute_error注意 sklearn 的惯例是返回负值。参数说明cv5在这个数据规模下是合理选择样本再少可以降到 3。如果交叉验证的均值比单次划分低不少说明你的单次划分幸运地分到了一组简单测试样本实际泛化能力没那么好——这类“假高分”在简历项目里被面试官一问就会原形毕露所以这个小步骤别省。5. 避坑保险费用预测最常见的 5 个翻车点与排查清单这部分全是我自己复现这个数据集时实打实踩过的坑每一条都有现象、原因和解决方式。你照着做能省下好几个小时的排查时间这里面的问题几乎每个人都会遇到。5.1 线性模型预测出负费用这是个基本行为模型在训练过程中产生了一些预测值输出结果里居然出现负值。保费和费用一定是非负的出现负预测意味着模型学到的东西与业务常识矛盾。原因在于 charges 分布严重右偏少数高额样本逼迫线性模型在拟合时拉高整体的截距和系数结果在低费用区间产生了负向预测。解决方式是对目标变量做 log1p 变换也就是y_log np.log1p(y)训练完成后再用np.expm1(y_pred_log)还原回到原始的保费尺度。这个技巧既解决了右偏问题也顺带稳定了梯度。5.2 全量数据标准化后再切分造成数据泄漏一开始你可能为了省事写完切分就顺手对全部数据做StandardScaler().fit_transform()这样测试集的信息会混入训练过程里。现象是线下 R² 很漂亮一旦换一批数据重新评估就明显缩水。原因就是标准化器偷看了测试集的均值和方差这部分信息被模型间接使用了。解决方式是用Pipeline封装标准化和模型仍然按train_test_split先切分再fit让标准化逻辑只接触训练集。这也是我前面代码里坚持用Pipeline的原因不是多此一举。5.3 对 region 用 LabelEncoder现象是模型预测表现尚可但当你查看线性模型的系数时发现 region 系数的解读完全没法落地。原因是 LabelEncoder 给四个地区生成了 0/1/2/3模型默认地区 3 和地区 1 之间存在两倍的数值关系事实上它们是并列的类别。解决方式是改用 OneHot 编码四分类变量展开成三个虚拟变量丢给线性模型系数解读变为“相对于基准地区的增量影响”这才有业务意义。5.4 见到 bmi 极端值就删行bmi 出现 50 以上的值很容易被当成脏数据直接删掉这个数据集的样本量本身不大删行的代价比想象中高得多。更微妙的现象是删除这些极端 bmi 样本后 R² 反而降低。原因是高 bmi 往往与高费用样本绑定在一起删掉 bmi 极端样本等于删掉了一部分目标值的高尾分布模型更难学习长尾。解决方式是保留这些样本用分箱或缩尾处理。我倾向把 bmi 大于 95 分位的值压缩到 95 分位值用np.minimum(bmi, threshold)实现既保留样本数量又降低单点影响。5.5 只看 R² 不看残差XGBoost 的 R² 做到 0.86 时看起来比线性模型的 0.78 好不少但如果你把预测值和真实值画成散点图会发现在真实费用最高的区间预测值全部向均值方向回缩了。原因是回归模型用的平方损失追求全局最小长尾上少数样本的贡献再大也拼不过主体样本的数量。解决方式是分档看误差把测试集按真实费用切成低、中、高三段分别计算 MAE你会直观看到高费用段的误差远大于低费用段。如果有人问你模型哪里不行这个分档误差比 R² 更有说服力。6. 用 SHAP 给模型做体检预测的可解释性落地训练完模型只看评估分数不够我一般会再花 10 分钟用 SHAP 分析一下模型的决策逻辑。SHAP 是一个可以解释任何黑匣子模型的方法它能把每个样本的预测值分解成各特征的贡献之和。对这个数据集而言它能直接回答一个核心问题模型到底是靠什么特征预测出高保费的代码只需要几行。import shap explainer shap.TreeExplainer(model) shap_values explainer.shap_values(X_test) shap.summary_plot(shap_values, X_test)逻辑说明TreeExplainer专门用于树模型计算速度比通用的KernelExplainer快很多shap_values的形状和X_test一致每个值代表对应特征在该样本上的贡献。summary_plot会生成一张汇总图横轴是 SHAP 值纵轴按特征重要性排序点的颜色表示特征值的高低。从图上你会看到两个典型现象smoker 的 SHAP 值跨度最大说明吸烟是区分高低保费的第一特征bmi 的散点呈现正相关趋势但只有 bmi 超过 30 时贡献才明显变大。region 的贡献通常最小它的颜色分布几乎不随区域变化这就是模型在告诉你“这个特征别花太多时间调了”。如果你是脚本运行而不是 Jupytersummary_plot弹出窗口可能不方便可以加上showFalse然后用plt.savefig(shap_summary.png)保存图片。SHAP 的价值在于让模型输出具备业务可解释性下次向别人介绍项目时你能直接拿出“吸烟者保费高、肥胖加重费用、地区影响不大”这三个结论而不是一句干巴巴的“模型准确率是 0.86”。我第一次做这个数据集时就盯着一堆指标自我感觉良好直到画出 SHAP 图才意识到模型真正依赖的特征只有三四个前面的分箱和交互项大部分是冗余劳动。现在我养成的习惯是训练完成先看 SHAP再决定要不要改特征调整逻辑反而更直接。希望帮到你。本文还有配套的精品资源点击获取
