资讯动态

医疗花费预测实战:用Python构建回归模型与特征工程

发布时间:2026/10/2 2:41:56 来源:尧图企业网站定制
简介面向机器学习课程设计与回归任务学习的Python医疗花费预测项目完整提供可直接运行源码与配套设计报告。压缩包共5个文件以Python源码、Word设计报告为核心另含Markdown说明与License许可整体仅1.29MB结构精简。项目基于一份1070行的小型医疗数据集先用describe与isnull().sum()完成数据概览和空值检查继而以全手写不调包方式实现随机森林与线性回归同时借助scikit-learn构建GBDT、SVR、LassoRegression和决策树模型。调参环节覆盖RandomizedSearchCV、GridSearchCV与手动调参三种策略并进一步采用直接平均、加权平均与stacking堆叠进行模型融合使用K折交叉验证和留一法评估效果可帮助读者系统掌握回归建模、调参与集成的完整流程。已有684人学习下载适合用作课程设计、毕业设计或机器学习进阶练习的参考资料。1. 医疗花费预测项目先用Python跑通数据再谈模型精度医疗花费预测是机器学习回归任务里最贴近业务价值的场景之一——保险定价、医院预算、个人健康管理都会用到它。用Python做这件事核心路径清晰加载数据、清洗特征、训练回归模型、评估误差。常见的公开数据集如Medical Cost Personal Dataset包含年龄、性别、BMI、吸烟、子女数、地区六个字段目标值是个人年度医疗费用。这个项目非常适合Python入门到进阶的人练手同时也能直接迁移到保险或医疗数据分析与可视化的实际工作中。但别以为它简单——特征编码、离群点、评估指标选择都是翻车重灾区。这篇就按我实际做过的路径把原理、代码、参数和坑一次讲透。2. 医疗花费预测的原理与数据准备选型逻辑与特征清洗2.1 为什么医疗花费适合树模型从线性回归到集成学习的选型逻辑一上来别急着跑模型。医疗花费数据集有它的结构特征目标值年度医疗费用呈长尾分布少数高额患者会把均值拉得很高特征里既有连续值BMI、年龄又有类别值性别、地区、是否吸烟。这就决定了模型的选型方向。对这样的数据线性回归可以做基线。它的优点是可解释性强每个系数代表一个特征单位变化对费用的影响缺点是假设特征与目标之间是线性关系遇到BMI和费用的非线性交互、吸烟带来的乘数效应时拟合能力就不够。树模型是这里的主流选择。决策树天然处理类别特征和连续特征的混合不怕异常值因为分裂基于排序也能捕捉交互。随机森林通过bagging降低方差XGBoost通过boosting逐步减小偏差两者都在Kaggle这类表格数据任务里被验证过稳定可靠。我的习惯是先跑线性回归拿基线再上随机森林和XGBoost对比最后用交叉验证选模型。这符合python数据分析与可视化的常见工作流也让后期调参与解释更有据可依。需要说清楚医疗花费预测不是一个需要深度学习的场景。表格数据上树模型在中小样本量下往往比神经网络更稳——训练快、没有复杂调参、结果可解释。如果你的机器还在纠结python安装sklearn库这一步说明环境还没就位先把pandas、scikit-learn、xgboost装好再开始下面的内容。2.2 用pandas加载并清洗医疗花费数据集字段类型与缺失值处理数据集通常是一个CSV文件列是age、sex、bmi、children、smoker、region、charges。加载代码import pandas as pd import numpy as np df pd.read_csv(medical_cost.csv) print(df.shape) print(df.head()) print(df.dtypes)这里打印shape确认行数和列数用head看前五行用dtypes检查各列类型。正常情况下age和children是整数bmi和charges是浮点数sex、smoker、region是object字符串。如果某列读出来类型不对八成是文件里有脏值需要进一步处理。先做缺失值和重复值检查print(df.isnull().sum()) print(df.duplicated().sum())如果存在缺失值常见处理是删除该行或者用中位数填充。医疗费用数据里性别和地区列出现缺失直接删除比瞎填更安全因为这两个类别字段没有合理的默认值。重复行要检查是数据录入重复还是真实重复样本确认是录入问题再df.drop_duplicates(inplaceTrue)。清洗的重点不是缺失值而是字段的实际含义。BMI超过60的样本在真实医疗保险数据里几乎不存在遇到这种值要考虑是不是录入错误。children字段的范围一般是0到5如果有人为填写的数字超出常识区间也值得标记出来。我一般会用describe()先看统计分布再决定处理策略。2.3 特征工程三个必做步骤区间化、编码与交互特征医疗花费预测的特征工程有几个固定动作。第一步是年龄区间化。age是连续整数但18岁和30岁的费用差异可能不大40岁和65岁的差异才明显。可以先把年龄切成区间不过我的经验是树模型里保留原始连续值效果更好因为切分会丢失信息线性回归里可以尝试把age的平方项加进去用来表达年龄的非线性影响。第二步是类别编码。sex、smoker、region都是类别字段需要转成模型能用的形式。smoker是二分类映射成0/1最直接。region是四个类别用one-hot编码展开成四个布尔列。sex同理。这里有个常见误区直接用LabelEncoder给region编码成0、1、2、3这会让模型以为地区之间存在顺序关系。具体后果放在第四章讲。第三步是交互特征。医疗费用数据里吸烟和年龄存在交互效应——吸烟者随年龄增长的费用上涨速度远高于不吸烟者。可以构造smokerage、smokerbmi这样的交互列。树模型其实能自己学出这类交互但手动加上可以让模型更容易捕捉到也方便后续做特征重要性解释。这一步对线性回归尤其重要因为线性模型无法自动表达特征间的乘法关系。到这里数据准备基本完成。把处理好的特征存成DataFrame目标列charges单独拎出来模型训练就可以开始了。3. 基于Python训练医疗花费预测模型五步核心代码与参数调优3.1 训练集与测试集划分shuffle与stratify的取舍划分数据集是所有回归任务的起手式。用train_test_split之前先想清楚两件事要不要shuffle要不要stratify。回归任务里stratify参数不能直接用于目标值它会要求传入离散标签但可以按目标值的分桶来近似分层划分。如果数据量只有一千多条直接随机划分可能导致训练集和测试集的费用分布不一致高费用样本全跑到训练集里测试集分数看着漂亮实际上线却翻车。我一般这么做from sklearn.model_selection import train_test_split df[charge_bucket] pd.qcut(df[charges], q4, labelsFalse) train_df, test_df train_test_split( df, test_size0.2, random_state42, stratifydf[charge_bucket] ) X_train train_df.drop(columns[charges, charge_bucket]) y_train train_df[charges] X_test test_df.drop(columns[charges, charge_bucket]) y_test test_df[charges]stratify按费用的四分位分层确保高低费用样本在训练集和测试集中占比一致。random_state固定为42是为了复现实验结果调参时可以换成其他值验证稳定性。charge_bucket是临时列划分后要删掉避免它泄漏到特征里。3.2 基线线性回归模型代码、参数与评估指标解读先把线性回归跑通拿一个可对比的基线分数。代码如下from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score model_lr LinearRegression() model_lr.fit(X_train, y_train) pred_lr model_lr.predict(X_test) print(MAE:, mean_absolute_error(y_test, pred_lr)) print(RMSE:, mean_squared_error(y_test, pred_lr, squaredFalse)) print(R2:, r2_score(y_test, pred_lr))这里用MAE来衡量平均预测误差——医疗服务场景里关心的是预测值平均偏差多少钱而不是平方后的偏差。RMSE对离群点更敏感R²衡量模型解释了目标方差的百分比。医疗花费数据的R²通常受离群点影响很大单看它会误判模型质量。线性回归跑完查看系数解释模型方向coef_df pd.DataFrame({ feature: X_train.columns, coef: model_lr.coef_ }) print(coef_df.sort_values(coef, ascendingFalse))这个表告诉我们哪些特征对费用有正向贡献、哪些是负向。smoker的系数应该是正数且绝对值最大符合业务直觉。如果某个系数方向与常识相悖例如吸烟者费用反而更低很可能数据有问题或者特征之间存在共线性。注意squaredFalse是scikit-learn 1.0以上版本的写法更早版本没有这个参数需要先计算mean_squared_error再手动开方。如果你的环境是旧版本优先把scikit-learn升到1.2以上。3.3 随机森林与XGBoost训练核心参数表与调参顺序树模型在这个任务上通常能碾压线性回归。先跑随机森林from sklearn.ensemble import RandomForestRegressor model_rf RandomForestRegressor( n_estimators300, max_depthNone, min_samples_split4, min_samples_leaf1, max_featuressqrt, random_state42, n_jobs-1 ) model_rf.fit(X_train, y_train) pred_rf model_rf.predict(X_test) print(RF MAE:, mean_absolute_error(y_test, pred_rf)) print(RF R2:, r2_score(y_test, pred_rf))参数含义要说清楚n_estimators是树的数量300在这个数据规模下够用再多提升有限但训练变慢。max_depthNone让树生长到全部叶子纯化随机森林依赖随机性防止过拟合限制深度反而会降低表现。min_samples_split4控制节点分裂所需的最小样本数防止节点过细。max_featuressqrt是回归任务常见选择每棵树只随机取平方根个特征增强树之间的差异。n_jobs-1用满所有CPU核心。接下来是XGBoostimport xgboost as xgb model_xgb xgb.XGBRegressor( n_estimators400, learning_rate0.05, max_depth4, subsample0.8, colsample_bytree0.8, reg_lambda1.0, random_state42 ) model_xgb.fit( X_train, y_train, eval_set[(X_test, y_test)], verboseFalse ) pred_xgb model_xgb.predict(X_test) print(XGB MAE:, mean_absolute_error(y_test, pred_xgb)) print(XGB R2:, r2_score(y_test, pred_xgb))运行前需要先执行pipinstall xgboost这是一个开源梯度提升库不在scikit-learn默认包里。与随机森林的差异在于XGBoost的learning_rate设小一点0.05配合更多棵树400效果更好。max_depth4控制树深防止过拟合因为boosting每棵树都拟合残差树太深很容易在训练集上过度学习。subsample0.8让每轮迭代只用80%的数据训练colsample_bytree0.8每棵树只用80%的特征双重随机让模型更稳。reg_lambda1.0是L2正则控制叶子权重的大小。eval_set传入测试集可以在训练过程中监控过拟合信号不过verboseFalse关掉了输出。调参顺序我的习惯是先固定learning_rate和n_estimators调整max_depth然后是subsample和colsample_bytree最后调reg_lambda。没有固定公式看验证集误差变化来定。深度从3到6逐个试哪个验证误差低用哪个。3.4 用交叉验证验证模型稳定性别只盯着测试集R²单次划分的测试集分数有运气成分。用交叉验证来评估稳定性更可靠from sklearn.model_selection import cross_val_score cv_r2 cross_val_score(model_rf, X_train, y_train, cv5, scoringr2) print(CV R2 mean:, cv_r2.mean().round(4)) print(CV R2 std:, cv_r2.std().round(4)) cv_mae cross_val_score(model_rf, X_train, y_train, cv5, scoringneg_mean_absolute_error) print(CV MAE mean:, (-cv_mae.mean()).round(2)) print(CV MAE std:, cv_mae.std().round(2))交叉验证的意义在于五次划分的R²均值接近单次测试分数且标准差很小说明模型在不同数据划分下表现稳定如果某一个fold的分数特别差说明数据顺序里有隐藏模式比如按时间排序导致某段时间的分布不同需要排查。负号是scoring策略的统一约定MAE这类误差指标要以最大化形式传入所以内部取了负值打印时再取回来。对比三个模型的结果选R²均值高且MAE低的项目不要只凭单次测试集分数做决定。我在实际项目中见过随机森林单次R² 0.92交叉验证均值只有0.87的例子这种差距说明偶然性很大。4. 医疗花费预测避坑指南四条血泪经验覆盖数据泄漏与过拟合4.1 坑1把“子女数”当连续特征直接喂给线性回归现象线性回归跑出来的R²只有0.75且children系数非常小接近0。业务方质疑家里有孩子的家庭医疗支出应该更高才对吧怎么模型说没影响原因children是0到5的整数直接喂给线性回归时模型只能学到“每多一个孩子费用增加固定金额”的线性关系。但实际数据分布里0个孩子和1个孩子的差异与3个孩子和4个孩子的差异并不是等距的。LinearRegression的天然假设就是等距影响所以系数被稀释。解决把children处理成类别变量或one-hot编码。用pandas的get_dummies把children拆成is_child_1、is_child_2等布尔列或者直接用OrdinalEncoder让树模型自己处理顺序。我的习惯是children只有0-5六种取值直接转成category类型线性回归里用one-hot表达更符合业务语义因为“有孩子”本身就是一个质变。4.2 坑2用LabelEncoder处理region列模型学到虚假顺序现象模型在测试集上R²不错但特征重要性输出里region对预测结果的影响是单向上升的——region编码为0的地区费用最低编码为3的最高。业务方追问不同地区的医疗价格差异怎么可能是一条单调曲线原因LabelEncoder把四个地区映射成0、1、2、3线性模型会把它当作连续变量学到“地区每增加一档费用增加若干”的错误关系。树模型虽然不受线性假设影响但也会用“region 2”这样的分裂条件四个本来独立的类别被强行赋予了顺序关系。解决region用pd.get_dummies展开成三列或四列布尔变量注意drop_first避免共线性或者用OneHotEncoder注入到pipeline里。处理后模型学到的就不再是顺序关系而是“是否属于东北地区”“是否属于东南地区”这种独立特征。树模型还可以继续用OrdinalEncoder但线性回归必须one-hot。4.3 坑3不去除极端高额索赔MAE被一个离群点拉偏现象RMSE高达15000MAE也有8000检查预测结果时发现费用最高的5%样本预测误差巨大其余95%的样本预测还挺准。去掉这些样本重新计算MAE直接降到4000。原因医疗费用分布的长尾效应。少数年花费超过5万美元的重症患者与常规门诊患者的费用模式完全不同模型在训练时为了拟合它们把偏差分摊到了所有样本上。最坏情况下模型把所有样本都预测成中等费用然后在少数高费用样本上产生巨大误差拉高了全局MAE。解决先画出charges的分布直方图确认长尾形状。然后决定处理策略一个是用IQR或分位数比如99%分位截断极端值另一个是取目标值的log变换压窄分布。我在做保险定价场景时会选择log变换而不是删除高费用样本——保险公司恰恰最关心高费用人群删除等于放弃业务上最有价值的样本。log变换后用exp恢复预测值注意要修正偏差用exp(mean(log_pred))会有系统偏差实际应用中用exp(median(log_pred))更稳。4.4 坑4只盯R²不看预测偏差上线后一测真实数据就翻车现象模型R² 0.88业务方看着满意正式接真实数据后反馈预测普遍偏低10%以上模型被退回。原因R²衡量的是模型与数据均值的相对好坏不代表绝对误差小。只有当目标值方差大、均值高时R²才会好看真实数据分布如果有偏移比如换了地区、年份不同、物价上涨R²再高也救不了绝对偏差。解决模型评估阶段就要把MAE和MAPE平均绝对百分比误差当核心指标。MAPE的计算方式是用绝对误差除以真实值它衡量“预测偏差占总费用的比例”。医疗服务场景里MAPE控制在15%以内才算可用超过20%就要重新审视特征工程。另外上线前的预测值要做一次分布对比把预测值的直方图和真实值的直方图叠在一起如果形态差异明显说明模型没有学到真实分布需要返工。5. 从验证到落地解释用特征重要性和SHAP回答业务方的追问5.1 特征重要性排序树模型自带的重要度与排列重要性的取舍模型训练完不等于项目结束。保险公司、医院业务方一定会问哪些因素在驱动医疗费用回答这个问题靠特征重要性。随机森林自带feature_importances_属性直接取来用importance pd.DataFrame({ feature: X_train.columns, importance: model_rf.feature_importances_ }).sort_values(importance, ascendingFalse) print(importance)这个数值衡量的是每个特征在所有决策树分裂中带来的不纯度减少量总和。它的局限是对高基数类别特征有偏向且无法体现负向影响。更稳的做法是算排列重要性——随机打乱某一列后看模型误差上升多少from sklearn.inspection import permutation_importance perm_result permutation_importance( model_rf, X_test, y_test, n_repeats5, random_state42, scoringneg_mean_absolute_error )排列重要性更贴近“特征被移除后预测误差变化多少”的业务语义两个结果交叉验证后排在前面的特征才是真正在驱动预测结果的。5.2 用SHAP解释单个预测回答“为什么这个人花了这么多钱”医疗花费业务场景做出一个高费用预测后业务方会要求解释这个结论。SHAP的核心思路对每个样本把目标值分解成“基线值 每个特征的贡献”贡献越大说明该特征对这个预测的推动越强。基本用法import shap explainer shap.TreeExplainer(model_xgb) shap_values explainer.shap_values(X_test) shap.summary_plot(shap_values, X_test) shap.force_plot(explainer.expected_value, shap_values[0], X_test.iloc[0])这张summary图横轴是SHAP值纵轴按特征排列点的颜色表示特征值大小。从这张图能直接读出smoker的SHAP值跨度最大颜色越红smoker1点越靠右说明吸烟是拉高费用的最大单一因素。年龄次之BMI在边缘有贡献。force_plot展示单个样本的预测分解蓝色特征下拉费用红色特征上拉费用业务方一看就懂。项目走到这一步才算真正闭环数据加载、清洗、建模、评估、解释都齐了。做这类项目模型本身只占三分之一工作量数据和解释占大头——这是我被测试集R²骗过一次、被业务方退回过一次之后才养成的习惯。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑