
最近做了一期心脏病预测的模拟项目从临床日常数据入手用Python走完了数据清洗、特征分析、模型训练到结果解读的全流程。之所以选这个题目是因为心血管疾病相关指标在体检和门诊记录中非常常见数据字段也相对规整非常适合作为机器学习二分类问题的入门实战。这篇就把完整代码和踩坑过程整理出来给想用Python做医疗数据预测的同学一个能直接跑起来的参考方案。适用人群大概是这三类一是刚学完Pandas和Sklearn、想找个完整项目练手的人二是生物医学或公共卫生方向的从业者需要快速验证一批指标对疾病的区分度三是想了解“预测模型到底怎么落地”的产品或临床辅助决策相关岗位。项目不涉及复杂网络结构也不依赖高算力一台普通笔记本就能跑完。先说清楚一个核心观点很多人拿到医疗数据就急着调模型、堆参数其实医疗预测项目的成败往往在建模之前就已经决定了。特征理解、缺失值处理、样本分布检查这些“脏活”占了整个项目七成的工作量。这篇会把这一部分讲透因为这才是“临床日常数据”和“通用机器学习教程”之间最大的差异所在。1. 项目整体设计与思路拆解1.1 为什么用临床日常数据做心脏病预测心脏病预测在机器学习领域属于经典二分类问题但“临床日常数据”这几个字决定了它和普通的Kaggle练习赛有很大区别。日常数据指的是体检记录、门诊常规检查、护士问诊采集的基本生命体征这些低门槛信息比如年龄、性别、静息血压、胆固醇值、最大心率、运动诱发的心绞痛情况等。这类数据的特点是单看某一项指标患者和健康人的分布大量重叠单变量几乎没有区分度。比如胆固醇正常范围和高风险范围之间有很长的灰色地带。但把多项指标组合起来建模就能捕捉到协同变化的模式这正是机器学习模型的用武之地。1.2 建模方案选型的核心逻辑模型选型上我最终采用了逻辑回归和随机森林双轨并行而不是一上来就上XGBoost或深度学习。原因有两点。第一是可解释性。医疗场景的预测结果往往需要给医生或患者一个“为什么”逻辑回归的系数直接告诉你“该指标每增加一个单位患病风险的对数几率变化多少”随机森林的特征重要性也能给出排序。黑盒模型虽然精度可能更高但在这种场景下说服力不足。第二是样本量和数据维度。日常临床数据整理下来一般只有几百到几千条样本、十几个特征这种规模下复杂模型容易过拟合反而线性模型和浅层树模型表现更稳定。具体的建模路线分三步先跑逻辑回归作为基线再看随机森林能否通过非线性交互提升效果最后用ROC曲线和召回率精确率权衡来决定最终阈值。整个过程控制在一次跑批内完成。2. 环境准备与数据获取2.1 Python依赖库清单写代码之前先把环境备好。我用的Python版本是3.9核心库版本如下pandas 1.5.x numpy 1.23.x scikit-learn 1.2.x matplotlib 3.6.x seaborn 0.12.x安装命令直接一把梭pip install pandas numpy scikit-learn matplotlib seaborn建议用虚拟环境隔离项目依赖别把全局环境搞乱了。装完检查一下版本不同大版本之间API差异还是有的后面代码我在关键位置做了兼容处理。2.2 数据字段说明模拟项目使用了公开的心血管疾病数据集剔除个体隐私信息后包含以下核心字段这些字段在真实体检和门诊场景中都能找到对应项字段名含义类型age年龄数值型sex性别1男0女二分类cp胸痛类型0-3多分类trestbps静息血压数值型chol血清胆固醇数值型fbs空腹血糖120mg/dl二分类restecg静息心电图结果多分类thalach最大心率数值型exang运动诱发心绞痛二分类oldpeakST段压低值数值型slope运动高峰ST段斜率多分类ca主要血管数量数值型thal地贫类型多分类target是否患病1是0否二分类2.3 数据导入与基本信息体检数据加载后第一时间做三件事看形状、看类型、看缺失。代码如下import pandas as pd import numpy as np df pd.read_csv(heart.csv) # 1. 数据集规模 print(数据集形状:, df.shape) # 2. 字段类型概览 print(df.dtypes) # 3. 缺失值检查 print(缺失值统计:\n, df.isnull().sum()) # 4. 目标值分布 print(患病分布:\n, df[target].value_counts())我第一次跑这个数据的时候发现ca和thal两个字段存在少量缺失值这种缺失如果直接丢给模型有些算法会报错。处理策略后面单开一节讲这里先留个印象。3. 数据探索性分析与可视化3.1 目标变量分布检查先把目标变量分布打出来。这是建模前必做的一步因为分布形态直接决定评估指标怎么选。import seaborn as sns import matplotlib.pyplot as plt sns.countplot(xtarget, datadf) plt.show() # 输出比例 print(df[target].value_counts(normalizeTrue))如果样本中患病和未患病比例严重失衡比如只有10%是患者那模型全猜“未患病”就能有90%准确率。这种情况下要看召回率和AUC不能只看精确率。3.2 数值特征分布对比我习惯用分组箱线图对比患者和健康人群在各数值特征上的分布差异这比直接看相关性矩阵更直观。num_cols [age, trestbps, chol, thalach, oldpeak] fig, axes plt.subplots(2, 3, figsize(15, 10)) for idx, col in enumerate(num_cols): row idx // 3 col_idx idx % 3 sns.boxplot(xtarget, ycol, datadf, axaxes[row][col_idx]) plt.tight_layout() plt.show()通过这个图能快速定位哪些特征区分度比较明显。实际观察来看thalach和oldpeak两个特征在两组间差异显著患者组的最大心率普遍偏低ST段压低值偏高。这个观察和临床经验是一致的也说明数据质量靠谱。3.3 相关性矩阵速览相关性矩阵能看到特征之间的线性关系但如果建模用了树模型还得靠特征重要性看非线性关系。plt.figure(figsize(12, 10)) sns.heatmap(df.corr(), annotTrue, fmt.2f, cmapcoolwarm) plt.show()跑完相关性矩阵我注意到了一个值得警惕的点某些特征之间存在中等强度的相关性比如oldpeak和slope。说明它们在某种程度上携带了相似信息建模时不需要担心共线性对树模型的影响但对逻辑回归这种线性模型来说共线性会放大系数估计的方差解释系数时要谨慎一点。4. 特征工程与数据预处理4.1 缺失值处理策略前面提到的ca和thal字段有缺失我先确认了缺失数量和占比# 查看缺失值详情 print(df[[ca, thal]].isnull().sum()) print(df[[ca, thal]].info())这两个字段都是类别型或顺序型。处理方式选择众数填充因为对这类离散字段用均值填充会产生不存在的“小数类别”不合理。# 众数填充 df[ca].fillna(df[ca].mode()[0], inplaceTrue) df[thal].fillna(df[thal].mode()[0], inplaceTrue)这里有一个技术细节fillna默认不会修改原DataFrame必须加上inplaceTrue或重新赋值。我早期做项目时忘记加后续建模直接报错提示有NaN排查了半天才发现问题。4.2 数值特征标准化逻辑回归这类线性模型对特征尺度敏感如果chol的数值范围是几百而oldpeak只有0到几梯度下降收敛会非常慢。用StandardScaler做标准化让每个特征均值为0方差为1。from sklearn.preprocessing import StandardScaler num_features [age, trestbps, chol, thalach, oldpeak] scaler StandardScaler() df_scaled df.copy() df_scaled[num_features] scaler.fit_transform(df_scaled[num_features])如果是随机森林这类树模型理论上不依赖特征尺度但做完标准化对后续统一流程管理有好处一套数据能同时喂不同的模型。4.3 特征与标签拆分建模前的最后一步是把特征矩阵和目标变量分开然后划分训练集和测试集。这里要注意stratify参数的用法它能保证训练集和测试集中正负样本比例和原始数据一致避免因为随机划分导致测试集里全是某一类样本。from sklearn.model_selection import train_test_split X df_scaled.drop(target, axis1) y df_scaled[target] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) print(训练集样本数:, X_train.shape[0]) print(测试集样本数:, X_test.shape[0])4.4 为什么需要分层抽样这里多说一句stratify。假设原始数据患病率是50%如果不分层某些极端随机划分可能导致测试集患病率变成40%或60%模型评估结果就会失真。分层抽样就是保证划分后各集合的患病率仍然接近50%让评估结果更可信。5. 模型构建与训练5.1 逻辑回归基准模型第一个模型用逻辑回归跑出一个可对比的基线。这里加了一个小技巧增加max_iter参数不然逻辑回归在标准化数据上迭代次数不够会报收敛警告。from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score, classification_report, roc_auc_score lr LogisticRegression(max_iter1000, random_state42) lr.fit(X_train, y_train) y_pred_lr lr.predict(X_test) y_prob_lr lr.predict_proba(X_test)[:, 1] print(逻辑回归准确率:, accuracy_score(y_test, y_pred_lr)) print(逻辑回归AUC:, roc_auc_score(y_test, y_prob_lr)) print(classification_report(y_test, y_pred_lr))跑出来的结果里逻辑回归的准确率大概在85%上下AUC在0.90左右。这个基线不算差也符合预期线性模型已经能捕捉到大部分区分信号。5.2 随机森林模型第二个模型用随机森林它能自动处理特征交互和非线性关系理论上能在逻辑回归基础上再提升一点。from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier( n_estimators200, max_depth5, random_state42 ) rf.fit(X_train, y_train) y_pred_rf rf.predict(X_test) y_prob_rf rf.predict_proba(X_test)[:, 1] print(随机森林准确率:, accuracy_score(y_test, y_pred_rf)) print(随机森林AUC:, roc_auc_score(y_test, y_prob_rf)) print(classification_report(y_test, y_pred_rf))随机森林的参数设置在第一次实验中没有做网格搜索max_depth限制为5n_estimators选了200主要是为了防止过拟合。医疗数据的样本量不大树太深很容易把训练集背下来泛化能力反而下降。5.3 两种模型效果对比用测试集评估后的关键指标对比如下指标逻辑回归随机森林准确率0.850.87AUC0.900.92精确率0.830.86召回率0.880.89随机森林在各项指标上都有小幅领先但说实话差距并没有想象中大。原因在于这个数据集中特征和目标之间基本是单调关系复杂的非线性交互贡献有限。如果换了更复杂的数据源随机森林的优势会更明显。5.4 特征重要性分析随机森林训练完成后顺手把特征重要性画出来。这一步对向别人解释模型极有帮助也能反过来验证数据理解是否正确。import matplotlib.pyplot as plt importance pd.Series( rf.feature_importances_, indexX.columns ).sort_values(ascendingTrue) importance.plot.barh(figsize(10, 8)) plt.title(Random Forest Feature Importance) plt.show()从重要性排序来看thalach、oldpeak、cp是前三位的重要特征。这三个指标恰好对应临床上“运动负荷下心脏反应”的核心维度逻辑上完全讲得通。6. 模型评估与优化6.1 混淆矩阵分析准确率和AUC是总体性指标但医疗场景更关心“患者被漏诊了没有”。这时候必须看混淆矩阵。from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay cm confusion_matrix(y_test, y_pred_rf) disp ConfusionMatrixDisplay(confusion_matrixcm) disp.plot() plt.show()混淆矩阵里的四个数字分别对应真正例、假正例、真负例、假负例。假负例就是患者被判断为健康这在医疗场景是代价最高的一种错误。我跑出来的结果中随机森林的假负例数量很少说明模型在“尽量别漏掉病人”这个目标上表现不错。6.2 ROC曲线与AUCROC曲线展示的是不同阈值下真正例率和假正例率的权衡关系。AUC越大说明模型整体排序能力越强。from sklearn.metrics import roc_curve, auc fpr, tpr, thresholds roc_curve(y_test, y_prob_rf) roc_auc auc(fpr, tpr) plt.figure(figsize(8, 6)) plt.plot(fpr, tpr, labelfRandom Forest (AUC {roc_auc:.2f})) plt.plot([0, 1], [0, 1], k--) plt.xlabel(False Positive Rate) plt.ylabel(True Positive Rate) plt.title(ROC Curve) plt.legend() plt.show()AUC为0.92意味着模型有92%的概率把一个随机患者和一个随机健康人正确区分开这个区分度在临床辅助筛查里已经算不错的水准了。6.3 阈值选择的实际考量模型默认用0.5作为分类阈值但实际部署中可以调整。如果希望提高召回率就降低阈值到0.4甚至0.3代价是健康人被误判为患者的比例上升如果希望减少误报就提高阈值。# 自定义阈值提高召回率 y_prob rf.predict_proba(X_test)[:, 1] custom_threshold 0.4 y_pred_custom (y_prob custom_threshold).astype(int) print(阈值0.4的召回率:, recall_score(y_test, y_pred_custom)) print(阈值0.4的精确率:, precision_score(y_test, y_pred_custom))这里没有绝对的对错完全取决于使用场景。做大规模初筛时宁可误报一些也要减少漏诊做确诊前辅助判断时则要控制误报率。7. 常见问题与排查技巧实录7.1 收敛警告困扰很多人第一次跑逻辑回归会遇到ConvergenceWarning眼看着代码没报错但心里总觉得不踏实。这个警告通常是因为迭代次数不足加一个max_iter1000就能消除。还有一种可能是特征没有标准化导致梯度下降震荡数据预处理部分的StandardScaler就是为了解决这个问题。7.2 训练集和测试集数据泄露有个隐蔽但常见的坑把数据标准化放在train_test_split之前scaler在全部数据上fit了均值和方差这样测试集的信息间接进入了训练过程属于轻微的数据泄露。严格的做法是先拆分再对训练集fit标准化器然后对测试集用同一个标准化器transform。scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)这个细节对最终评估结果的影响可能只是一两个百分点但养成这个习惯换到更大的项目时能少走很多弯路。7.3 特征相关性的冗余风险如果在做相关性矩阵时发现两个特征相关系数超过0.7就要小心了。树模型尚可容忍线性模型则可能出现系数不稳定的情况。处理方式有两种删除其中一个特征或者合并成新特征。我在这个项目里没有做这一步因为相关性最强的一对也只到0.5左右没有达到危险线。7.4 数据量太少时的过拟合征兆评估结果如果出现训练集准确率接近100%、测试集却掉到80%以下基本可以判定过拟合。随机森林可以尝试减小max_depth、增大min_samples_split逻辑回归可以加大正则化系数C。网格搜索里把这些参数放进去跑一遍选出测试集表现最好的组合。常见问题的速查表问题现象可能原因解决方向逻辑回归收敛警告迭代不足或未标准化增大max_iter检查标准化训练集好测试集差过拟合限制树深度加正则化测试集某类样本太少未分层抽样启用stratify参数特征缺失导致报错未处理NaN众数/中位数填充类别极度不平衡采样或阈值问题过采样、欠采样、调整阈值8. 实操中的经验心得迭代8.1 从单次预测到全流程闭环跑完一遍模型不是终点。这个项目的最佳落地形态是把整个流程固化为一个可复用的函数输入一份临床指标数据输出患病概率和风险分级。def predict_heart_disease(input_data): # 标准化 scaled scaler.transform(input_data) # 预测 prob rf.predict_proba(scaled)[:, 1] risk_level 高 if prob 0.4: risk_level 低 elif prob 0.7: risk_level 中 return prob[0], risk_level这样封装之后后续接API或导入新数据做批量预测都很方便。我实际测试过对这一批数据用统一流程跑批量预测输出稳定耗时也在秒级以内对日常保健或队列随访场景完全够用。8.2 模型解释要与业务对齐做模型从来不只是调包调参更要把模型输出转换成业务含义。比如特征重要性分析发现thalach最关键映射到临床语言就是“运动负荷下心率反应能力是风险分层的重要参考”。8.3 后续可以怎么扩展这个项目目前用的是结构化表格数据后续扩展方向至少有三个一是引入时间序列信息比如多次体检的心率变化趋势二是加入更多生活行为特征比如吸烟、饮酒、运动频率三是用SHAP值做更细致的个体化解释解释每一个患者为什么被判为高风险。我最深的体会是医疗预测项目里数据理解比算法堆叠重要合理的评估比调参重要模型的可解释性比精度的微小提升更重要。这个项目虽然代码量不大但把从数据到决策的完整链路走通了后续换数据、换场景时这套框架可以直接复用。最后补一句这个方案里的代码逻辑和流程设计基于模拟数据集真实医疗场景需结合临床规范和当地伦理要求进一步验证但作为学习与工程实践参考是足够扎实的。你拿到代码后建议第一步不急着改参数先把“数据体检-特征理解-基线模型-评估”这条链路完整跑通一遍然后再根据自己的数据集特点做调整。这样踩的坑最少成长也最快。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。