
简介这是一套以 Jupyter Notebook 为核心的糖尿病预测入门项目面向机器学习初学者与数据分析爱好者演示如何利用 Pima Indians Diabetes 数据集完成从数据清洗、EDA 可视化、特征工程到模型训练与评估的完整流程。压缩包共 4 个文件、约 322KBipynb 文件是分步分析笔记py 脚本提供可复用的训练入口md 文档说明项目结构与依赖joblib 文件则是已训练好的模型加载后可直接用于个体风险预测。项目覆盖逻辑回归、决策树、随机森林等常见算法的对比并通过准确率、精确率、召回率、F1 分数与 ROC/AUC 等指标解释模型评估与调优思路让读者在复现过程中理解分类任务的关键环节。对希望快速上手医疗健康类数据挖掘、并获取完整可运行代码与训练模型的学习者这套资源具有直接的参考价值目前已有 236 人学习。1. 一个能跑通的糖尿病预测项目从 Jupyter Notebook 到完整建模链路拿到这份 Diabetes-Predictor 工程包时我第一反应是「这不过又是一个拿公开糖尿病数据集跑逻辑回归的 Jupyter Notebook」。直到顺着单元格一路往下执行完才发现里面把血糖为 0 的脏数据、缺失值填充、分层切分、模型持久化这些坑都提前填好了。它不追求把准确率刷到天上而是给出一条完整可复现的建模链路数据进来之后怎么清洗、怎么构造特征、怎么训练、怎么评估每一步都有输出可核对。如果你正在用 Jupyter Notebook 做医疗数据分类任务或者想找一个能照着改的机器学习工程模板这份资源值得花半小时跑一遍。新手能顺着执行顺序复现结果熟手可以直接跳过探索性分析部分把训练管道提取出来接到自己的项目里。整份工程围绕「可复现」三个字组织这也是它和我见过的大部分单文件 Notebook 最大的区别。2. 项目结构与数据流Notebook 里从原始表格到训练集的全链路2.1 目录结构与文件职责一份能长期复用的 Notebook 工程不能只有一个孤零零的 .ipynb 文件。建模过程中的中间数据、训练好的模型、依赖版本缺一样都会让「换个环境就翻车」变成常态。这个工程包的目录拆得很朴素但每样东西都有明确的去处路径职责data/diabetes.csv原始糖尿病筛查数据768 条样本、9 列字段data/processed_diabetes.csv清洗和特征工程之后的中间结果跑完预处理单元格后落盘notebooks/Diabetes_Predictor.ipynb主 Notebook按从上到下的顺序执行即可models/logistic_model.joblib训练并序列化之后的逻辑回归模型models/scaler.joblib训练集上拟合的标准化器预测时与模型配套加载models/roc_curve.pngROC 曲线输出图评估时自动生成requirements.txt锁定 pandas、scikit-learn 等依赖版本README.md环境准备与运行顺序说明我一般会把原始数据和处理后数据分开放而不是在同一个 DataFrame 上反复覆盖。原因有两层第一数据清洗的规则比如哪些列要把 0 当成缺失经常会改保留原始数据意味着改完规则后重新执行清洗单元格即可不用重新下载数据第二拿 processed_diabetes.csv 做探索性分析或者喂给别的模型时不用再背一遍 Notebook 的执行顺序。养成这个习惯之后我的 Notebook 项目基本不再出现「跑完一次再跑就报错」的尴尬。中间数据落盘还有一个实际好处是复现速度。特征工程改了一版之后只需要重跑后半段的建模单元格前面的清洗结果直接读 CSV 就有了。数据量小的时候这个优势不明显等到数据变成几万行、几十万行少跑一遍预处理能省下大量等待时间。另一个容易被忽略的点是processed 文件本身就是数据质量的存档哪天有人质疑你的模型结果把它和原始数据做一次 diff清洗逻辑是否可靠一目了然。2.2 数据字段与建模流程先看数据集本身。这份公开的糖尿病筛查数据一共 9 列前 8 列是特征最后一列是二分类标签。下面是每个字段的含义以及我在清洗时关注的点字段含义处理注意点pregnancies怀孕次数真实 0 值有意义不做缺失处理glucose口服葡萄糖耐量试验 2 小时血糖0 视为测量缺失转 NaN 后填充blood_pressure舒张压mm Hg0 视为缺失转 NaN 后填充skin_thickness三头肌皮褶厚度mm0 视为缺失转 NaN 后填充insulin2 小时血清胰岛素mu U/ml0 视为缺失转 NaN 后填充bmi体质指数0 视为缺失转 NaN 后填充pedigree糖尿病家族遗传函数浮点数直接使用age年龄连续特征直接使用outcome是否患病1 患病 / 0 未患病标签列建模前必须从特征中剔除这里最关键的设计判断是「哪些 0 是真实值哪些 0 是缺失值」。怀孕次数为 0 完全正常但血糖、血压、BMI 为 0 在医学测量上没有物理意义通常是设备未记录或记录缺失。如果不做处理模型会把「血糖为 0」当成一种风险低的真实信号训练出来的系数带着系统性偏差。判断标准很简单看列的业务含义只要是测量类字段0 大概率是缺失占位。建模流程是一条串行管道读原始数据检查形状做去重和 0 值替换用中位数填充缺失构造 BMI 和血糖分箱特征做分层切分最后训练逻辑回归并做五折交叉验证。每个阶段都设计了可检查的输出用来确认前一步正常再继续。例如去重后打印样本数、填充后打印剩余缺失数、切分后打印各类别占比。这些零散的打印语句解决了 Notebook 项目最大的黑匣子问题变量在内存里改了但看不到到底改了什么。用 Jupyter Notebook 承载这套流程而不是直接写一个 train.py 脚本是有意为之。Notebook 对数据分析阶段的友好度远高于脚本每一步数据变换的中间结果都能直接看到表格和图形发现异常可以立刻在当前状态上修改不用从头再跑一遍。这也是数据探索类项目最常见的载体形式。等训练管道彻底稳定下来再按第 6 章的方式迁移成脚本也不迟。另外要明确一点这份数据的样本量不算大任何模型在小样本上都存在方差问题所以工程里把五折交叉验证当作评估主指标而不是只盯着单次测试集结果。这也是为什么 3.3 会单独用一个单元格做 CV而不是直接 fit 完就交差。3. 核心代码拆解清洗、特征工程与模型训练的逐段实现3.1 数据加载与缺失值处理Notebook 的前几个单元格是纯数据检查。加载数据后先打印形状再对五个「0 值无意义」的列做批量替换这是整个工程最值钱的几行代码。import pandas as pd import numpy as np df pd.read_csv(data/diabetes.csv) print(原始形状:, df.shape) # 医学测量值不可能为 0这里的 0 是缺失标记 zero_cols [glucose, blood_pressure, skin_thickness, insulin, bmi] for col in zero_cols: df[col] df[col].replace(0, np.nan) # 用中位数填充避免异常值把均值拉偏 df[zero_cols] df[zero_cols].fillna(df[zero_cols].median()) print(剩余缺失值:, df.isnull().sum().sum())replace 操作比 loc 加判断写起来更直白它把指定列里的所有 0 一次性换成 nan。选择中位数而不是均值是因为这些测量列的分布本身偏斜少数极端值会显著拉高均值而中位数对异常值不敏感填充后不会人为放大样本之间的差异。执行完这个单元格所有 0 值缺失都被填补剩余缺失值打印出来应该是 0。需要注意pedigree 和 age 这两列没有参与替换因为它们的 0 值如果有在业务上说得通。这个取舍直接决定后面训练集的质量也是我最早做类似项目时最容易翻车的地方——当时图省事把全表的 0 都填了结果怀孕次数这一列的信息被彻底抹掉模型性能肉眼可见地变差。处理缺失值的粒度一定要到列不能批量洗全表。3.2 特征工程与数据切分原始特征里BMI 和血糖是连续值但医生在临床上更习惯按区间判断风险。这一格把两个连续特征改造成分箱特征再用独热编码转成模型可用的数值列。from sklearn.model_selection import train_test_split # 连续特征分箱保留区间语义 df[bmi_category] pd.cut(df[bmi], bins[0, 18.5, 24, 28, 100], labels[偏瘦, 正常, 超重, 肥胖]) df[glucose_risk] pd.cut(df[glucose], bins[0, 100, 140, 200, 300], labels[正常, 偏高, 高风险, 极高]) # 原始连续特征加上分箱独热特征 features [pregnancies, blood_pressure, skin_thickness, insulin, bmi, pedigree, age] X df[features].copy() X pd.concat([ X, pd.get_dummies(df[bmi_category], prefixbmi), pd.get_dummies(df[glucose_risk], prefixglucose) ], axis1) y df[outcome] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) print(训练集:, X_train.shape, 测试集:, X_test.shape)pd.cut 的 bins 参数按医学常识设定BMI 以 18.5、24、28 为切点血糖以 100、140、200 为切点。分箱之后再用 get_dummies 做独热编码而不是把「超重」「肥胖」直接转成 1、2、3原因在于类别之间没有天然的大小关系强行用数字编码会引入不存在的顺序假设。独热之后特征维度从 7 列扩到 17 列左右训练集会变成 (614, 17) 的形状。train_test_split 里有两个参数很关键。stratifyy 表示按标签比例分层采样保证训练集和测试集里患病样本占比大致相同random_state42 固定随机种子让每次切分结果完全一致。前者解决数据分布漂移后者解决结果可复现缺一个都会让「这次跑了 78%下次跑只有 70%」的玄学问题出现。固定随机种子相当于给实验吃了后悔药至少能保证你改完特征后看到的性能变化是代码带来的还是运气带来的。切分之后还有一个常见的后续动作标准化。逻辑回归对特征尺度敏感bmi 是几十的量级pedigree 是零点几的量级系数会因此被扭曲。我一般会在切分后补一步 StandardScaler并且只对训练集 fitfrom sklearn.preprocessing import StandardScaler scaler StandardScaler().fit(X_train) X_train_scaled scaler.transform(X_train) X_test_scaled scaler.transform(X_test) joblib.dump(scaler, models/scaler.joblib)fit_transform 和 transform 绝对不能混用。scaler 只能在训练集上 fit测试集只能调用 transform否则测试集的均值、方差会泄漏进预处理逻辑导致交叉验证结果虚高。这个细节和标签泄漏一样隐蔽但它直接决定模型上线后的真实表现。3.3 模型训练与交叉验证模型部分选了逻辑回归作为基线。这个选择不是因为它最先进而是因为样本量只有 768 条逻辑回归在小样本二分类上稳定、可解释、不容易过拟合训练速度也快非常适合作为第一个版本的参照系。from sklearn.linear_model import LogisticRegression from sklearn.model_selection import cross_val_score # C 越小正则化越强max_iter 调大防止不收敛 model LogisticRegression(max_iter500, C0.1, solverliblinear) cv_scores cross_val_score(model, X_train, y_train, cv5, scoringroc_auc) print(5 折交叉验证 AUC: %.3f (/- %.3f) % (cv_scores.mean(), cv_scores.std())) model.fit(X_train, y_train)cross_val_score 把训练集切成 5 份轮流拿出 1 份做验证剩下 4 份训练最后返回 5 个 AUC 分数。这样得到的性能估计比单次切分更稳也不容易受某一份数据运气影响。代码里用训练集做交叉验证测试集保留到最终评估两者职责分开这是建模流程里必须守住的红线。参数 max_iter500 是我加上的血泪经验。逻辑回归用迭代法求解默认 max_iter100 在处理标准化后的高维特征时偶尔会在收敛之前被强制停止弹出 ConvergenceWarning。C0.1 控制正则化强度数值越小对系数的惩罚越大能抑制过拟合solver 选了 liblinear它是小数据集上效率很高的一种求解方式比默认的 lbfgs 在这个场景下更快也更稳。如果想换更强一点的模型把 LogisticRegression 换成 RandomForestClassifier 只需要改几行。但要注意分箱特征对树模型不是必须的而且随机森林在小样本上更容易过拟合交叉验证 AUC 通常反而不如逻辑回归。所以我一般会把逻辑回归当作这个数据量的默认选择等样本量上来再试梯度提升类模型。4. 结果怎么看混淆矩阵、ROC 曲线与特征重要性4.1 评估指标与可视化模型训练完成后Notebook 里并不满足于打印一个准确率就收工。二分类任务在正负样本不平衡时准确率会骗人所以工程里对测试集同时输出分类报告和 AUC 分数并绘制 ROC 曲线。from sklearn.metrics import classification_report, roc_auc_score, roc_curve import matplotlib.pyplot as plt y_pred model.predict(X_test) y_proba model.predict_proba(X_test)[:, 1] print(classification_report(y_test, y_pred)) print(测试集 AUC:, roc_auc_score(y_test, y_proba)) fpr, tpr, _ roc_curve(y_test, y_proba) plt.plot(fpr, tpr) plt.xlabel(False Positive Rate) plt.ylabel(True Positive Rate) plt.savefig(models/roc_curve.png, dpi150, bbox_inchestight)分类报告里需要重点看的是 recall它表示「真正患病的人里模型能找出多少」。医疗筛查场景下漏掉一个患者的代价远高于误报一次所以召回率比准确率更有业务意义。AUC 则是把不同阈值下的表现汇总成一个数字0.5 等于抛硬币0.8 以上对这个体量的数据来说已经是可以用的水平。ROC 曲线保存到 models 目录方便后面写报告直接引用不用重新运行 Notebook。指标关注的问题什么时候重要accuracy整体预测对的占比正负样本均衡时precision预测为患病的人里真患病的比例误报代价高的场景recall真实患病的人里被找出的比例漏报代价高的场景AUC排序能力强弱与阈值无关模型对比、调参时首选混淆矩阵也值得单独看一眼特别是预测结果里「假阴性」这一格。如果假阴性数量明显偏高说明模型在漏人这时候与其换模型不如调整决策阈值。predict_proba 返回的是 0 到 1 的连续概率predict 默认按 0.5 切分这个 0.5 并不是不可动的天条# 把阈值降到 0.3提高召回率减少漏检 y_high_recall (y_proba 0.3).astype(int)阈值调整是在召回率和精确率之间做取舍降阈值能捞回更多患病样本代价是误报变多。实际业务里阈值应该由需求方拍板而不是拍脑袋取 0.5。换环境或者换数据之后阈值也要重新在验证集上确认一次不能一直沿用旧值。4.2 特征影响力检查逻辑回归这类线性模型的优势在于系数大小可以直接当特征重要性来看。Notebook 的最后一个分析型单元格把系数按绝对值排序打印出来用来验证模型学到的规则是否符合医学直觉。coef pd.Series(model.coef_[0], indexX_train.columns) coef coef.sort_values(keyabs, ascendingFalse) print(coef.head(10))运行结果里排在前面的通常是血糖分箱里的「高风险」「极高」档、BMI 分箱里的「肥胖」档以及 pedigree 和 age。这符合临床直觉血糖和体重本身就是糖尿病风险的核心指标。反过来如果排序结果里出现某个毫无业务关联的特征排第一那大概率是数据泄漏或者清洗环节出了问题这时候返回去检查上游单元格比继续调参更有意义。特征重要性检查还有一个作用就是判断特征的稳定性。比如 pregnancies 在系数表里往往接近尾部说明在控制了血糖、年龄这些强变量之后怀孕次数本身对患病风险的增量贡献有限。这类结论可以直接写进数据分析报告让整个 Notebook 的价值不只有「一个模型」这么单薄。还要解释一下交叉验证 AUC 和测试集 AUC 的关系。交叉验证输出的是训练集内部 5 折的平均表现测试集 AUC 是模型在没见过的数据上的最终评估两者差太多说明训练策略有问题训练集高、测试集低就是过拟合两者都低则要回头查特征工程。大多数情况下测试集 AUC 会比交叉验证均值略低一点这是正常的低太多才需要警惕。把模型和特征列表保存下来是收尾动作。joblib.dump(model, models/logistic_model.joblib) 会把训练好的模型序列化到磁盘后面做预测接口或者换脚本复用时直接 load 就能用不用重跑整个 Notebook。注意保存时把特征列的清单也一起存下来我在第 6 章会展示为什么这个清单在预测阶段必不可少。5. 运行复现与常见排查把 Notebook 从我的电脑搬到任何环境5.1 环境准备与依赖安装这个工程对运行环境没有特殊要求Python 3.9 到 3.11 之间的版本都能跑。建议先建一个独立虚拟环境避免把依赖装进全局环境里跟其他项目打架。python -m venv .venv source .venv/bin/activate pip install -r requirements.txt为什么强调用虚拟环境而不是直接 pip install 到全局数据科学项目里最常见的冲突就是 pandas 和 scikit-learn 的版本互踩A 项目要 pandas 1.5B 项目要 pandas 2.0装在一个环境里总有一方要翻车。venv 隔离之后每个项目各用各的依赖互不干扰。requirements.txt 里锁定了主要库的版本大致是这样pandas2.0.3 numpy1.24.3 scikit-learn1.3.0 matplotlib3.7.2 joblib1.3.2锁定版本是复现的前提。机器学习库的接口变动很频繁pandas 一次大版本升级就可能让 cut 或 fillna 的行为变化scikit-learn 换版本后再用旧 joblib 文件加载模型也可能报不兼容。新环境里装最新版看着省事实际上是在给复现埋雷这也是我踩过坑之后才养成的习惯。5.2 一键复现的关键操作环境准备好之后进入 notebooks 目录启动 Jupyter选择 .venv 对应的 kernel然后按顺序从上到下「Run All」。整条链路大约一分钟就能跑完输出的 AUC、分类报告、ROC 图都会出现在对应单元格下方。复现时有一个容易被忽略的细节必须重启 kernel 再 Run All而不是接着上次的执行结果继续往下跑。因为 Notebook 的变量是常驻内存的如果中途改了某个单元格再单独执行后面的单元格可能还在用旧变量结果看起来正常实际上已经不是一个干净的实验了。重启 kernel 能清掉所有内存状态保证每个变量都是从当前单元格重新算出来的。跑完对照一下几个关键输出确认结果落在正常范围检查点预期输出原始形状(768, 9)填充后缺失值0训练集形状(614, 17)测试集形状(154, 17)五折交叉验证 AUC0.80 上下的一个稳定数值测试集 AUC与 CV 均值相差不超过 0.030.05因为 random_state 已经固定正常情况下两次执行的结果完全一致如果不一致优先检查是不是有单元格被跳过或者改过执行顺序。这个对照表我一般会直接写进项目 README省得每次都要去翻历史输出来判断「这次跑得对不对」。5.3 常见踩坑记录下面几条是我反复遇到、也最值得提前知道的坑按「现象 → 原因 → 解决」整理如下。现象训练时弹出 ConvergenceWarning模型 AUC 明显偏低。 原因逻辑回归默认 max_iter100特征经过独热编码后维度过高迭代次数不够就提前停了。 解决把 max_iter 调到 500或者换 solverliblinear两种方式都能让求解器充分收敛。如果换了之后警告依然存在检查特征列里是否混入了 NaN 或无穷值。现象换一台电脑或者隔了几天再跑结果完全变了。 原因要么没固定 random_state要么依赖库版本不同导致算法行为差异。 解决确认 train_test_split 里有 random_state42所有模型相关的随机参数也固定新环境一律按 requirements.txt 安装。这条也是复现性检查里最先排查的两项。现象准确率奇高测试集直接上 95%但换一批数据就崩。 原因最常见的可能性是把 outcome 标签列误放进了特征里模型等于拿着答案做题属于数据泄漏。 解决建模前列出 features 清单做一次人工核对确保 outcome 不在其中再用第 4 章的系数排序检查是否有异常特征。数据泄漏是这类项目里最隐蔽又最致命的问题。现象joblib 加载模型报 ModuleNotFoundError 或 ValueError。 原因保存和加载时 scikit-learn 版本不一致pickle 序列化对版本兼容很敏感。 解决在保存模型的环境里先导出 requirements.txt换环境加载前先按同一份依赖安装如果版本差异过大重新训练是最干脆的方案。现象血糖列里出现大量 0 值模型学出「血糖为 0 患病的风险极低」这种荒谬规律。 原因没有把 0 当作缺失值处理模型把编码缺失的占位符当成了真实的测量值。 解决严格按 3.1 节的替换逻辑把 glucose、blood_pressure、skin_thickness、insulin、bmi 的 0 先转 NaN 再用中位数填充这一步是整个数据清洗里优先级最高的事。如果 Run All 中途报错还有一个通用排查路径先看报错单元格是否依赖前一个单元格的输出比如变量名拼错、单元格被跳过再看是否已经按 requirements.txt 装好依赖最后确认当前 kernel 是 .venv 而不是外面的全局环境。把这三个检查做一遍能解决九成环境类问题不需要一上来就怀疑代码本身。6. 二次开发思路从单次训练到可调用的预测接口6.1 把训练管道压成可复用函数Notebook 适合做探索但真要接进业务系统第一步是把清洗、训练、评估压成一个函数。把 3.1 到 3.3 的单元格合并之后对外只暴露一个入口def train_pipeline(data_path, save_dirmodels): df pd.read_csv(data_path) # 清洗与分箱逻辑复用 Notebook 中的实现 X, y build_features(df) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42) model LogisticRegression(max_iter500, C0.1, solverliblinear) model.fit(X_train, y_train) joblib.dump(model, f{save_dir}/logistic_model.joblib) return model这一步的改动价值在于Notebook 里的分析结果和工程代码开始解耦业务脚本每次调用 train_pipeline 都能重新产出模型而不会依赖 Jupyter 环境。回归测试时也可以反复调用这个函数对比不同版本模型的指标变化。6.2 用一个轻量接口接收预测请求需要给业务方提供预测能力时可以用 FastAPI 包一个最小接口。加载已保存的模型和 scaler把请求里的字段对齐到训练时的特征顺序返回患病概率和风险等级。from fastapi import FastAPI from pydantic import BaseModel import joblib, pandas as pd app FastAPI() model joblib.load(models/logistic_model.joblib) scaler joblib.load(models/scaler.joblib) class Patient(BaseModel): pregnancies: int glucose: float blood_pressure: float bmi: float pedigree: float age: int app.post(/predict) def predict(p: Patient): df pd.DataFrame([p.model_dump()]) # 补上清洗、分箱、独热和标准化与训练时保持一致 proba model.predict_proba(df)[:, 1][0] return {prob: round(proba, 4), risk: high if proba 0.5 else low}这段代码最需要留意的坑是特征对齐接口收到的字段必须按训练时 build_features 的输出顺序排列少一个列、多一个列模型预测结果都会是错的。我的做法是把训练用的特征列名存成 JSON 落到 models 目录接口启动时读进来做校验比在代码里硬编码列名可靠得多。从那以后我每次做这类数据项目都强制把「固定随机种子、锁定依赖版本、特征清单落盘」这套动作走一遍再收工。这次拆解 Diabetes-Predictor 也让我更确信这个习惯的价值希望帮到你。本文还有配套的精品资源点击获取