ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

糖尿病风险预测实战:端到端可部署的机器学习项目

糖尿病风险预测实战:端到端可部署的机器学习项目

简介:本资源是一个基于Jupyter Notebook的糖尿病风险预测实战项目,面向数据科学初学者、机器学习入门者及公共卫生领域对AI建模感兴趣的实践者,旨在通过真实医疗数据集(如Pima Indians Diabetes Dataset)完成端到端的预测建模流程。压缩包共4个文件,含1个核心.ipynb分析脚本(涵盖数据清洗、EDA可视化、特征工程、多种模型训练与评估)、1个.py预处理与推理脚本、1个.joblib保存的训练完成模型、1份.md格式README说明文档,整体仅322KB,轻量易部署。已有238人学习下载,适合快速复现、理解模型构建逻辑并迁移至其他二分类健康预测任务。读者可直接运行Notebook完成全流程复现,调用joblib模型进行新样本预测,并参考Python脚本封装推理接口,同时借助README快速掌握项目结构与关键实现细节。

1. Diabetes-Predictor 是什么:一个能跑通、能改、能部署的端到端糖尿病风险预测实战项目

你手头有一份医院体检数据表(年龄、BMI、血糖、胰岛素、家族史等字段),想快速验证“用机器学习判断某人未来1年内是否可能被诊断为2型糖尿病”这件事到底靠不靠谱?别再从 sklearn 导入开始写起,也别在 Kaggle 上扒一堆没注释的 notebook 硬啃——Diabetes-Predictor 就是为此而生的:它不是一个教学 Demo,而是一个完整闭环的工程化预测流程,从原始 CSV 数据清洗、缺失值策略选择(不是简单填均值)、特征工程逻辑封装(比如 Glucose 和 BMI 的交互项是否该做标准化后再乘)、5 种主流模型并行训练与自动超参调优(含 XGBoost 的 early_stopping 配置细节),到最后生成可解释性报告(SHAP 值热力图 + 单样本预测路径图)。它用 Jupyter Notebook 组织,但结构清晰到能直接抽离成 Python 模块;它默认跑在本地,但所有路径、随机种子、模型保存格式都做了参数化抽象。适合刚学完 pandas 和 scikit-learn 的工程师快速上手复现,也适合有部署经验的人直接拿去改造成 FastAPI 接口——因为它的预测函数predict_risk()输入是 dict,输出是带置信区间的 JSON 字典,没有 notebook 黑匣子依赖。


2. 为什么选这个结构:Jupyter Notebook 不是玩具,而是可调试的 ML 工程骨架

2.1 Notebook 的三层组织逻辑:从探索到封装再到复用

Diabetes-Predictor 的 notebook 并非线性流水账,而是按「探索层 → 构建层 → 接口层」分块设计。第一块(Exploratory Analysis)用 Seaborn 绘制了血糖 vs BMI 的双变量密度图,并叠加了阳性样本的核密度估计曲线——这不是为了好看,而是为了确认二者存在非线性协同效应,从而支撑后续构造Glucose_BMI_ratio特征的合理性。第二块(Model Building)把数据预处理封装成DataPreprocessor类,关键点在于:它不继承 sklearn 的 BaseEstimator,而是显式暴露.fit_transform()和.transform()方法,并在内部强制校验列名一致性(防止训练/预测时字段顺序错乱)。第三块(Prediction API)提供了一个独立函数make_prediction(input_dict: dict) -> dict,它会自动加载已训练好的模型文件(.pkl)、调用预处理器、返回结构化结果。这种结构让 notebook 同时具备“可读性”和“可移植性”:你可以逐 cell 调试数据分布异常,也可以直接复制第三块代码到你的 Flask 项目里。

# 示例:接口层核心函数(摘自 notebook 第三块) def make_prediction(input_dict: dict) -> dict: """ 输入:包含10个字段的字典,如 {"age": 45, "bmi": 28.3, "glucose": 142, ...} 输出:{"risk_score": 0.732, "risk_level": "high", "explanation": [...]} """ # 加载预训练模型和预处理器(路径已参数化,支持相对路径) model = joblib.load("models/best_xgb_model.pkl") preprocessor = joblib.load("models/preprocessor.pkl") # 转换输入为 DataFrame(强制列名对齐,防错) input_df = pd.DataFrame([input_dict]) input_df = input_df[preprocessor.expected_columns] # 关键防御 # 执行预测与解释 pred_proba = model.predict_proba(input_df)[:, 1] shap_values = compute_shap_explanation(model, input_df) return { "risk_score": float(pred_proba[0]), "risk_level": classify_risk(pred_proba[0]), "explanation": shap_values.tolist() }

提示:preprocessor.expected_columns是预处理器在.fit()时记录的原始列名列表,不是硬编码字符串。这是避免“训练时用 10 列,预测时传入 9 列”类错误的核心机制。

2.2 特征工程不是魔法:每一步都有临床依据和数值鲁棒性设计

很多糖尿病预测项目直接对所有数值列做 StandardScaler,这在临床上是危险的。Diabetes-Predictor 的特征工程模块明确区分三类变量:

  • 生理阈值型(如 Glucose ≥ 126 mg/dL 定义为空腹高血糖):不做标准化,而是构造二值标志位is_fasting_hyperglycemia;
  • 连续比例型(如 BMI):先做 Box-Cox 变换(scipy.stats.boxcox)再标准化,解决右偏分布问题;
  • 交互衍生型(如insulin / glucose):先对 insulin 和 glucose 分别做 log 变换(防除零和极端值),再相除,最后 Winsorize 截断上下 1%。

这些操作全部封装在create_features()函数中,并附带单元测试(见tests/test_feature_engineering.py):用固定 seed 生成 1000 行模拟数据,验证变换后insulin_glucose_ratio的分布峰度 < 3.5,确保不会因异常值导致模型过拟合。

2.3 模型选型不是堆参数:XGBoost 为何成为默认主力?

项目默认使用 XGBoost 而非 Random Forest 或 Logistic Regression,理由非常具体:

  • 在本数据集上,XGBoost 的 AUC 提升 0.023(0.841 → 0.864),但训练时间仅增加 1.8 倍(vs RF 的 3.2 倍),符合“临床场景需平衡精度与响应速度”的需求;
  • 其内置的missing参数天然支持 NaN 处理(本数据集中胰岛素值缺失率达 22%),无需额外插补;
  • feature_importances_输出可直接映射到医学解释(如“胰岛素水平比 BMI 对风险贡献大 3.2 倍”),比 SHAP 值更易向非技术人员传达。

配置文件config/model_config.yaml中明确约束了关键参数:

xgboost: n_estimators: 300 max_depth: 6 learning_rate: 0.05 subsample: 0.8 colsample_bytree: 0.9 missing: NaN # 显式声明,非默认值 random_state: 42

注意missing: NaN这一行——它不是字符串 "NaN",而是 Python 的float('nan'),XGBoost C++ 库要求如此。若此处写错,模型会静默忽略缺失值处理逻辑,导致线上预测偏差。


3. 怎么跑起来:从下载到本地验证的四步实操链

3.1 下载与目录解压:确认三个核心文件的存在性

项目以 ZIP 包形式分发,解压后必须存在以下三个一级目录/文件(缺一不可):

  • notebooks/:主 notebook 文件diabetes_predictor_main.ipynb
  • data/:含train.csv(892 行)、test.csv(200 行)、sample_input.json(供 API 测试)
  • models/:空目录(首次运行时自动生成.pkl文件)

注意:不要手动创建models/下的任何文件。所有模型持久化均由 notebook 内部save_model()函数完成,该函数会校验模型哈希值并写入版本信息(如xgboost_v1.7.5_20240521.pkl),防止混用不同训练轮次的模型。

3.2 环境配置:用 conda 创建隔离环境(非 pip)

项目依赖明确要求xgboost==1.7.5(因新版 2.x 在 Windows 上存在 SHAP 兼容问题),且scikit-learn>=1.2.2,<1.3.0(为兼容imblearn的 SMOTE 实现)。推荐用 conda 创建环境,因其能精确控制底层 BLAS 库版本:

# 创建环境(Python 3.9 兼容性最佳) conda create -n diabetes-env python=3.9 conda activate diabetes-env # 安装核心包(按此顺序,避免 xgboost 编译冲突) conda install -c conda-forge xgboost=1.7.5 pip install scikit-learn==1.2.2 pip install shap==0.42.1 imbalanced-learn==0.10.1

提示:若用 pip 直接安装 xgboost,Windows 用户大概率遇到xgboost.libpath.XGBoostLibraryNotFound错误。conda-forge 渠道预编译的 wheel 已链接 OpenMP,可跳过源码编译。

3.3 Notebook 执行顺序:必须严格遵循的 Cell 执行流

打开notebooks/diabetes_predictor_main.ipynb后,禁止从头到尾按 Shift+Enter 盲跑。正确顺序是:

  1. Cell 1(导入):执行,检查无ModuleNotFoundError
  2. Cell 3(数据加载):执行,观察输出Train shape: (892, 11),确认列数为 11(含 target)
  3. Cell 5(缺失值分析):执行,查看insulin列缺失率是否为 22.3%,若偏差 > 0.5%,说明数据文件损坏
  4. Cell 8(模型训练):执行,等待约 90 秒(i5-1135G7),最终输出Best CV AUC: 0.864 ± 0.021
  5. Cell 10(API 测试):执行,输入sample_input.json内容,应返回risk_score在 0.6~0.8 之间

关键逻辑:Cell 5 训练时会自动触发models/目录创建,并保存两个文件:best_xgb_model.pkl和preprocessor.pkl。后续 Cell 10 的make_prediction()函数依赖这两个文件存在。若跳过 Cell 5 直接跑 Cell 10,会报FileNotFoundError。

3.4 首次运行失败的快速定位法:看日志而非报错堆栈

当 Cell 执行中断时,不要第一眼盯 traceback 最后一行。先检查以下三处输出:

  • Cell 输出框上方的小图标:若显示 ⏳(沙漏),说明卡在数据加载(检查data/train.csv路径是否被杀毒软件锁定);
  • Cell 输出框内最后一行文本:若为Fitting preprocessor...后无下文,大概率是insulin列存在非法字符(如"NA"字符串而非NaN),需手动用 Excel 清洗;
  • Jupyter 控制台(Terminal)输出:若出现OMP: Error #15: Initializing libiomp5md.dll, but found libiomp5md.dll already initialized.,说明 conda 环境混用了多个 OpenMP 版本,需重装 xgboost(conda remove xgboost && conda install -c conda-forge xgboost=1.7.5)。

4. 避坑指南:五个真实翻车现场与血泪修复方案

4.1 现象:训练时 AUC 稳定在 0.50~0.55,远低于文档声称的 0.86

原因:train.csv中 target 列名为Outcome,但 notebook 中硬编码为target,导致y_train全为None,模型实际在拟合随机噪声。
解决:打开notebooks/diabetes_predictor_main.ipynb,搜索y = df['target'],改为y = df['Outcome']。本项目未使用配置文件统一管理列名,此为已知设计缺陷,已在 GitHub Issue #12 中标记为 high-priority。

4.2 现象:make_prediction()返回risk_score: nan

原因:输入字典中传入了字符串型数值,如"age": "45"(应为整数 45)或"glucose": "142.0"(应为浮点 142.0)。预处理器内部类型校验失败,astype(float)抛出异常后被静默捕获。
解决:在调用前强制类型转换:

input_dict = {k: float(v) if k in ["age", "bmi", "glucose"] else int(v) for k, v in input_dict.items()}

4.3 现象:SHAP 解释图显示所有特征贡献为 0

原因:compute_shap_explanation()函数中,explainer = shap.TreeExplainer(model)被错误初始化为shap.Explainer(model)(后者适用于神经网络,不兼容 XGBoost 树模型)。
解决:定位到该函数定义处,将shap.Explainer替换为shap.TreeExplainer,并传入model和X_train_sample(用于背景数据)。

4.4 现象:Windows 系统下joblib.load()报UnicodeDecodeError: 'ascii' codec can't decode byte 0x80

原因:模型文件由 Linux 环境生成(默认 pickle protocol 4),Windows Python 默认用 ascii 解码。
解决:修改加载代码,显式指定 encoding:

model = joblib.load("models/best_xgb_model.pkl", mmap_mode='r') # 或更稳妥:用 pickle.load 配合 bytesIO with open("models/best_xgb_model.pkl", "rb") as f: model = pickle.load(f, encoding='latin1')

4.5 现象:pip install -r requirements.txt失败,提示xgboost 1.7.5 has no wheels

原因:PyPI 上 xgboost 1.7.5 的 wheel 仅提供 macOS 和 Linux 版本,Windows 用户需从 conda-forge 安装。
解决:删除requirements.txt中xgboost==1.7.5行,改用 conda 命令安装(见 3.2 节)。这是跨平台项目常见的依赖陷阱,非用户操作失误。


5. 进阶技巧:把 notebook 模块化,三步接入你自己的 Web 服务

5.1 抽离核心逻辑:创建diabetes_core/包结构

将 notebook 中可复用的代码提取为标准 Python 包,目录结构如下:

diabetes_core/ ├── __init__.py ├── data/ │ ├── __init__.py │ └── loader.py # load_train_data(), load_test_data() ├── features/ │ ├── __init__.py │ └── engineering.py # create_features(), DataPreprocessor ├── models/ │ ├── __init__.py │ └── predictor.py # make_prediction(), load_model() └── utils/ ├── __init__.py └── validation.py # validate_input_dict()

关键改造点:predictor.py中的make_prediction()函数移除所有 notebook 特有依赖(如IPython.display),只保留joblib、numpy、pandas三类基础库。这样它就能被任何 Python 环境导入,包括 AWS Lambda。

5.2 FastAPI 快速封装:一个可 curl 测试的端点

新建app.py,用 12 行代码启动 HTTP 服务:

from fastapi import FastAPI, HTTPException from pydantic import BaseModel from diabetes_core.models.predictor import make_prediction app = FastAPI() class PredictionInput(BaseModel): age: int bmi: float glucose: float insulin: float # ... 其他 6 个字段(省略) @app.post("/predict") def predict_risk(input_data: PredictionInput): try: result = make_prediction(input_data.dict()) return {"status": "success", "data": result} except Exception as e: raise HTTPException(status_code=400, detail=str(e)) # 启动命令:uvicorn app:app --reload

启动后执行curl -X POST http://localhost:8000/predict -H "Content-Type: application/json" -d @data/sample_input.json,即可获得 JSON 响应。整个过程不依赖 Jupyter,真正实现“一次开发,多端部署”。

5.3 模型监控埋点:在预测函数中加入数据漂移检测

临床场景中,输入数据分布可能随时间偏移(如新设备测得的血糖值系统性偏低)。我们在make_prediction()开头插入轻量级漂移检测:

def make_prediction(input_dict: dict) -> dict: # 新增:检查 glucose 是否超出历史训练集 3σ 范围 train_glucose_mean = 120.9 # 预计算值,存于 config/ train_glucose_std = 32.4 if abs(input_dict["glucose"] - train_glucose_mean) > 3 * train_glucose_std: logging.warning(f"Glucose outlier detected: {input_dict['glucose']}") # 可选:降级为 LogisticRegression 预测,或返回 warning 字段 return {"risk_score": 0.5, "warning": "glucose_out_of_range"} # 原有逻辑...

这个改动只需 5 行代码,却能在数据异常时主动告警,避免模型在失效状态下静默输出错误结果——这才是生产级预测服务该有的底线。

从那以后我每次把 notebook 改造成服务,都会在make_prediction()开头加一道数据范围校验,哪怕只是检查age是否在 1~120 之间。不是为了炫技,而是因为见过太多次“模型准确率 95%,但线上请求里 30% 的 age 是 -1 或 9999”。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进