简介:面向机器学习课程设计与回归任务学习的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(inplace=True)。
清洗的重点不是缺失值,而是字段的实际含义。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"], q=4, labels=False) train_df, test_df = train_test_split( df, test_size=0.2, random_state=42, stratify=df["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, squared=False)) 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", ascending=False))这个表告诉我们哪些特征对费用有正向贡献、哪些是负向。smoker的系数应该是正数且绝对值最大,符合业务直觉。如果某个系数方向与常识相悖,例如吸烟者费用反而更低,很可能数据有问题或者特征之间存在共线性。
注意:squared=False是scikit-learn 1.0以上版本的写法,更早版本没有这个参数,需要先计算mean_squared_error再手动开方。如果你的环境是旧版本,优先把scikit-learn升到1.2以上。
3.3 随机森林与XGBoost训练:核心参数表与调参顺序
树模型在这个任务上通常能碾压线性回归。先跑随机森林:
from sklearn.ensemble import RandomForestRegressor model_rf = RandomForestRegressor( n_estimators=300, max_depth=None, min_samples_split=4, min_samples_leaf=1, max_features="sqrt", random_state=42, 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_depth=None让树生长到全部叶子纯化,随机森林依赖随机性防止过拟合,限制深度反而会降低表现。min_samples_split=4控制节点分裂所需的最小样本数,防止节点过细。max_features="sqrt"是回归任务常见选择,每棵树只随机取平方根个特征,增强树之间的差异。n_jobs=-1用满所有CPU核心。
接下来是XGBoost:
import xgboost as xgb model_xgb = xgb.XGBRegressor( n_estimators=400, learning_rate=0.05, max_depth=4, subsample=0.8, colsample_bytree=0.8, reg_lambda=1.0, random_state=42 ) model_xgb.fit( X_train, y_train, eval_set=[(X_test, y_test)], verbose=False ) 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_depth=4控制树深防止过拟合,因为boosting每棵树都拟合残差,树太深很容易在训练集上过度学习。subsample=0.8让每轮迭代只用80%的数据训练,colsample_bytree=0.8每棵树只用80%的特征,双重随机让模型更稳。reg_lambda=1.0是L2正则,控制叶子权重的大小。eval_set传入测试集,可以在训练过程中监控过拟合信号,不过verbose=False关掉了输出。
调参顺序我的习惯是:先固定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, cv=5, scoring="r2") 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, cv=5, scoring="neg_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高达15000,MAE也有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", ascending=False) print(importance)这个数值衡量的是每个特征在所有决策树分裂中带来的不纯度减少量总和。它的局限是:对高基数类别特征有偏向,且无法体现负向影响。更稳的做法是算排列重要性——随机打乱某一列后看模型误差上升多少:
from sklearn.inspection import permutation_importance perm_result = permutation_importance( model_rf, X_test, y_test, n_repeats=5, random_state=42, scoring="neg_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值跨度最大,颜色越红(smoker=1)点越靠右,说明吸烟是拉高费用的最大单一因素。年龄次之,BMI在边缘有贡献。force_plot展示单个样本的预测分解,蓝色特征下拉费用,红色特征上拉费用,业务方一看就懂。
项目走到这一步,才算真正闭环:数据加载、清洗、建模、评估、解释都齐了。做这类项目,模型本身只占三分之一工作量,数据和解释占大头——这是我被测试集R²骗过一次、被业务方退回过一次之后才养成的习惯。希望帮到你。
本文还有配套的精品资源,点击获取