基于机器学习的糖尿病预测系统:从数据清洗到Web部署的全过程复盘
又是一年毕业设计季,后台私信里问"糖尿病预测系统怎么做"的读者明显多起来了。说实话,这个题目在机器学习方向里属于典型的"入门友好、进阶有料"选题——数据集成熟公开、特征维度不高、模型对比空间大,做完之后无论是写论文还是准备答辩,都有充足的材料可以讲。但我也发现一个普遍现象:不少同学用的是网上流传的老版本代码,训练集和测试集划分不合理、缺失值处理时机搞错、模型评估只看准确率不看召回率,最后交上去的系统存在明显的逻辑硬伤。
这篇文章我会把我自己完整做过的方案拿出来拆解,包括数据集选择、特征工程、模型对比、调参思路、模型持久化、Flask接口封装、前端交互,以及几个我实际踩过的坑。全程用Pima印第安人糖尿病数据集作为示例,代码基于Python编写,依赖sklearn、pandas、XGBoost这类常规工具库。适合正在做毕业设计、课程设计,或者想系统走一遍机器学习项目流程的读者。
先说清楚一个前提:这个系统定位是"机器学习应用演示与学术研究",预测结果不能作为临床诊断依据。代码和方案我会尽量给得完整,但核心价值在于讲解"每一步为什么要这么做",而不是让你直接复制粘贴交差。
1. 项目整体设计与核心思路
1.1 这个题目到底在考你什么
糖尿病预测系统这个题目,本质上是要求你完成一个有监督的二分类任务:给定一组体检指标(比如血糖、血压、BMI、年龄),训练一个模型去判断某人是否患有糖尿病。整个项目的链条是:数据理解 → 数据清洗 → 特征分析 → 模型训练 → 模型评估 → 模型部署。
很多同学把这个项目做成了"调包侠"项目,也就是加载数据集、sklearn里调一个RandomForestClassifier、fit一下、打印准确率,然后就没有然后了。这种做法在答辩时非常容易被老师追问到哑口无言,因为老师通常不会问你"准确率是多少",而会问"你为什么选这个模型?数据里缺失值怎么处理的?样本不均衡怎么办?你这个系统给医生用,最关心哪个指标?"。
所以我在设计这个项目时,把重点放在了三个地方。第一是数据质量:原始数据集里的缺失值、异常值必须处理得有理有据。第二是评估指标:不是只看准确率,而是要结合混淆矩阵、召回率、ROC-AUC综合分析。第三是模型对比:至少用三个不同类别的模型做横向对比,用数据说明你最终选择某个模型的原因。
1.2 技术选型与数据来源
数据集方面,我使用的是Pima Indians Diabetes Database,这是UCI机器学习库里的经典数据集,也是Kaggle上最常被用来做糖尿病预测练习的数据集。它一共包含768条样本,8个特征加1个标签列。8个特征分别是:怀孕次数、口服葡萄糖耐量试验2小时血糖浓度、舒张压、肱三头肌皮褶厚度、2小时血清胰岛素、体重指数BMI、糖尿病遗传函数、年龄。标签代表5年内是否患有糖尿病。
选择这个数据集有三个原因。一是样本量适中,768条样本做模型对比和交叉验证都很方便,训练速度快。二是特征全部是数值型,不需要做复杂的文本编码处理,适合用来讲解完整的建模流程。三是它天然带有明显的数据质量问题——有些特征存在缺失值(原数据缺失值用0填充,这本身就是一个坑),特征之间的量纲差异很大,这恰好提供了做数据清洗和标准化处理的空间。
工具链方面,我用的是Python 3.9配合Jupyter Notebook做探索性分析,最终系统部署用Flask框架。依赖库包括pandas、numpy、matplotlib、seaborn用于数据处理和可视化,scikit-learn用于建模和评估,imbalanced-learn处理类别不平衡,pickle和joblib做模型持久化。模型方面除了sklearn自带的LogisticRegression、DecisionTreeClassifier、RandomForestClassifier、SVC之外,还用了XGBoost。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据探索与特征工程实操
2.1 数据加载与基础检查
我习惯在拿到任何数据集之后,先做一个"三分钟体检":看数据形状、看字段类型、看缺失值、看标签分布。这几个操作可以帮你快速判断这个数据集是"干净可用"还是"需要花大力气洗"。
python复制import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
df = pd.read_csv('diabetes.csv')
print(df.shape)
print(df.info())
print(df.describe())
# 检查缺失值
print(df.isnull().sum())
# 标签分布
print(df['Outcome'].value_counts())
print(df['Outcome'].value_counts(normalize=True))
实际运行中你会发现一个非常关键的问题:df.isnull().sum()显示所有特征都没有NaN,但describe()的结果里,比如Glucose的最小值是0,BMI的最小值是0,BloodPressure的最小值也是0。医学上这些指标不可能为0,所以这其实是用0值掩盖了缺失。
这就是我前面说的"原始数据用0填充缺失值"的坑。如果你不做处理,直接把数据扔给模型,相当于把缺失样本当成有效样本参与训练,会让模型学到错误的模式。所以第一步,我们必须把这些0值视为缺失值处理。
2.2 缺失值处理:先判断,再动手
哪些字段里0是合理的?只有Pregnancies(怀孕次数)为0是合理的,其他特征如果为0,一律视为缺失值。所以处理逻辑是:对Glucose、BloodPressure、SkinThickness、Insulin、BMI这5列,把0替换为NaN。
处理方式我用了两种对比:均值填充和中位数填充。均值填充适合数据接近正态分布的场景,中位数填充对离群点鲁棒性更好。实际情况中,Insulin字段缺失非常严重(约50%),此时用均值填充会引入较大偏差,我更倾向用中位数,或者用其他特征做预测填充——但后者的实现复杂度对毕设来说偏高,所以我最终建议中位数。
python复制# 将医学上不可能的0值视为缺失值
cols_with_zero = ['Glucose', 'BloodPressure', 'SkinThickness', 'Insulin', 'BMI']
for col in cols_with_zero:
df[col] = df[col].replace(0, np.nan)
# 中位数填充
for col in cols_with_zero:
df[col] = df[col].fillna(df[col].median())
这里要注意一个容易被答辩老师抓住的问题:缺失值填充必须先拆分训练集和测试集,再在训练集上计算统计量,然后应用到测试集。如果你在拆分之前就对全数据集做了填充,那么测试集的信息就泄露到了训练过程中,模型评估结果会偏乐观,这在学术上是不严谨的。
我在项目里用了一个更严谨的Pipeline方式,把填充操作封装到ColumnTransformer和Pipeline中,让sklearn在交叉验证的每一折里自动重新fit填充器。这样虽然代码稍微复杂一点,但逻辑是完全经得起推敲的。
2.3 特征分布与相关性分析
数据清洗之后,我做了两个可视化:一个是特征分布直方图,一个是特征相关性热力图。分布图帮你判断是否存在明显的偏态分布,相关性热力图帮你预判哪些特征与标签关联较强。
python复制# 相关性热力图
plt.figure(figsize=(10, 8))
sns.heatmap(df.corr(), annot=True, cmap='coolwarm', fmt='.2f')
plt.show()
从相关性矩阵可以直观看到,Glucose与Outcome的相关性最高(大约0.49),BMI次之,Age和DiabetesPedigreeFunction也有一定的相关性。SkinThickness和Insulin与Outcome的相关性相对较弱。这个线索直接指导了后面的特征选择思路——我们可以先全特征建模,再尝试去掉最弱的特征看效果变化。
单看相关性不能完全决定特征去留,因为特征间还存在交互效应。比如Insulin单独相关弱,但它可能与Glucose联合之后对预测有重要贡献。所以我的策略是:先保留全部特征建立基线模型,再通过特征重要性排序决定要不要精简。
2.4 特征缩放与数据划分
糖尿病数据集的特征量纲差异巨大:Age在20到80之间,Insulin可能到800,BMI在18到50之间。对于距离类模型(SVM、KNN、逻辑回归),量纲不统一会导致训练过程不稳定,甚至收敛变慢。
标准化我选择StandardScaler,即减去均值除以标准差。这样所有特征都变成均值0、方差1的标准正态分布。需要注意,像决策树、随机森林这类树模型对特征缩放并不敏感,因为它们在分裂时只做阈值比较。所以在最终Pipeline里我会同时训练两组模型,一组做标准化的线性模型,一组不做标准化的树模型,对比效果。
数据划分方面,我用train_test_split,测试集比例设为20%,同时设置random_state=42保证结果可复现。这里我特别强调分层抽样stratify=y,因为原始数据集标签分布大约是65%负样本对35%正样本,如果不分层,随机划分可能导致训练集和测试集的正负比例相差过大,影响模型评估的可靠性。
3. 模型构建与优化过程
3.1 建立评估基准:为什么不能只看准确率
很多初学者看到准确率90%就觉得自己模型很好了,但在这个数据集上,由于正样本只有35%左右,一个"无脑预测全部为负"的模型也能达到大约65%的准确率。所以你只用准确率评估,根本无法区分模型是真的学到了模式,还是只是在复读多数类。
我采用的评估指标体系是:准确率、精确率、召回率、F1-score、ROC-AUC、混淆矩阵。召回率在这个医疗场景里格外重要,因为它衡量的是"有病的人里有多少被正确找出来了"。漏掉一个真正的糖尿病患者,比误报一个健康人,后果要严重得多。因此在最终模型对比时,我会优先关注F1-score和ROC-AUC,而不是单一准确率。
python复制from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score
X = df.drop('Outcome', axis=1)
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
)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
重点提示:
StandardScaler必须先对训练集调用fit_transform,再对测试集调用transform。千万不要对训练集和测试集单独fit,否则计算均值和标准差所用的样本范围不一致,数据分布会被扭曲。
3.2 多模型横向对比实验
我挑选了5个有代表性的模型做对比:逻辑回归、决策树、随机森林、支持向量机、XGBoost。选择这些模型不只是因为它们常见,更重要的是它们代表了不同的建模思想——逻辑回归是线性模型的代表,擅长给出概率解释;决策树是可解释性最强的模型,但容易过拟合;随机森林是集成学习中Bagging思想的代表;SVM擅长高维边界的划分;XGBoost是Boosting思想的代表,也是目前表格数据竞赛中最常用的模型之一。
python复制from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from xgboost import XGBClassifier
models = {
'LogisticRegression': LogisticRegression(max_iter=1000),
'DecisionTree': DecisionTreeClassifier(random_state=42),
'RandomForest': RandomForestClassifier(random_state=42),
'SVM': SVC(probability=True, random_state=42),
'XGBoost': XGBClassifier(random_state=42, eval_metric='logloss')
}
for name, model in models.items():
model.fit(X_train_scaled, y_train)
y_pred = model.predict(X_test_scaled)
acc = model.score(X_test_scaled, y_test)
roc_auc = roc_auc_score(y_test, model.predict_proba(X_test_scaled)[:, 1])
print(f'{name}: 准确率={acc:.4f}, ROC-AUC={roc_auc:.4f}')
在我实际跑出来的结果里,逻辑回归和XGBoost的表现都比较好,准确率在75%到80%之间,ROC-AUC大约在0.83到0.85。决策树在未调参状态下容易过拟合,测试集表现往往低于随机森林。SVM在标准化后表现稳定,但可解释性差,不太好向答辩老师解释"为什么边界长这样"。
需要说明的是,由于训练集只有600多条样本,模型间的差距不会特别大。我在论文中展示的是"基于多个指标的综合对比结果",并且用交叉验证代替单次划分,这样得出的结论更有说服力。
3.3 交叉验证与网格搜索调参
单次划分的训练结果有运气成分,不能作为最终结论。所以我用GridSearchCV做5折交叉验证,同时搜索超参数。以随机森林为例,搜索的参数有n_estimators、max_depth、min_samples_split、min_samples_leaf。以XGBoost为例,则重点搜索learning_rate、max_depth、subsample、colsample_bytree。
python复制from sklearn.model_selection import GridSearchCV
param_grid = {
'n_estimators': [50, 100, 200],
'max_depth': [3, 5, 7, None],
'min_samples_split': [2, 5, 10],
'min_samples_leaf': [1, 2, 4]
}
rf = RandomForestClassifier(random_state=42)
grid = GridSearchCV(rf, param_grid, cv=5, scoring='roc_auc', n_jobs=-1)
grid.fit(X_train_scaled, y_train)
print(grid.best_params_)
这里我为什么要用roc_auc作为网格搜索的评分标准?因为医疗预测场景里,单纯用准确率做优化目标,模型倾向于把所有样本都判为负类,以换取较高的准确率。而roc_auc综合考虑了不同阈值下的真阳性率和假阳性率,能更客观地衡量模型的排序能力。
调参建议不要一步到位。我的经验是先固定n_estimators,微调max_depth和min_samples_split,观察训练集和测试集的表现差距,如果训练集准确率远高于测试集,说明过拟合了,应该增大min_samples_leaf或者减小max_depth。如果两边都不高,说明模型欠拟合,可以考虑增加n_estimators。
3.4 类别不平衡问题处理
虽然这个数据集的类别不平衡程度不算极端(约65:35),但我仍然做了对比实验。用class_weight='balanced'让模型自动调整类别权重,以及用SMOTE算法生成少数类样本,然后对比两者对召回率的影响。
python复制from imblearn.over_sampling import SMOTE
from sklearn.ensemble import RandomForestClassifier
smote = SMOTE(random_state=42)
X_train_resampled, y_train_resampled = smote.fit_resample(X_train_scaled, y_train)
rf_balanced = RandomForestClassifier(class_weight='balanced', random_state=42)
实际测试中,使用SMOTE后,正样本召回率明显提升,但精确率可能略有下降。这是正常现象——你愿意用更多的误报来换取更少的漏报。在医疗系统里,这个取舍通常是值得的。我在系统界面里也加入了"敏感度优先模式"和"均衡模式"的切换选项,让使用者可以按实际需求调整。
4. 系统实现:从模型到可用的Web应用
4.1 模型持久化与封装
模型训练完成后,需要保存下来供Web服务调用。我用joblib保存模型对象,同时也保存训练好的StandardScaler。有一个细节值得注意:如果你在Pipeline里组合了Scaler+Model,那么部署时只需要保存这一个Pipeline对象即可,不需要分别保存预处理模型和分类模型。这样做的好处是,调用预测时不需要记得"应该先标准化还是先预测",Pipeline会自动执行完整流程。
python复制import joblib
from sklearn.pipeline import Pipeline
pipeline = Pipeline([
('scaler', StandardScaler()),
('classifier', RandomForestClassifier(random_state=42))
])
pipeline.fit(X_train, y_train)
joblib.dump(pipeline, 'model_pipeline.pkl')
保存文件名我习惯带版本号,比如model_pipeline_v1.0.pkl。这个习惯在后续迭代模型时非常有用,你不会覆盖掉旧版本,回滚也方便。如果是要把模型部署到生产环境的API服务,还可以用MLflow这类工具管理模型版本,不过对毕业设计来说,joblib加版本号足够了。
4.2 Flask接口设计与预测逻辑
后端我选择了Flask,原因很简单:轻量、灵活、和Python生态无缝集成。接口设计上,我暴露两个路由:GET /返回前端页面,POST /predict接收JSON格式的特征数据,返回预测结果和概率。
python复制from flask import Flask, request, jsonify, render_template
import joblib
import numpy as np
app = Flask(__name__)
model = joblib.load('model_pipeline.pkl')
@app.route('/')
def index():
return render_template('index.html')
@app.route('/predict', methods=['POST'])
def predict():
data = request.get_json()
features = np.array([
data['Pregnancies'],
data['Glucose'],
data['BloodPressure'],
data['SkinThickness'],
data['Insulin'],
data['BMI'],
data['DiabetesPedigreeFunction'],
data['Age']
]).reshape(1, -1)
prob = model.predict_proba(features)[0][1]
prediction = 1 if prob >= 0.5 else 0
result = '患有糖尿病' if prediction == 1 else '未患有糖尿病'
return jsonify({
'prediction': result,
'probability': round(prob, 4),
'confidence': round(max(prob, 1 - prob), 4)
})
if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0', port=5000)
接口返回的probability是模型预测的正类概率,confidence表示模型对该判断的确信程度。把概率输出出来很有必要,因为单纯的0/1判断非常生硬,医生或患者看到"患病概率72%"和"患病概率51%"的感受是完全不一样的。
4.3 前端页面与交互体验
前端我只做了一个简单的HTML页面,配合Bootstrap做样式,用Fetch发送异步请求。页面包含8个输入框、一个提交按钮、一个结果展示区域。这样设计是因为毕设系统的核心是机器学习流程,不需要在页面上投入过多精力。但输入框我做了一些基础的校验,比如数值范围检查,避免用户输入BMI为负数、血糖为0这类明显不合理的数据。
html复制<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>糖尿病风险预测系统</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5" style="max-width: 640px;">
<h3 class="mb-4">糖尿病风险预测系统</h3>
<form id="predictForm">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">怀孕次数</label>
<input type="number" class="form-control" name="Pregnancies" value="1" min="0">
</div>
<div class="col-md-6">
<label class="form-label">葡萄糖浓度</label>
<input type="number" class="form-control" name="Glucose" value="120" min="0">
</div>
<!-- 其余字段省略展示,完整代码如下 -->
</div>
<button type="submit" class="btn btn-primary mt-4 w-100">开始预测</button>
</form>
<div id="result" class="mt-4"></div>
</div>
<script>
document.getElementById('predictForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const payload = {};
formData.forEach((value, key) => {
payload[key] = parseFloat(value);
});
const res = await fetch('/predict', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
document.getElementById('result').innerHTML = `
<div class="alert ${data.prediction.includes('患有') ? 'alert-danger' : 'alert-success'}">
预测结果:<strong>${data.prediction}</strong><br>
患病概率:${data.probability * 100}%<br>
模型置信度:${data.confidence * 100}%
</div>`;
});
</script>
</body>
</html>
前端页面的颜色逻辑也很直观:绿色表示低风险,红色表示高风险。如果概率接近0.5(比如45%到55%之间),我会额外显示一条"建议进一步检查"的提示,这个边界情况的处理确实让整个系统看起来完整很多。
4.4 完整流程演示
启动服务后,我拿测试集中的一条真实样本进行演示。输入数据为:怀孕次数1、血糖89、血压66、皮褶厚度23、胰岛素94、BMI 28.1、遗传函数0.167、年龄21。系统返回"未患有糖尿病",置信度约78%。这条样本对应真实标签是0,模型预测正确。
再用第二条样本演示:怀孕次数8、血糖183、血压64、皮褶厚度0、胰岛素0、BMI 23.3、遗传函数0.672、年龄32。由于这一条里皮褶厚度和胰岛素在原始数据中为0,我特意演示了缺失值填充后的效果——如果直接拿0值去预测,模型的输出会明显偏移。这恰好说明前面数据清洗环节的价值。
5. 常见问题与避坑指南
5.1 数据范围不一致导致预测异常
很多同学在部署时遇到的第一个坑是:训练时数据做过标准化,但部署时忘了用同一个Scaler处理新输入,导致新输入的数值范围与训练数据严重不一致,模型输出概率几乎恒定为0或1。解决方法是把Scaler和模型一起放进Pipeline对象中保存,就像前面代码演示的那样。
还有一种情况是前端传参时输入了字符串或空值。Flask后端接收JSON后应该做类型校验,直接float()强制转换如果失败会导致500错误。我建议在后端加一层try-except,返回友好的错误提示而不是直接抛出异常。
5.2 过拟合识别与应对
训练集准确率95%以上但测试集只有70%,这是典型的过拟合信号。我在实验记录中专门保留了一张对比表:未调参的决策树在训练集上准确率接近100%,测试集只有约72%。通过交叉验证和网格搜索调整超参数后,测试集准确率提升到78%左右,训练集准确率则控制在85%左右。
如果过拟合仍然严重,优先考虑三个手段:增大正则化参数、增加min_samples_leaf、减少max_depth。另外也可以考虑添加更多数据或做特征降维。在答辩时,老师非常喜欢追问"你是怎么发现过拟合的",这时候拿出训练集和测试集的指标对比表,回答就很有说服力。
5.3 类别不平衡被忽略
数据集中负样本约500条,正样本约268条,比例大约为1.9比1。如果不做处理,模型会倾向于把样本预测为负类,使召回率偏低。我在实验中发现,直接训练的逻辑回归模型召回率只有约60%,而使用class_weight='balanced'后召回率提升到了74%。
在系统设计中,用户可以在界面上看到"模型敏感度"选项,默认使用均衡模式,但可以切换到"敏感模式"以提高对糖尿病患者的检出率。这个功能虽然代码实现不复杂,但给系统的完整性和可解释性加了很多分。
5.4 对轮训练思路
最后再分享一个我自己实践下来的经验。整个项目我建议按照四个阶段推进,每个阶段留出时间余量。第一阶段做数据探索,输出EDA报告和可视化图表;第二阶段做基线模型,跑通完整的训练评估流程;第三阶段做模型优化,通过交叉验证和调参得出最终模型;第四阶段做系统集成,封装接口并制作演示页面。
我见过太多同学卡在第三阶段反复调参出不了结果,最后草草交了一个notebook。其实这个选题最大的优势在于它足够经典,你不需要追求SOTA成绩,而是要把流程做完整、逻辑讲清楚。哪怕最终准确率只有76%,只要你能解释清楚"为什么是这个模型、为什么是这个阈值、指标之间如何取舍",答辩通过和拿到好评都不是问题。
把训练日志、实验对比表、可视化图表全部保留下来,它们比最终预测结果本身更有说服力。做项目本身就是积累经验的过程,这条流程走通之后,其他二分类问题、回归问题、甚至多分类问题,你都能很快地复制这套方法论。
