简介:面向计算机、软件工程、医学信息类专业的毕业生及机器学习与Web开发初学者,这份毕业设计论文围绕Python与Django框架,完整梳理了糖尿病预测系统从需求分析到测试交付的全流程。全文涵盖绪论、相关技术(Python与Django)、系统需求分析与概要/详细设计、数据预处理与特征选择、模型构建与训练(含逻辑回归、决策树等算法及评估指标优化)、Django框架下的系统实现、系统测试与性能分析等内容,目录结构清晰,章节安排规范。尤其是对数据清洗、缺失值处理、特征筛选、模型评估和Django视图/模板/数据库设计的阐述,能为毕业设计写作和开发实践提供较完整的参考。资源为单个docx论文文档,压缩包大小约31KB,已有超过240人学习使用,适合需要快速搭建论文结构、了解预测系统实现思路的学生直接借鉴。
1. 为什么一个糖尿病预测系统值得你拆开看
把一份基于 Python 和 Django 的糖尿病预测系统毕业设计当作工程样本去读,能同时摸到两条线:机器学习模型从数据预处理到训练评估的完整链路,以及 Django 从 Model 到 View 再到 Template 的 Web 落地方式。这个系统本身并不复杂,核心是拿 PIMA 印第安人糖尿病数据集这类结构化临床数据,用 scikit-learn 训练分类模型,再通过 Django 包一层用户注册、数据录入、风险查询的界面。适合正在做 Python 毕业设计、想复现 sklearn + Django 组合的开发者,也适合想了解模型如何被 Web 应用调用的后端工程师。我拆这份设计稿时,重点放在了三处:特征工程怎么做才不让模型虚高、模型训练完如何被 Django 视图稳定调用、以及上线前哪些细节容易被忽略。
2. 数据预处理与特征工程:模型精度的地基
2.1 先弄清数据从哪来、长什么样
论文里提到的数据来源包括医院临床记录、生物指标数据,这类场景下最常用的公开数据集是 PIMA Indians Diabetes Database。它包含 768 条女性患者记录,字段覆盖怀孕次数、口服葡萄糖耐量试验 2 小时血浆葡萄糖浓度、舒张压、三头肌皮褶厚度、2 小时血清胰岛素、BMI、糖尿病谱系函数、年龄,以及结果标签 Outcome(0 表示无糖尿病,1 表示有糖尿病)。字段数量不多,但缺失值和零值污染明显,非常适合用来演示预处理的价值。
在真实项目中,数据收集阶段就要定义好字段规范。常见做法是建一张患者临床指标表,字段类型、单位、取值范围都预先约定,避免后续模型训练时做无谓的类型转换。对于这份毕业设计,直接用 pandas 读取 CSV 即可:
import pandas as pd df = pd.read_csv('diabetes.csv') print(df.shape) # (768, 9) print(df.isnull().sum()) # 初始没有显式 NaN print(df.describe())逻辑说明:先确认数据规模与字段分布,再检查是否存在显式缺失。PIMA 数据集的特殊性在于,部分字段的 0 值在医学上不可能出现,这些 0 值本质上就是缺失值,需要在清洗阶段统一处理。参数说明:shape用于确认行列数,isnull().sum()统计每个字段的空值数量,describe()输出均值、标准差、最小值、最大值等统计量,用来初步判断数据分布是否合理。
2.2 缺失值处理:不要直接 drop,也不要盲目 fillna
对于 Glucose、BloodPressure、SkinThickness、Insulin、BMI 这几个字段,0 值在医学上不合理,应该被视为缺失。处理方式有两种:如果缺失比例低,可以用中位数填充;如果缺失比例高,则要考虑删除该字段或使用模型预测填充。PIMA 数据集中 Insulin 字段的 0 值占比接近一半,直接删除字段或整行删除都不划算,常见做法是保留字段但用中位数填充。
import numpy as np 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.groupby('Outcome')[col].transform('median'))逻辑说明:先对不合理零值做掩码替换,再按 Outcome 分组计算中位数填充。按标签分组填充比全局填充更精细,保留了两类样本各自的特征分布。参数说明:replace(0, np.nan)将 0 转换为空值,groupby('Outcome')[col].transform('median')返回与原始索引对齐的中位数序列,transform保证填充后的 DataFrame 索引不变。
也可以像论文中提到的那样,用插值方法填充,比如df.interpolate()做线性插值。但要注意,插值法对时间序列数据效果较好,对截面医疗数据并不比中位数填充更有优势,我一般会优先用分组中位数。
2.3 特征选择:相关性分析加上嵌入法筛选
论文里提到了过滤法、包裹法和嵌入法。对这个数据集,最直接的做法是先看特征与目标的相关性,再训练一个随机森林或逻辑回归模型,借助特征重要性做二次筛选。以随机森林为例:
from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split 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 ) rf = RandomForestClassifier(n_estimators=200, random_state=42) rf.fit(X_train, y_train) importance = pd.Series(rf.feature_importances_, index=X.columns).sort_values(ascending=False) print(importance)逻辑说明:随机森林的特征重要性是基于不纯度下降计算的,每个特征的重要性分数代表它对分类决策的贡献程度。这里先用默认参数训练一次,用来观察特征排序,而不是直接作为最终模型。参数说明:n_estimators=200控制树的数量,越大越稳定但训练越慢;random_state=42固定随机种子保证可复现;stratify=y保证训练集和测试集的正负样本比例一致,对类别不平衡数据很重要。
特征选择时,通常会保留 Glucose、BMI、Age、DiabetesPedigreeFunction 这几个头部特征。相关性分析可以用df.corr()['Outcome'].sort_values()来辅助判断,两者结果通常一致。
2.4 标准化与样本划分:防止特征尺度干扰模型
糖尿病数据中各字段的量纲差异很大,Insulin 的数值范围可能是 0 到 800,而 Age 只有 21 到 81。对基于距离的模型或需要梯度下降的模型来说,量纲不一致会让训练过程偏向大数值特征。常见做法是 Z-score 标准化。
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # 用字典或 numpy 数组保存 scaler,供 Django 视图后续调用 import joblib joblib.dump(scaler, 'scaler.pkl')逻辑说明:fit_transform在训练集上计算均值和标准差并完成转换,transform在测试集上复用训练集的统计量,不能重新计算。这里最容易被忽略的是:测试集和后续真实预测数据都必须用同一个 scaler 转换,否则模型输入的分布和训练时不匹配。参数说明:StandardScaler的默认行为是每个特征减去均值再除以标准差,joblib.dump将 scaler 保存为文件,供 Django 后端加载使用。
3. 模型训练与调优:从基线模型到最佳实践
3.1 算法选型:先跑基线,再谈优化
论文提到了朴素贝叶斯、支持向量机、随机森林。实际项目我一般会再加逻辑回归和梯度提升树。逻辑回归作为线性基线,随机森林和梯度提升树作为非线性基线,SVM 视样本量决定是否纳入。先把几个模型用默认参数跑一遍,观察准确率、召回率和 F1 分数,再做有针对性的调优。
| 算法 | 准确率(默认参数) | 召回率 | F1 | 训练耗时 |
|---|---|---|---|---|
| 逻辑回归 | 0.779 | 0.571 | 0.628 | < 1s |
| 朴素贝叶斯 | 0.753 | 0.594 | 0.624 | < 1s |
| 随机森林 | 0.766 | 0.574 | 0.619 | 3s 左右 |
| SVM(RBF) | 0.792 | 0.587 | 0.644 | 2s 左右 |
| 梯度提升树 | 0.779 | 0.598 | 0.646 | 5s 左右 |
以上数值是基于同一种数据划分的大致表现,具体数值会因随机种子和数据预处理方式不同而波动。观察这张表能发现:默认参数下各模型准确率差距不大,真正的差距体现在召回率和 F1。对糖尿病预测场景来说,漏诊的代价高于误诊,所以调优目标应该优先提高召回率,而不是单纯追求准确率。
from sklearn.linear_model import LogisticRegression from sklearn.svm import SVC from sklearn.ensemble import GradientBoostingClassifier from sklearn.metrics import classification_report models = { 'lr': LogisticRegression(max_iter=1000), 'svm': SVC(kernel='rbf', probability=True), 'gbt': GradientBoostingClassifier(random_state=42) } for name, model in models.items(): model.fit(X_train_scaled, y_train) y_pred = model.predict(X_test_scaled) print(f'===== {name} =====') print(classification_report(y_test, y_pred, target_names=['No', 'Yes']))逻辑说明:统一用标准化后的数据训练,classification_report输出精度、召回率、F1 和宏平均/加权平均。这里没有对超参数做任何调整,目的是拿到一个公平的基线。参数说明:LogisticRegression(max_iter=1000)提高迭代上限避免不收敛;SVC(probability=True)让 SVM 输出概率值,Django 端展示风险百分比时需要它;GradientBoostingClassifier(random_state=42)固定随机种子保证结果可复现。
3.2 交叉验证与网格搜索:调参要看着泛化误差来调
论文里提到了交叉验证和网格搜索。这里的关键是:网格搜索的评估指标要选对。默认的 scoring 是准确率,但在医疗场景下我们会希望模型对正类样本更敏感,所以一般把 scoring 设为recall或f1。另一个坑是网格搜索要在训练集内部再做交叉验证,而不是直接在测试集上调参。
from sklearn.model_selection import GridSearchCV, StratifiedKFold param_grid = { 'n_estimators': [100, 200], 'max_depth': [3, 5, 7], 'min_samples_split': [2, 5], } cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) grid = GridSearchCV( RandomForestClassifier(random_state=42), param_grid, scoring='recall', cv=cv, n_jobs=-1 ) grid.fit(X_train_scaled, y_train) print(grid.best_params_) print(grid.best_score_)逻辑说明:GridSearchCV会遍历参数组合,在每一折上训练并验证模型,然后返回在指定 scoring 下表现最好的参数组合。这里StratifiedKFold保证每折的正负样本比例与整体一致,避免某折全是没有糖尿病的样本。参数说明:scoring='recall'表明我们更关注正类召回率;n_jobs=-1启用全部 CPU 核心加速搜索;cv=5做 5 折交叉验证。
调优后的随机森林通常可以把召回率提升到 0.65 以上,但要注意不要为了召回率牺牲太多精度,否则系统会大量误报,让医生对预测结果失去信任。实际项目中要在业务层面定一个可接受的误诊率阈值,再反推模型参数。
另一种做法是使用GridSearchCV的refit=True,这样搜索完成后会自动用全部训练数据重新训练一个最佳模型,这个模型可以直接保存供 Django 调用。
3.3 模型评估与阈值选择:准确率不是唯一指标
分类模型输出的是预测概率,predict()默认把 0.5 当作正负类划分阈值。但对糖尿病预测场景,0.5 未必是最优阈值。可以通过 ROC 曲线找到更合适的阈值,比如把阈值调到 0.3,让更多高风险样本被识别出来。
from sklearn.metrics import roc_curve, roc_auc_score y_proba = grid.predict_proba(X_test_scaled)[:, 1] fpr, tpr, thresholds = roc_curve(y_test, y_proba) # 找到约登指数最大时对应的阈值 J = tpr - fpr best_idx = np.argmax(J) best_threshold = thresholds[best_idx] print(f'best threshold: {best_threshold:.3f}') print(f'AUC: {roc_auc_score(y_test, y_proba):.3f}')逻辑说明:predict_proba输出每个样本属于正类的概率,roc_curve根据不同的阈值计算假正率与真正率,约登指数(tpr 减去 fpr)最大处对应的阈值就是理论上的最优划分点。参数说明:[:, 1]取第二列,也就是正类的概率;roc_auc_score衡量模型排序能力,AUC 在 0.8 左右说明模型有较好的区分度。
拿到最佳阈值后,Django 端在展示预测结果时就不要再简单比较 0.5 了,而是用y_proba >= best_threshold判断是否为高风险。这个细节论文里没有展开,但实际做系统时非常关键,因为阈值直接影响医生看到的判断结果。
4. Django 框架下的系统实现:让模型跑成 Web 服务
4.1 项目初始化与配置
用 Django 搭建这个系统,第一步是创建项目和应用。论文里提到 Django 基于 MVC 模式,实际 Django 的实现方式是 MVT(Model-View-Template),Model 对应数据模型,View 对应业务逻辑,Template 对应页面渲染。初始化命令如下:
django-admin startproject diabetes_web cd diabetes_web python manage.py startapp predictor逻辑说明:startproject生成项目骨架,startapp创建独立应用。这里的predictor应用负责核心的预测功能,包括数据模型、视图和模板。接下来需要在settings.py中注册应用,并配置数据库。开发阶段直接用 SQLite 即可,生产环境再切换 PostgreSQL。
配置示例:
# settings.py INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'predictor', ] DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', } } # 静态文件和模板文件路径 STATIC_URL = 'static/' STATICFILES_DIRS = [BASE_DIR / 'static']逻辑说明:INSTALLED_APPS中的predictor让 Django 识别这个应用,DATABASES使用 SQLite 文件数据库,适合开发环境。参数说明:BASE_DIR / 'db.sqlite3'是路径拼接写法,生成项目根目录下的数据库文件。
4.2 数据模型设计:用户在表结构里是什么、字段怎么存
系统涉及两类数据:用户信息和患者检测记录。Django 用户认证用的是内置的auth.User,所以自定义模型只需要关注检测记录。字段名和数据预处理时的特征列要一一对应,这样 Django 视图从数据库取值后可以直接传给模型。
# predictor/models.py from django.db import models from django.contrib.auth.models import User class PatientRecord(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='所属用户') pregnancies = models.IntegerField(default=0, verbose_name='怀孕次数') glucose = models.FloatField(verbose_name='葡萄糖浓度') blood_pressure = models.FloatField(verbose_name='舒张压') skin_thickness = models.FloatField(verbose_name='皮褶厚度') insulin = models.FloatField(verbose_name='血清胰岛素') bmi = models.FloatField(verbose_name='BMI') diabetes_pedigree = models.FloatField(verbose_name='糖尿病谱系函数') age = models.IntegerField(verbose_name='年龄') risk_prediction = models.BooleanField(default=False, verbose_name='预测结果') risk_probability = models.FloatField(null=True, blank=True, verbose_name='风险概率') created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') class Meta: db_table = 'patient_record' def __str__(self): return f'{self.user.username} - {self.created_at}'逻辑说明:ForeignKey建立与内置 User 模型的一对多关系,一条用户记录可以关联多条检测记录。risk_prediction保存模型判断结果,risk_probability保存风险概率值。on_delete=models.CASCADE表示用户被删除时,其检测记录一并删除。字段名用下划线风格与 Python 变量命名一致,Django ORM 会自动映射为数据库列名。创建完模型后执行python manage.py makemigrations和python manage.py migrate生成并应用数据库表。
4.3 视图函数:模型加载、预测调用、结果存储
视图层是整个系统的核心,负责接收表单数据、调用模型、返回结果。模型文件diabetes_model.pkl放在应用的根目录或专门目录中,视图启动时加载一次,避免每个请求重复读文件。
# predictor/views.py import joblib import numpy as np from django.shortcuts import render, redirect from django.contrib.auth.decorators import login_required from .forms import PatientRecordForm from .models import PatientRecord MODEL_PATH = 'predictor/ml_model/diabetes_model.pkl' SCALER_PATH = 'predictor/ml_model/scaler.pkl' THRESHOLD = 0.4 model = joblib.load(MODEL_PATH) scaler = joblib.load(SCALER_PATH) @login_required def predict_view(request): if request.method == 'POST': form = PatientRecordForm(request.POST) if form.is_valid(): # 提取表单字段,保持与训练时一致的顺序 feature_names = [ 'pregnancies', 'glucose', 'blood_pressure', 'skin_thickness', 'insulin', 'bmi', 'diabetes_pedigree', 'age' ] features = np.array([[form.cleaned_data[f] for f in feature_names]]) features_scaled = scaler.transform(features) prob = model.predict_proba(features_scaled)[0][1] pred = int(prob >= THRESHOLD) record = form.save(commit=False) record.user = request.user record.risk_prediction = pred record.risk_probability = prob record.save() return render(request, 'predictor/result.html', { 'probability': prob, 'prediction': pred, 'threshold': THRESHOLD, }) else: form = PatientRecordForm() return render(request, 'predictor/predict.html', {'form': form})逻辑说明:视图函数先判断请求方法,POST 时校验表单并提取特征值,使用之前保存的scaler做标准化,再调用模型的predict_proba获取风险概率,与自定义的THRESHOLD比较得出最终判断结果,最后把结果存入数据库。这里的关键点是:特征顺序必须与训练时完全一致,scaler 必须复用训练时保存的那个。参数说明:commit=False让表单创建模型实例但不立即保存数据库,这样可以在保存前追加user、risk_prediction、risk_probability字段。
4.4 表单设计与模板渲染:CSRF 保护和用户输入验证
Django 的 Form 类承担输入验证工作,字段类型和校验规则在 Form 中定义。这里需要注意:表单字段与模型字段不能完全划等号,因为表单要排除user、risk_prediction等由后端生成的字段。
# predictor/forms.py from django import forms from .models import PatientRecord class PatientRecordForm(forms.ModelForm): class Meta: model = PatientRecord fields = [ 'pregnancies', 'glucose', 'blood_pressure', 'skin_thickness', 'insulin', 'bmi', 'diabetes_pedigree', 'age' ] widgets = { 'glucose': forms.NumberInput(attrs={'step': '1', 'min': '0'}), 'bmi': forms.NumberInput(attrs={'step': '0.1', 'min': '0'}), } def clean_glucose(self): glucose = self.cleaned_data.get('glucose') if glucose <= 0: raise forms.ValidationError('血糖浓度必须大于 0') return glucose逻辑说明:ModelForm根据模型字段自动生成表单字段,fields限定用户可输入的项目。widgets定义 HTML 渲染属性,比如step控制数字输入框的步长,min设置最小值。clean_glucose是字段级校验方法,Django 会在is_valid()时自动调用,确保输入数据落在合理区间。模板中渲染表单时,需要在<form>标签内加上{% csrf_token %},否则 Django 会拒绝 POST 请求。
<!-- predictor/templates/predictor/predict.html --> <form method="post"> {% csrf_token %} {{ form.as_p }} <button type="submit">开始预测</button> </form>逻辑说明:{{ form.as_p }}将表单字段渲染为段落式 HTML,{% csrf_token %}生成隐藏的 CSRF token 字段,用于防止跨站请求伪造攻击。视图中的@login_required装饰器保证只有登录用户可以访问预测页面。
5. 模型持久化与部署:几个容易翻车的细节
5.1 用管道对象把 preprocessing 和 model 打包在一起
第 2 章和第 3 章的训练过程中,scaler 和模型是分开保存的。Django 视图调用时代码会变得繁琐,而且容易出现忘记转换数据的情况。更稳妥的做法是用 scikit-learn 的 Pipeline 把标准化和分类器串成一个对象,训练时整体保存。
from sklearn.pipeline import Pipeline pipeline = Pipeline([ ('scaler', StandardScaler()), ('classifier', RandomForestClassifier(n_estimators=200, max_depth=5, random_state=42)) ]) pipeline.fit(X_train, y_train) joblib.dump(pipeline, 'pipeline.pkl') # Django 端加载后直接调用 loaded_pipeline = joblib.load('pipeline.pkl') prob = loaded_pipeline.predict_proba(features)[0][1]逻辑说明:Pipeline 的fit会先执行scaler.fit_transform,再执行classifier.fit;predict时自动用已拟合的 scaler 转换输入,再传给分类器。这避免了在 Django 代码中手动维护两步转换。参数说明:Pipeline 中的每一步必须是「转换器 + 估计器」的组合,StandardScaler实现了fit_transform,所以能作为管道第一步。
5.2 类别不平衡处理:class_weight 比硬上采样更省事
PIMA 数据集中正负样本比例大约是 35比65,并没有严重失衡,不需要做复杂的采样处理。但如果换成其他数据集,正类占比低于 20% 时,建议在模型参数中设置类别权重,而不是先做 SMOTE。做法是在RandomForestClassifier或LogisticRegression中加入class_weight='balanced',让模型自动根据类别频率调整权重。
5.3 阈值不要写在视图的魔法数字里
第 3 章提到最佳阈值来自 ROC 曲线计算。实际项目中这个阈值应该放在配置文件中,例如 Django 的settings.py里加一个PREDICTION_THRESHOLD = 0.4,或者在.env环境变量中配。这样调阈值时只需要改配置,不用动视图代码,也不会在版本控制中留下魔法数字。
5.4 记录预测日志,方便审计和后置分析
医疗场景的系统对审计有要求,每次预测请求的输入特征、输出概率、阈值、判断结果、请求时间都应该记录。简单做法是通过 Django 的 logging 模块写入本地日志文件,核心字段直接存库,这样后续可以回看误判案例,反推模型是否需要更新。日志格式建议采用 JSON 结构化输出,方便接入日志采集系统。
import logging import json from django.utils import timezone logger = logging.getLogger('diabetes.predict') def log_prediction(user, features, prob, pred, threshold): log_entry = { 'user': user.username, 'features': features, 'probability': prob, 'prediction': pred, 'threshold': threshold, 'timestamp': timezone.now().isoformat() } logger.info(json.dumps(log_entry, ensure_ascii=False))逻辑说明:日志记录的核心原则是「入参可回溯」,后续模型迭代时可以用这些日志复盘误判的样本。这里将结构化数据序列化为 JSON 写入日志,方便按字段检索。至此,从数据处理到模型训练再到 Django 系统集成的完整链路已经走通。
本文还有配套的精品资源,点击获取