
简介基于Python的医疗花费预测项目包含设计报告Word文档与全部源码面向机器学习初学者、课程设计及期末项目场景。项目涵盖全手写不调包的随机森林与线性回归实现同时借助scikit-learn构建GBDT、SVR、Lasso回归、决策树模型调参环节分别使用RandomizedSearchCV、GridSearchCV及手动调参并对多种模型进行直接平均、加权平均和stacking堆叠融合配合K折交叉验证与留一法评估。资源共5个文件以2个Python脚本为主辅以设计报告docx、说明文档md及license压缩包仅1.29MB结构清晰便于阅读。目前已有684人学习下载。读者可获得完整可运行代码、详细设计报告及模型调优思路适合快速复现医疗费用预测流程并用于课程汇报或算法对比实验。1. 医疗花费预测一份能直接跑通的 Python 课设源码做课设最难受的不是模型不会选而是代码跑起来全是玄学报错。这份医疗花费预测资源里作者把随机森林和线性回归各写了一份不调包实现又用 scikit-learn 跑了 GBDT、SVR、Lasso、决策树再加上三种模型融合方式几乎把回归问题的完整流程都过了一遍。我拆完第一反应是这不像作业更像一份可以拿去扩展成毕设基底的完整工程。适合的读者很明确机器学习课程设计、复试机试前练手、准备转数据分析但没写过完整项目的人。你拿到手能跑通、能看懂、能改后面我会把数据检查、手写算法、调参和融合的细节按复现顺序拆给你。2. 读懂数据describe、isnull 与 1070 行测试集的取舍2.1 数据基本结构先看 shape 和 describe我做这类回归项目的第一件事永远是df.info()和df.describe()而不是急着训练。原始数据集是医疗保险账单记录字段大概是 age、sex、bmi、children、smoker、region 和 charges 标签。分类特征里 smoker 是强影响因子region 是四分类sex 是二分类。代码里第一步一般长这样import pandas as pd df pd.read_csv(insurance.csv, encodingutf-8-sig) print(行数/列数:, df.shape) print(df.describe(includeall)) print(df.isnull().sum())逻辑很简单shape告诉你数据规模describe(includeall)能同时能看到数值列的均值、标准差、四分位数和分类列的唯一值数量isnull().sum()是排除空值干扰。跑完之后你会发现数值列没有空值但分类列里有没有空值不能只看describeisnull().sum()才是最准的。这一步存在的意义是避免后面做特征工程时被空值打断。2.2 缺失值与重复值为什么 1070 行小数据集不用采样原报告里写得很清楚读入后没有空值测试集约 1070 行数据量不大不需要删除也不需要考虑欠采样。但要注意这不代表可以直接进模型。我在复现时习惯补一步重复值检查dup_count df.duplicated().sum() print(重复行数量:, dup_count)duplicated()默认按整行完全重复判断。如果结果不是 0就说明数据源带重复记录。这时候直接随机切分会产生一个隐患同一条记录的副本可能同时进训练集和测试集后面模型评估指标会虚高。原项目在报告里提到“数据中存在相同值”但他并没有直接删而是选择保留。我建议的做法是先在探索阶段统计出来后面用df.drop_duplicates(inplaceTrue)单独验证一次比较删与不删的效果差别。测试集只有 1070 行意味着训练数据总量也可能只有几千行。这种量级下不适合做复杂采样常见做法是保留全量数据用 K 折交叉验证来评估。留一法也能用但计算量会随样本量线性上涨几万行就别碰了。这份资源里留一法只是作为对比实验出现不是主验证手段。2.3 特征与目标的关系建模前先做分布探查正式训练前我会先画四个图charges 的直方图、smoker 分组的 charges 箱线图、bmi 对 charges 的散点图、age 对 charges 的散点图。这样做的原因是医疗花费通常是右偏分布直接拿原始标签做回归误差会被少数高额账单主导。import matplotlib.pyplot as plt plt.hist(df[charges], bins30, edgecolork) plt.xlabel(charges) plt.ylabel(count) plt.title(charges distribution) plt.show()如果看到明显的长尾后面可以考虑np.log1p(df[charges])做标签变换。原项目没有强制要求但我在复现时发现模型融合后的 RMSE 在 log 变换下会明显更低。这只是探索阶段的一个备选动作不是必须步骤。真正要确认的是 smoker 这类二值特征影响是否够大——这决定手写随机森林时要不要把特征随机范围调小。3. 从零手写随机森林与线性回归不调包的核心逻辑3.1 手写线性回归梯度下降与特征归一化不调包实现线性回归最常见的路线是梯度下降因为正规方程需要求逆矩阵样本多时很慢。原始代码的核心思路是先对特征做标准化再迭代更新权重。我写一个最小可用版本import numpy as np def mse_loss(y, y_pred): return np.mean((y - y_pred) ** 2) def linear_regression_gd(X, y, lr0.01, epochs1000): # X 和 y 均为 ndarray n, m X.shape W np.zeros(m) b 0.0 for epoch in range(epochs): y_pred X W b grad_w (2 / n) * (X.T (y_pred - y)) grad_b (2 / n) * np.sum(y_pred - y) W - lr * grad_w b - lr * grad_b if epoch % 100 0: print(epoch, epoch, mse, mse_loss(y, y_pred)) return W, b这段代码有几个关键点X W是向量内积要求 X 已经做过标准化否则 bmi 这种几十量级的特征会把 age 这种几十量级的特征淹没。梯度公式里X.T (y_pred - y)是把每个样本的误差乘以对应特征值再累加求平均。学习率lr0.01不是固定值如果 loss 震荡把它降到 0.001 或 0.0005如果下降太慢提到 0.05。epochs 设为 1000 是经验值判断收敛看 loss 是否不再下降就行。注意手写版本没有封装 one-hot 编码。真正跑之前需要手动把 sex、smoker、region 变成数值列否则矩阵运算会直接报错。这也是手写和调包最大的差别sklearn 里的模型会帮你处理一部分数据检查手写必须自己负责。3.2 手写随机森林Bootstrap 采样与特征随机随机森林的手写难点不在“森林”而在“树”。如果你一上来就写完整 CART 树代码量会很可观。原始实现里是用不调包的方式写的我拆解出的最小骨架包含两步Bootstrap 采样生成多个训练子集每个子集上建一颗决策树分裂时随机选一部分特征。def bootstrap_sample(X, y): # 有放回采样允许同一行被抽到多次 idx np.random.choice(len(y), len(y), replaceTrue) return X[idx], y[idx] def random_forest_predict(trees, X): # trees 是fit阶段返回的决策树列表 preds np.column_stack([tree_predict(tree, X) for tree in trees]) return np.mean(preds, axis1)代码里没有展开tree_predict因为那依赖你自己实现的二叉搜索结构。核心思想是每棵树只见到约 63.2% 的原始样本剩下的是袋外样本袋外误差可以直接用来评估。特征随机则体现在每棵树分裂时不扫描全部特征而是随机挑max_features个。原项目里手写实现没有直接使用 sklearn 的 DecisionTree所以你要额外控制两个参数树的最大深度和叶子最少样本数。不控制的话每棵树都能把训练集完美切分最终结果就是训练集 R2 接近 1测试集一塌糊涂。我复现时发现手写随机森林最容易错的地方是分裂点计算。如果你用方差减少量作为分裂指标需要把当前节点的目标方差和左右子节点的加权方差做差。这一步如果用循环嵌套写数据量不大时还能跑一旦树深超过 10速度会肉眼可见地变慢。3.3 对比 scikit-learn 实现为什么结果对得上手写版本做完下一步一定是和 sklearn 的随机森林对比否则你不知道自己写没写对。常见做法是这样from sklearn.ensemble import RandomForestRegressor rf_sk RandomForestRegressor(n_estimators50, max_depth5, min_samples_leaf5, random_state42) rf_sk.fit(X_train_scaled, y_train) print(sklearn R2:, rf_sk.score(X_test_scaled, y_test))如果两边 R2 误差在 0.02 以内说明手写版本的基本逻辑没问题。如果差距大优先检查你的分裂特征是否包含类别变量转换后的脏列。另外sklearn 的 RandomForest 默认bootstrapTrue这和手写的 Bootstrap 采样一致但 sklearn 默认max_features1.0回归树也就是分裂时用全部特征手写版本一般会设置sqrt两者要调成一致再对比。4. 调包实现 GBDT、SVR、Lasso 与决策树参数设置的取舍4.1 四个模型的最小可用代码原项目用 scikit-learn 实现了 GBDT、SVR、Lasso、决策树。这四个模型覆盖了回归问题的三种思路树模型、核方法、线性稀疏模型。先给一套能跑的最小代码from sklearn.ensemble import GradientBoostingRegressor from sklearn.svm import SVR from sklearn.linear_model import Lasso from sklearn.tree import DecisionTreeRegressor from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler models { gbr: GradientBoostingRegressor(n_estimators200, max_depth3, learning_rate0.05), svr: Pipeline([ (scaler, StandardScaler()), (svr, SVR(C1.0, epsilon0.1)) ]), lasso: Lasso(alpha0.1, max_iter10000), dt: DecisionTreeRegressor(max_depth5, min_samples_leaf5) }这里有几个参数选择逻辑。GBDT 的learning_rate和n_estimators是配套的学习率越小需要的树越多0.05 配 200 棵是常见起点。SVR 对特征尺度极其敏感所以必须包在 Pipeline 里做标准化。注意 Lasso 的alpha默认是 1.0但在这个数据集上 1.0 会把系数压得太狠0.1 起步更保险。决策树则不必做标准化因为树模型不受尺度影响但max_depth必须限制否则一棵树就过拟合了。4.2 参数网格怎么设计从默认值出发这五个模型里GBDT 和决策树的参数最值得调SVR 的 C 和 gamma 是经典难题Lasso 主要调 alpha。设计网格时不要贪多先粗后细。我一般先用 RandomizedSearchCV 定一个大范围from sklearn.model_selection import RandomizedSearchCV param_dist { n_estimators: [100, 200, 300], max_depth: [2, 3, 4, 5], learning_rate: [0.01, 0.05, 0.1], subsample: [0.8, 0.9, 1.0] } random_search RandomizedSearchCV( GradientBoostingRegressor(), param_dist, n_iter20, cv5, scoringneg_mean_squared_error, random_state42 ) random_search.fit(X_train, y_train) print(random_search.best_params_)n_iter20的意思是只随机组合 20 组参数不是全排列。这一步跑得很快能帮你排除明显不好的学习率和深度。拿到粗结果后再以最优值附近的小范围做 GridSearchCV 细搜。如果直接 GridSearchCV上面那个网格就是 3×4×3×3108 组一组 5 折数据量再小也要跑很久。4.3 模型评判K 折交叉验证与留一法的适用边界评判方式原项目提到了 K 折交叉验证和留一法。留一法在理论上无偏但它的方差大而且每折只留一个样本模型要重新训练 N 次。这份数据几千行跑留一法没问题但没必要K 折交叉验证更实用。我常用的配置是 K5配合回归指标from sklearn.model_selection import cross_val_score scores cross_val_score( GradientBoostingRegressor(n_estimators200, max_depth3, learning_rate0.05), X_train, y_train, cv5, scoringneg_mean_squared_error ) print(RMSE:, (-scores.mean()) ** 0.5)cross_val_score返回 5 个负均方误差取负号再开方就是 RMSE。这里有个坑它默认返回的是负的 MSE有人直接打印 scores 看到负数以为模型崩了。另外如果你在 GridSearchCV 里用scoringr2得到的是 R2 分数两个指标的最佳参数可能不同最终报告要写清楚自己用的是哪个。5. 避坑指南重复值、过拟合与调参顺序的三个真实翻车做这个项目时我踩过几个让心态爆炸的坑逐个记录在这里都是血泪经验。坑 1重复值导致验证集指标虚高现象我跑模型融合后训练集 R2 0.97测试集 R2 0.95好到不像真的。核对原始数据后才发现df.duplicated().sum()不等于 0有几条记录一模一样。原因数据本身有重复。随机划分时同一条记录的副本同时出现在训练集和测试集模型等于“记住”了答案泛化指标失真。解决先df.drop_duplicates(inplaceTrue)删掉重复后再划分。删完后测试集 R2 降到 0.92 左右这才是真实水平。如果不想删也可以把重复值单独归入训练集但这样做逻辑上更绕。坑 2手写随机森林训练集完美测试集翻车现象手写随机森林在训练集上 R2 0.99测试集 R2 0.31比 sklearn 默认决策树还差。原因每棵树不受深度限制长成了完整树Bootstrap 采样又让每棵树高度相似整体方差大过拟合严重。解决给每棵树加限制max_depth5min_samples_leaf3同时把特征随机范围设成sqrt(n_features)。改完以后测试集 R2 回到 0.85 以上。坑 3SVR 调参直接 GridSearchCV跑了整整一晚上现象SVR 的 C 和 gamma 网格分别设了 10 个值共 100 组每组 5 折训练 500 次跑完发现最优值在最边缘等于白跑。原因SVR 的 C 和 gamma 作用范围是数量级的在 0.1 到 10 之间均匀取值根本找不到规律。应该先在 log 尺度上粗搜再用 RandomizedSearchCV 缩小范围。解决第一轮用C[0.1,1,10,100]、gamma[0.001,0.01,0.1,1]通过 RandomizedSearchCV 跑 30 组第二轮在最优值附近取更细的值。坑 4Stacking 泛化比单模型还差现象用训练集预测结果作为 meta 特征Stacking 的测试集 R2 比 GBDT 单独跑还低 0.05。原因meta 特征来自基模型在训练集上的预测这些预测已经“见过”训练集标签再拿去训练上层模型造成严重的数据泄露。解决用交叉验证生成 out-of-fold 预测。sklearn 的StackingRegressor默认就是做这个传cv5即可不要自己手拼训练集预测。坑 5GBDT 学习率调成 0.5loss 直接发散到 NaN现象手动把learning_rate从 0.05 改成 0.5想让模型更快收敛结果训练到一半 loss 变成nan。原因学习率太大梯度更新跨度过大损失函数涡旋发散。GBDT 每一步都在拟合负梯度学习率过大会让叠加结果剧烈震荡。解决回归问题学习率从 0.01 到 0.1 起步最多试到 0.2。如果 loss 出现 NaN第一时间降低学习率而不是调树深。6. 模型融合进阶直接平均、加权平均到 Stacking 的验证技巧6.1 直接平均与加权平均权重不是拍脑袋直接平均法很简单就是把 GBDT、SVR、Lasso、决策树、随机森林五个模型的预测值等比例相加除以 5。我在实际项目里发现等权平均往往比单独挑最优模型更稳因为不同模型的偏差方向不同平均能抵消一部分噪声。加权平均则需要验证权重最常见的手段是拿验证集预测值做线性回归让 scikit-learn 帮你解权重from sklearn.linear_model import Ridge # preds_val 的每一列是单模型在验证集上的预测 meta_model Ridge(alpha1.0) meta_model.fit(preds_val, y_val) print(权重:, meta_model.coef_)Ridge在这里的意义是防止某些模型权重大到接管全部结果。如果算出来的权重接近等权你就不用花时间手工调权重了。6.2 Stacking 的层级设计与特征泄露Stacking 规范做法是让每个基模型在训练集上做 5 折预测把预测结果拼接成 meta 特征上层模型再用这个 meta 特征学习。代码长这样from sklearn.ensemble import StackingRegressor from sklearn.ensemble import GradientBoostingRegressor from sklearn.svm import SVR from sklearn.linear_model import Lasso base_models [ (gbr, GradientBoostingRegressor(n_estimators200, max_depth3, learning_rate0.05)), (svr, Pipeline([(scaler, StandardScaler()), (svr, SVR(C1.0))])), (lasso, Lasso(alpha0.1)), (rf, RandomForestRegressor(n_estimators50, max_depth5, min_samples_leaf5, random_state42)) ] stack StackingRegressor( estimatorsbase_models, final_estimatorRidge(alpha1.0), cv5, stack_methodpredict ) stack.fit(X_train, y_train) print(stack test R2:, stack.score(X_test, y_test))stack_methodpredict表示 meta 特征用预测值也可以改成predict_proba但回归任务里没概率输出就用 predict。cv5是 Stacking 核心它保证了 meta 特征不会包含训练集过拟合信息。如果最后分数只比单模型高一点点先把基模型调好再来谈融合基模型太弱时 Stacking 只会叠加噪声。6.3 最终验证和报告里的 R2 对齐做完融合后我习惯把五个单模型和三个融合方式的 RMSE、R2 整理成一张结果表检查两个东西第一GBDT 是不是单模型里最强第二Stacking 是否在所有融合方式里排第一。原项目用 K 折和留一法分别评估说明作者经历过模型验证不稳定问题。从那以后我每次跑完这类项目都强制走一遍“数据检查-重复值删除-手工基线-调包对比-融合验证”的全流程确认没有数据泄露才敢拍照记录结果。希望帮到你这份资源值得下载跑一遍特别是手写随机森林那部分能补齐你调 sklearn 时不知道内部细节的短板。本文还有配套的精品资源点击获取