ARTICLE DETAIL

资讯详情

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

健康保险理赔预测实战:用Python线性回归从数据探索到模型部署

健康保险理赔预测实战:用Python线性回归从数据探索到模型部署 简介基于Kaggle健康保险医疗费用数据集打造的理赔支出预测Python源码包面向机器学习初学者与保险数据分析人员可帮助快速掌握回归建模与特征可视化分析流程。项目聚焦年龄、性别、身体质量指数、子女数、吸烟、地区等特征对医疗费用的影响建模并包含完整的数据预览与模型对比思路。压缩包共30个文件大小仅1.25MB其中7个Python脚本负责训练、测试与可视化11个pkl格式的模型文件存放已训练好的线性回归、支持向量回归、套索回归、岭回归等模型10张图片展示散点矩阵、蜂群图等分析结果另有1个CSV数据集和1个说明文档。当前已有779人学习浏览。借助这份实践包读者不仅能获得可直接运行的预测链路还能基于模型对比脚本评估不同回归算法的效果结合可视化图表深入理解各特征与理赔支出的关系是一份轻量但完整的实践参考。1. 理赔支出预测到底在预测什么健康保险理赔预测这几年几乎是每家保险公司精算部和数据部门的标配需求。它解决的并不是单一问题产品定价时要回答“将来这个保单要赔多少钱”核保时要给高风险人群定系数做偿付能力评估时要在分子端估算未决赔款准备金。把这些场景抽象出来本质是一个回归问题——用历史保单的特征去拟合一个连续的金额目标。本文要展开的正是这套用 Python 完整落地的最小闭环拿到一份含健康指标与理赔金额的数据集先做可视化分析摸清分布和强特征再用线性回归模型做机器学习训练与评估最后把模型封装成可复用的预测脚本。适合刚学完 Python 基础和 pandas 准备上手第一个完整项目的人也适合已经用树模型做理赔预测、想回头把统计基座补扎实的从业者。2. 用 Python 做健康保险理赔数据的可视化分析建模之前先看数据这不是流程礼貌而是线性回归对数据分布和特征形态极其敏感。理赔金额这类响应变量几乎必然是右偏的年龄、BMI 对金额的影响也通常不是同方差线性这些都得在 EDA 阶段暴露出来否则后面的回归系数和显著性检验全部失真。2.1 数据集应该长什么样字段、类型与缺失值排查这类健康保险理赔预测项目通常用一张保单级明细表每行是一个被保人核心字段大致是年龄、性别、BMI、子女数、是否吸烟、所在地区以及目标列理赔金额也就是模型要学习的真实支出。有的版本会带既往病史、住院天数等更多保险字段但判断数据集是否可用先看这六点。import pandas as pd df pd.read_csv(insurance_claims.csv) print(df.shape) print(df.dtypes) print(df.isnull().sum()) print(df.describe(percentiles[0.25, 0.5, 0.75, 0.9, 0.99]).T)这里df.dtypes用于确认年龄、BMI、理赔金额被正确读成数值类型而不是 object这一步容易被轻描淡写实际很多 CSV 在导出时会把数字列混入空格或货币符号直接导致后续回归报 TypeError。isnull().sum()检查缺失值如果理赔金额这一列有缺失按正常业务处理应该是剔除而不是填充理由在于一个没有真实支出的样本无法参与监督学习。describe里的百分位参数建议显式传尤其要关注0.99分位因为理赔数据的极大值经常出现在这里。2.2 分布形态决定了后面要不要做对数变换拿到 describe 之后先看理赔金额列。如果均值明显大于中位数且 99 分位是 75 分位的十倍以上基本可以判断是典型右偏分布。对右偏目标做线性回归有两个后果一是残差在尾部方差极大违背 OLS 的同方差假设二是个别巨额理赔样本会把回归线整个拉向自身让系数估计变得极不稳定。import matplotlib.pyplot as plt import seaborn as sns fig, axes plt.subplots(1, 2, figsize(12, 4)) sns.histplot(df[charges], bins60, kdeTrue, axaxes[0]) axes[0].set_title(Raw Charges Distribution) import numpy as np sns.histplot(np.log1p(df[charges]), bins60, kdeTrue, axaxes[1]) axes[1].set_title(Log-transformed Charges) plt.tight_layout() plt.show()这里用np.log1p而不是np.log是为了在理赔金额存在 0 值时仍然安全log1p对 0 映射为 0不会产生负无穷。看过两张图对比之后建模阶段应当用log1p变换后的序列作为回归目标。注意一个容易犯的错训练时做了对数变换预测值时就必须做np.expm1逆变换才能还原成理赔金额否则你给出的预测结果是“对数尺度”业务方拿到这个数字会觉得模型彻底不靠谱。2.3 相关性分析与 smoker 这个强特征连续特征与目标的关系先看相关性矩阵分类特征则用分组聚合来观察。健康保险理赔数据里吸烟是一个几乎必然决定建模效果的分水岭变量。fig, axes plt.subplots(1, 2, figsize(14, 4)) numeric_cols [age, bmi, children, charges] corr df[numeric_cols].corr() sns.heatmap(corr, annotTrue, fmt.2f, cmapcoolwarm, axaxes[0]) axes[0].set_title(Correlation Heatmap) sns.boxplot(datadf, xsmoker, ycharges, axaxes[1]) axes[1].set_title(Charges by Smoker Status) plt.tight_layout() plt.show() print(df.groupby(smoker)[charges].agg([mean, median, count]))热力图里 age 与 charges 的相关性通常只有 0.2 左右而凡是包含 smoker 的交叉分析组间均值差距可以达到数倍。这提示一个线性回归建模的关键思路只放年龄、BMI 这些连续变量模型 R² 可能只有 0.1 甚至更低但一旦把 smoker 编码放进去模型质量会出现跳变。再看箱线图时注意吸烟组与非吸烟组的分布重叠其实很小这个变量在四个地区、所有年龄段都有稳定区分力属于可以直接进入模型的强特征。特征建议处理方式原因age保留原始数值对理赔金额接近线性sexOneHot 编码分类变量无自然顺序bmi保留原始数值观察离群点极端值可能影响回归截距children保留原始数值或有序编码取值 0-5等距可视作连续smokerOneHot 编码全数据集最强分类信号regionOneHot 编码四分类注意哑变量陷阱3. 线性回归模型从最小二乘到 sklearn 落地可视化分析做完进入建模阶段。线性回归在理赔支出预测里地位特殊它不像树模型那样能自动捕捉非线性交互但它的系数有明确业务含义能直接读出“是否吸烟使理赔支出增加多少元”这一点在保险定价场景中具备天然的可解释性优势。3.1 为什么线性回归能做理赔支出预测理赔支出预测的本质是建立一个条件均值函数 E(charges | X)即给定一组特征时理赔金额的期望。线性回归对这个函数做了全局线性假设用最小二乘法估计系数。用符号表示就是 y Xβ εOLS 的目标是让残差平方和最小。scikit-learn 的LinearRegression在底层调用了 LAPACK 库求解正规方程数据量在几千到几万行这个量级时速度远快于梯度下降。但这里的“能用”有两个前提条件需要说明白。第一特征与目标的关系至少要近似单调如果某个特征对目标的影响是先增后减线性假设会强行拟合出一条直线误差会集中在转折点附近。第二特征之间不能存在严重多重共线性这在包含 region 这类多分类 OneHot 变量时很容易出现因此后面处理时要主动 drop 掉第一个类别。3.2 特征编码与训练集切分性别、吸烟状态、地区这三列都是字符串必须转成数值。常见做法是OneHotEncoder但这里有个细节值得展开是否要指定dropfirst。如果不 drop三个地区会生成三列加上截距项之后矩阵满秩但存在精确共线性线性回归虽然仍能收敛系数的可解释性却会丧失——所有类别的系数都相对于一个隐藏的基准类别当你想说“西南地区比东北地区理赔高多少”时需要自己算差值非常绕。from sklearn.model_selection import train_test_split from sklearn.preprocessing import OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline X df.drop(charges, axis1) y np.log1p(df[charges]) categorical_cols [sex, smoker, region] preprocessor ColumnTransformer( transformers[ (cat, OneHotEncoder(dropfirst, handle_unknownignore), categorical_cols) ], remainderpassthrough ) 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)ColumnTransformer在这里做的是把分类列交给 OneHotEncoder数值列年龄、BMI、子女数原样透传。handle_unknownignore是给未来线上推理留后路训练集里没有出现的地区在预测时不会导致程序崩溃而是该列全为 0。random_state42不是随便写的一个数它保证两次运行切分结果一致这是机器学习项目里最基本的可复现要求。3.3 模型训练与评估指标解读from sklearn.linear_model import LinearRegression model Pipeline(steps[ (preprocessor, preprocessor), (regressor, LinearRegression()) ]) model.fit(X_train, y_train) train_pred model.predict(X_train) test_pred model.predict(X_test)Pipeline 把预处理和回归器绑定在一起之后保存模型、跨环境复用都不需要单独为测试数据重新实现编码逻辑。训练完成后计算回归指标理赔场景最常用的组合是 MAE 用于业务沟通RMSE 用于衡量大额误差R² 用于模型整体解释力。from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score def evaluate(y_true, y_pred, name): y_true_exp np.expm1(y_true) y_pred_exp np.expm1(y_pred) print(f{name} MAE: {mean_absolute_error(y_true_exp, y_pred_exp):.2f}) print(f{name} RMSE: {mean_squared_error(y_true_exp, y_pred_exp, squaredFalse):.2f}) print(f{name} R2: {r2_score(y_true, y_pred):.4f}) evaluate(y_train, train_pred, TRAIN) evaluate(y_test, test_pred, TEST)注意评估时先把预测值和对数值通过expm1还原成真实金额再算 MAE 和 RMSE这样业务方看到的误差单位是“元”而不是“对数元”。对数空间里算 R² 是允许的因为 R² 本质是方差解释比例与尺度无关。训练集 R² 如果远高于测试集说明过拟合如果两者都低问题不在模型复杂度而在特征本身携带的预测信息不足这个判断在回归诊断里非常基本但新手往往会先去调模型而忽略特征。指标数值方向在理赔场景的业务含义MAE越小越好平均每次理赔预测误差业务最直观RMSE越小越好对巨额理赔样本更敏感反映尾部风险R²越接近 1 越好模型解释的理赔金额方差比例4. 特征工程与模型诊断让线性回归真正可用第一次跑通的线性回归通常表现平庸这在健康保险理赔预测里不是异常现象。真正有效的不是换一个更强的回归器而是先诊断现有模型的系统性偏差再针对性地做特征变换。线性回归是少数几个你能用残差图清楚看出问题出在哪里的机器学习算法。4.1 对右偏的 charges 做对数变换后再看系数建模前对目标做log1p变换除了改善分布形态还有一个容易被忽略的实际收益将连乘关系转化为加法关系。理赔支出的生成过程通常不是“基础费用 各项附加”而是“基础费率 × 各个风险因子”这种乘法结构直接套线性回归拟合的是一条加法直线无论如何都会在高端值上出现持续低估。对数变换正好把乘法变成加法使线性回归的假设与业务生成机制对齐。model.named_steps[regressor].intercept_ feature_names ( model.named_steps[preprocessor] .named_transformers_[cat] .get_feature_names_out([sex, smoker, region]) .tolist() ) print(feature_names)用这段代码可以把管道里的特征名取回来。实操中 smoker_yes 的系数通常落在 1.4 到 1.7 之间含义是对数空间里吸烟者相对非吸烟者的理赔支出增量。对这个数字做expm1还原为 3 到 4 倍这个倍数关系与精算经验高度吻合也是模型可信度的一个感性验证。一个系数动辄数十万的预警值通常意味着某个分类下样本量过少或者存在离群点这时候要去看分组计数而不是怀疑算法。4.2 残差分析发现非线性与异方差residuals y_test - test_pred fig, axes plt.subplots(1, 2, figsize(14, 4)) sns.scatterplot(xtest_pred, yresiduals, alpha0.5, axaxes[0]) axes[0].axhline(y0, colorr, linestyle--) axes[0].set_xlabel(Fitted Values) axes[0].set_ylabel(Residuals) from scipy import stats stats.probplot(residuals, distnorm, plotaxes[1]) axes[1].set_title(Q-Q Plot of Residuals) plt.tight_layout() plt.show()从残差与拟合值的散点图里重点看两点。如果残差随拟合值增大而呈扇形发散说明高阶样本的预测方差更大即异方差仍未被对数变换完全消除可考虑对 BMI 或 charges 做平方根变换。如果残差在拟合值低端全部为正、高端全部为负说明存在明显的非线性结构而 Q-Q 图里如果尾部偏离直线严重则说明残差分布偏厚尾t 检验的 p 值可信度下降。4.3 多项式特征与正则化什么时候值得加在选择要不要引入多项式特征时先问一个业务问题年龄与理赔支出的关系是单调的吗大多数保险数据里年龄偏低段支出平稳中年以后理赔急剧上升这个“拐点后加速”的形态是线性项无法刻画的。此时给 age 加上平方项能显著改善拟合度而给 bmi 加平方项则通常无效因为 BMI 与理赔的关系更接近一个先平缓后上升的阈值形态。from sklearn.preprocessing import PolynomialFeatures from sklearn.linear_model import Ridge def build_model(poly_degree, alpha1.0): return Pipeline(steps[ (preprocessor, preprocessor), (poly, PolynomialFeatures(degreepoly_degree, include_biasFalse)), (regressor, Ridge(alphaalpha)) ]) baseline build_model(1).fit(X_train, y_train) poly_model build_model(2).fit(X_train, y_train) for name, m in [(linear, baseline), (poly2, poly_model)]: r2_train r2_score(y_train, m.predict(X_train)) r2_test r2_score(y_test, m.predict(X_test)) print(f{name}: train R2{r2_train:.4f}, test R2{r2_test:.4f})这里有两个决策点。首先加多项式特征后如果测试 R² 不升反降说明模型过拟合了那些只在训练集存在的样本点应降低 degree 或放弃多项式其次出现线性回归系数数值极大且正负交错时替换LinearRegression为Ridge正则化是更稳妥的做法alpha需要人工尝试从 1.0 起步按量级 0.1、1.0、10 依次验证。变换方案训练 R² 特征测试泛化适用场景仅原始特征低低基线必须做加 age²中中年龄非线性明显时加交互项 age×bmi较高不确定存在复杂联动时需验证全部特征加 Ridge中高相对稳健特征多且共线性严重时5. 把预测模型封装成可复用的理赔支出预测脚本到这一步模型质量已经基本达标但机器学习项目的交付物不能是 Jupyter Notebook 里的一串单元格。保险业务场景的模型最终要么被核保系统以接口形式调用要么被精算师用脚本批量跑历史保单。这要求我们把训练得到的模型和预处理逻辑固化成可导入、可复用的 Python 模块。5.1 模型持久化与预测函数设计import joblib joblib.dump(model, health_insurance_claim_model.joblib) loaded_model joblib.load(health_insurance_claim_model.joblib) def predict_charges(age, sex, bmi, children, smoker, region): age: int, 被保人年龄 sex: male 或 female bmi: float, 体质指数 children: int, 子女数 smoker: yes 或 no region: northeast / northwest / southeast / southwest input_df pd.DataFrame([[age, sex, bmi, children, smoker, region]], columns[age, sex, bmi, children, smoker, region]) log_charge loaded_model.predict(input_df)[0] return round(float(np.expm1(log_charge)), 2) print(predict_charges(35, male, 28.5, 0, no, northeast))训练时用joblib.dump保存的是整个 Pipeline里面已经包含了ColumnTransformer的编码逻辑所以预测函数不需要再单独对 sex、smoker、region 做映射。这是一个常被忽略的便利点如果用LinearRegression直接训练编码字典、数值列顺序、多项式开关都要自己维护而现在预测函数的输入是与原始 DataFrame 相同语义的普通参数字典。joblib与 pickle 的区别在于对 numpy 数组的内存映射支持更好模型文件较大时加载速度优势明显。注意跨环境部署时pandas与scikit-learn的版本差异是 joblib 加载失败的头号原因条件允许时用 Docker 固定环境镜像。5.2 用边界样本验证模型的稳定性封装完成后的最后一步是做一次业务视角的冒烟测试。不要只测试“普通样本”要特意构造边界条件来观察模型是否出现意料之外的行为。test_cases [ {age: 18, sex: female, bmi: 18.5, children: 0, smoker: no, region: northeast}, {age: 65, sex: male, bmi: 45.0, children: 5, smoker: yes, region: southwest}, {age: 40, sex: female, bmi: 22.0, children: 2, smoker: no, region: unknown}, ] for case in test_cases: try: result predict_charges(**case) print(f{case[age]}岁 {case[smoker]} 理赔预测: {result} 元) except Exception as e: print(f预测失败: {e})第三个用例故意传入了训练集中不存在的地区名unknown这正是此前设置OneHotEncoder(handle_unknownignore)的回报预测不会抛异常而是把所有地区哑变量置 0。此时模型会基于全局基准水平给出一个参考值比崩溃退出优雅得多。看到结果后还应对比一下非吸烟年轻人的预测值是否低于吸烟高 BMI 的中年人如果这个常识性关系没有出现在预测结果里说明模型在特征编码或训练阶段存在 bug需要回头检查预处理管道而不是继续调参。本文还有配套的精品资源点击获取
返回列表