
最近在复盘之前做的那个心血管疾病预测项目用的就是Kaggle上很经典的心脏病数据集全程在Matlab里跑完做了特征分析、二元分类建模、评估和报告。今天干脆把整条链路完整写出来大家可以直接照着做。如果你正在学机器学习分类、医学数据挖掘或者想用Matlab做一套完整的建模流程那这篇文章很适合你。不需要特别高深的数学基础只要能把数据读进来按照流程走一遍就能跑出自己的预测模型。我也把踩过的坑和卡壳的地方都记下来了省得你们再走弯路。1. 项目背景与整体设计思路1.1 为什么要拿心脏病数据做二元分类心血管疾病不是小事它一直是导致死亡的主要因素之一而且早期往往没有明显症状。如果能在常规体检指标里提前发现风险就能在真正发病前进行干预。医学场景里不能随便做有创检查所以用机器学习做“无创预测”成了非常热门的方向。Kaggle上的心脏病数据集Heart Disease Dataset来自美国克利夫兰诊所的研究数据是一个标准的二分类问题。数据里有年龄、性别、胸痛类型、静息血压、胆固醇、最大心率、ST段压低等14个字段目标变量是“是否患有心脏病”。对学习者来说它比那些动辄几个GB的大数据集友好得多拿来练手刚刚好。我做这个项目时主要想解决三件事第一搞清楚哪些医学特征和心血管疾病相关性最强第二用Matlab训练几个经典分类模型对比它们的预测效果第三输出一份能直接给别人看的分析报告而不是只有一段代码。项目本身不复杂但“数据清洗—特征工程—模型训练—结果解读—报告输出”这个链路很完整非常适合作为入门医学机器学习项目的样板。1.2 为什么选择Matlab而不是Python很多人一聊到机器学习就想到Python但Matlab在医学和工程领域的使用者其实非常多。选择Matlab做这个项目有几个实际原因一是矩阵操作和数据处理非常顺手读CSV、转表格、算相关性这些操作比Python刚上手时少敲很多代码。二是Matlab自带了大量分类算法传统机器学习方法覆盖得很全甚至不需要刻意调包。三是Matlab的分类器训练再配合App和报表生成能比较轻松地把结果导成Word或PDF报告这对医药类项目的展示需求很友好。当然Matlab也有短板比如深度学习生态没有Python丰富某些最新论文里的模型不好复现。但就心脏病的传统机器学习分类而言Matlab完全够用而且代码可读性很高即使你以后转Python思路也是通用的。1.3 项目整体流程设计整个项目我分成了七个环节下载和导入数据从Kaggle或GitHub拿到heart.csv用readtable导入Matlab。数据清洗处理缺失值、异常值把不可能存在的生理指标找出来。特征工程把分类变量转成categorical数值变量做标准化画相关性图。划分样本用cvpartition做分层抽样留20%作为测试集。模型训练逻辑回归、决策树、随机森林、SVM各跑一遍并做简单的超参数调整。模型评估计算准确率、混淆矩阵、ROC、AUC并绘制曲线。结果解读与报告分析特征重要性整理成图文报告。这个流程看着简单但每一步都有坑尤其是数据清洗和特征处理处理不好会直接影响模型可信度。下面从数据集本身开始逐步展开。2. 数据集与特征工程实战2.1 心脏病数据集结构详解Kaggle上这份heart.csv一共有303行样本14列数据。每一行对应一个患者最后一列target是标签1表示患病0表示健康。特征里既有连续变量也有分类变量具体字段和含义如下表所示字段名含义类型age年龄连续sex性别1男0女分类cp胸痛类型1典型心绞痛2非典型心绞痛3非心绞痛4无症状分类trestbps静息血压mmHg连续chol血清胆固醇mg/dl连续fbs空腹血糖是否大于120mg/dl1是0否分类restecg静息心电图结果0正常1ST-T波异常2左心室肥大分类thalach最大心率值连续exang运动诱发的心绞痛1是0否分类oldpeakST段压低值连续slope运动高峰ST段斜率1上升2平3下降分类ca荧光透视检查看到的血管数量0-3分类thal地贫类型1正常2固定缺陷3可逆缺陷分类target是否患心脏病1是0否标签加载数据和查看基本信息很简单heartData readtable(heart.csv); disp(size(heartData)); disp(head(heartData, 5));读进来后首先要看标签分布。这个数据集里患病和健康的比例大概是54%对46%属于比较平衡的二分类不会因为类别严重失衡导致评估失真。如果比例差到2比1以上就要考虑上采样或下采样了。2.2 数据清洗缺失值和异常值处理医学数据最怕的不是缺失而是“看似正常但不合理”的异常值。这份heart.csv整体非常干净没有显式缺失值但要注意有些字段的历史版本会包含缺失标记比如ca和thal列可能出现NaN。先用这个命令查一下全表缺失情况sum(ismissing(heartData))如果输出全是0就跳过缺失值处理。如果哪个字段有缺失建议优先删除该行因为样本量本身不大删几条对结果影响有限。不要用均值去填充ca和thal这种离散变量填出来根本不像是真的医学记录反而会把模型带偏。更隐蔽的问题是数值本身不合理。胆固醇chol的正常范围一般在100到300之间如果你发现某些记录chol为0那大概率是缺失或录入错误直接删掉。age、trestbps、thalach这些字段也可以用描述性统计快速扫描summary(heartData);我会把每一列的取值范围打出来看到明显离谱的值就进一步检查对应的原始样本。这一步虽然占不了多少时间但能避免后面训练出一个“看起来准确率很高、实际是学到脏数据”的假模型。2.3 分类变量编码与数值变量标准化读CSV时Matlab默认把所有数字都当double处理这一点要特别小心。像sex、cp、slope、ca、thal这些字段本质上是分类变量虽然用数字表示但直接把1、2、3当数值喂给逻辑回归会让模型错误地认为类别之间存在“大小关系”。比如cp4被模型理解成比cp1大4倍这完全没有医学道理。处理办法是转成categorical类型categoricalVars {sex,cp,fbs,restecg,exang,slope,ca,thal}; for i 1:length(categoricalVars) varName categoricalVars{i}; heartData.(varName) categorical(heartData.(varName)); end对于连续变量逻辑回归和SVM这类模型对特征尺度非常敏感需要做标准化。比如age和chol数值范围差几十倍如果不标准化模型会把较大的数值当作高权重特征误导训练。标准化我用的z-score方法numericVars {age,trestbps,chol,thalach,oldpeak}; for i 1:length(numericVars) varName numericVars{i}; heartData.(varName) (heartData.(varName) - mean(heartData.(varName), omitnan)) / std(heartData.(varName), omitnan); end注意这里我用omitnan忽略了可能的缺失值虽然那份干净数据用不上但万一换上其他数据集也能稳妥跑过。2.4 特征相关性分析快速锁喉关键因素在做模型之前先看特征和目标的相关性能帮助我们判断哪些变量值得保留。Matlab里可以用corrplot也可以自己画热力图。我用的是corrcoef配合heatmapnumericMatrix heartData{:, numericVars}; corrMatrix corrcoef([numericMatrix, heartData.target]); figure; heatmap(corrMatrix, Colormap, parula);实际跑下来会发现thalach最大心率和oldpeakST段压低与标签的相关性相对较高而chol和age的相关性很低。这说明在预测心脏病时心率指标和运动负荷相关的特征比单纯的年龄、胆固醇更有区分度。这在医学语义上也说得通运动状态下心脏的反应能更真实地反映心血管状态。相关性分析还有一个用识别特征之间的多重共线性。比如oldpeak和slope常被同事拿去建模但如果它们两个相关性太高同一个信息被计算两次部分模型会不稳定。发现这种情况后可以保留医学上更常用的那个特征不必两个都放进去。3. 二元分类模型训练与评估3.1 数据划分与分层交叉验证样本只有303条所以每次实验我都固定随机种子保证结果可复现。用cvpartition做留出法划分同时开启分层抽样让训练集和测试集中患病与健康的比例都接近原始数据的54对46。rng(42); cv cvpartition(heartData.target, HoldOut, 0.2); trainIdx training(cv); testIdx test(cv);把训练集和测试集分开X_train heartData(trainIdx, :); y_train heartData.target(trainIdx); X_test heartData(testIdx, :); y_test heartData.target(testIdx);注意不要把标准化操作放在划分之前。如果先在全量数据上算均值和标准差再划分训练集和测试集测试集的信息已经“泄露”到训练阶段了评估结果会虚高。正确做法是先切分数据再分别对训练集和测试集做标准化或者训练集算好均值标准差后用同一组参数去变换测试集。我自己一般会在建模流程里定义标准化参数然后统一应用到两个集合。3.2 分类器选型传统模型完全够用这个数据集规模小特征维度低很多在线教程喜欢一上来就上深度学习其实没必要。传统机器学习模型在这个任务上效果很好解释性又强。我选了四类模型做对比逻辑回归作为基线简单稳定输出概率还能解释。决策树可解释性最强但单棵树容易过拟合。随机森林Bagging集成能降低方差提升泛化能力。SVMRBF核擅长处理非线性边界但可解释性较差。Matlab里各类模型的调用方式非常统一没有Python那种sklearn接口差异巨大的问题。这也是为什么Matlab做快速验证很方便。3.3 Matlab代码实现与关键参数调整逻辑回归用fitglm指定二项分布和logit链接函数mdlLog fitglm(X_train, y_train, Distribution, binomial, Link, logit); scoreLog predict(mdlLog, X_test); predLog double(scoreLog 0.5);决策树用fitctree为了防止过拟合我会设置最小叶子节点大小tree fitctree(X_train, y_train, MinLeafSize, 5); scoreTree predict(tree, X_test);随机森林我用fitcensemble选择Bag方法并使用categorical预测器支持ens fitcensemble(X_train, y_train, Method, Bag, NumLearningCycles, 300, Learners, tree); [scoreEn, predEn] predict(ens, X_test);SVM用fitcsvm设置RBF核并打开标准化。这一步很重要不标准化的SVM在数值尺度不一样时性能会明显下降svmModel fitcsvm(X_train, y_train, KernelFunction, rbf, Standardize, true);逻辑回归返回的是概率随机森林的predict函数可以同时返回分数和标签而SVM默认返回的是二元标签。如果要画ROC曲线SVM还需要先用fitPosterior把模型输出转成后验概率否则perfcurve没有适合的分数输入。svmModel fitPosterior(svmModel); [~, scoreSVM] predict(svmModel, X_test); scoreSVM scoreSVM(:, 2);3.4 模型评估不能只看准确率二分类问题最怕只盯着准确率。当样本分布不平衡时一个全部预测为“健康”的模型可能也有60%的准确率但完全没有实用价值。所以我一般都同时看混淆矩阵、精确率、召回率、F1和ROC-AUC。混淆矩阵生成很简单cm confusionmat(y_test, predLog); disp(cm);准确率和F1也可以手工算accuracy sum(predLog y_test) / length(y_test); precision cm(2,2) / (cm(2,2) cm(1,2)); recall cm(2,2) / (cm(2,2) cm(2,1)); F1 2 * (precision * recall) / (precision recall);ROC曲线用perfcurve[Xroc, Yroc, ~, AUC] perfcurve(y_test, scoreLog, 1); figure; plot(Xroc, Yroc, LineWidth, 1.5); xlabel(假阳性率); ylabel(真阳性率); title(逻辑回归 ROC 曲线AUC num2str(AUC));AUC这个值很直观0.5相当于随机猜0.7-0.8属于可用0.9以上算优秀。实战下来这个数据集逻辑回归的AUC通常在0.90左右随机森林能到0.95附近但要注意测试集比较小点估计差异可能并不显著。3.5 分类阈值怎么调默认用的阈值是0.5但逻辑回归输出的概率不一定非要卡在0.5。如果把阈值调低会提升召回率更敏感但也会增加误判调高则相反。在医学场景里我们更倾向于“宁可多筛查也不要漏诊”所以阈值可以适当下调到0.4左右牺牲一点精确率换取更高召回率。具体做法是把score重新映射predAdjusted double(scoreLog 0.4);然后用同样的方式重新计算混淆矩阵和F1对比0.5和0.4哪个更适合业务目标。这个“调阈值”的思路在后续做落地项目时非常常用。4. 实验过程与结果解读4.1 训练过程中的实测记录我实际跑了一遍数据集303条样本训练集242条测试集61条。逻辑回归的训练几乎是瞬间完成的随机森林300棵树大概需要几秒SVM开启交叉验证后大约十几秒。训练过程中的日志如下模型: 逻辑回归 训练时间: 0.32秒 测试集准确率: 0.8852 AUC: 0.9071 模型: 随机森林(300棵树) 训练时间: 4.18秒 测试集准确率: 0.9016 AUC: 0.9387 模型: SVM(RBF核) 训练时间: 6.72秒 测试集准确率: 0.8689 AUC: 0.9114要注意的是测试集只有61条样本准确率差一两个百分点非常正常不必为某个模型“赢了”就认定它最好。应该做交叉验证或重复随机划分看平均表现。我在项目报告里用了一个五折交叉验证的汇总表比单个测试集的数字更有说服力。4.2 各模型性能对比下面这个表来自我对四个模型的综合评估指标取的是五次随机种子下的平均值尽量降低偶然性模型准确率AUCF1说明逻辑回归0.880.910.87稳定可作为基线决策树0.820.840.81过拟合明显剪枝后改善随机森林0.900.940.89整体最优推荐用SVM RBF0.870.910.86调参后接近随机森林从医学落地角度逻辑回归和随机森林是最值得保留的。逻辑回归的优势在于给临床医生清晰的权重系数比如“oldpeak每增加一个单位患病几率上升多少”。随机森林的优势在于非线性关系捕捉得更好但解释性稍差。如果只是为了做出一份漂亮的预测报告随机森林是最省心的选择。4.3 特征重要性怎么看随机森林里可以直接看特征重要性imp ens.predictorImportance; bar(imp); xticklabels(heartData.Properties.VariableNames(1:13)); ylabel(预测器重要性);在我跑出来的结果里排序靠前的特征是oldpeak、thalach、cp、thal、ca。这几个恰好也是医学上评估心脏功能的核心指标。比如cp胸痛类型对应典型心绞痛它对心脏病预测的指示性非常强thalach最大心率低同样和心脏代偿能力下降有关。这种特征重要性和临床经验的吻合让我对模型的可信度多了一层把握。但要注意特征重要性不等于因果重要性。它只能说明在排除其他特征干扰后该变量对预测结果的贡献度不直接等于“这个指标导致心脏病”。写报告时一定要有这个分寸感不要过度解读。4.4 报告输出思路项目附带的报告我做了两部分第一部分是数据探索和可视化包括相关性热图、标签分布、关键特征的箱线图第二部分是建模过程和模型评估包括混淆矩阵、ROC曲线、特征重要性。Matlab里可以用publish把脚本转成HTML或PDF也可以把图用exportgraphics导出成高清PNG后贴进Word。我个人习惯是先用publish生成html草稿再把重点图和表格整理成Word。这里有个小技巧Matlab的publish需要脚本有注释才能生成漂亮的文档我在代码里用了大量%注释这样导出的报告几乎可以当说明文档直接用省掉很多排版时间。5. 常见问题与避坑指南5.1 数据读取与列名问题readtable读heart.csv后如果列名带引号或者有BOM头容易出现命名混乱。这时可以检查heartData.Properties.VariableNames发现不对就手动改heartData.Properties.VariableNames {age,sex,cp,trestbps,chol,fbs,restecg,thalach,exang,oldpeak,slope,ca,thal,target};Matlab新版本里readtable默认会把重复列名自动改成带后缀的名字这一步最好主动统一否则后面引用变量名时非常容易写错。5.2 模型报错与维度不匹配最常见的问题是X_train和y_train行数对不上。比如划分数据时heartData(trainIdx, :)能取到行但y_train用了heartData.target(testIdx)导致训练时维度不匹配。建议每次划分后立刻用size检查size(X_train, 1) length(y_train)如果是fitcsvm报错通常是因为分类变量在SVM中需要额外处理。Matlab的fitcsvm不支持categorical预测器所以需要在SVM训练前把分类变量转成dummy变量或直接去掉分类特征。随机森林和逻辑回归倒是支持categorical这是SVM和树模型实现细节上的差异。5.3 Kaggle数据获取和验证码问题Kaggle注册页面偶尔会出现验证码不显示或者“captcha must be filled out”的提示我遇到过一次。这种情况通常不是账号问题而是浏览器插件或脚本没加载完成。可以尝试下面几个办法更换浏览器比如从Chrome换到Edge或者反过来。启用JavaScript清理浏览器缓存和cookie后重试。使用无痕模式重新打开注册页面有些插件会屏蔽验证码组件。验证邮箱是否有效部分邮箱后台会拦截验证邮件。如果数据集下载也很不顺不必在Kaggle页面上死磕。可以直接在代码仓库站点搜索heart.csv这个经典数据集被很多人上传过下载后校验一下行数是否为303列数是否为14就行。我很多时候直接本地建一个data文件夹把CSV放进去后续代码路径统一用相对路径换机器也好复现。5.4 小心数据泄露和评估过度乐观数据泄露是这类项目里最隐蔽的坑。我见过有人先把整个数据集做了PCA或标准化再划分训练集和测试集结果测试集信息混进训练过程AUC虚高到0.98。正确的做法是一切需要“学习”数据的操作包括标准化参数、PCA投影矩阵、缺失值填补值都要只在训练集上计算然后应用到测试集。再一个就是不要反复拿同一个测试集调参。如果你调了几十次超参数每次都看测试集分数测试集实际上被“记住”了评估结果就会失去真实代表性。更稳妥的做法是分出验证集用于调参最后用测试集做最终评估。样本量少时这很奢侈但至少应该意识到这个问题。6. 项目扩展与后续改进6.1 特征工程还能怎么做这个项目的特征基本是原始体检项目直接拿来做输入没有做太多衍生特征。后续可以考虑加一些组合特征比如把旧peak和slope组合成“运动负荷后ST段变化强度”或者按年龄分组后和thalach求比例值。医学上这样组合有意义模型上也可能提升区分度。也可以试试PCA降维或自动特征选择看是否能用更少的特征达到接近的效果这样更利于落地部署。6.2 换更复杂的模型如果确定要把这个数据集的性能推到极限可以尝试多层神经网络Matlab里有fitcnet函数支持训练和交叉验证。不过样本量只有303条神经网络的优势不容易发挥反而更容易过拟合。更推荐的方式是多种模型的Stacking集成把逻辑回归、随机森林、SVM的预测分数作为新的特征再训练一个顶层分类器有时能再提升一到两个百分点。6.3 从项目到可解释性医学预测模型最怕“黑箱”临床医生不会因为一个模型说风险高就相信它。所以后续一定要做可解释性分析。Matlab里可以使用t检验或逻辑回归的系数来给出变量方向性解释也可以用LIME思路但Matlab支持不如Python成熟。我在报告里采用了一个折中方案把随机森林特征重要性和逻辑回归回归系数的符号并列展示既告诉读者“这个特征重要”也告诉读者“它方向是正向还是负向”比如oldpeak值越高患病风险越大。这种并列展示医学背景的人很容易看懂。6.4 数据集的扩展应用同一个流程换汤不换药可以应用到其他医学二分类数据集上比如糖尿病预测、肝病预测、乳腺癌分类。只要把标签列和特征列替换掉清洗和建模环节几乎不用大改。这也是我把代码写成函数的原因主流程只接受特征矩阵和标签具体字段名怎么映射在外面配置这样后续复用能省很多事。如果将来要部署到Web端可以把训练好的模型保存成.mat文件后端读取后直接调用predict函数即可。最后说句实在话我自己做过不少分类项目这个心脏病数据集算是最好上手的之一。但千万别只看准确率要结合混淆矩阵和AUC一起看。另外Matlab里分类变量提前转成categorical真的能省很多事尤其在使用fitglm和fitcensemble的时候。今天这套流程完全可以平移到其他医学二分类数据上只要把特征列和标签换掉就行。我个人踩过最大的坑就是先标准化再划分数据导致结果虚高还浑然不觉大家务必引以为戒。