☰
Python心脏病预测源码实战:数据处理到Tkinter界面部署
2026/10/3 4:47:16 网站建设 项目流程

简介:基于Python的心脏病预测完整项目,自带图形用户界面,面向计算机相关专业学生的课程设计、毕业设计,也适合Python初学者进阶练习。项目从数据读取、特征清洗、模型训练到预测接口、可视化展示,形成完整链路,代码稳定可运行,可在现有基础上修改算法或扩展功能,便于答辩演示与初期立项。包内共17个文件,主体为6个Python脚本,职责涵盖接口测试、模型调用与主程序入口;另有2个CSV样本数据、1个pkl权重文件、1个模型文件、UI界面及XML配置等,数据与模型均已配套齐全。压缩包仅23KB,模块划分清楚,轻量而完整,阅读和调试都很方便,无需复杂配置即可运行演示。目前已有174人学习下载,对于需要一套可直接运行的心脏病预测图形界面项目、或希望参考课设毕设项目结构的同学,具有很高的参考价值。

1. 拿到这份 Python 心脏病预测源码,先别急着双击

很多人在网上下载到「基于Python心脏病预测,带图形界面源码.zip」这类压缩包时,第一反应是解压后双击主文件,祈祷界面弹出来就能用。但作为一个跑过十几套课程设计和开源项目的工程师,我可以直接告诉你:这类源码包的通用结构基本是三件事——数据处理、模型训练、图形界面封装。真正让新手翻车的往往不是模型训练,而是数据字段对不上、界面里输入的参数顺序和训练时不一致、以及本地 Python 环境缺库。这份笔记我会按「先理顺数据、再训练模型、最后套上图形界面」的顺序拆解,每步给可直接运行的代码,并把我在实际复现过程里踩过的坑一并讲清楚。无论你是要做课程设计还是给科室做一个内部风险评估小工具,这个方向都值得投入,因为公开数据集成熟、模型解释性好、界面工作量可控。

2. 先摸清数据:这 13 个特征决定了模型上限

拿到源码包以后,先别急着打开 Python 文件看模型。图形界面只是外壳,数据决定上限。心脏病预测这类项目的训练数据通常来自 UCI Heart Disease 数据集或 Kaggle 上的 heart.csv。两者字段几乎一致,都围绕年龄、性别、血压、胆固醇、心率等体检指标展开,目标列是是否患病。你要做的事只有一件:弄清楚每个字段的含义和类型,然后决定哪些做数值缩放、哪些做独热编码。

2.1 公开数据集的两种常见来源:UCI Heart Disease 与 Kaggle heart.csv

UCI 的 Heart Disease 数据集原始版本有 4 个来源,样本量从 200 到 300 多不等,最常用的是 Cleveland 子集,共 303 条样本。Kaggle 上流传最广的 heart.csv 正是这个子集的清洗版本,14 列,结构稳定,被大量课程设计和教学项目采用。两者核心字段相同,差别只在于部分分类字段是否已经做过映射。第一次做这个项目,我建议直接用 Kaggle 版本,少踩数据格式的坑。

字段名含义类型处理方式
age年龄数值标准化
sex性别二分类保留 0/1
cp胸痛类型多分类独热编码
trestbps静息血压数值标准化
chol血清胆固醇数值标准化
fbs空腹血糖>120二分类保留 0/1
restecg静息心电图结果多分类独热编码
thalach最大心率数值标准化
exang运动诱发心绞痛二分类保留 0/1
oldpeak运动诱发ST段压低数值标准化
slopeST段斜率多分类独热编码
ca主要血管数量多分类保留 0-3 整数
thal地中海贫血类型多分类独热编码
target是否患病二分类预测目标

第一次接触这份数据的人最常犯的错是把 cp、thal 这类分类字段当数值直接喂给模型。直觉上它们确实有数字编号,但这个编号没有大小含义,cp=4 不代表比 cp=2 严重两倍。正确的做法是独热编码。而 ca 字段虽然也是编号,但 0 到 3 的递进确有血管堵塞数量的实际意义,保留原始数值更合理。这种细微差别直接影响模型收敛速度,也影响后续界面对输入的容错。

2.2 清洗入口:缺失值、分类字段与目标列的三种处理

拿到数据后第一步永远是df.info()和df.isnull().sum(),先看有没有缺失值。heart.csv 里 typical 的做法是 ca 和 thal 两列各有几条缺失。处理方式很简单,直接删除这几行,303 条样本只少个位数,对模型影响可忽略。不要用均值填充这种花哨操作,因为血管数量和地中海贫血类型不是连续变量,填均值会产生一个现实中不存在的值。

分类字段处理我习惯放在切分数据之前完成,用 pandas 的get_dummies一次性完成。数值字段的标准化必须放在切分之后,而且只能对训练集fit_transform,测试集只做transform。这个顺序一旦颠倒,就等于在训练时偷看了测试集的均值方差,属于典型的数据泄漏,会让模型评估结果虚高。

import pandas as pd df = pd.read_csv("heart.csv") print(df.info()) print(df.isnull().sum()) # 缺失值直接删除,ca 和 thal 的缺失行占样本总量比例很小 df = df.dropna().reset_index(drop=True) # 分类字段独热编码;数值类字段按原始值保留,后续再做缩放 df = pd.get_dummies(df, columns=["cp", "restecg", "slope", "thal"], drop_first=False) print(df.shape)

逻辑说明:get_dummies会把 cp 拆成 cp_0、cp_1、cp_2、cp_3 多列,每列取值 0 或 1,表示该样本是否属于这个类别。drop_first=False保留全部类别列,这样图形界面上每个选项都能对应到独立特征列,字段对齐更直观。代码里的df.shape打印用于确认列数变化,原始 14 列经过独热编码后会膨胀到 20 列左右,这个数字在后续写图形界面时要牢牢记住,因为界面的每个输入控件都要对应一列特征。

2.3 特征变换为什么不能“一把梭”

一份源码里最不显眼却最容易出问题的就是特征变换顺序。我见过不少项目把标准化放在独热编码之前,结果对 0/1 列也做了均值方差缩放,导致原本二分类的特征变成负数和浮点数。这就是典型的“一把梭”式特征处理副作用。正确的流程是:先做独热编码,再切分训练集和测试集,最后只对连续型的 age、trestbps、chol、thalach、oldpeak 五列做标准化。

标准化器的保存同样重要。StandardScaler要用joblib.dump单独存一份文件,因为图形界面里用户输入的新样本必须用同样的均值方差做缩放,不能每次启动重新算。这类细节决定了你的源码从「能跑」升级到「能被别人独立复现」,这也是 zip 包存在的意义——拿到源码的人不应该需要读文档才能猜出变换规则。

3. 训练模型:从逻辑回归基线到随机森林的取舍

数据准备工作就绪后进入模型训练。这个项目的核心问题是一个二分类任务:根据体检指标预测心脏病风险。我一般不会直接上复杂模型。先跑一个逻辑回归作为基线,用它的结果做参考,再尝试随机森林,比较两者的准确率和可解释性,最后根据实际场景决定哪个模型进图形界面。

3.1 为什么先选逻辑回归做第一版

心脏病预测场景里,逻辑回归的地位一直很稳。它训练快、参数少、输出的是概率而不是硬分类,医生或用户看到「患病概率 72%」比看到「高风险」更有参考价值。逻辑回归的系数还能直接解释「年龄每增加一岁,风险提升多少」,这对医疗场景的交付很重要。随机森林则适合处理特征间存在非线性关系的场景,但在只有 300 条样本的数据集上,树模型容易过拟合,即使限制了深度,泛化能力也不一定比逻辑回归强。

我用同一份数据跑过这两个模型,逻辑回归的准确率在 82% 到 86%,随机森林在 80% 到 88% 之间浮动,后者方差明显更大。这说明样本量小的时候树模型的优势发挥不出来。所以第一版界面我强烈建议内置逻辑回归,跑通全流程后再把随机森林作为可选模型加进去。

3.2 跑通训练脚本:完整代码与参数说明

import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score, roc_auc_score, classification_report import joblib df = pd.read_csv("heart_encoded.csv") X = df.drop("target", axis=1) y = df["target"] # 切分数据,stratify 保证训练集和测试集的正负样本比例一致 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) # 数值列标准化:训练集 fit_transform,测试集只 transform num_cols = ["age", "trestbps", "chol", "thalach", "oldpeak"] scaler = StandardScaler() X_train[num_cols] = scaler.fit_transform(X_train[num_cols]) X_test[num_cols] = scaler.transform(X_test[num_cols]) # 逻辑回归基线,max_iter 提到 1000 保证收敛 model = LogisticRegression(max_iter=1000, C=1.0, random_state=42) model.fit(X_train, y_train) pred = model.predict(X_test) prob = model.predict_proba(X_test)[:, 1] print("accuracy:", round(accuracy_score(y_test, pred), 4)) print("auc:", round(roc_auc_score(y_test, prob), 4)) print(classification_report(y_test, pred)) # 保存模型和标准化器,图形界面会分别加载这两个文件 joblib.dump(model, "heart_model.joblib") joblib.dump(scaler, "heart_scaler.joblib")

逻辑说明:stratify=y是容易被忽略的一个参数,它保证切分后训练集和测试集里患病与不患病的比例和原始数据一致。如果不加,小数据集上纯随机切分很可能把某一类样本大部分分到测试集,导致训练出来的模型偏向多数类。max_iter=1000是因为 sklearn 逻辑回归默认迭代次数是 100,当前特征经过独热编码后有近 20 列,默认次数在某些数据分布下会不收敛,调大以后训练时间依然在毫秒级,没有副作用。

标准化器在这里只对五列数值特征操作,界面端用户输入原始数值后必须先调用同一个 scaler 做转换,再喂给模型。模型和标准化器分别存储,这是图形界面前后处理链路的关键一环。C=1.0是正则化强度的默认值,越小正则越强,对于这份小样本数据,保持默认即可,调参收益不大。

3.3 网格搜索调参:两个参数就够看趋势

如果要在逻辑回归上做一点调参,我建议只关注C和solver。网格搜索范围不必太大:

from sklearn.model_selection import GridSearchCV param_grid = { "C": [0.1, 0.5, 1.0, 2.0, 5.0], "solver": ["liblinear", "lbfgs"] } grid = GridSearchCV(LogisticRegression(max_iter=1000), param_grid, cv=5, scoring="roc_auc") grid.fit(X_train, y_train) print(grid.best_params_)

逻辑说明:cv=5表示五折交叉验证,每折用 80% 数据训练、20% 验证,五次结果取平均。这是小数据集上最可靠的调参方式,比只看单次切分结果有说服力。scoring="roc_auc"比 accuracy 更能反映排序能力,因为心脏病数据集中正负样本比例接近但仍有偏差,AUC 对阈值不敏感,适合评估风险概率的输出质量。调参结果如果是C=2.0、solver=lbfgs,差距也就在 1% 左右,不必追求极致,基线模型已经能进界面了。

4. 图形界面落地:Tkinter 表单从布局到预测联动

图形界面是这份源码最醒目的部分,也是大多人下载的原因。Python 图形界面方案不少,但在这个项目中,Tkinter 是性价比最高的选择。

4.1 选 Tkinter 还是 PyQt:标准库带来的部署收益

PyQt 功能更强、控件更现代,但它的授权协议和打包体积对课程设计或个人工具不友好。Tkinter 是 Python 标准库的一部分,安装 Python 后就自带,不需要额外pip install,打包成 exe 时体积也更小。对 20 个左右的输入字段,Tkinter 的grid布局完全够用。做图形界面最大的成本不在控件库选择,而在于输入和模型特征的对齐,这块无论用什么框架都躲不掉。Tkinter 的 ttk 控件在 Windows 下原生外观还不错,汉字显示也没有额外障碍。

4.2 最小可用界面的完整代码

import tkinter as tk from tkinter import ttk, messagebox import joblib import numpy as np import pandas as pd # 模型和标准化器在启动时一次性加载 model = joblib.load("heart_model.joblib") scaler = joblib.load("heart_scaler.joblib") # 特征列名列表,顺序必须与训练时 X_train 的列顺序完全一致 columns = model.feature_names_in_.tolist() # 数值类输入框,界面收集后再做标准化 num_cols = ["age", "trestbps", "chol", "thalach", "oldpeak"]
root = tk.Tk() root.title("心脏病风险预测工具") root.geometry("520x680") entries = {} row_idx = 0 # 第一区块:数值输入 for col in num_cols: tk.Label(root, text=col).grid(row=row_idx, column=0, sticky="w", padx=10, pady=4) ent = tk.Entry(root, width=20) ent.grid(row=row_idx, column=1, pady=4) entries[col] = ent row_idx += 1 # 第二区块:二分类与多分类的下拉选择 categorical_options = { "sex": [("0", 0), ("1", 1)], "cp": [("0 无症状", 0), ("1 典型心绞痛", 1), ("2 非典型", 2), ("3 其他", 3)], "restecg": [("0", 0), ("1", 1), ("2", 2)], "slope": [("0 上升", 0), ("1 平坦", 1), ("2 下降", 2)], "thal": [("1 正常", 1), ("2 固定缺陷", 2), ("3 可逆缺陷", 3)], "exang": [("0 否", 0), ("1 是", 1)], "fbs": [("0 否", 0), ("1 是", 1)] } var_map = {} for col, options in categorical_options.items(): tk.Label(root, text=col).grid(row=row_idx, column=0, sticky="w", padx=10, pady=4) var = tk.StringVar(value=str(options[0][1])) ttk.Combobox(root, textvariable=var, values=[o[1] for o in options], width=18).grid(row=row_idx, column=1, pady=4) var_map[col] = var row_idx += 1
result_var = tk.StringVar(value="等待输入") def on_predict(): try: data = {} for col in num_cols: data[col] = float(entries[col].get()) for col, var in var_map.items(): data[col] = int(var.get()) data["ca"] = int(entries["ca"].get() if "ca" in entries else 0) except ValueError: messagebox.showwarning("输入错误", "请检查所有字段是否填写完整且为数字") return sample = pd.DataFrame([data]) sample = pd.get_dummies(sample, columns=["cp", "restecg", "slope", "thal"], drop_first=False) sample = sample.reindex(columns=columns, fill_value=0) sample[num_cols] = scaler.transform(sample[num_cols]) prob = model.predict_proba(sample)[0][1] result_var.set(f"患病概率:{prob:.1%}") tk.Button(root, text="开始预测", command=on_predict).grid(row=row_idx, column=0, columnspan=2, pady=12) tk.Label(root, textvariable=result_var, font=("微软雅黑", 14)).grid(row=row_idx + 1, column=0, columnspan=2, pady=6) root.mainloop()

逻辑说明:界面启动时把模型和标准化器读入内存,避免每次点击预测时重复读磁盘。model.feature_names_in_是训练后模型记住的特征列名,界面端拿到它做样本的reindex,就能保证输入列顺序和训练时完全一致,这是整个界面代码里最关键的容错设计。get_dummies在界面端重复一遍训练时的编码逻辑,得到宽表后按模型列名对齐,缺失的列自动补 0。

参数说明:数值字段用的tk.Entry需要用户手动输入,二分类和多分类字段全部用下拉框,从根源上杜绝格式错误。ca字段比较特殊,它是 0 到 3 的整数,我一般把它也设为下拉框而不是输入框,因为用户可能输入 5 这种训练时没出现过的值。概率显示用百分比格式,比显示 0.72 这种小数直观得多。

4.3 预测按钮背后:收集、验证、推理三步

按钮触发的on_predict函数内部做了三件事。第一,从所有输入控件收集数据,并做类型转换,这一步用try-except捕获非数字输入,弹窗提示而不是直接崩溃。第二,把字典转成 DataFrame,做和训练时相同的独热编码,然后reindex对齐列。第三,先用 scaler 变换数值列,再调用模型的predict_proba取正类概率。这三步顺序不能换,尤其是reindex必须在predict_proba之前完成,否则特征顺序错位会导致输出一个没有意义的概率。这一步也是源码包中最容易在二次开发时被删掉的代码,删掉后模型准确率断崖式下跌,而且极难排查。

5. 避坑:源码跑通后最容易翻车的六个地方

图形界面写完了,很多人以为大功告成,结果一跑就出问题。这些坑我自己都踩过,有些甚至花了好几天才定位到原因。

5.1 特征顺序错位是最隐蔽的“黑匣子”玄学

现象:界面运行不报错,但预测结果完全离谱,比如所有输入都给出 99% 患病概率,或者输出值和训练时预测结果完全对不上。

原因:训练时get_dummies生成的特征列顺序和界面端生成的不一致。训练数据里有全部类别,界面输入的样本可能缺少某个类别,列数对不上。这是这个项目里最典型的黑匣子问题——模型没有报错,但逻辑已经全错了。

解决:用model.feature_names_in_拿到训练时的列名,界面端做完独热编码后reindex(columns=columns, fill_value=0)。这一步强制列对齐,缺失补 0,多出来的列自动丢弃。从根上消除了人对字段顺序的依赖。

5.2 joblib 加载失败:换了机器就报错

现象:源码在自己电脑上运行正常,压缩包发给同学后,对方运行界面时报错,提示找不到模块或无法反序列化。

原因:joblib 和 pickle 序列化模型时记录了 Python 环境和 sklearn 版本信息。对方电脑 sklearn 版本不一致,轻则警告,重则加载失败。这在课程设计群里是高频事故。

解决:zip 包里同时提供 requirements.txt,固定关键库版本。同时把训练脚本也放进去,让对方实在加载不了就重新跑一遍训练脚本生成模型文件。这是最短路径的后悔药。

5.3 Windows 下 Tkinter 中文乱码

现象:界面按钮和标签上的中文显示为方块或乱码。

原因:Tkinter 默认编码在部分 Windows 区域设置下无法正确渲染中文字体,尤其当系统语言不是简体中文时更明显。

解决:Tk 的默认字体指定为微软雅黑,同时代码文件头添加# -*- coding: utf-8 -*-。如果你在开发环境里一切正常,打包成 exe 后乱码,那多半是打包时没有把字体资源带上。最简单的方法是控件文本尽量简短,必要时用英文标签。

5.4 点击“预测”界面卡死无响应

现象:点击按钮后窗口整个无响应,转圈几秒后才恢复,如果模型更复杂甚至直接退出。

原因:预测逻辑在主线程中同步执行,期间窗口事件循环被阻塞。对于这个项目的逻辑回归模型,推理只要毫秒级,一般不会卡死。但如果替换成深度学习模型或在大数据量上做预测,卡顿立刻出现。

解决:保持模型小而快是治本。如果确实要接复杂模型,用threading.Thread把预测放到子线程,主线程用after轮询结果。这里不推荐在 Tkinter 里直接改多线程,线程安全和控件刷新关系复杂,简单项目不值得引火烧身。

5.5 PyInstaller 打包后找不到模型文件

现象:源码运行时一切正常,打包成单个 exe 文件后双击,界面能打开,一点预测就报错说找不到 heart_model.joblib。

原因:PyInstaller 的 onefile 模式会把资源文件解压到临时目录,程序运行时当前路径不在模型文件所在目录。joblib.load("heart_model.joblib")使用的是相对路径,在临时目录里自然找不到。

解决:打包时用--add-data把模型文件打进包,运行时代码里判断是否处于打包状态,通过sys._MEIPASS获取临时解压路径,再把模型路径拼接为绝对路径。这一步不做,你的图形界面就只能在开发环境里跑,换个电脑就露馅。

5.6 准确率 90% 的陷阱:没有交叉验证的评估

现象:训练脚本打印出 90% 以上的准确率,但这个模型上线后对新数据的效果远不如预期。

原因:单次切分的测试集只有几十条样本,随机性太大。如果训练脚本里没有交叉验证,这 90% 很可能只是运气好,分到了一个容易预测的测试集。另一个常见原因是特征变换在切分前执行,导致数据泄漏。

解决:至少用cross_val_score跑五折交叉验证,取平均准确率和标准差。标准差超过 5% 就说明模型不稳定,换随机森林或者重新清洗特征。评估这一关不过,后续界面做得再好看也没有意义。

6. 进阶:用 SHAP 让单次预测结果可解释

模型跑通、界面能出概率以后,还差最后一块拼图:用户问「为什么给我预测 75% 概率」时,界面得能回答。SHAP 是目前解释树模型和线性模型特征贡献最成熟的手段,对逻辑回归同样适用。它能把一次预测拆解成每个特征贡献的正负和大小,比如「年龄偏大使风险提升 12%」「静息血压正常使风险下降 8%」。把这个能力加进图形界面,项目的完成度和说服力会上一个档次。

import shap explainer = shap.LinearExplainer(model, X_train) shap_values = explainer.shap_values(sample)

这段代码里sample是界面里用户输入的那一行标准化后的数据。shap_values返回每个特征的贡献值,取绝对值排序后展示前五个,用 Tkinter 的Text控件列出行名、贡献值和方向即可。需要注意LinearExplainer要求特征列顺序和数据完全一致,feature_names_in_在这里再次派上用场。SHAP 对逻辑回归的计算几乎是瞬时的,加到按钮事件里不会有卡顿感。

我做这类带界面源码项目的一个习惯是:永远在result_var.set()旁边同时更新 SHAP 的文本区域。用户看到的不只是概率数字,还有「哪些因素把这个数字推高了」。这在医疗场景里非常重要——医生不会只凭一个概率做判断,他要的是解释链条。至于 SHAP 值展示的排序逻辑,按绝对贡献度从大到小排,只保留贡献超过 5% 的特征,避免信息过载。

最后提一句我的血泪经验:完成这个项目后,一定把训练脚本和界面脚本解耦。训练脚本负责出 joblib 文件,界面脚本只负责加载和推理。这样换模型、调阈值、加新特征都不会牵一发而动全身。把这个习惯带给你的同学,能让他们少熬两个通宵。希望帮到你。

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

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

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

立即咨询