
简介一份基于Jupyter Notebook的糖尿病预测机器学习项目面向数据科学初学者与医疗健康研究者基于公开的Pima Indians糖尿病数据集构建风险预测模型覆盖从数据清洗到模型评估的完整流程。压缩包共含4个文件总大小约322KB分别提供Python脚本、Notebook分析文档、Markdown说明及训练好的joblib模型文件脚本用于数据清洗与特征处理Notebook呈现完整的分析建模过程说明文档辅助理解项目结构模型文件则支持直接加载使用。目前已有235人学习下载。项目中系统展示了缺失值填充、探索性可视化、特征工程、逻辑回归与随机森林等多模型对比、交叉验证及准确率与AUC等指标评估可帮助读者快速掌握疾病预测建模的实战路径训练好的模型还支持即时推理与二次开发对健康数据分析场景具有较高的参考价值。1. Diabetes-Predictor 是什么一个需要认真对待的医疗预测项目把糖尿病预测做成一个能跑通的脚本门槛并不高但把它做成一个结果可信、边界清楚、敢给别人参考的预测器是另一回事。Diabetes-Predictor 正是这样一个典型方向基于一批体检指标预测一个人是否患糖尿病听起来像经典机器学习入门题但数据质量、类别不平衡、评估指标选错任何一个环节都会让模型在真实场景里翻车。这篇文章面向的读者是打算自己动手复现或扩展这个项目的人不管你是想交课程作业、写技术博客还是做一个真正能给家人朋友当健康参考的小工具都需要先想清楚一个问题预测准确率之外你还关心什么本文会按数据、模型、评估、避坑、部署这条主线把整个方向拆开讲透。2. 数据先行PIMA 数据集与特征工程的四个关键决策2.1 为什么选 PIMA 数据集以及它的三个先天缺陷绝大多数 Diabetes-Predictor 类型的项目都会从 PIMA 印第安人糖尿病数据集开始这个数据集属于经典公开数据集包含 768 条样本、8 个数值特征标签是是否患病。它之所以被广泛使用是因为特征全部是可测量的生理指标——血糖、血压、BMI、年龄等——不需要处理文本或图像非常适合用来做机器学习流程的完整演练。但这不代表它是一个好数据集它有三个先天缺陷你必须心里有数。第一样本量只有 768 条对现代机器学习标准来说非常小深度模型基本不用考虑传统模型和集成模型才是合适的范围。第二类别不平衡数据集中负类样本明显多于正类样本直接训练出来的模型会偏向预测多数类准确率虚高是常态。第三特征全部是生理测量值缺少生活方式、家族史、饮食结构这类在临床上同样重要的信息这意味着模型的上限从一开始就被锁死了。所以我对这个项目的态度是PIMA 适合用来学方法不适合用来宣称“我做出了一个能用于临床筛查的产品”。如果某篇文章说自己的模型准确率 99%基本可以断定是数据泄露或者评估方式出了问题。正确做法是把 PIMA 当作一个标准测试平台先把流程跑通再考虑换更大的真实数据集。表格对比一下常见选型数据集样本量特征类型适合场景PIMA7688 个数值特征学习流程、算法对比医院体检数据自采数千到数万数值 类别 文本真实部署、临床辅助公开健康调查数据数万多样特征工程练习2.2 特征标准化与缺失值处理的参数选择这个阶段最容易犯的错误是拿到数据直接往模型里塞。PIMA 数据集的 8 个特征量纲差异很大比如血糖值通常在 70 到 200 之间而 BMI 在 18 到 50 之间年龄在 21 到 80 之间。如果你用基于距离的模型或正则化的线性模型量纲大的特征会主导梯度更新导致模型把注意力全放在数值大的特征上这显然不是我们想要的。常见的做法是先做标准化。我习惯用StandardScaler而不是MinMaxScaler原因是 StandardScaler 对异常值的敏感度更低。MinMaxScaler 把数据压到 0 到 1 之间但如果某个极端值出现在训练集里其他样本会被压缩到非常窄的区间信息被挤压掉了。StandardScaler 用均值和标准差做变换即使有少量异常值整体分布也不会被破坏得太厉害。from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_scaled scaler.fit_transform(X_train) # 打印缩放后的均值与标准差应该分别是 0 和 1 print(f均值: {X_scaled.mean(axis0)}) print(f标准差: {X_scaled.std(axis0)})逻辑说明fit_transform在训练集上计算均值和标准差然后用这两个统计量做变换。这里的关键是绝不能用全部数据去 fit否则会把测试集的信息泄漏进训练过程。测试集必须用训练集拟合好的 scaler 去做transform而不是重新计算。这是数据泄露最常见的来源之一后面避坑章节会重点展开。另一个容易被忽略的点是缺失值处理。PIMA 数据集里存在一些特征值为 0 的情况比如血糖、血压、BMI 为 0 在生理上是不可能的这实际上是缺失数据的编码方式。简单删除这些样本会进一步缩小本就有限的数据集所以一般用中位数填充或不处理交给树模型。import pandas as pd df pd.read_csv(diabetes.csv) # 将部分生理特征中的 0 视为缺失值 zero_cols [Glucose, BloodPressure, SkinThickness, Insulin, BMI] for col in zero_cols: df[col] df[col].replace(0, pd.NA) # 用中位数填充 df[zero_cols] df[zero_cols].fillna(df[zero_cols].median()) print(f填充后剩余缺失值数量: {df[zero_cols].isna().sum().sum()})逻辑说明中位数比均值更适合存在偏态分布的医学指标皮肤厚度和胰岛素这两个特征经常有重尾分布用均值填充会让高值样本的权重进一步被拉偏。如果不做填充而是直接删除768 条里可能要扔掉近 200 条模型可用的样本量会严重缩水在这个数据集规模下很难接受。3. 模型选型从 Logistic Regression 到 XGBoost 的对比实验3.1 基线模型怎么搭Logistic Regression 的最小可运行代码任何预测项目都应该从基线模型开始。对二分类问题Logistic Regression 是天然的起点它不是最准的但它能给你一个“下限”——如果后面的复杂模型连 LR 都打不过说明问题出在数据或特征工程上而不是模型复杂度不够。from sklearn.linear_model import LogisticRegression from sklearn.model_selection import cross_val_score lr LogisticRegression( C1.0, solverliblinear, max_iter1000, random_state42 ) scores cross_val_score( lr, X_scaled, y, cv5, scoringroc_auc ) print(fLogistic Regression AUC: {scores.mean():.4f} ± {scores.std():.4f})逻辑说明C是正则化强度的倒数C 越小正则化越强。PIMA 只有 8 个特征不需要过强的正则化C1.0 是一个合理的默认值。solverliblinear对二分类和小数据集表现稳定liblinear不支持所有 solver 都支持的自动多分类模式但这种规模下效率反而是优势。max_iter1000防止某些特征尺度不均匀导致收敛失败。scoringroc_auc是故意的——准确率在这个不平衡数据集上没有参考价值后面会详细讲。如果 LR 的 AUC 在 0.80 附近说明特征和数据的信号已经被利用得不错了。如果低于 0.75优先检查数据清洗和特征工程而不是换更复杂的模型。3.2 树模型的优势与集成学习的参数要点Logistic Regression 是线性模型它假设特征和 log-odds 之间存在线性关系。但血糖、BMI、年龄之间可能存在复杂的交互效应比如年轻但高 BMI 的人群风险模式可能和中老年完全不同这种非线性关系用 LR 很难捕获。这时候就需要树模型出场了。决策树天然处理非线性但单棵树方差太大容易过拟合。标准做法是集成学习XGBoost 是这个项目的首选之一也是用 XGBoost 开个近似爆款的关键词之一。不过 XGBoost 在小数据集上如果不调参会很容易过拟合因为默认参数是为大规模数据设计的树深度和叶子节点数都偏大。import xgboost as xgb xgb_model xgb.XGBClassifier( n_estimators200, max_depth3, learning_rate0.05, subsample0.8, colsample_bytree0.8, reg_alpha0.1, reg_lambda1.0, eval_metricauc, use_label_encoderFalse, random_state42 ) xgb_model.fit(X_train, y_train, verboseFalse)逻辑说明max_depth3在小数据集上控制树复杂度防止每个叶子都分到极少数样本。learning_rate0.05降低每棵树对最终结果的贡献配合n_estimators200达到一个偏保守的组合。subsample0.8和colsample_bytree0.8分别是行采样和列采样增加了模型的随机性有助于减少过拟合。reg_alpha和reg_lambda加上 L1 和 L2 正则化对只有几百条样本的数据集来说正则化比堆树数量更重要。还有一个细节值得注意use_label_encoderFalse是因为较新版本的 XGBoost 不再推荐自动编码标签显式关闭可以避免版本更新带来的行为变化。这属于我从实际运行中遇到的问题——不同版本默认行为不同写死参数至少保证代码可复现。4. 训练、验证与评估别让准确率骗了你4.1 分层采样与交叉验证的设置进入训练阶段后第一个要决定的是怎么划分数据。很多人习惯用train_test_split(X, y, test_size0.2)看起来很标准但在类别不平衡的数据集里有隐患如果随机切分导致训练集里正类比例偏低模型的召回率会很难看。正确做法是给train_test_split传入stratifyy参数做分层采样。from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X_scaled, y, test_size0.2, stratifyy, random_state42 ) print(f训练集正类占比: {y_train.mean():.4f}) print(f测试集正类占比: {y_test.mean():.4f})逻辑说明stratifyy保证训练集和测试集中正负类比例与原始数据一致。打印两个比例应该非常接近如果差异超过 0.03说明样本量太小或者分层没有生效。对于只有 768 条的 PIMA 来说分层不是可选项而是必选项。交叉验证方面我倾向于用StratifiedKFold而不是默认的KFold原因和上面一样。在类别不平衡场景下KFold可能出现某一折里正类样本特别少的情况导致该折的评估结果波动剧烈。分层交叉验证让每一折都保持类别比例评估方差更小。from sklearn.model_selection import StratifiedKFold skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) for fold, (train_idx, val_idx) in enumerate(skf.split(X_scaled, y)): X_fold_train X_scaled[train_idx] X_fold_val X_scaled[val_idx] y_fold_train y[train_idx] y_fold_val y[val_idx] print(fFold {fold 1}: 训练集 {len(train_idx)} 条, 验证集 {len(val_idx)} 条)逻辑说明shuffleTrue在划分前打乱数据避免原始数据的顺序对结果产生影响。random_state42固定随机种子保证实验可复现。折叠数n_splits5在 768 条样本上每折验证集约 154 条这是一个相对平衡的选择——折数太多每折样本太少折数太少评估可能不稳定。4.2 混淆矩阵、ROC-AUC 与召回率的读法模型训练完成后第一反应是打印accuracy_score这几乎是下意识行为。但在糖尿病预测这个场景里准确率是一个带有误导性的指标。假设测试集中 65% 是负类一个什么都不预测、永远输出“健康”的模型也能拿到 65% 的准确率。这个数字会让你误以为模型有实用价值实际上它什么都没学到。正确的做法是同时看混淆矩阵、精准率、召回率和 ROC-AUC。其中召回率在医疗筛查场景里有特别的意义漏掉一个真实患者——假阴性——的代价比误报一个健康人——假阳性——要高得多。所以在调参时我一般会优先保证召回率达到一个可接受的水平再尽量压低假阳性率。from sklearn.metrics import confusion_matrix, classification_report, roc_auc_score y_pred xgb_model.predict(X_test) y_pred_proba xgb_model.predict_proba(X_test)[:, 1] cm confusion_matrix(y_test, y_pred) tn, fp, fn, tp cm.ravel() print(f混淆矩阵: TN{tn}, FP{fp}, FN{fn}, TP{tp}) print(f召回率(敏感度): {tp / (tp fn):.4f}) print(f特异度: {tn / (tn fp):.4f}) print(fROC-AUC: {roc_auc_score(y_test, y_pred_proba):.4f})逻辑说明predict_proba返回的是每个类别的概率取[:, 1]即正类概率ROC-AUC 用的就是这个概率而不是硬分类结果这样能评估模型排名的质量不受分类阈值影响。混淆矩阵四个格子要分开读FN 是漏诊FP 是误报在糖尿病预测场景下漏诊的代价通常高于误报。这里补充一个通用的评估建议如果两个模型的 AUC 接近但一个召回率高、一个精准率高怎么选要看应用场景。做个人健康参考工具宁可多提醒几次让用户去复查也不能漏掉真实风险做医疗资源分配系统则要控制误报数量避免浪费医疗资源。这个分寸是业务决定的不是技术指标能替你决定的。5. 避坑指南Diabetes-Predictor 最常见的四个坑5.1 数据泄露特征缩放放错了位置现象训练集 AUC 是 0.95交叉验证 0.80测试集 AUC 突然跌到 0.72而且不管怎么调参都拉不回来。原因在切分训练集和测试集之前就对全部数据做了fit_transformStandardScaler的均值和标准差是从整个数据集算出来的测试集的信息已经“泄露”到训练过程里了。交叉验证里的每一折也使用了全局缩放后的数据所以评估结果偏乐观但真实测试集是全新数据分布和缩放参数不一定匹配一上场就现原形。解决严格遵循“先切分再缩放”的顺序。对训练集执行fit_transform然后把同一个 scaler 对象用于测试集的transform。在 Pipeline 中定义这个过程是更稳妥的做法不会因为手动顺序出错。from sklearn.pipeline import Pipeline pipeline Pipeline([ (scaler, StandardScaler()), (classifier, LogisticRegression(C1.0, solverliblinear)) ]) scores cross_val_score( pipeline, X_train, y_train, cv5, scoringroc_auc ) print(fPipeline 交叉验证 AUC: {scores.mean():.4f})逻辑说明将缩放器放进 Pipeline 后交叉验证的每一折都会在训练子集上重新 fit 缩放器然后在验证子集上做 transform从根本上避免了泄露。这也是我在做过一次翻车之后养成的习惯——凡是涉及特征变换的都往 Pipeline 里放不再手动写。5.2 类别不平衡准确率虚高的陷阱现象模型训练完打印准确率 0.85看起来很理想。但看一眼混淆矩阵发现 F1 值只有 0.55召回率只有 0.42。模型几乎把大部分真实患者都判成了健康。原因PIMA 数据集中正类占比约 35% 不到负类占多数。模型在学习过程中发现“全部预测为健康”就能拿到约 65% 的准确率优化目标已经把模型引向了这个方向。如果不使用适合不平衡数据的评估指标准确率这个数字就会把问题掩盖掉。解决换用 AUC 作为主要优化指标同时关注召回率和精准率的平衡。如果召回率持续过低考虑使用class_weightbalanced或者调整分类阈值后者的效果往往比改模型结构更直接。from sklearn.linear_model import LogisticRegression lr_balanced LogisticRegression( C1.0, solverliblinear, class_weightbalanced, max_iter1000, random_state42 )逻辑说明class_weightbalanced会根据类别频率自动调整正负类的权重让模型在训练时更关注少数类。这个方法在逻辑回归上很有效但对 XGBoost 来说除了用scale_pos_weight也可以先尝试调低分类阈值。阈值调整的意思是默认预测正类的概率阈值是 0.5如果实际数据中正类比例小可以降到 0.3 或 0.4用更高的误报率换取更低的漏诊率。5.3 过拟合XGBoost 默认参数在小样本上的翻车现象训练集 AUC 达到 0.98交叉验证只有 0.78测试集 0.74。三者差距较大明显模型记熟了训练数据的噪声却在没见过的地方发挥不稳定。原因XGBoost 默认的max_depth6对几百条样本来说太深加上默认学习率偏高、没有足够的正则化约束模型在训练集上能轻松做到“完美记忆”。这种参数是为大规模数据设计的在小数据集上并不适用。解决降低max_depth到 3 或 2调低learning_rate到 0.05 以下并打开正则化参数。同时可以用早停来控制训练轮数。xgb_model xgb.XGBClassifier( n_estimators500, max_depth2, learning_rate0.03, subsample0.7, colsample_bytree0.7, reg_alpha0.2, reg_lambda2.0, eval_metricauc, use_label_encoderFalse, random_state42 ) xgb_model.fit( X_train, y_train, eval_set[(X_val, y_val)], verboseFalse )逻辑说明eval_set把验证集传入训练过程配合eval_metricauc可以观察每一轮在验证集上的表现。注意此处没有再传early_stopping_rounds——因为在这个代码片段里验证集已经被用于观察如果继续用同一个验证集做早停再评估结果也会有乐观偏差。更干净的方案是再切一块数据或直接靠交叉验证的均值判断。5.4 特征重要性误读不是所有高权重特征都是因果现象模型训练完成后输出feature_importances_发现血糖权重最高、BMI 次之。有的人就会得出结论控制血糖就能预防糖尿病。从统计角度看这个推论有风险。原因相关性不等于因果。血糖和糖尿病高度相关也可能因为血糖升高是糖尿病发生后的表现两者存在反向因果BMI 和血糖本身也互相影响。模型的特征重要性只反映统计关联强度不反映医学因果路径。解决在文档中明确区分“预测关联”和“医学因果”。对模型的使用者说明结果是风险提示不是诊断结论。部署一个预测器时不要让用户自己解读“哪个指标最重要”要给出整体概率和对应的建议动作。某实验室做过一个类似模拟项目X正是在部署阶段弱化了特征重要性展示才避免了一批用户对“体重最重要”这类结论的误解。6. 部署与进阶把模型封装成可用的预测接口6.1 模型持久化与 API 封装模型调好之后真正的落地是把预测能力封装成接口。不能每次预测都重新训练需要把训练好的模型、缩放器等全部持久化。我一般用joblib来处理 sklearn 和 xgboost 模型。import joblib artifacts { scaler: scaler, model: xgb_model, feature_names: feature_cols } joblib.dump(artifacts, diabetes_predictor.joblib)逻辑说明把缩放器和模型打包到一个字典里整体保存后续加载时不会漏掉任何组件。feature_names记录特征顺序因为预测接口收到的输入可能是 JSON 或字典格式必须按照训练时的特征顺序重新排列这是一个容易踩坑的细节——顺序错了模型的基础就错了。加载时对应的代码loaded joblib.load(diabetes_predictor.joblib) scaler loaded[scaler] model loaded[model] def predict_single(input_dict): import pandas as pd df pd.DataFrame([input_dict])[loaded[feature_names]] df_scaled scaler.transform(df) prob model.predict_proba(df_scaled)[0, 1] return prob逻辑说明函数接收一个字典形式的输入比如{Glucose: 120, BMI: 28.5}按feature_names顺序构建 DataFrame 后缩放。返回的是正类概率即患糖尿病的风险概率。调用方可以根据概率值做后续处理比如超过 0.5 建议就医检查0.3 到 0.5 提示注意生活方式。6.2 输入校验与边界条件的防御模型接口上线后最怕的不是模型本身不准而是传入非法数据导致不可预期的结果。比如血糖值为负、BMI 为 500、缺失了某个特征这些都需要在进入模型之前拦截。VALID_RANGES { Pregnancies: (0, 20), Glucose: (40, 250), BloodPressure: (30, 140), SkinThickness: (0, 80), Insulin: (0, 500), BMI: (10, 60), DiabetesPedigreeFunction: (0.0, 3.0), Age: (20, 100) } def validate_input(data): errors [] for col, (low, high) in VALID_RANGES.items(): if col not in data: errors.append(f缺少字段: {col}) elif not (low data[col] high): errors.append(f{col} 超出合理范围: {data[col]}) return errors逻辑说明每个特征的合理范围来自 PIMA 数据集的分布和医学常识的交叉。超出范围直接拒绝预测并让使用者提供新的输入。之所以单独做校验函数是因为模型对分布外数据的行为是黑匣子——它可能给出一个看似合理的概率但那个概率没有任何可信度。我不希望用户拿着一个伪造的数值得到一个误导性的结论这种情况下宁可报错。对于预测结果的解释一个具体的落地习惯是只输出概率不输出“是/否”的硬结论。概率天然传递不确定性而二分类的“是/否”会让人忽略模型的置信度。把概率映射成可执行的建议比如概率低于 0.3维持常规体检频率概率在 0.3 到 0.6建议做空腹血糖和糖化血红蛋白检查概率高于 0.6尽快去内分泌科做口服葡萄糖耐量测试这个阈值不是模型定的是使用场景定的。我见过不少人把predict_proba的结果直接套用 0.5 做二分类结果在真实人群中敏感度不够被边缘案例打脸。给出概率分档的目的是让模型输出的数字变成非专业人士也能理解的动作而不是一个冷冰冰的判定。最后想分享一个我自己做这个方向时学到的教训模型从 0.80 的 AUC 提到 0.84 可能花了很多精力调参但在落地时输入校验和数据接口的稳定性对用户体验的影响远大于这 0.04。很多练习项目止步于拿到一个好看的测试分数而真正能让人愿意持续使用下去的是把技术细节做扎实把边界条件想清楚。希望这篇文章能帮你在自己的 Diabetes-Predictor 项目中少走几步弯路。本文还有配套的精品资源点击获取