简介:这是一份基于机器学习实现的糖尿病预测系统项目,源码与文档说明齐备,适合计算机、人工智能、通信工程、自动化、电子信息等专业的在校生用于毕设、课设或项目立项演示,也适合机器学习初学者对照练习。代码已经过完整测试并成功运行,答辩评审均分达96分,可放心下载使用。压缩包共22个文件,以XML配置、properties属性文件、Java核心类、JSP页面及Scala组件为主,附带样式表与工程描述文件,结构紧凑,整包仅24KB,便于快速部署查看。目前已有199人浏览学习。通过学习该项目,可掌握从数据处理、特征构建到模型预测的完整流程,理解工程目录与配置组织方式,并可在现有代码基础上扩展其他健康预测功能;下载后先查看README.md可快速上手,适合作为入门进阶的实用参考。
1. 糖尿病预测不是体检报告:这是机器学习最完整的一道入门题
机器学习里,糖尿病预测系统是流传最广的入门题目之一。它解决的不是"你现在的血糖高不高",而是基于血糖、血压、BMI、年龄、家族史这些体检特征,预测一个人未来几年患糖尿病的风险概率。Pima Indians Diabetes 数据集是这道题最常用的公共数据集,768 条样本、8 个特征、二分类标签,小到笔记本 CPU 就能跑完,但流程一个不少:数据清洗、特征工程、模型训练、效果评估、结果部署。它适合正在做机器学习课程设计、想从看教程过渡到跑通第一个完整项目的从业者。读完这篇,你能从零复现一个可运行的预测系统,拿到带注释的源代码级别的完整实现思路。
2. 数据底子先摸清:Pima 特征分布与三类模型选型
2.1 特征与标签的分布真相:为什么这个数据集自带坑
Pima Indians Diabetes 数据集来自美国国家糖尿病与消化及肾脏疾病研究所,记录的是亚利桑那州皮马印第安人的体检数据。768 条样本中,正例(5 年内确诊糖尿病)268 条,负例 500 条,类别比例约 1:1.9,不是严重不平衡,但足以让"无脑预测负例"拿到 65% 的准确率。8 个特征的含义和风险信号如下表:
| 特征 | 含义 | 取值范围 | 风险信号 |
|---|---|---|---|
| Pregnancies | 怀孕次数 | 0-17 | 多次怀孕与妊娠期糖尿病史相关 |
| Glucose | 口服葡萄糖耐量测试 2 小时浓度 | 0-199 mg/dL | 全集中最强的单一特征 |
| BloodPressure | 舒张压 | 0-122 mmHg | 存在生理不可能的 0 |
| SkinThickness | 三头肌皮褶厚度 | 0-99 mm | 测量误差较大 |
| Insulin | 2 小时血清胰岛素 | 0-846 mu U/ml | 0 值占比接近 50% |
| BMI | 身体质量指数 | 0-67.1 | 大于 30 为肥胖,强风险因素 |
| DiabetesPedigreeFunction | 糖尿病遗传函数 | 0.08-2.42 | 家族病史的数值化 |
| Age | 年龄 | 21-81 | 2 型糖尿病随年龄风险上升 |
新手最容易忽略的点是:数据里大量存在 0 值,而 Glucose、BloodPressure、BMI 这些字段生理上不可能是 0。这里的 0 表示"未检测",是典型的缺失值编码方式,不是真实测量结果。如果不处理,模型会把"血糖为 0"当作合法特征区间去拟合,预测逻辑被彻底带偏。
做探索性数据分析时,我的习惯是先看特征与标签的相关性,用几行代码快速定位风险信号最强的字段:
import pandas as pd df = pd.read_csv("diabetes.csv") corr = df.corr() print(corr["Outcome"].sort_values(ascending=False))这段代码输出每个特征与 Outcome 的相关系数,典型的排序结果是 Glucose(约 0.47)、BMI(约 0.29)、Age(约 0.24)、Pregnancies(约 0.22)排在前列,SkinThickness 和 Insulin 垫底。同时要注意特征之间的相关性:Pregnancies 与 Age 的相关系数约 0.54,说明两个字段信息有重叠。树模型对冗余特征不敏感,逻辑回归会受影响,所以要不要做降维,放到模型对比阶段再决定。机器学习实战里,先看相关性再谈模型,能避免很多无效尝试。
2.2 三类模型的适用边界:逻辑回归、随机森林、XGBoost 怎么选
模型选型不需要一上来就上大杀器。768 条样本能承载的模型复杂度有限,选型核心逻辑是"先定基线,再谈提升"。我一般按下面顺序跑三组基线,每组固定同一个数据划分,保证对比公平:
| 模型 | 特点 | 适用场景 | 在 Pima 上的预期指标 |
|---|---|---|---|
| 逻辑回归 | 可解释性强、训练极快、对线性边界敏感 | 基线对比、需要向医生解释权重 | 准确率 75~78%,F1 约 0.65 |
| 随机森林 | 非线性、抗过拟合、输出特征重要性 | 课程设计主力模型 | 准确率 77~82%,F1 约 0.70 |
| XGBoost | 精度上限最高、调参维度多 | 追求高分、对比实验加项 | 准确率 80~84%,F1 约 0.72 |
逻辑回归的价值不在于分数,而在于诊断基线:如果逻辑回归准确率已到 78%,随机森林只有 79%,说明数据里有效信号基本是线性的,硬上复杂模型收益很小。反过来,如果随机森林显著超过逻辑回归,说明特征之间存在交叉作用,值得去挖特征工程。在实际项目里我会把逻辑回归的系数表打印出来,Glucose 每提高一个单位、风险对数几率增加多少,这种解释写进文档说明非常加分。
随机森林是这组数据上最稳的选择。它不需要对特征做严格归一化,对异常值容忍度高,训练速度快——768 条样本、100 棵树,单机秒级完成。它还有一个对课程设计很友好的特性:model.feature_importances_ 直接输出每个特征的重要性,写文档说明时能引用 Glucose、BMI、Age 的占比排序,让报告有数据支撑,而不是空谈"模型效果良好"。机器学习模型选型的通用经验是:小样本表格数据优先树模型,深度学习在这里几乎没有优势,样本量撑不起神经网络的参数规模。
XGBoost 我放在最后一位,因为它要调的参数维度更多:learning_rate、max_depth、subsample、colsample_bytree、n_estimators。在 768 条样本上它容易过拟合,需要配合 early stopping 和交叉验证。如果课程设计目标是拿高分,XGBoost 值得加跑一组;如果目标是跑通全流程、把文档写清楚,随机森林已经足够。机器学习和深度学习的分界点也在这里:数据量到万级以上、特征有图像或文本语义时,深度学习才值得考虑。
把三组基线跑完,你会得到一张对比表。写文档说明时,这张表就是机器学习项目报告里最有含金量的部分:横坐标是模型,纵坐标是准确率、精确率、召回率、F1、训练耗时。老师问"为什么选随机森林",你直接指这张表说"因为它在 F1 和训练耗时之间最均衡",比背课本定义有说服力得多。
3. 清洗与特征工程:零值填充、离群点截断与归一化参数
3.1 零值不等于缺失:按生理阈值批量纠正异常值
这个数据集的第一个坑在源头。前面提到,Glucose、BloodPressure、SkinThickness、Insulin、BMI 这五个字段都存在生理上不可能的 0。处理策略要根据每个字段的 0 值占比单独定,不能套同一套规则。
常见的决策分三步。第一步统计 0 值占比:Glucose 约 0.65%,BloodPressure 约 4.6%,SkinThickness 约 29.6%,Insulin 约 48.7%,BMI 约 1.4%。第二步决定填充还是删除——768 条样本本来就不多,删除会损失样本量,所以优先填充;SkinThickness 和 Insulin 的缺失比例太高,填充值本身可信度有限,可以考虑后续做特征删除对比实验。第三步选填充值——用中位数而不是均值,因为 Insulin 这类字段右偏严重,少数人的高胰岛素值会把均值拉高,中位数更稳健。
下面是我常用的清洗代码,直接可跑:
import pandas as pd df = pd.read_csv("diabetes.csv") # 这五个字段生理上不可能为 0,0 代表未检测 zero_cols = ["Glucose", "BloodPressure", "SkinThickness", "Insulin", "BMI"] for col in zero_cols: zero_ratio = (df[col] == 0).mean() print(f"{col}: {zero_ratio:.2%} 的样本为 0") # 关键:用非零部分的中位数替换 0 值 median_val = df.loc[df[col] > 0, col].median() df.loc[df[col] == 0, col] = median_val print("清洗后剩余样本数:", len(df)) df.to_csv("diabetes_clean.csv", index=False)这段代码的逻辑分三步:先打印每个字段的 0 值占比,让你对数据质量有直观认识;再取非零子集的中位数作为填充值;最后原地替换并落盘。为什么不直接用 df.median()?全列中位数被 0 拉低,用非零子集的中位数才接近真实分布。
Insulin 是这里面最尴尬的字段。0 值占比接近 50%,意味着填充后有一半的值是"编"的。我在实际项目里的做法是跑一个特征删除对比:一组保留填充后的 Insulin,一组直接删掉 Insulin,对比交叉验证 F1。如果删掉后分数没有明显下降,就删掉。少一个可疑特征比多一个噪声特征更安全,这是机器学习特征工程里"少即是多"的典型体现。
3.2 离群点处理与归一化:StandardScaler 的参数细节
清洗完 0 值还要处理离群点。Age 最大 81,BMI 有超过 60 的极端值,Insulin 的上限 846 更是把分布拉得不成样子。对随机森林这类树模型,离群点影响不大;对逻辑回归,离群点会显著拉偏梯度。所以这一步要按模型分开考虑。
我的原则是先用分位数做检查,而不是机械删除 3 倍标准差之外的点。糖尿病数据本身是右偏分布,BMI 高和年龄大恰恰是风险因素,把它们当离群点删掉等于删掉正例,人为制造类别不平衡。真正值得处理的,是那些明显超出生理合理范围的记录。处理方式也不是删除,而是用分位数截断(winsorize),把超过 99% 分位数的值压到 99% 分位数。
归一化这一步,逻辑回归和 KNN 需要,随机森林不需要。但为了三组模型能在同一套特征上公平对比,我统一做一次标准化,保持流程一致:
from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split X = df.drop(columns=["Outcome"]) y = df["Outcome"] # stratify 保证训练集和测试集正负比例与全量一致 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)这里两个参数值得说清楚。test_size=0.2 意味着 768 条样本留 154 条做测试,训练集只有 614 条,单次划分的评估方差偏大,所以调参必须用交叉验证。stratify=y 按标签比例分层抽样,保证训练集和测试集中患者的占比都和全量一致,避免测试集恰好全是正例或全是负例的极端情况。
StandardScaler 的 fit_transform 和 transform 必须分开用:fit 只在训练集上计算均值和标准差,transform 把这个统计量应用到测试集。这是机器学习项目里最容易翻车的细节。
注意:fit_transform 只允许出现在训练集上,测试集永远只走 transform。这个顺序一旦错了,模型训练时就见过测试集信息,评估分数全部作废。
这个错误叫做数据泄露,是机器学习应用流程里最隐蔽的坑,后面的避坑章节还会展开。认真做完这些,你手上就有一份干净的 diabetes_clean.csv,可以进入训练环节了。
4. 训练与评估:最小可复现的糖尿病预测系统源代码
4.1 训练脚本拆解:从加载数据到输出混淆矩阵
模型选型和数据准备就绪后,训练脚本本身不复杂。下面是完整的最小可复现训练代码,我把它组织成函数式结构,方便扩展成交叉验证或网格搜索。这段代码是整个系统的核心源代码:
import numpy as np import pandas as pd from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import ( accuracy_score, precision_score, recall_score, f1_score, confusion_matrix, classification_report ) def train_diabetes_model(df_path="diabetes_clean.csv"): # 加载清洗后的数据 df = pd.read_csv(df_path) X = df.drop(columns=["Outcome"]) y = df["Outcome"] # 分层划分,固定随机种子保证可复现 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) # 随机森林:100 棵树,限制最大深度防止过拟合 model = RandomForestClassifier( n_estimators=100, max_depth=5, min_samples_leaf=3, random_state=42, n_jobs=-1 ) model.fit(X_train, y_train) # 在测试集上评估 y_pred = model.predict(X_test) print("准确率:", accuracy_score(y_test, y_pred)) print("精确率:", precision_score(y_test, y_pred)) print("召回率:", recall_score(y_test, y_pred)) print("F1:", f1_score(y_test, y_pred)) print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred)) # 输出特征重要性,写文档说明时直接引用 feature_importance = pd.Series( model.feature_importances_, index=X.columns ).sort_values(ascending=False) print(feature_importance) return model, X_test, y_test model, X_test, y_test = train_diabetes_model()几个参数是经过权衡的。n_estimators=100 是随机森林的默认推荐值,树太少模型不稳定,树太多在 768 条样本上收益递减。max_depth=5 是防止过拟合的关键,默认的"不限制深度"会让每棵树长到叶子纯为止,在小数据集上几乎必然过拟合。min_samples_leaf=3 要求叶子节点至少 3 个样本,进一步平滑预测边界。n_jobs=-1 让所有 CPU 核心并行,训练时间从秒级降到亚秒级。
评估指标上,准确率不是唯一标准。这个数据集正例占 35%,如果模型把所有样本都预测为负例,准确率也有 65%——所以必须看混淆矩阵。糖尿病预测场景里,漏诊(假阴性)比误诊(假阳性)代价更高,因此召回率是关键指标,F1 用来平衡精确率和召回率。除了这四个指标,我还会画 ROC 曲线并计算 AUC,AUC 不依赖阈值,能更全面反映模型排序能力。对这个数据集,AUC 一般落在 0.83~0.86 区间。画 ROC 用 sklearn.metrics 里的 roc_curve 和 auc 函数,把测试集的 predict_proba 传进去就能出图。
运行这段代码,预期结果在准确率 78% 左右、F1 0.68~0.72。这个数字与 Kaggle 上同类公开方案基本吻合。如果分数偏离太远,优先检查清洗环节是否漏掉了 0 值处理,以及是不是错误地对全量数据做了标准化。机器学习项目排错的第一步永远是回溯数据,第二步才是怀疑模型。
最后补一个环境细节。scikit-learn 版本从 0.22 到 1.x,随机森林的默认参数和部分算法实现有过调整,同一份代码在不同版本下跑出的 F1 可能差 0.02。写文档时在开头注明 Python 版本、pandas 版本和 scikit-learn 版本,这是可复现性的一部分。答辩时被问"为什么你的结果和网上教程不一样",第一反应先比对版本。
4.2 关键参数怎么调:用网格搜索确认随机森林的稳定区间
上面的参数是调试过的经验值,但不是唯一答案。随机森林在这个数据集上最值得调的是 max_depth、min_samples_leaf、n_estimators 三个参数。调参的正确姿势不是靠感觉试,而是用小范围网格搜索配合交叉验证:
from sklearn.model_selection import GridSearchCV param_grid = { "n_estimators": [50, 100, 200], "max_depth": [3, 5, 7, None], "min_samples_leaf": [1, 2, 3, 5] } rf = RandomForestClassifier(random_state=42, n_jobs=-1) grid = GridSearchCV( rf, param_grid, cv=5, # 五折交叉验证 scoring="f1", # 用 F1 而不是准确率做选择依据 verbose=1, n_jobs=-1 ) grid.fit(X_train_scaled, y_train) print("最优参数:", grid.best_params_) print("最优交叉验证 F1:", grid.best_score_)cv=5 表示训练集被切成 5 份,轮流拿 4 份训练、1 份验证,最终分数是 5 次的均值。这比单次划分可靠,因为 768 条样本的划分随机性很大。scoring="f1" 用 F1 而不是准确率作为选择依据,理由前面说过:准确率在这个数据集上有欺骗性。param_grid 里 max_depth 的 None 选项代表不限制深度,它是用来做对照的极端点,多数情况下不会胜出。
一个常见误区是搜出来的"最优参数"一定要用。实际上 768 条样本上,max_depth=7 和 max_depth=5 的 F1 差距通常在 0.01 以内,属于噪声范围。网格搜索的意义是确认参数的稳定区间,而不是追求理论最优值。如果你发现最优参数的 F1 只比默认参数高 0.005,果断用默认参数——泛化到真实数据时,简单模型的鲁棒性更好。机器学习实战里过度调参的翻车案例比欠调参多得多。
另外提醒:不要在测试集上反复调参。测试集的定位是"最后考一次试",你在它上面调一次,它就泄露一次信息。正确流程是先交叉验证调参,参数定死后跑一次测试集,拿到最终数字写进文档。这是机器学习应用流程里的纪律,也是答辩时老师最常追问的问题。
5. 糖尿病预测系统的 4 个常见翻车点与排查方法
5.1 现象:准确率 98%,F1 却不到 0.5
这是课程设计答辩里最常见的翻车现场。模型在测试集上准确率高达 98%,看起来很完美,但 F1 只有 0.4,混淆矩阵显示模型几乎把所有样本都预测成了负例。
原因基本锁定在类别不平衡。Pima 数据集正例占 35%,模型无脑预测负例就有 65% 准确率。一旦训练集划分没有用 stratify,或者清洗阶段误删了大量正例样本,模型就会走"全预测负例"的捷径,准确率虚高但毫无意义。
解决方法是先看混淆矩阵,确认假阴性数量是否异常高。然后用 stratify 重新划分数据集,并给模型加上 class_weight="balanced",让训练时正例获得更高权重。这个参数在 scikit-learn 的随机森林里是内置的,一行改动就能让 F1 明显回升。我在项目里加了之后 F1 从 0.61 提到 0.69,效果直接可见。排查顺序记牢:先看混淆矩阵,再查划分方式,最后才动模型参数。
5.2 现象:测试集表现好,换一批真实数据就崩
模型在 Pima 测试集上准确率 80%,拿去预测医院的新患者数据却一塌糊涂。这是典型的过拟合叠加数据泄露。
过拟合的原因前面提过:768 条样本训练的模型学到的是皮马印第安人特有的模式,这个人群的饮食习惯、代谢特征与普通医院就诊人群差异很大。数据泄露更隐蔽——如果你在清洗阶段对全量数据做均值填充或标准化(包括测试集),模型训练时已经见过测试集信息,评估分数虚高,真实场景自然达不到。
解决思路有两条。一是代码纪律:填充统计量和标准化器都只在训练集上计算,测试集只做 transform。二是用五折交叉验证替代单次划分,把模型稳定性估出来。如果课程设计能有额外真实数据做外部验证,这个数字最可信,写进文档说服力也最强。从机器学习模型的角度看,跨人群泛化能力本来就是公平性研究的大课题,课程设计阶段只要不把话说满,把局限写在文档里,反而是加分项。
5.3 现象:指标全绿,医生却不敢用
模型准确率 82%、F1 0.71、混淆矩阵也漂亮,但把系统推荐给临床使用时被拒绝了。原因是没有可解释性。医生要回答患者"为什么你说我有风险",而随机森林的黑匣子特性无法给出能说出口的解释。
这不是算法问题,是产品问题。解决方法是加一层特征解释:用 SHAP 值输出每条预测的贡献度分解,让系统能说"这项预测主要受血糖 178 和 BMI 33.5 影响"。SHAP 库对树模型有原生快速实现:
import shap explainer = shap.TreeExplainer(model) shap_values = explainer.shap_values(X_test) # 对单条样本输出特征贡献的力图 shap.force_plot(explainer.expected_value, shap_values[0], X_test.iloc[0])如果不想引入 SHAP 依赖,至少要做特征重要性排序并写进文档说明。课程设计的评分里,"为什么是这个特征起主要作用"的讨论往往比模型分数更值钱。这也是为什么我建议主力模型用随机森林而不是 XGBoost——前者有现成的 feature_importances_,后者同等功能需要额外配置。
5.4 现象:同一个脚本两次运行结果不同
昨天跑 F1 是 0.70,今天跑变成 0.73,代码一行没改。原因是随机森林的行采样和特征采样自带随机性,train_test_split 的划分也有随机性。没有固定随机种子,每次运行都是不同的划分、不同的树。
解决方法是固定所有环节的随机种子。scikit-learn 模型传 random_state=42,train_test_split 也传 random_state=42,同时固定 NumPy 和 Python 内置随机源:
import numpy as np import random np.random.seed(42) random.seed(42)这个问题的反方向也值得注意:做模型对比实验时固定种子是对的;但评估模型真实稳定性时,要刻意不固定种子跑 10 次取均值。我的习惯是两种都做——固定种子保证报告可复现,再跑 10 次记录均值和标准差作为鲁棒性证据。这组数字写进文档说明里,比单次结果更有说服力,老师也会觉得你考虑了模型方差问题。
6. 保存模型到命令行预测:最后一公里的验证技巧
训练评估做完,模型还躺在内存里,关掉终端就没了。落地成"系统"的第一步是把模型和标准化器一起持久化。我用 joblib,它对 numpy 数组和 scikit-learn 对象的序列化支持最好,比 pickle 更稳:
import joblib joblib.dump(model, "diabetes_model.pkl") joblib.dump(scaler, "diabetes_scaler.pkl") # 加载并预测新样本,注意先 scale 再 predict loaded_model = joblib.load("diabetes_model.pkl") loaded_scaler = joblib.load("diabetes_scaler.pkl") new_sample = [[2, 145, 78, 28, 140, 32.5, 0.5, 42]] new_sample_scaled = loaded_scaler.transform(new_sample) prob = loaded_model.predict_proba(new_sample_scaled)[0][1] print(f"糖尿病风险概率: {prob:.2%}")这里有两个验证点值得养成习惯。第一,加载模型后用测试集里的几条样本做预测对照,确认保存前后的输出完全一致,这是模型文件的回归测试。第二,predict_proba 输出的概率比 predict 的二分类结果更有用。实际使用时设置风险阈值,比如概率超过 0.6 建议转诊,比硬性二分类更贴合医疗场景。我自己早期犯过的错是只保存了模型没保存标准化器,换电脑后加载模型直接 predict 原始值,输出全乱——保存的时候一定把 scaler 一起存,加载时先 transform 再 predict。希望这些经验能帮你在机器学习入门的第一个完整项目里少走几个弯路。
本文还有配套的精品资源,点击获取