
简介面向保险数据分析与机器学习初学者的Python理赔支出预测源码包基于Kaggle健康保险医疗费用个人数据集完整实现了从特征可视化、数据探索到回归模型训练与评估的流程。压缩包共30个文件约1.25MB包含7个Python脚本、11个训练好的模型文件、10张特征关系图表、1份CSV数据集及1份README说明脚本划分清晰便于按步骤复现。代码覆盖线性回归、岭回归、Lasso、弹性网络及SVR等多种模型并附有模型对比脚本可直接运行查看不同算法在理赔支出预测上的效果。目前已有781人学习下载适合正在学习机器学习回归任务或需要处理保险类数据集并完成预测项目的读者参考。1. 理赔支出预测从一张表到一版能用线性回归交付的机器学习基线理赔支出预测是保险行业里最常见的机器学习落地题。用 Python 源码配一份健康保险理赔数据集做特征可视化再用线性回归跑出「预计赔付金额」新手能跟、熟手能复现。这篇笔记就沿着链路讲数据怎么理解、可视化怎么画、线性回归怎么训练、指标怎么解读、坑在哪里。很多人以为预测赔付必须上 XGBoost 甚至深度学习实际上一版基线用线性回归就够。它快、可解释业务能直接看懂「吸烟让赔付金额翻几倍」这类结论。适合保险风控、精算助理、数据分析师以及刚入门机器学习的 Python 开发者。先立住一个前提理赔支出预测是回归问题不是分类。输出是连续金额不是「赔/不赔」这决定了后续的评估方式、可视化角度和模型选择——所有动作都围绕「金额预测准不准」展开而不是「分类对不对」。2. 理赔数据集与特征可视化先看清数据再谈建模2.1 健康保险理赔数据集的字段构成做理赔支出预测手上这份数据集通常是「每行一个被保险人、每列一个特征、最后一列是理赔金额」的宽表结构。这类健康保险理赔数据集大多沿用 Medical Cost Personal 的字段设计常见 7 个字段字段类型说明age数值被保险人年龄理赔经验里年龄与慢性病风险强相关sex分类性别male / femalebmi数值身体质量指数体重kg除以身高m的平方children数值受抚养人数反映家庭保单结构smoker分类是否吸烟yes / no理赔金额最强的单一信号region分类居住地区northeast / northwest / southeast / southwestcharges数值理赔支出金额即预测目标 y拿到源码包先别急着跑模型第一件事是用 pandas 把形状、类型和缺失情况看一遍。数据通常在一两千行量级、十几个特征以内线性回归完全跑得动。需要注意目标列 charges 往往右偏得厉害大部分人数千元少数人几万元甚至更高这种长尾分布会把基于平方误差的线性回归拽着走所以后面一定少不了对数变换这一步。import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns df pd.read_csv(insurance.csv) print(shape:, df.shape) # 行数 × 列数 print(df.head()) # 前 5 行确认字段语义 print(df.info()) # 每列非空数量与 dtype print(df.describe(includeall).T)df.shape看数据集规模df.info()看有没有空值describe一次性输出数值列的均值、分位数和分类列的频次。跑完问自己三个问题有没有空值、有没有类型不对的列、charges 的均值是不是明显大于中位数。均值远大于中位数就是右偏的实锤之后的建模策略就得按这个分布来设计。2.2 可视化分析分布直方图、分组箱线图与相关性热力图特征可视化在这类项目里承担两个职责一是给业务看把「吸烟对赔付的影响」画成一目了然的图二是给自己看在训练前发现偏态分布、离群点和特征相关性。我画图按「目标分布 → 分组对比 → 相关性」的顺序来先看 y 的偏态再看关键特征的分组差异最后用相关矩阵确认哪些特征值得进模型。fig, axes plt.subplots(2, 2, figsize(14, 10)) # 1. 年龄分布 axes[0, 0].hist(df[age], bins30, edgecolorwhite) axes[0, 0].set_title(Age Distribution) # 2. 理赔金额分布重点看右偏程度 axes[0, 1].hist(df[charges], bins50, edgecolorwhite) axes[0, 1].set_title(Charges Distribution (right-skewed)) # 3. BMI 分布顺带检查极端值 axes[1, 0].hist(df[bmi], bins30, edgecolorwhite) axes[1, 0].set_title(BMI Distribution) # 4. 分类列转数值后画相关矩阵 df_num df.copy() df_num[sex] (df_num[sex] male).astype(int) df_num[smoker] (df_num[smoker] yes).astype(int) df_num pd.get_dummies(df_num, columns[region], drop_firstTrue) sns.heatmap(df_num.corr(numeric_onlyTrue), annotTrue, cmapRdBu, center0, axaxes[1, 1]) axes[1, 1].set_title(Correlation Matrix) plt.tight_layout() plt.show()pd.get_dummies(..., drop_firstTrue)把 region 转成 3 列哑变量并丢掉第一列避免和截距项共线corr(numeric_onlyTrue)只对数值列算皮尔逊相关系数annotTrue 在格子里显示数值。这张热力图通常会给出三个结论charges 和 smoker 的相关系数在 0.79 左右是压倒性的第一特征age 在 0.30 上下bmi 在 0.20 上下sex、region 的相关系数接近 0。也就是说后续模型里 sex 和 region 大概率不显著但为保持字段完整性我一般先留着。分组对比用箱线图更直观尤其适合给业务解释「为什么吸烟要被加费」fig, axes plt.subplots(1, 2, figsize(12, 5)) df.boxplot(columncharges, bysmoker, axaxes[0]) axes[0].set_title(Charges by Smoker) df[age_group] pd.cut(df[age], bins[0, 30, 50, 100], labels[30, 30-50, 50]) df.boxplot(columncharges, byage_group, axaxes[1]) axes[1].set_title(Charges by Age Group) plt.tight_layout() plt.show()吸烟者的理赔中位数和上四分位数整体抬高一截且高赔付的离群点几乎全落在吸烟组年龄分组里 50 岁以上组的中位数明显爬升。这两张图做完业务的第一个问题「哪些人理赔高」就有了答案吸烟、高龄、高 BMI三者叠加最危险。这个定性判断会和后面线性回归的系数互相印证也能用来复核模型有没有学歪。2.3 数据清洗的边界缺失值、重复值与不合理 BMI理赔数据在进模型前还要过一遍清洗但清洗要有边界不能凭感觉删。常见做法是检查三件事缺失值、重复行、物理上不可能的值。print(缺失值) print(df.isnull().sum()) print(重复行数, df.duplicated().sum()) # BMI 低于 10 或等于 0 在成年人里几乎不可能 print(df[df[bmi] 10])缺失值在这个数据集里通常很少直接看数量决定是删行还是补全重复行如果只有个位数一般直接drop_duplicates()删掉训练集多一行重复样本会让模型对这条记录过度加权BMI 小于 10 的记录要核实可能是录入错误也可能是孩子保单混了进来这时候结合 age 一起判断。注意清洗步骤必须在拆分训练集之前做但清洗规则一旦定下来就不要反复改不然验证集的「新鲜度」就没了。3. 线性回归建模从最小二乘到可解释的理赔预测3.1 为什么理赔预测首选线性回归而不是树模型理赔支出预测的初版模型我几乎无脑选线性回归。理由有四个。第一是快一两千行数据、十几个特征的 OLS 训练时间是毫秒级第二是可解释线性回归输出的是每个特征的系数业务方要的是「吸烟影响多大、年龄每长一岁赔付要加多少」这种能写进定价规则的结论而不是黑匣子的 feature importance第三是稳健特征少时线性回归不容易过拟合作为后续复杂模型的基线非常合适第四是和精算传统接得上——精算定价里常用的 GLM 就是线性回归在损失分布上的推广理赔金额用 log 连接函数时OLS 对 log(y) 回归就是它的工程近似系数可以直接平移过去理解。线性回归的数学形式是 y β₀ β₁x₁ … βₚxₚ ε通过最小化残差平方和求解 β也就是最小二乘估计。它的基本假设包括误差独立、同方差、近似正态。实战中我不会一上来做一堆统计检验先看残差图残差图合格就继续用不合格再想办法补——加交互项、加权最小二乘或者换分布假设。什么时候该放弃线性回归加完交互项、多项式之后残差图依然有明显的喇叭形或曲线形并且业务指标没有改善这时候再上 XGBoost、LightGBM 不迟。但即使上了树模型线性回归的系数表仍然有参考价值它告诉你在平均意义上哪个因子最值钱这也是机器学习模型落地时最容易被忽略的证据链。提示环境需要 Python 3.8 以上装好 pandas、numpy、scikit-learn、statsmodels、matplotlib、seaborn。如果还没装齐先pip install pandas numpy scikit-learn statsmodels matplotlib seaborn一次性补齐。3.2 特征预处理三步走哑变量编码、对数变换、数据拆分预处理阶段就三件事把分类变量编码成数值、把右偏的 y 取对数、按比例拆分训练集和测试集。顺序不能乱具体代码如下。from sklearn.model_selection import train_test_split # 1. 目标变量做对数变换缓解右偏 df[log_charges] np.log(df[charges]) # 2. 去掉原始目标列和辅助列分类变量 one-hotdrop_first 防共线 X df.drop(columns[charges, log_charges, age_group]) X pd.get_dummies(X, drop_firstTrue) y df[log_charges] # 3. 固定随机种子拆分 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) print(X_train.shape, X_test.shape) print(X.dtypes)drop_firstTrue是哑变量编码最容易忽略的参数。region 有四个类别编码后只保留 3 列剩下那个作为基准组否则特征矩阵内部线性相关statsmodels 会直接报奇异矩阵或输出 NaN 系数。np.log取自然对数预测完要np.exp还原成金额。random_state42固定随机种子保证每次跑的拆分结果一致这是复现和多人协作的基础。test_size 0.2 在千行数据上是常见默认值如果数据只有几百行建议提到 0.3 防止测试集太薄。这里有一个细节对数变换只对 y 做特征里的 age、bmi 不用标准化。线性回归本身对特征尺度不敏感系数会自适应标准化反而让系数失去「年龄每增一岁赔付变化多少」的直接解释力。当然如果后面用 Ridge 或 Lasso 这类带正则的模型标准化才有必要。3.3 训练模型并解读系数statsmodels 出检验scikit-learn 出预测我习惯双轨跑statsmodels 输出带 p 值和置信区间的完整检验表scikit-learn 负责统一的训练预测接口。先看 statsmodels。import statsmodels.api as sm X_train_const sm.add_constant(X_train) # 加截距列 ols sm.OLS(y_train, X_train_const).fit() print(ols.summary())sm.add_constant手动加一列 1 作为截距因为 pandas 出来的 DataFrame 默认不带。summary()输出里重点看四个数R² 大概在 0.75 上下说明对数理赔金额约七成五的方差被特征解释coef 列是每个特征的回归系数P|t| 小于 0.05 的特征视为显著age、bmi、smoker 通常显著region 大概率不显著F-statistic 做整体检验判断模型是否优于「均值预测」这个朴素基线。再用 scikit-learn 出一份系数表方便直接转成业务语言from sklearn.linear_model import LinearRegression lr LinearRegression() lr.fit(X_train, y_train) coef_df pd.DataFrame({ feature: X_train.columns, coef: lr.coef_, exp_coef: np.exp(lr.coef_) }) print(coef_df.sort_values(coef, keyabs, ascendingFalse))因为目标变量是对数系数要解读成乘法效应。exp_coef这一列把系数还原成「倍率」比如 smoker_yes 的系数约 1.5exp 之后约 4.5意思是其他条件不变时吸烟者的预计理赔金额大约是非吸烟者的 4.5 倍age 系数约 0.028exp 后约 1.03即年龄每增加一岁理赔金额平均上浮约 3%。这种解读方式对业务方是零门槛的比抛一个 R² 有用得多。4. 模型评估与误差分析R²、MAE、RMSE 和残差图怎么读4.1 回归指标的业务口径R² 看拟合MAE 看钱理赔预测的评估不能只看一个数至少要看三个R²、MAE、RMSE。它们的口径不同各自回答的问题也不同指标计算空间业务含义本基线常见量级R²对数空间方差解释比例模型比均值预测好多少0.75 左右MAE原始金额平均每个样本预测差多少钱30004000RMSE原始金额对 大额理赔误差更敏感的均方根误差5000 以上R² 在 0 到 1 之间0.8 不算差MAE 单位是原始金额业务方最爱看RMSE 因为平方运算放大了离群值而保险场景里大额理赔虽然少却直接决定赔付率所以 RMSE 同样不能放。计算代码如下。from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score y_pred_log lr.predict(X_test) # 还原到原始金额再算业务口径指标 y_pred np.exp(y_pred_log) y_test_orig np.exp(y_test) print(R²对数空间:, round(r2_score(y_test, y_pred_log), 4)) print(MAE原始金额:, round(mean_absolute_error(y_test_orig, y_pred), 0)) print(RMSE原始金额:, round(np.sqrt(mean_squared_error(y_test_orig, y_pred)), 0))注意 R² 是在对数空间算的MAE 和 RMSE 在原始金额空间算两个口径不能混。对数空间的 R² 高不代表原始金额的误差就小因为 exp 还原会放大右尾差异同样原始金额的 MAE 低也不一定对数空间拟合好。稳妥做法是每次报告都标注清楚计算空间。这个基线模型跑下来MAE 大概在 3000 到 4000 的量级对理赔金额中位数约 9000 的数据来说平均误差占中位数三分之一左右——作为一版基线可以接受后续加交互项还能往下压。4.2 残差图与 Q-Q 图模型的体检报告评估指标只是结果残差图才是诊断。残差是真实值减预测值好模型的残差应该随机分布在 0 附近没有明显形状。残差图不是玄学它是模型最诚实的体检报告我每次训练完必画。import scipy.stats as st resid y_test - y_pred_log fig, axes plt.subplots(1, 2, figsize(12, 4)) axes[0].scatter(y_pred_log, resid, alpha0.5) axes[0].axhline(0, colorred, linestyle--) axes[0].set_xlabel(Fitted Value (log charges)) axes[0].set_ylabel(Residual) axes[0].set_title(Residuals vs Fitted) st.probplot(resid, distnorm, plotaxes[1]) axes[1].set_title(Q-Q Plot of Residuals) plt.tight_layout() plt.show()右边的 Q-Q 图如果点大致落在直线上说明残差近似正态OLS 的置信区间和 p 值才可信。反之如果残差图出现从左到右逐渐张开的喇叭形就是异方差说明某个高危人群通常是大额理赔那批没有被模型吃透此时系数虽然仍是无偏估计但标准误和 p 值已经不可信Q-Q 图两尾偏离直线则说明残差有厚尾和理赔金额的长尾对上了。我们的基线模型跑完残差图往往带一点喇叭形Q-Q 图两尾轻微翘起。这不算病入膏肓但明确告诉你纯线性主效应不够下一步要加交互项。4.3 加交互项与多项式特征逼近真实理赔曲线理赔数据里最典型的非线性藏在交互里吸烟和高 BMI 的叠加效应大于两者单独相加年龄和吸烟的乘积也有类似趋势。把交互项加进去线性回归依然可解释但拟合能力上一个台阶。X2 X.copy() X2[bmi_x_smoker] X2[bmi] * X2[smoker_yes] X2[age_x_smoker] X2[age] * X2[smoker_yes] X2[age2] X2[age] ** 2 X2_train, X2_test, y_train, y_test train_test_split( X2, y, test_size0.2, random_state42 ) lr2 LinearRegression() lr2.fit(X2_train, y_train) y_pred2_log lr2.predict(X2_test) print(R²对数空间:, round(r2_score(y_test, y_pred2_log), 4)) print(MAE原始金额:, round( mean_absolute_error(np.exp(y_test), np.exp(y_pred2_log)), 0))bmi_x_smoker是我在理赔项目里加得最多的交互项业务含义很直接吸烟者叠加高 BMI 的赔付增幅比吸烟者和 BMI 各自贡献之和还要高。age2是年龄的平方项用来捕捉年龄与理赔金额的曲线关系——年龄对理赔的影响不是线性的50 岁以后加速上涨。加完这两类特征R² 通常能从 0.75 提到 0.82 左右MAE 也能再降几百。代价是特征从 9 列变成 12 列但离过拟合还远得很。提示交互项必须有业务理由再放不要穷举两两乘积。特征爆炸会降低可解释性也会让模型去拟合数据里的噪音。5. 理赔预测实战避坑五个高频踩坑记录5.1 目标变量没做对数变换模型被大额理赔带偏现象R² 看起来不低但 MAE 大得离谱预测值全部挤在理赔金额均值附近高赔付样本一个都没学到。原因charges 右偏严重少数大额理赔在平方误差里占据绝大部分权重OLS 为了压低这些点的误差把决策边界整体抬高小额样本全被牺牲。解决对 y 做np.log或np.log1p防零值在对数空间训练预测后再np.exp还原。这是理赔金额回归最经典的处理没有之一。做完再看 MAE通常能降三分之一以上。5.2 哑变量编码漏掉 drop_first触发多重共线性现象statsmodels 的summary()里某个类别系数显示为 NaN或者 scikit-learn 训练完的系数表里出现明显不合理的正负交替。原因region 四个类别被编码成四列四列之和恒为 1与截距列完全共线特征矩阵不满秩最小二乘解不唯一。解决pd.get_dummies(X, drop_firstTrue)丢掉基准列或者用OneHotEncoder(dropfirst)。检查方式很简单打印X.shape如果列数和直觉对不上多半就是这里出了问题。5.3 数据拆分前先做全量预处理测试集信息泄漏现象训练验证指标很漂亮上线跑真实数据立刻翻车预测误差翻倍。原因在train_test_split之前就做了标准化、填补缺失值或 one-hot测试集的信息混进了训练过程模型相当于「开卷考试」拿了高分。解决严格按「先拆分后处理」来。拆分之后标准化器、填补器、编码器都只在训练集上fit测试集只transform。sklearn 里更推荐用Pipeline把预处理和模型串起来天然规避这个隐患。对线性回归来说特征不标准化影响不大但做缺失值填补时一定要按这个规矩来。5.4 只看 R² 不看残差高拟合度掩盖系统性偏差现象R² 到了 0.8业务觉得模型很好但按赔付金额分层看低风险人群被系统性高估高风险人群被系统性低估。原因R² 是整体拟合度对局部偏差不敏感。理赔数据里 80% 的人理赔额不高模型只要把这一片拟合好R² 就不会差但那 20% 的高理赔人群才是保险公司最关心的。解决每次评估必须配残差图。残差图有喇叭形就加交互项或换加权最小二乘有曲线形就加多项式特征。另外按预测分位分组看实际均值确保每一层的预测偏差方向不是单调的。5.5 把「事后特征」当特征工程模型上线直接报废现象有人把「是否已理赔」「理赔审核状态」「医院等级」放进特征训练 R² 冲到 0.95结果上线后这些字段根本拿不到模型等于废了。原因这些特征是事件发生之后才产生的属于标签泄漏机器学习的「预测」变成了「事后解释」。理赔预测的定义是在理赔发生之前用保单上已有的信息预估支出。解决特征工程阶段给每个特征打一个「可得性」标签投保时点能否拿到、理赔发生前能否拿到。拿不到的哪怕相关性强得惊人也直接删。这种错误和数据泄露同等严重区别在于它往往是自己把自己骗了。6. 从预测到落地用分层验证和保单定价逻辑检验模型模型跑通不等于能用落地前我要做一道业务验证分层检验。把测试集按预测理赔金额从低到高分成十组看每一组的实际平均赔付是否跟着预测单调上升。这个逻辑和保险定价完全一致——预测越高理赔风险的人群实际赔付就应该越高如果模型在这一层失真再高的 R² 也没有定价价值。results pd.DataFrame({actual: y_test_orig, pred: np.exp(y_pred_log)}) # 按预测值分 10 层0 最低、9 最高 results[decile] pd.qcut(results[pred], 10, labelsFalse, duplicatesdrop) decile_summary results.groupby(decile).agg( 平均实际赔付(actual, mean), 平均预测赔付(pred, mean), 样本量(actual, count) ).round(0) print(decile_summary)pd.qcut按分位数切层保证每组样本量接近。期望看到 0 到 9 层的平均实际赔付基本单调上升尤其最高组的平均实际赔付要明显超过整体均值。我自己的习惯是把最高层实际均值除以整体均值的比值作为「提升度」做到 2 以上这版模型才敢说对承保决策有参考价值。接着做时间维度的回测。理赔数据通常有年度趋势和季节性随机拆分验证的是「同一时间段内的泛化能力」而保险业务真正面对的是「用过去预测未来」。正式交付前我会按时间切一刀前几年做训练、最后一年做验证。如果时间切分的指标比随机切分差很多说明模型学到了不少时间上的偶然性需要回头检查特征里有没有不该存在的时点信息。再往后才是模型升级。加完交互项的线性回归已经足够作为精算估损的基线如果还想压误差下一步是 Ridge、Lasso 处理特征相关性和正则化再下一步才是试 XGBoost 这类树模型和线性回归对照着看提升到底值不值得牺牲可解释性。我做这类项目吃过最大的亏是不做分层验证就把 R² 0.8 的模型拿给业务结果被一组反问戳穿预测最高那档的人实际赔付真的最高吗从此分层表成了每次交付的必带附件。先跑通基线、再谈复杂模型业务验证永远排在指标美化前面这个顺序希望帮到你。本文还有配套的精品资源点击获取