☰
基于LightGBM的糖尿病高血压预测毕设全流程实战
2026/10/12 18:38:09 网站建设 项目流程

简介:这是一份面向计算机相关专业毕业设计与课程实践的完整项目包,基于LightGBM框架构建糖尿病与高血压风险预测系统,涵盖数据处理、模型训练、评估与可视化展示全流程,适合正在做毕设或需要项目实战练习的学生参考复用。压缩包共139个文件,约118.03MB,主要包含Python源码、训练好的pkl模型、xlsx数据集、可执行exe程序以及126张界面截图,截图可直观展示系统各模块运行效果,md文档则提供项目结构与应用逻辑说明。项目经导师指导并评审通过,评价96.5分,代码均测试运行成功后上传,读者可直接运行查看预测效果,也可基于源码调整特征或算法进行二次开发,作为课程设计、期末大作业或项目初期立项演示均有较强参考价值。目前已有204人学习下载,适合希望快速上手LightGBM疾病预测实战的学习者。

1. 为什么拿它当毕业设计:从赛题到落地只差一个 LightGBM

如果手里这份毕业设计叫《基于 LightGBM 的糖尿病高血压预测 python 源码 + 数据集 + 模型》,那它解决的并不是一个纯算法题,而是一整条“数据清洗 → 特征工程 → 模型训练 → 界面打包 → 论文展示”的链路。做过毕设的人都懂,导师真正看重的不是你把准确率刷到多高,而是你能不能讲清楚每个环节为什么这么做。这份资源刚好把整条链路补齐了,从 hypertension.pkl、diabetes.pkl 两个现成数据集,到训练好的模型,再到能跑出预测结果的 .exe 程序,适合计算机相关专业学生做毕设、课程设计或期末大作业,也适合想从零上手 LightGBM 的初学者照着复现。我拆完第一感受是:它最值钱的部分不是模型本身,而是那份把“二分类风险预测”讲成完整故事的项目文档。

2. 数据集与特征工程:先解决“预测什么”和“喂什么”

2.1 先看清两个 pkl 文件里到底装了什么

拿到压缩包后,先别急着跑训练脚本,第一步是确认数据形态。压缩包里最核心的输入是hypertension.pkl和diabetes.pkl,这两个是序列化后的 DataFrame,直接 pandas.read_pickle 就能读。我一般会在项目根目录建一个explore.py,把字段名列出来,确认标签列到底叫什么,再做后面的特征处理。

import pandas as pd df_diabetes = pd.read_pickle("diabetes.pkl") df_hyper = pd.read_pickle("hypertension.pkl") print("diabetes shape:", df_diabetes.shape) print("diabetes columns:", df_diabetes.columns.tolist()) print(df_diabetes.head(3).to_string()) print("hypertension shape:", df_hyper.shape) print("hypertension columns:", df_hyper.columns.tolist()) print(df_hyper.head(3).to_string())

这段代码的作用是把 pkl 文件里的数据先“打回原形”。pd.read_pickle读取的是二进制序列化对象,列名、行数、前几行样本一打印出来,就能判断这份数据是原始问卷还是已经做过清洗。常见做法是先看标签列,比如label或target,再看特征列里有没有身份证号、姓名这类不该进模型的字段。参数上只有一个encoding值得注意,如果读取报 UnicodeDecodeError,通常不是 pkl 的问题,而是你环境里 pandas 版本差异,可以在读取后统一df.columns = [str(c) for c in df.columns]规避。

2.2 缺失值、重复行和数值分布:不处理干净就训不出能答辩的模型

LightGBM 本身支持缺失值,很多新手以为“模型能处理”就等于“不用管”,这是最容易翻车的地方。LightGBM 处理缺失值是把缺失值默认分到增益最大的一侧,这在样本量大的时候问题不大,但毕设数据往往只有几千行,缺失比例超过 20% 的字段就会变成隐形的噪声源。

我建议的固定流程是:

  1. 先统计每列缺失比例;
  2. 缺失低于 1% 的列直接删除对应行;
  3. 缺失在 1% 到 20% 的数值列,用中位数填充,分类列用众数填充;
  4. 缺失超过 20% 的列,除非它跟标签有强相关性,否则直接删列。
import pandas as pd import numpy as np def clean_df(df, label_col): df = df.copy() missing_ratio = df.isnull().mean() drop_cols = missing_ratio[missing_ratio > 0.20].index.tolist() df = df.drop(columns=drop_cols) for col in df.columns: if col == label_col: continue if df[col].dtype in ["float64", "int64"]: df[col] = df[col].fillna(df[col].median()) else: df[col] = df[col].fillna(df[col].mode()[0]) df = df.drop_duplicates() return df

这段函数维护了一个筛选逻辑:超过 20% 缺失的列直接丢弃,数值列用中位数,分类列用众数。drop_duplicates()必须放在填充之后,因为填充前两行可能因缺失值不同被误判为不重复,填充后才会暴露出真正重复的样本。答辩时如果被问到“为什么用中位数不用均值”,标准回答是:均值对离群点敏感,中位数稳健,LightGBM 是树模型,对特征尺度不敏感,所以不需要标准化。

2.3 特征怎么选:别把十来个字段全塞进去

项目正文里没有给出完整的特征清单,但这类疾病风险预测数据集通常围绕年龄、性别、血压、血糖、BMI、吸烟史、家族病史等字段展开。如果原始数据只有十几列,全部塞进模型也能跑,但如果字段里有强相关的重复信息,比如同时存在“收缩压”和“血压等级”,就要处理共线性。

我一般会先跑一版全特征模型,然后看feature_importance,把重要性接近 0 的列删掉再跑一版对比。LightGBM 的默认feature_importance基于分裂次数,打印出来长这样:

import lightgbm as lgb importance = pd.DataFrame({ "feature": model.feature_name(), "gain": model.feature_importance(importance_type="gain") }).sort_values("gain", ascending=False) print(importance.head(20))

importance_type="gain"和默认的"split"含义不同:split是特征被用来分裂的次数,gain是特征带来的总信息增益。后者更能反映真实贡献。如果某个字段在两次交叉验证里 gain 都排在倒数,且删除后验证集 AUC 不降,就把它拿掉。这步不是必须做,但做了之后论文里能多一张“特征筛选前后对比”的表,答辩老师通常吃这套。

3. 训练与调参:交叉验证和早停才是 LightGBM 的正确打开方式

3.1 数据划分:不能把 train_test_split 当默认答案

很多入门教程直接train_test_split(test_size=0.2),放在毕设里也能跑通,但风险是随机划分可能把同一个人的多条体检记录同时分到训练集和测试集,造成数据泄漏。如果数据集里有patient_id或id字段,必须先按个体分群,再划分。

更稳妥的做法是直接上 StratifiedKFold,保持每一次划分里正负样本比例跟全量一致。疾病预测场景里糖尿病和高血压的患病率通常不是 50%,如果直接随机划分,很可能某一折里正样本只有十几个,模型训练时决策边界偏移,验证集表现忽高忽低。

from sklearn.model_selection import StratifiedKFold X = df.drop(columns=["label"]) y = df["label"] folds = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) for fold_idx, (trn_idx, val_idx) in enumerate(folds.split(X, y)): X_trn, X_val = X.iloc[trn_idx], X.iloc[val_idx] y_trn, y_val = y.iloc[trn_idx], y.iloc[val_idx] print(f"fold {fold_idx}: train {len(trn_idx)}, val {len(val_idx)}, " f"pos_rate {y_val.mean():.3f}")

这段代码的核心在StratifiedKFold的shuffle=True和random_state=42:shuffle 让样本顺序不再影响折的划分,固定随机种子保证每次复现结果一致。如果你发现某个折的正样本比例波动超过 2%,回头检查是不是没有分层,或者label本身是连续值,那要先做阈值二值化。答辩时最常被问的就是“你这个验证集是怎么来的”,直接答五折分层交叉验证,比“我随便切的”有说服力得多。

3.2 先给一组确定性强的 baseline 参数

LightGBM 新手最常见的误区是一上来就调n_estimators=5000、learning_rate=0.01,结果训练几个小时后发现过拟合。我一般会让初版参数保守一点,跑通后再往深处调。

import lightgbm as lgb from sklearn.metrics import roc_auc_score lgb_params = { "objective": "binary", "metric": "auc", "learning_rate": 0.05, "num_leaves": 31, "max_depth": -1, "min_child_samples": 20, "feature_fraction": 0.8, "bagging_fraction": 0.8, "bagging_freq": 1, "lambda_l1": 0.0, "lambda_l2": 1.0, "verbose": -1, "seed": 42, }

num_leaves=31是默认值,代表单棵树的叶子数上限,对应模型复杂度;min_child_samples=20强制每个叶子至少 20 个样本,是防止过拟合的第一道闸门;feature_fraction=0.8让每棵树只用 80% 的特征,增加树的多样性。bagging_fraction=0.8配合bagging_freq=1做行采样,两者组合起来相当于给模型加随机性,泛化能力通常比纯列采样更好。verbose=-1是关掉训练日志,不关的话终端会被刷屏,影响你观察早停信息。

3.3 早停:拿着验证集当“体温计”

毕设数据量不大时,训练轮数不是越多越好。每轮迭代完用验证集算一次 AUC,连续多轮不涨就停,这是 LightGBM 里最实用的技能,也是答辩时可以展开讲的细节。

dtrn = lgb.Dataset(X_trn, label=y_trn) dval = lgb.Dataset(X_val, label=y_val, reference=dtrn) model = lgb.train( lgb_params, dtrn, num_boost_round=1000, valid_sets=[dval], valid_names=["val"], callbacks=[lgb.early_stopping(stopping_rounds=50), lgb.log_evaluation(50)] ) val_pred = model.predict(X_val, num_iteration=model.best_iteration) print("val auc:", roc_auc_score(y_val, val_pred)) print("best iteration:", model.best_iteration)

early_stopping(stopping_rounds=50)表示连续 50 轮验证集 AUC 没提升就停止训练,num_iteration=model.best_iteration预测时必须指定最佳迭代次数,否则 LightGBM 会默认用最后一轮,效果反而变差。这里有个容易踩的坑:如果log_evaluation没设置,终端会疯狂刷 1000 多行日志;设置成 50 代表每 50 轮打印一次,干净很多。还有一点,验证集必须通过reference=dtrn关联到训练集,否则模型不知道特征对齐关系,后续做预测时容易出现列顺序错乱。

3.4 参数再调:不是靠玄学,是靠固定变量

调参只要守住一个原则:一次只动一个参数。把 grid search 或者 Optuna 留到你确实需要刷分的时候,毕设场景下手工调三组就够了。

常见的顺序是:

  • 第一轮固定其他参数,调num_leaves,候选值[15, 31, 63];
  • 第二轮固定最优num_leaves,调min_child_samples,候选值[5, 20, 50];
  • 第三轮把learning_rate从 0.05 降到 0.03,同时把num_boost_round上限提高,让早停决定最终轮数。

我踩过的教训是:num_leaves=63配合小数据集时,验证集 AUC 在训练前期上涨很快,但到第 300 轮左右就开始过拟合,训练集 AUC 一路冲到 1.0,验证集却原地不动。所以调num_leaves时一定要同步观察best_iteration有没有变早。如果变早说明模型变复杂了,需要同时加大min_child_samples压制复杂度。这个联动逻辑写进论文里,比贴一张网格搜索的表格更有技术含量。

4. 打包 .exe 与运行避坑:从 .py 到可双击程序的完整链路

4.1 为什么毕设一定要打包 .exe

很多学生把模型训练完就交差,但导师演示的时候不可能在你机器上配环境。打包成疾病风险预测程序.exe后,演示环节就是双击、输入信息、看结果,不需要现场装 Python,也不需要解释“我这环境有问题”。这份资源里已经附带打包好的 exe,但你自己改过模型或界面后,还是要学会重新打包。

打包工具有 PyInstaller 和 Nuitka 两类,PyInstaller 更成熟,踩坑经验在网上也更多。毕设场景我推荐 PyInstaller,因为 LightGBM 的模型文件是 pkl 或 txt 格式,PyInstaller 打包时容易漏掉动态库,需要手动补。

pip install pyinstaller pyinstaller --noconfirm --onefile --windowed \ --name 疾病风险预测程序 \ --add-data "hypertension.pkl;." \ --add-data "diabetes.pkl;." \ --add-data "lightgbm_model.txt;." \ app.py

这个命令里的--onefile是把所有文件塞进单个 exe,代价是启动速度慢一点;--windowed是去掉命令行黑窗口,GUI 程序必须加。--add-data在 Windows 上用分号分隔源文件和目标目录,Linux/macOS 上要用冒号,这个细节能卡住一半的人。如果你用的是新版 PyInstaller,--add-data前面不用写=,直接空格分隔即可。

4.2 打包后双击没反应:先看这三个位置

.exe打包成功不代表直接能跑。我拆这个项目时第一次双击后什么反应都没有,任务管理器里能看到进程一闪而过,典型的运行时崩溃。排查顺序固定是:先在终端里跑dist/疾病风险预测程序.exe,把报错信息打出来,然后逐个检查模型文件路径、动态库依赖、pkl 版本三个位置。

cd dist .\疾病风险预测程序.exe

如果在命令行里能看到ModuleNotFoundError: No module named 'pandas',通常是 PyInstaller 的依赖分析和你的虚拟环境不一致;如果在代码里的模型加载位置报FileNotFoundError,问题基本出在sys._MEIPASS路径处理上。PyInstaller 打包后,--add-data的文件不在 exe 旁边,而是被解压到临时目录里,必须用下面这套逻辑取路径。

import sys import os def resource_path(relative_path): base_path = getattr(sys, "_MEIPASS", os.path.abspath(".")) return os.path.join(base_path, relative_path) model = lgb.Booster(model_file=resource_path("lightgbm_model.txt")) df = pd.read_pickle(resource_path("hypertension.pkl"))

这段代码是 PyInstaller 打包场景的标准解法:sys._MEIPASS在打包后指向临时解压目录,在源码运行时不存在,所以用getattr(sys, "_MEIPASS", os.path.abspath("."))兜底,开发时读当前目录,打包后读临时目录。论文里把这个函数贴出来,再配一句“兼容开发环境和打包环境”,导师会觉得你确实跑过完整链路。

提示:如果 exe 用--onefile打包后在别人电脑上杀毒软件报毒,不要慌,多半是 PyInstaller 的加壳行为触发误报。换个思路,用--onedir模式打包成文件夹,误报率会低很多,演示时把整个文件夹拷过去即可。

4.3 界面交互里的三个真实翻车点

项目里有Dark screenshot.png和Light screenshot.png两张截图,说明它支持深色和浅色主题切换。这种纯前端逻辑的坑不多,真正的坑都在“用户输入到模型预测”这一段。

第一个坑:用户输入的字段类型和模型训练时不一致。比如训练时age是数值型,但界面拿到的输入是字符串,直接 predictions 前必须强转。第二个坑:模型训练时的特征列顺序,和界面组装的特征顺序不一致。LightGBM 的 Booster 对列顺序敏感,训练时列顺序是[age, bmi, blood_glucose, ...],界面组装时万一少传一列,预测结果会完全乱掉。第三个坑:pkl 里的模型可能是 sklearn 的LGBMClassifier,也可能是原生lgb.Booster,两种对象的 predict 接口不一样。

import pandas as pd feature_cols = ["age", "bmi", "blood_glucose", "hypertension_history", "smoke"] def assemble_input(raw_form): row = [] for col in feature_cols: val = raw_form.get(col, 0) if col in ["age", "bmi", "blood_glucose"]: val = float(val) else: val = int(val) row.append(val) return pd.DataFrame([row], columns=feature_cols) input_df = assemble_input(form_data) pred_proba = model.predict(input_df)[0]

这段代码的feature_cols是一个显式白名单,作用是强制保证预测时的特征顺序和训练时一致。float和int的转换必须写在进模型之前,不要等着模型内部去自动识别,否则传入字符串时 LightGBM 会直接抛异常或者按缺失值处理。答辩现场最容易露怯的场景就是:老师随手输入一个年龄,界面崩了。提前把类型转换放在assemble_input里,能救回来。

4.4 打包避坑清单:照着逐条排除

这份资源的 exe 是已经打包好的,但如果你改了模型想重新打包,下面三条是最真实的踩坑记录。

现象一:打包时提示ModuleNotFoundError: No module named 'lightgbm.some_inner_module'。原因:PyInstaller 对 LightGBM 的动态库扫描不完全,常见于 Windows 上安装了多个 LightGBM 版本。解决:在app.py顶部显式import lightgbm,并把完整包路径手动加进--hidden-import,必要时复制 lightgbm 目录下所有 DLL 到项目根目录。

现象二:exe 打开后点预测按钮,程序卡死但 CPU 没跑满。原因:不是模型推理慢,是 exe 里加载的 pandas 在初始化时耗时,--onefile模式还要先解压临时目录,首次启动慢很正常。解决:不要判断程序死了,等 5 到 10 秒;如果实在要提速,--onedir比--onefile启动快一半以上。

现象三:exe 在自己电脑上正常,换到教室电脑上缺 DLL。原因:--onefile没带上 VC++ 运行库。解决:打包机的 Python 尽量用官方安装包,不要用精简版;到目标机器上后如果报缺VCRUNTIME140.dll,直接装一次 VC++ 运行库,一劳永逸。这个坑跟代码无关,但答辩现场最容易因为这个问题翻车,提前在虚拟机里测一遍最保险。

5. 模型验证与进阶:除了 AUC 还要看什么

5.1 混淆矩阵能看出阈值选错了

AUC 高不代表实际可用。糖尿病和高血压预测是典型的“宁可误报也不漏报”场景,模型默认的 0.5 阈值往往不是最优选择。我训练完模型后,一定会在验证集上画混淆矩阵,找到敏感度和特异度的平衡点。

from sklearn.metrics import confusion_matrix, classification_report opt_threshold = 0.4 val_binary = (val_pred >= opt_threshold).astype(int) print(classification_report(y_val, val_binary, target_names=["negative", "positive"])) cm = confusion_matrix(y_val, val_binary) print(cm)

classification_report会一次性给出精确率、召回率、F1 值,比单独看 AUC 直观得多。这里的opt_threshold=0.4是举例,真实场景应该用验证集画出 PR 曲线或 ROC 曲线后,在曲线上选点。一个简单的做法是:遍历 0.3 到 0.7 间隔 0.05 找 F1 最高的阈值。答辩时如果被问到“为什么阈值是 0.4 不是 0.5”,回答“我对验证集做了阈值扫描,选择了在漏报率与误报率之间平衡的点”,比“我随便设的”好太多。

5.2 单样本预测的调试技巧:断点打在哪很重要

模型整体指标好看,不代表单个样本的预测结果可信。我调试这类疾病风险预测程序时有个习惯:从界面里拿一条真实输入,单独写一段脚本,打印每一层的中间结果。

sample = pd.DataFrame([ {"age": 58, "bmi": 27.5, "blood_glucose": 6.8, "hypertension_history": 1, "smoke": 1} ]) sample = sample[feature_cols] raw_prob = model.predict(sample)[0] print("raw probability:", raw_prob) if hasattr(model, "predict_proba"): proba = model.predict_proba(sample)[0] print("proba:", proba)

这段代码里hasattr(model, "predict_proba")是判断模型类型的技巧:原生lgb.Booster没有predict_proba,只有 predict,返回的是正样本概率;sklearn 的LGBMClassifier有predict_proba,返回的是[负样本概率, 正样本概率]。两者下标含义不一样,混淆之后界面显示的风险值会反着来。这个坑我见过不止一次,把类型判断写进代码,后面打包成 exe 也不会因为模型对象不同而显示错乱。

5.3 SHAP 才是把模型“讲明白”的终极大招

毕设答辩走到高阶阶段,老师大概率会问一句:“你凭什么相信这个模型?”光靠 AUC 不够,SHAP 值可以把每个特征对预测结果的贡献可视化出来。如果项目里没有现成的 SHAP 分析模块,你可以自己补,因为它不依赖模型结构,只依赖你的训练数据。

import shap explainer = shap.TreeExplainer(model) shap_values = explainer.shap_values(X_val) shap.summary_plot(shap_values, X_val, feature_names=feature_cols)

TreeExplainer专门针对树模型,LightGBM 直接用性能很好。shap.summary_plot会画一张散点图,横轴是 SHAP 值,纵轴是特征,颜色代表特征值高低。这张图放进论文里价值极高:它能直观证明“血糖越高,预测风险越高”,而不是你自己嘴上说说。SHAP 安装偶尔会跟 numpy 版本冲突,常见报错是AttributeError: 'NoneType' object has no attribute 'split',原因是 shap 版本偏老,升级到 0.44 以上基本能解决。这个坑很小,但足以让人在答辩前夜崩溃,提前装好省心。

5.4 我的一个验证习惯,供你参考

从那以后,我每次跑完一组实验都会做三件事:先把验证集预测结果导出成 CSV,附上原始标签和概率值;然后把特征重要性表和最关键的两个 SHAP 图存成图片;最后把训练参数、数据集行数、AUC、最佳迭代轮数写成一份 Markdown 存档。下次不管是谁来问“这模型到底靠谱吗”,我直接把这份档案翻出来,对着数据说话,而不是凭印象说“还行”。这个习惯也是从拆这份资源时学到的:它自带一份《基于LightGBM的糖尿病高血压预测程序.md》,把项目前因后果都写清楚了,你复现完之后完全可以模仿它,把自己这版的数据也补一份文档。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询