
简介这份源码资源面向具备Python基础、希望入门机器学习与桌面应用开发的学习者提供一套完整的心脏病风险预测图形界面方案。项目以PyQt5构建交互窗口用户可输入年龄、性别、胆固醇、血压等健康指标由后台预训练模型输出患病概率或风险等级覆盖GUI设计、数据清洗预处理、模型加载预测与结果展示等完整链路。压缩包共16个文件约22KB以6个py脚本为核心辅以xml与ui界面配置、csv训练数据、pkl与model模型文件及iml工程配置结构紧凑便于逐模块研读。资源已有144人学习下载读者可从中掌握QMainWindow与各类小部件的布局用法、Pandas数据预处理流程、逻辑回归或随机森林等算法的调用方式以及输入校验与结果解释的交互设计思路适合作为课程设计、毕业项目或医疗健康类机器学习练手参考。1. 心脏病预测 GUI 到底解决什么问题从 UCI 数据集到 PyQt5 桌面工具很多做机器学习入门的朋友都有个共同困惑模型在 Jupyter Notebook 里跑出 85% 的准确率可一旦要拿给别人用对方连 Python 都不会装。心脏病预测这个场景尤其典型——UCI 的 Cleveland 数据集只有 303 条样本、14 个字段逻辑回归和随机森林都能跑到 80% 以上的准确率但真正让一个临床方向的学生或者课程设计评审老师眼前一亮的是你把它封装成了一个双击就能运行的 PyQt5 图形界面。输入年龄、性别、胸痛类型、静息血压、胆固醇等指标点一下按钮界面直接告诉你预测结果和患病概率。这篇文章就围绕「基于 Python 实现心脏病预测图形界面 PyQt5 源码」这个方向把数据预处理、模型训练、界面设计、信号槽绑定、打包发布整条链路拆开讲清楚。适合有 Python 基础、想做一个完整可演示项目的在校生和转行入门者也适合已经会建模但没做过桌面 GUI 的算法同学补齐工程化这一环。2. 数据与模型心脏病预测的输入输出到底怎么定2.1 UCI 心脏病数据集的字段含义与清洗要点做任何预测界面之前先把数据搞清楚。UCI Heart Disease 数据集常用的 Cleveland 子集包含 14 个字段目标字段是 num血管狭窄程度 0-4二分类任务通常把 0 视为无病、1-4 视为有病。字段清单如下字段名含义类型取值范围age年龄数值29-77sex性别类别1男, 0女cp胸痛类型类别0-3trestbps静息血压数值94-200chol血清胆固醇数值126-564fbs空腹血糖120类别0/1restecg静息心电图类别0-2thalach最大心率数值71-202exang运动诱发心绞痛类别0/1oldpeakST 段压低数值0-6.2slopeST 段斜率类别0-2ca主血管数类别0-3thal地中海贫血类别1-3num目标类别0-4清洗时最常见的坑是缺失值。Cleveland 子集里 ca 和 thal 有少量问号占位直接pd.read_csv会读成 object 类型训练时报错。我一般用na_values?参数在读取阶段就处理掉然后对数值列用中位数填充、类别列用众数填充。另一个容易忽略的点是chol 和 trestbps 存在极端值但不要急着删303 条样本本来就少删掉十几条对模型影响很大用 RobustScaler 做标准化比直接删更稳妥。import pandas as pd import numpy as np from sklearn.preprocessing import RobustScaler # 读取时直接把问号识别为缺失值 columns [age,sex,cp,trestbps,chol,fbs,restecg, thalach,exang,oldpeak,slope,ca,thal,num] df pd.read_csv(processed.cleveland.data, namescolumns, na_values?) # 目标二值化0 为无病1-4 为有病 df[target] (df[num] 0).astype(int) df df.drop(num, axis1) # 数值列中位数填充类别列众数填充 num_cols [age,trestbps,chol,thalach,oldpeak] cat_cols [sex,cp,fbs,restecg,exang,slope,ca,thal] for col in num_cols: df[col] df[col].fillna(df[col].median()) for col in cat_cols: df[col] df[col].fillna(df[col].mode()[0]) # 数值列用 RobustScaler对极端值不敏感 scaler RobustScaler() df[num_cols] scaler.fit_transform(df[num_cols]) print(df.shape, df[target].value_counts().to_dict())这段代码的关键点有三个na_values?在读取阶段就把脏数据变成 NaN避免后续类型混乱目标二值化把多分类问题简化成二分类更适合 GUI 展示「有病/无病」这种直观结论RobustScaler 用中位数和四分位距做缩放比 StandardScaler 更抗极端值。参数方面fillna的中位数和众数都要在训练集上统计如果后面要做交叉验证填充逻辑必须放进 Pipeline 里否则会有数据泄漏。2.2 选逻辑回归还是随机森林GUI 场景下的模型取舍GUI 场景和纯建模场景的选型逻辑不一样。纯建模追求 AUC 和 F1GUI 场景还要考虑推理速度、模型文件大小、可解释性。逻辑回归模型文件通常几十 KB推理时间在毫秒级系数还能解释成「年龄每增加一岁患病风险变化多少」随机森林准确率通常高 2-3 个百分点但模型文件几 MB推理稍慢可解释性差一些。我的建议是GUI 里默认用逻辑回归因为 303 条样本用复杂模型容易过拟合而且逻辑回归的输出概率经过 sigmoid 后天然在 0-1 之间直接显示「患病概率 73%」很自然。如果要做对比演示可以在界面里加一个下拉框切换模型但默认选项一定是逻辑回归。from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split, cross_val_score from sklearn.pipeline import Pipeline import joblib X df.drop(target, axis1) y df[target] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy) # 逻辑回归默认模型可解释性强 lr LogisticRegression(C1.0, max_iter1000, solverlbfgs) lr.fit(X_train, y_train) print(LR 测试集准确率:, lr.score(X_test, y_test)) print(LR 5折交叉验证:, cross_val_score(lr, X, y, cv5).mean()) # 随机森林备选模型准确率略高 rf RandomForestClassifier(n_estimators100, max_depth5, random_state42) rf.fit(X_train, y_train) print(RF 测试集准确率:, rf.score(X_test, y_test)) # 保存模型和标准化器GUI 加载时要用同一套变换 joblib.dump({model: lr, scaler: scaler, num_cols: num_cols}, heart_model.pkl)参数说明C1.0是正则化强度的倒数值越小正则化越强303 条样本建议在 0.1-1.0 之间调max_iter1000是因为默认的 100 在特征标准化后有时不收敛stratifyy保证训练集和测试集的正负样本比例一致小数据集必须加。随机森林的max_depth5是防止过拟合的关键不限制深度的话训练集能到 100%测试集反而掉到 75% 左右。最后用 joblib 把模型、scaler 和数值列名一起打包保存GUI 端加载后对新输入做同样的 RobustScaler 变换这一步漏了的话预测结果会完全错乱。3. PyQt5 界面搭建从布局到信号槽的完整实现3.1 用 QGridLayout 排布 13 个输入控件心脏病预测界面有 13 个输入字段用绝对定位setGeometry在窗口缩放时会错位必须用布局管理器。QGridLayout 是最合适的选择左边一列放 QLabel 标签右边一列放输入控件数值型字段用 QDoubleSpinBox 或 QSpinBox类别型字段用 QComboBox。这样排出来整齐而且窗口拉伸时控件会自动调整。from PyQt5.QtWidgets import (QWidget, QLabel, QComboBox, QSpinBox, QDoubleSpinBox, QGridLayout, QPushButton, QTextEdit) from PyQt5.QtCore import Qt class HeartPredictWindow(QWidget): def __init__(self): super().__init__() self.setWindowTitle(心脏病预测系统) self.resize(520, 600) self.init_ui() def init_ui(self): layout QGridLayout() self.inputs {} # 数值型字段年龄、血压、胆固醇、最大心率、ST压低 num_fields [ (年龄, age, 29, 77, 1, 0), (静息血压, trestbps, 94, 200, 1, 0), (胆固醇, chol, 126, 564, 1, 0), (最大心率, thalach, 71, 202, 1, 0), (ST段压低, oldpeak, 0.0, 6.2, 0.1, 1), ] row 0 for label, key, lo, hi, step, decimals in num_fields: layout.addWidget(QLabel(label), row, 0) if decimals: box QDoubleSpinBox() box.setDecimals(1) else: box QSpinBox() box.setRange(lo, hi) box.setSingleStep(step) layout.addWidget(box, row, 1) self.inputs[key] box row 1 # 类别型字段性别、胸痛类型、空腹血糖等 cat_fields [ (性别, sex, [女, 男]), (胸痛类型, cp, [典型心绞痛, 非典型心绞痛, 非心绞痛, 无症状]), (空腹血糖120, fbs, [否, 是]), (静息心电图, restecg, [正常, ST-T异常, 左心室肥大]), (运动诱发心绞痛, exang, [否, 是]), (ST段斜率, slope, [上升, 平坦, 下降]), (主血管数, ca, [0, 1, 2, 3]), (地中海贫血, thal, [正常, 固定缺陷, 可逆缺陷]), ] for label, key, options in cat_fields: layout.addWidget(QLabel(label), row, 0) combo QComboBox() combo.addItems(options) layout.addWidget(combo, row, 1) self.inputs[key] combo row 1 self.btn_predict QPushButton(开始预测) self.result_box QTextEdit() self.result_box.setReadOnly(True) layout.addWidget(self.btn_predict, row, 0, 1, 2) layout.addWidget(self.result_box, row 1, 0, 1, 2) self.setLayout(layout)这段代码的核心思路是用字典self.inputs统一管理所有输入控件键名和数据集字段名一一对应。数值型字段用 QSpinBox整数和 QDoubleSpinBox小数类别型字段用 QComboBox选项文本是中文但提交给模型时要映射回数值编码。注意oldpeak用 QDoubleSpinBox 且setDecimals(1)因为原始数据有一位小数ca和thal虽然本质是类别但用 QComboBox 显示字符串更友好。布局最后一行用addWidget(widget, row, 0, 1, 2)让按钮和结果框跨两列视觉上更平衡。3.2 信号槽绑定与预测逻辑点击按钮后发生了什么PyQt5 的信号槽机制是界面和逻辑解耦的关键。按钮的clicked信号连接到自定义的on_predict槽函数槽函数里做三件事从控件读取值、映射成模型输入格式、调用模型预测并更新结果框。这里最容易翻车的地方是类别字段的文本到数值的映射——如果界面上选「男」但模型训练时 1 代表男映射反了预测结果就完全不可信。from PyQt5.QtWidgets import QApplication, QMessageBox import sys import numpy as np import joblib class HeartPredictWindow(QWidget): # ... 上面的 init_ui 代码 ... def __init__(self): super().__init__() self.bundle joblib.load(heart_model.pkl) self.model self.bundle[model] self.scaler self.bundle[scaler] self.num_cols self.bundle[num_cols] self.setWindowTitle(心脏病预测系统) self.resize(520, 600) self.init_ui() self.btn_predict.clicked.connect(self.on_predict) def collect_input(self): # 类别字段文本到数值的映射表必须和训练时编码一致 cat_map { sex: {女: 0, 男: 1}, cp: {典型心绞痛: 0, 非典型心绞痛: 1, 非心绞痛: 2, 无症状: 3}, fbs: {否: 0, 是: 1}, restecg: {正常: 0, ST-T异常: 1, 左心室肥大: 2}, exang: {否: 0, 是: 1}, slope: {上升: 0, 平坦: 1, 下降: 2}, ca: {0: 0, 1: 1, 2: 2, 3: 3}, thal: {正常: 1, 固定缺陷: 2, 可逆缺陷: 3}, } row {} for key, widget in self.inputs.items(): if key in cat_map: row[key] cat_map[key][widget.currentText()] else: row[key] widget.value() return row def on_predict(self): try: data self.collect_input() feature_order [age,sex,cp,trestbps,chol,fbs,restecg, thalach,exang,oldpeak,slope,ca,thal] X_new np.array([[data[k] for k in feature_order]], dtypefloat) # 对数值列做和训练时一致的缩放 num_idx [feature_order.index(c) for c in self.num_cols] X_new[:, num_idx] self.scaler.transform(X_new[:, num_idx]) prob self.model.predict_proba(X_new)[0][1] label 患病 if prob 0.5 else 无病 self.result_box.setText( f预测结果{label}\n患病概率{prob:.2%}\n f建议{请尽快就医做进一步检查 if prob 0.5 else 当前指标未见明显异常}) except Exception as e: QMessageBox.critical(self, 预测失败, str(e)) if __name__ __main__: app QApplication(sys.argv) win HeartPredictWindow() win.show() sys.exit(app.exec_())逻辑说明collect_input把界面控件的值收集成一个字典类别字段通过cat_map映射回训练时的数值编码这一步是血泪经验——很多人界面做得漂亮但映射表写错预测结果全是反的。on_predict里先按训练时的特征顺序拼成数组再对数值列做同样的 scaler 变换最后调predict_proba拿患病概率。阈值 0.5 是默认值实际医疗场景可以调到 0.4 提高召回率宁可误报也别漏报。异常处理用 QMessageBox 弹窗避免用户输入异常时程序直接崩溃。4. 避坑与排查PyQt5 心脏病预测项目最常见的 5 个翻车点4.1 模型加载报错pickle 版本不兼容现象在自己电脑上训练保存的 pkl 文件换一台机器加载时报ModuleNotFoundError或AttributeError: Cant get attribute。原因通常是 sklearn 版本不一致或者保存时用了自定义类而加载环境里没有这个类。解决方法是训练和部署用同一个虚拟环境保存时用 joblib 而不是 pickle并且把 sklearn 版本号写进 requirements.txt。如果必须跨版本重新训练比强行加载更省时间。4.2 界面卡死预测逻辑写在主线程里现象点击预测按钮后界面短暂无响应如果模型复杂或者做了交叉验证窗口直接显示「未响应」。原因是 PyQt5 的所有界面更新都在主线程耗时操作会阻塞事件循环。解决方法是把预测逻辑放进 QThread 子线程通过信号把结果传回主线程更新界面。对于逻辑回归这种毫秒级推理其实感知不到但如果加了模型对比或者批量预测功能必须上 QThread。4.3 输入值域不匹配训练用标准化数据界面传原始值现象界面输入年龄 55、血压 130预测结果永远是「无病」或者概率接近 0.5 没有区分度。原因是训练时对数值列做了 RobustScaler界面直接把原始值传给模型量纲差了十几倍。解决方法是在 GUI 端保存训练时的 scaler预测前对数值列做同样的 transform。注意 scaler 的transform方法接收的是二维数组单个样本要 reshape 成 (1, n_features)。4.4 中文显示乱码matplotlib 或 QLabel 字体问题现象结果框里中文显示成方块或者用 matplotlib 画特征重要性图时标题乱码。原因是 PyQt5 默认字体不支持中文matplotlib 默认字体也不支持。解决方法是在代码开头设置plt.rcParams[font.sans-serif] [SimHei]QLabel 和 QTextEdit 通过setFont(QFont(Microsoft YaHei, 10))指定中文字体。Linux 环境下没有 SimHei 的话用fc-list :langzh查一下系统里有什么中文字体。4.5 打包后模型文件找不到相对路径的坑现象用 PyInstaller 打包成 exe 后双击运行报「FileNotFoundError: heart_model.pkl」。原因是打包后工作目录变了代码里的相对路径heart_model.pkl找不到文件。解决方法是把模型文件一起打包进去用sys._MEIPASS获取临时解压目录import sys, os def resource_path(relative): if hasattr(sys, _MEIPASS): return os.path.join(sys._MEIPASS, relative) return os.path.join(os.path.abspath(.), relative) self.bundle joblib.load(resource_path(heart_model.pkl))PyInstaller 命令用pyinstaller -F -w --add-data heart_model.pkl;. main.py-w去掉控制台窗口--add-data把模型文件打进去。Windows 下分隔符是分号Linux 和 macOS 下是冒号这一点不注意打包后照样翻车。5. 进阶技巧把预测概率变成可解释的界面反馈5.1 用逻辑回归系数做单样本特征贡献可视化逻辑回归最大的优势是可解释性。模型训练完后每个特征有一个系数系数乘以标准化后的特征值就是该特征对 log-odds 的贡献。在 GUI 里加一个「查看特征贡献」按钮用横向条形图展示当前输入下哪些指标推高了患病风险比单纯显示一个概率更有说服力。import matplotlib matplotlib.use(Qt5Agg) from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg from matplotlib.figure import Figure import numpy as np def plot_contribution(self, X_scaled, feature_names): coef self.model.coef_[0] contrib coef * X_scaled[0] order np.argsort(np.abs(contrib))[::-1][:8] fig Figure(figsize(5, 3)) ax fig.add_subplot(111) colors [#d9534f if contrib[i] 0 else #5bc0de for i in order] ax.barh([feature_names[i] for i in order], [contrib[i] for i in order], colorcolors) ax.set_xlabel(对患病风险的贡献) ax.axvline(0, colorgray, linewidth0.8) fig.tight_layout() canvas FigureCanvasQTAgg(fig) self.layout().addWidget(canvas, 15, 0, 1, 2)红色条表示该指标推高患病风险蓝色条表示降低风险。注意matplotlib.use(Qt5Agg)必须在导入 pyplot 之前设置否则嵌入到 PyQt5 窗口时会报后端冲突。特征名用中文的话记得设置中文字体否则全是方块。5.2 用 QThread 做批量预测和进度反馈如果要做「导入 CSV 批量预测」功能几百条数据逐条推理会卡界面。正确做法是把批量预测放进 QThread通过 pyqtSignal 把进度百分比发回主线程更新进度条。核心代码结构是定义一个继承 QThread 的 Worker 类run 方法里循环预测并 emit 进度信号主线程连接信号到进度条和结果表格。这样界面全程可响应用户能看到进度而不是以为程序死了。5.3 模型更新与界面解耦最后一个习惯模型文件和界面代码不要耦合。我一般把模型训练脚本单独放一个 train.pyGUI 只负责加载 pkl 和推理。这样换模型、调参数、重新训练都不用动界面代码重新跑一遍 train.py 覆盖 pkl 文件就行。界面里可以加一个「重新加载模型」按钮调用joblib.load刷新 self.model方便演示不同模型的效果。这个习惯让我在课程设计答辩时省了很多事——评委问「换个模型会怎样」我当场重新训练再点一下按钮就切换了不用改一行界面代码。希望帮到你。本文还有配套的精品资源点击获取