ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

用LASSO筛特征+逻辑回归建模:心脏衰竭预测全流程实战

用LASSO筛特征+逻辑回归建模:心脏衰竭预测全流程实战 简介针对心脏衰竭致死相关因素分析这一医学数据处理场景压缩包整合了完整的机器学习预测方案适合具备一定Python基础、希望系统掌握Lasso特征选择与多种分类建模流程的数据分析学习者。包内共10个文件以5个Python脚本和1个R语言脚本为核心分别实现逻辑回归、支持向量机、随机森林及Lasso回归并附有临床数据集CSV、PDF版分析报告和说明文档整体大小仅240KB轻量而紧凑。已有804人学习下载资源热度可观。读者可基于原始数据复现从可视化、统计学相关性检验到Lasso筛选关键因子、三种模型对比评估的完整流程同时借助报告和说明文档快速理解设计思路便于直接改造用于其他疾病预测项目。整体目录结构清晰代码与报告分层放置适合课程设计、考研面试或医学数据分析入门复用。1. 心脏衰竭预测项目LASSO筛特征、逻辑回归出结论这份代码能交付什么“机器学习预测心脏衰竭”这标题听着唬人真正动手时大家都会在一个地方翻车变量虽然有十几个但彼此相关直接跑逻辑回归系数乱跳报告根本写不下去。我见过太多人把全部字段一股脑塞进模型出了一个AUC却解释不了为什么血清肌酐系数是负的。合理的做法是先用LASSO回归把特征压到可控范围内再用逻辑回归建模最后把系数变成一张能写进报告的表格。这套流程尤其适合三类人做医学课程设计或毕业设计的学生、刚接触医疗数据的机器学习从业者、需要从零搭建预测流程但没时间从头看公式的数据工程师。全文代码按“数据处理、特征筛选、建模评估、报告生成”的顺序走改动字段名就能复用在其他病种上。2. 数据清洗与特征准备哪些字段能进模型哪些必须剔除读入数据之后第一件事不是跑模型而是看字段字典和缺失值。医疗数据的特点是小样本、字段少但语义重一个字段选错后面所有步骤都在给错误打补丁。我一般会把这一步拆成三件事确认目标字段、剔除泄漏字段、统一量纲。这三件事做完特征才敢交给算法。2.1 缺失值与离群值先按字段语义处理再谈统计第一件事是观察数据结构。我常用这段代码探查import pandas as pd df pd.read_csv(heart_failure.csv) print(df.shape) print(df.info()) print(df.isnull().sum())df.info()能一次性看到字段类型和缺失情况isnull().sum()给出每个字段缺失个数。医疗数据几十条到几百条都很常见样本量不大时缺失字段的处理优先级比模型参数高得多。注意先别急着删行心衰数据里的死亡事件本来就少删几行就可能把正样本删没了。缺失值填充我用中位数而不是均值for col in df.columns: if df[col].isnull().sum() 0: df[col] df[col].fillna(df[col].median())参数说明median()对右偏分布更稳健。血清肌酐这类化验值长尾明显个别危重病人的极高值会把均值拉高用均值填充等于给所有缺失样本注入了极端患者的特征。离群值处理我坚持一条原则只有“生理上不可能”的才删比如射血分数大于 80 这类超出医学上限的值。用统计箱线图去裁剪生理指标很容易误删真实危重病人这是我在项目里踩过的坑。提示缺失率超过 30% 的字段建议直接剔除而不是填充。医疗表里有些化验字段只在特定检查时录入缺失本身代表“没做这项检查”填充反而会把不存在的检查结果当成真实值。2.2 目标变量与训练测试切分stratify 必须写数据洗完之后进入切分环节。这一步有一个关键参数经常被忽略from sklearn.model_selection import train_test_split # time 是观察期时长直接剔除避免特征泄漏 X df.drop(columns[DEATH_EVENT, time]) y df[DEATH_EVENT] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) print(y_train.value_counts(normalizeTrue))逻辑说明train_test_split里stratifyy的作用是让训练集和测试集中的死亡事件占比保持一致。如果不写这一行小样本随机划分可能把死亡样本全部切进测试集模型在训练阶段见不到正例测试阶段也全是误判AUC 直接崩掉。random_state42固定随机种子保证报告里的数字可以复现换一次随机种子结果就变这种模型输出没法写进正式文档。value_counts(normalizeTrue)打印标签分布这一步能立刻告诉你类别是否平衡。如果死亡事件占比只有三成甚至更低后面所有评估指标都要围绕不平衡来设计。模型本身无所谓哪个类是多数但报告必须交代清楚这个比例。2.3 标准化先 fit 训练集再 transform 测试集逻辑回归对量纲敏感。血清肌酐的单位是 μmol/L射血分数是百分比两者数值范围差几十倍。LASSO 的 L1 惩罚对每个特征施加相同力度的系数压缩不标准化时数值大的特征天然容易被保留这不是特征重要性这是量纲作弊。标准化代码如下from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)fit_transform只能出现在训练集上测试集只能用transform。如果先对全量数据fit再切分测试集的均值和方差已经混入训练过程之后拿到的评估指标是偏乐观的。医疗场景里这种乐观偏差不可接受因为模型上线面对的是全新的病人数据让测试集信息提前渗入训练过程等于自己骗自己。更稳的做法是把标准化和模型封装进Pipeline后面调参时交给网格搜索统一处理避免任何人在改代码时把顺序打乱。这一步也是机器学习应用流程里最不起眼但最容易埋雷的环节。3. LASSO回归特征筛选C值怎么扫系数表怎么读特征准备干净后下一步不是直接训练完整逻辑回归而是先用 LASSO 看清楚哪些特征值得留下。LASSO 回归和逻辑回归在 sklearn 里的组合方式是在LogisticRegression中设置penaltyl1。L1 惩罚会把不重要的特征系数压成 0等价于自动完成特征选择而模型本身依然是输出概率的逻辑回归。这个组合既拿到了稀疏解又保留了概率解释能力正是心脏衰竭这类小样本医疗预测最需要的性质。3.1 为什么选 LASSO 而不是一把梭直接跑逻辑回归普通逻辑回归在特征相关性较强时系数会互相挤占解释力两个高度相关的特征一个系数变成正的大数另一个变成负的大数单个系数的方向完全失真。LASSO 的 L1 惩罚会强制弱的那个系数归零留下代表性强的那一个。两者的区别可以这样看模型特征处理可解释性常见问题普通逻辑回归所有特征全部保留系数不稳定共线特征互相抵消报告里解释不了系数符号L1 逻辑回归LASSO 式弱特征系数压成 0保留下来的特征一目了然C 值需要交叉验证认真调这两个机器学习算法的组合核心价值在于逻辑回归负责输出概率和可信的系数LASSO 负责筛掉噪音。在 sklearn 里实现时不要误以为要先跑一个Lasso再用它的结果喂给逻辑回归直接用LogisticRegression(penaltyl1)一步到位更省事系数也是在同一套损失函数下优化的。3.2 C值怎么扫用交叉验证打AUC而不是准确率L1 惩罚的核心参数是C它表示正则化强度的倒数。C越小惩罚越强特征越容易被压成 0C越大越接近普通逻辑回归。惩罚过猛会把所有特征压成 0过弱等于没做筛选。我一般用一个循环扫对数范围from sklearn.model_selection import cross_val_score from sklearn.linear_model import LogisticRegression for C in [0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10]: lr LogisticRegression( penaltyl1, solverliblinear, CC, max_iter2000, random_state42, ) auc cross_val_score( lr, X_train_scaled, y_train, cv5, scoringroc_auc ).mean() n_keep (lr.fit(X_train_scaled, y_train).coef_[0] ! 0).sum() print(fC{C:4}, AUC{auc:.3f}, 保留特征数{n_keep})参数说明solverliblinear适合小样本 L1 优化max_iter2000防止收敛警告。评分用roc_auc而不是accuracy因为心脏衰竭数据里死亡事件占比偏低准确率会偏向多数类AUC 对类别不平衡没那么敏感。每轮同时打印保留特征数帮你判断当前惩罚力度是在“合理筛特征”还是“一刀切”。如果某个C下 AUC 还可以但保留特征只剩 1 个说明惩罚已经过头特征信息被压掉了。选 AUC 最高的C后再重新训练一轮把lr.coef_[0]存下来。这一步是粗筛网格不用太细后面建模阶段还能用交叉验证复核。3.3 系数表看哪些特征被压成0哪些留下特征筛选结果我习惯整理成一张 DataFrame 直接打印coef_df pd.DataFrame({ feature: X_train.columns, coef: lr.coef_[0], }) coef_df[abs_coef] coef_df[coef].abs() print(coef_df.sort_values(abs_coef, ascendingFalse))逻辑说明coef_对应每个特征在模型里的权重系数大于 0 说明该特征值越大死亡风险越高小于 0 则相反绝对值为 0 表示该特征被 LASSO 剔除。以公开的心衰临床数据为例系数方向通常有以下规律特征系数方向倾向对预测方向的常见解读age正年龄越大风险越高ejection_fraction负射血分数越高风险越低serum_creatinine正肌酐值越高风险越高serum_sodium负血钠越低风险越高注意这张表的解读只适用于当前数据集。字段名以你实际拿到的表为准不要照着公开数据的解读硬套自己的结果。到这一步特征从十几个压到四五个报告就好写了。但系数符号只是相关性不是因果这一点后面避坑章节会专门讲。4. 逻辑回归建模与评估用AUC和阈值定判断线特征筛选完成之后用选出的特征重新训练一个逻辑回归模型。这个阶段的核心不是调参而是搞清楚模型的输出怎么变成临床可用的判断。逻辑回归模型输出的是一个概率病人发生死亡事件的估计概率。概率本身没有类别属性类别是你用阈值切出来的。阈值放哪里决定了这个模型是偏保守筛查还是偏严格确诊。4.1 概率输出与判断线输出是概率不是标签在 sklearn 里拿概率非常简单# 用第3章选出的最优 C 重新训练 best_C 0.5 # 以第3章扫描结果为准 lr_best LogisticRegression( penaltyl1, solverliblinear, Cbest_C, max_iter2000, random_state42, ) lr_best.fit(X_train_scaled, y_train) y_prob lr_best.predict_proba(X_test_scaled)[:, 1] print(y_prob[:10])predict_proba返回两列第一列是“未发生”的概率第二列是“发生死亡事件”的概率我取[:, 1]。直接调用predict()默认用 0.5 当阈值这在医疗场景里默认就是错的漏掉一个真实高风险病人比多筛查一个正常人代价大得多。所以先把概率拿到手阈值后面单独定。注意测试集通常只有二十几条记录阈值扫描得到的灵敏度、特异性波动会很大。不要拿测试集一锤定音后面用交叉验证汇总波动范围再下结论。4.2 阈值扫描用混淆矩阵定灵敏度与特异性阈值扫描是一个环把 0.3 到 0.7 之间的判断线全部跑一遍from sklearn.metrics import confusion_matrix for threshold in [0.3, 0.4, 0.5, 0.6, 0.7]: y_pred (y_prob threshold).astype(int) tn, fp, fn, tp confusion_matrix(y_test, y_pred).ravel() sensitivity tp / (tp fn) # 实际死亡且被检出的比例 specificity tn / (tn fp) # 实际存活且被判存活的比 precision tp / (tp fp) if (tp fp) 0 else 0 print( fthreshold{threshold:.1f}, fsensitivity{sensitivity:.2f}, fspecificity{specificity:.2f}, fprecision{precision:.2f} )参数说明sensitivity也叫召回率衡量的是“真正高危的病人里模型找回了多少”漏诊率就是1 - sensitivity。specificity衡量的是“真正低危的人里模型放过了多少”误诊率是1 - specificity。precision衡量的是“模型报出来的高危里有多少是真的”在资源有限的筛查场景里同样重要。两个场景的阈值选择逻辑完全不同。如果目标是初筛阈值调到 0.3 左右灵敏度拉高宁可多送检也不能漏掉高危病人如果目标是收治确认阈值调到 0.6 以上优先保特异性减少医疗资源挤兑。工程师能给的是一张权衡表最终阈值由业务方定这是医疗项目的交付姿态。4.3 类别不平衡class_weight 和 stratify 配合心脏衰竭数据里死亡事件通常占三成左右直接建模会偏向多数类“存活”。处理方式有两种一是前面已经用stratify保证切分时类别比例不变二是在模型里加class_weightlr_balanced LogisticRegression( penaltyl1, solverliblinear, Cbest_C, class_weightbalanced, max_iter2000, random_state42, ) lr_balanced.fit(X_train_scaled, y_train) y_prob_bal lr_balanced.predict_proba(X_test_scaled)[:, 1]class_weightbalanced会按照类别频率自动调整权重少数类死亡事件的误分类代价被抬高模型对少数类更敏感。加了之后通常召回率上升、精确率下降这在预期内。小样本下用这个参数要留个心眼死亡事件只有几十例时权重太高会把训练集里个例特征记住导致过拟合。我一般会把三组结果放在一起比较全字段的逻辑回归、LASSO 筛后的逻辑回归、LASSO 筛后加均衡权重的逻辑回归。对比出来的差异本身就是报告的一部分远比只交一个最终模型有说服力。5. 医疗预测项目避坑指南泄漏、不平衡和系数误读的血泪记录这份指南里的每一条都来自实际项目里的翻车经历。做医疗预测和做推荐系统最大的差别在于数据里没有“常识”兜底一个字段定义看错模型再漂亮也是错的。以下 5 条是我现在每接到一个医疗数据项目都会逐条核查的事项。5.1 特征泄漏time 字段制造的虚假 AUC现象模型交叉验证 AUC 达到 0.95报告写出来非常漂亮但临床医生看完直接质疑。原因数据集里有一个time字段表示病人从入组到死亡或随访结束的天数。死亡病人的time天然很短存活病人的time较长。把这个字段放进特征模型不需要任何医学知识只看time就能判断结局——等于提前看到了答案。这类字段在医疗数据里叫“泄漏变量”或“事后变量”。解决凡是与结局窗口重叠的字段默认不进特征集。拿到字段字典后先问一句这个字段是结局发生之前就能拿到的吗拿不到的剔除。time这类跟随结局变化的变量后续做生存分析可以单独建模但不能混进分类模型。5.2 类别不平衡准确率 98% 的模型其实不能交付现象模型在测试集上准确率 98%把混淆矩阵打出来却发现死亡事件一个都没预测中。估算一下类别占比死亡事件三成其余七成。模型把所有样本都判成“存活”准确率就有 70%如果再用阈值偏移优化一下98% 都能做到。原因准确率对多数类友好但在“预测罕见事件”的任务里没有任何指导意义。医疗场景关心的是漏诊率而不是总体对错比例。解决先打印value_counts(normalizeTrue)确认类别占比。评估阶段以 AUC、灵敏度、特异性为主报告里必须写混淆矩阵和这几个指标不要单独用 accuracy 下结论。stratifyy保证切分时类别比例不漂移class_weightbalanced压制多数类偏好。5.3 标准化顺序错误测试集被偷看现象线下交叉验证 AUC 0.85自认为模型很好部署到新数据上一跑只剩 0.78。原因有人把StandardScaler().fit_transform(X_all)写在切分之前让全量数据的均值方差参与了标准化。测试集的信息渗入训练过程评估结果属于乐观估计上线后打回原形。解决严格fit在训练集、transform在测试集。更保险的做法是把标准化封装进Pipeline和模型一起做交叉验证杜绝人为改顺序的空间。5.4 把系数当因果报告被临床退回现象报告里写“血清肌酐系数为正说明降低血清肌酐可降低死亡风险”。被临床医生退了回来理由是逻辑回归只能证明相关性不能证明干预后的因果效果。原因观测数据里的相关性可能来自混杂因素。肌酐高的人通常心肾功能差是疾病严重程度的表现而不是可以作为干预靶点的原因变量。解决报告里把系数换算成优势比OR exp(coef)并明确写一句“本分析基于回顾性观测数据结果表示关联而非因果因果结论需要前瞻性队列或干预研究验证”。医疗报告里这句话不是免责声明是学术规范。5.5 C值扫得太粗特征被整个压没现象coef_全部为 0交叉验证还在跑怀疑数据本身有问题。原因C 值设得太小惩罚过强所有系数被压成 0。直接把C0.001往下试是常见失误这不是特征没意义是惩罚力度大到模型不敢用任何特征。解决C 按对数空间从 0.01 到 10 扫描每轮同时打印“保留特征数”和 AUC。选交叉验证 AUC 最高且保留特征数不太多的 C。特征被压到只剩一两个时宁可降低惩罚力度给模型留出解释空间。6. 报告可复现用bootstrap验证特征稳定性并组织交付文档模型结果要变成一份能交付的报告单靠一次训练的输出不够。样本量小的时候一次划分训练出来的系数可能靠运气。我用 bootstrap 重采样验证特征的稳定性这个做法在医疗报告里性价比极高。6.1 bootstrap验证特征稳定性思路是从训练集里有放回地抽取同等数量的样本重复训练 200 次统计每个特征在多少次里被保留import numpy as np rng np.random.default_rng(42) keep_rate {col: 0.0 for col in X_train.columns} n_boot 200 for _ in range(n_boot): idx rng.choice(len(X_train), len(X_train), replaceTrue) Xb, yb X_train_scaled[idx], y_train.iloc[idx] m LogisticRegression( penaltyl1, solverliblinear, Cbest_C, max_iter2000, ) m.fit(Xb, yb) for col, coef in zip(X_train.columns, m.coef_[0]): if coef ! 0: keep_rate[col] 1 / n_boot result pd.Series(keep_rate).sort_values(ascendingFalse) print(result)逻辑说明rng.choice(..., replaceTrue)实现有放回抽样每次训练集和原始训练集一样大但里面有些样本被重复选中有些没被选中。一个特征在 200 次重采样中被反复保留说明它不是依赖个别样本出现的偶然因素。保留率接近 1 的特征才敢写进报告的正文保留率 0.6 以下的一律标注为“不稳定因素”。6.2 报告按这个顺序组织结论才站得住我最终交付的报告结构固定为六块数据概述、特征清洗说明、LASSO 筛选结果表、模型评估表、结论与局限性、可复现脚本清单。筛选结果表里包含系数、OR 值、bootstrap 保留率模型评估表里包含 AUC、阈值权衡表、混淆矩阵。局限性一段必须写样本量小、单中心数据、结果不代表因果这三句话能挡掉大部分误读。代码按data_process.py、lasso_select.py、train_eval.py、report_gen.py四个脚本串起来前一个脚本的输出是后一个脚本的输入任何人拿到这份代码都能从原始表一路跑到报告。我现在拿到医疗数据的第一件事是先写字段字典确认每个字段是事前变量还是事后变量再谈建模。代码写得再顺字段定义错了后面所有论证都要推倒重来。希望帮到你。本文还有配套的精品资源点击获取
返回列表