
简介Diabetes-Predictor 是一份面向数据科学初学者与医疗健康分析爱好者的机器学习实战资源围绕 Pima Indians 糖尿病数据集完整演示从数据预处理、探索性分析到模型训练与评估的预测流程帮助读者掌握疾病风险建模的基本方法。压缩包共 4 个文件约 322KB包含 1 个 Jupyter Notebook 用于分步讲解分析与建模过程1 个 Python 脚本便于直接运行复现1 个 joblib 格式的已训练模型文件可加载验证以及 1 个 Markdown 说明文档交代项目背景与使用方式。目前已有 235 人学习下载。通过这份资源读者可以对照 Notebook 理解特征工程、模型选择与交叉验证等关键环节借助脚本快速跑通预测流程并利用现成模型文件直接体验糖尿病风险预测的推理效果适合作为课程作业、自学练手或教学演示的参考案例。1. 从 Pima 数据集说起Diabetes-Predictor 到底在预测什么很多团队第一次接触糖尿病风险预测都是从一个叫 Pima Indians Diabetes 的公开数据集开始的。Diabetes-Predictor 这个标题背后指的是一类用结构化体检指标做二分类的机器学习项目输入怀孕次数、血糖、血压、皮脂厚度、胰岛素、BMI、糖尿病家族史、年龄这 8 个字段输出一个人是否属于糖尿病高风险人群。它解决的不是临床确诊问题而是筛查和分诊——在资源有限的门诊或体检场景里先把高风险的人挑出来再安排进一步检查。这件事适合谁做如果你手上有体检机构的脱敏表格、想验证一个特征工程流程、或者要给基层卫生站做一个轻量级风险打分工具Diabetes-Predictor 是个很好的起点。它数据量小、特征含义明确、训练成本低一台普通笔记本就能跑完全流程。但正因为简单很多人栽在数据清洗和阈值选择上模型 AUC 看着不错上线后却没人敢用。下面我把从数据到可解释输出的完整路径拆开讲。2. 数据清洗与特征工程Pima 数据集里那些不能直接喂给模型的坑2.1 零值不是缺失值但比缺失值更麻烦Pima 数据集最出名的血泪经验是Glucose、BloodPressure、SkinThickness、Insulin、BMI 这 5 列里0 是无效值不是真实测量结果。比如胰岛素为 0 在生理上几乎不可能它代表的是“未检测”。如果你直接把这些 0 当数值喂进去模型会学到一个荒谬的规律胰岛素越低越健康。常见做法是把 0 替换成 NaN再按类别或整体中位数填充。我一般会先看每列 0 的占比超过 15% 的列要谨慎填充方式会显著影响后续模型校准。import pandas as pd import numpy as np # 列名按 Pima 数据集标准顺序 cols [Pregnancies, Glucose, BloodPressure, SkinThickness, Insulin, BMI, DiabetesPedigreeFunction, Age, Outcome] df pd.read_csv(diabetes.csv, namescols, header0) # 这些列里的 0 视为缺失 zero_as_missing [Glucose, BloodPressure, SkinThickness, Insulin, BMI] df[zero_as_missing] df[zero_as_missing].replace(0, np.nan) # 查看缺失比例决定填充策略 print(df[zero_as_missing].isna().mean().sort_values(ascendingFalse))逻辑说明先替换再统计避免把 0 混进均值里。参数上replace(0, np.nan)只作用于指定列Pregnancies 的 0 是合理的未怀孕不能动。填充时我倾向用中位数而不是均值因为胰岛素和皮脂厚度分布明显右偏均值会被极端值拉高。2.2 用分组中位数填充比全局中位数更稳直接对整列取中位数填充会忽略 Outcome 分组带来的分布差异。糖尿病组的血糖中位数天然高于对照组如果混在一起填等于人为削弱了特征区分度。我一般按 Outcome 分组后分别填充再检查填充前后分布是否合理。# 按 Outcome 分组填充中位数 for col in zero_as_missing: df[col] df.groupby(Outcome)[col].transform( lambda x: x.fillna(x.median()) ) # 填充后确认没有残留缺失 assert df[zero_as_missing].isna().sum().sum() 0 print(df.groupby(Outcome)[[Glucose, Insulin, BMI]].median())逻辑说明groupby(Outcome).transform保证填充值来自同一组不会跨组污染。参数上中位数对偏态分布更稳健如果某组某列缺失过多中位数本身不可靠这时要考虑直接删列或改用模型插补。填充完打印分组中位数能快速判断两组差异是否还保留着——如果填充后两组中位数几乎一样说明填充策略把信号抹平了。2.3 特征缩放和派生特征别急着上复杂模型逻辑回归和 SVM 对尺度敏感树模型不敏感。我通常先做一个标准化版本用于线性模型同时保留原始尺度给树模型。派生特征方面BMI 和 Age 的交互、Glucose 与 Insulin 的比值HOMA-IR 的简化代理在业务解释上比原始字段更有说服力。from sklearn.preprocessing import StandardScaler # 派生特征血糖胰岛素比加一个小常数避免除零 df[Glucose_Insulin_Ratio] df[Glucose] / (df[Insulin] 1) # 标准化版本供逻辑回归使用 feature_cols [Pregnancies, Glucose, BloodPressure, SkinThickness, Insulin, BMI, DiabetesPedigreeFunction, Age, Glucose_Insulin_Ratio] scaler StandardScaler() X_scaled scaler.fit_transform(df[feature_cols])逻辑说明比值特征在医学文献里常见但要注意它和原始 Glucose、Insulin 存在共线性线性模型里建议做 VIF 检查。参数上1是防止胰岛素为 0 时除零虽然前面已经填充过但派生计算前保留这个保护更安全。标准化只在训练集上 fit验证集和测试集用同一个 scaler transform这是铁律。3. 模型训练与评估从逻辑回归到梯度提升的选型对比3.1 先跑一个逻辑回归基线别一上来就 XGBoost逻辑回归在 Pima 这种小数据集上往往表现不差而且系数可以直接解释成风险因素的方向和强度。我习惯先跑基线记录 AUC、召回率和混淆矩阵再决定要不要上复杂模型。如果基线召回率已经满足筛查需求复杂模型带来的边际收益可能不值得维护成本。from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score, recall_score, confusion_matrix X_train, X_test, y_train, y_test train_test_split( X_scaled, df[Outcome], test_size0.2, random_state42, stratifydf[Outcome] ) lr LogisticRegression(max_iter1000, class_weightbalanced) lr.fit(X_train, y_train) y_prob lr.predict_proba(X_test)[:, 1] y_pred (y_prob 0.5).astype(int) print(AUC:, roc_auc_score(y_test, y_prob)) print(Recall:, recall_score(y_test, y_pred)) print(confusion_matrix(y_test, y_pred))逻辑说明class_weightbalanced在阳性样本偏少时能提升召回筛查场景更看重召回而不是精确率。参数上max_iter1000是防止默认迭代次数不够导致不收敛stratify保证训练测试集阳性比例一致。跑完看混淆矩阵如果漏诊假阴性多就要降阈值或换模型。3.2 梯度提升树的参数怎么调三个必调项如果基线召回不够我会试 Gradient Boosting 或 XGBoost。小数据集上不要猛调参重点看三个树的数量、学习率、最大深度。树太多容易过拟合学习率太低训练慢深度太大直接记住噪声。from sklearn.ensemble import GradientBoostingClassifier from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [100, 200], learning_rate: [0.05, 0.1], max_depth: [2, 3] } gb GradientBoostingClassifier(random_state42) grid GridSearchCV(gb, param_grid, cv5, scoringroc_auc, n_jobs-1) grid.fit(X_train, y_train) print(Best params:, grid.best_params_) print(Best AUC:, grid.best_score_)逻辑说明max_depth限制在 2 到 3是因为 Pima 只有几百条样本深树必然过拟合。scoringroc_auc比准确率更适合不平衡数据。参数上cv5在样本量小时够用再大意义不大。跑完用最佳参数在测试集上验证如果测试 AUC 比交叉验证低超过 0.05说明过拟合了要减树或降深度。3.3 评估指标别只看准确率筛查场景盯住召回和校准糖尿病筛查里漏掉一个高风险患者的代价远大于误报。所以召回率是核心指标同时要看概率校准——模型输出的 0.7 是不是真的代表 70% 风险。如果校准差阈值就没法按业务需求灵活调整。from sklearn.calibration import calibration_curve import matplotlib.pyplot as plt prob_true, prob_pred calibration_curve(y_test, y_prob, n_bins10) plt.plot(prob_pred, prob_true, markero, labelModel) plt.plot([0, 1], [0, 1], linestyle--, labelPerfectly calibrated) plt.xlabel(Predicted probability) plt.ylabel(True probability) plt.legend() plt.show()逻辑说明校准曲线偏离对角线越远概率越不可信。参数上n_bins10是常用分箱数样本少时可以降到 5。如果校准差可以用 Platt scaling 或 isotonic regression 做后处理但要在独立校准集上做不能拿测试集调。4. 避坑与排查Diabetes-Predictor 落地时最容易翻车的 4 个地方4.1 现象交叉验证 AUC 很高上线后一塌糊涂原因数据泄漏。最常见的是在划分训练测试集之前就做了标准化或填充导致测试集信息渗入训练过程。另一个隐蔽来源是用了 Outcome 分组填充如果填充时用了全量数据的中位数测试集分布就被提前知道了。解决所有预处理必须放进 Pipeline在交叉验证内部 fit。用sklearn.pipeline.Pipeline把填充、缩放、模型串起来cross_val_score会自动在每个 fold 内独立处理。from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer pipe Pipeline([ (imputer, SimpleImputer(strategymedian)), (scaler, StandardScaler()), (clf, LogisticRegression(max_iter1000, class_weightbalanced)) ])4.2 现象模型把 Insulin 当成最强特征但业务上说不通原因Insulin 缺失率最高填充后分布失真模型可能学到了填充模式的伪信号。如果填充值恰好和 Outcome 有相关性就会放大这个特征的重要性。解决对比填充前后特征重要性排序如果 Insulin 排名剧烈变化考虑直接删掉这列或改用缺失指示变量。我一般会跑一版不含 Insulin 的模型看 AUC 掉多少掉得少就删。4.3 现象阈值设 0.5召回率只有 0.6漏掉太多高风险原因默认阈值不适合筛查场景。阳性样本少时模型概率普遍偏低0.5 会偏向预测阴性。解决在验证集上画 PR 曲线找召回优先的阈值。比如要求召回不低于 0.85然后看对应的精确率能不能接受。阈值确定后固化到配置文件不要每次手动改。from sklearn.metrics import precision_recall_curve precision, recall, thresholds precision_recall_curve(y_test, y_prob) # 找召回 0.85 时对应的最大阈值 idx np.where(recall 0.85)[0][-1] print(Threshold:, thresholds[idx], Precision:, precision[idx])4.4 现象换一批新数据模型输出全是 0 或全是 1原因新数据的特征尺度或分布和训练集差异大标准化参数不匹配。比如新数据血糖单位是 mmol/L训练集是 mg/dL直接 transform 后数值全乱。解决上线前做输入校验检查每个特征的均值和方差是否在训练集合理范围内。超出范围就报警不要硬跑。另外把 scaler 的 mean_ 和 scale_ 保存下来推理时用同一套参数。5. 把模型变成可解释的风险打分SHAP 值分析与阈值固化技巧模型跑通只是第一步真正让医生或体检机构愿意用得让他们知道“为什么这个人被标成高风险”。SHAP 是目前最实用的解释工具它对每个样本给出每个特征的贡献值正负方向明确。我一般会挑几个典型样本画出 SHAP 力图再汇总全局重要性。import shap explainer shap.TreeExplainer(grid.best_estimator_) shap_values explainer.shap_values(X_test) # 全局重要性 shap.summary_plot(shap_values, X_test, feature_namesfeature_cols) # 单个样本解释 shap.force_plot(explainer.expected_value, shap_values[0], X_test[0], feature_namesfeature_cols)逻辑说明TreeExplainer对树模型快且准线性模型可以用LinearExplainer。参数上X_test要是原始尺度还是标准化尺度取决于模型训练时用的数据——这里模型是在标准化数据上训练的所以传入标准化后的数组。SHAP 值正负代表推高还是拉低风险绝对值代表影响大小。阈值固化方面我习惯把验证集上选定的阈值、对应的召回和精确率写进一个 JSON 配置文件推理服务启动时加载。这样业务方改阈值不用动代码也避免不同环境阈值不一致。import json config { threshold: float(thresholds[idx]), expected_recall: 0.85, expected_precision: float(precision[idx]), model_version: lr_balanced_v1 } with open(predict_config.json, w) as f: json.dump(config, f, indent2)最后说一个我自己的习惯每次重新训练模型我都会把新模型的 SHAP 全局重要性图和上一版并排看。如果排名前三的特征换了哪怕 AUC 涨了我也会先怀疑数据出了问题而不是急着上线。这个习惯帮我拦下过好几次因为上游数据表字段错位导致的“假提升”。希望帮到你。本文还有配套的精品资源点击获取