ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

从逻辑回归到XGBoost:医疗数据二分类实战指南

从逻辑回归到XGBoost:医疗数据二分类实战指南 简介面向初学者的第七届“泰迪杯”数据挖掘挑战赛B题“直肠癌淋巴结转移的智能诊断”完整解决方案包适合正在备战数据挖掘竞赛、希望从零上手医学影像分类流程的大学生或入门选手。压缩包共24个文件以Python脚本11个py、Jupyter Notebook4个ipynb为核心辅以PNG/GIF示意图、TXT说明和README文档整体仅2.37MB便于快速下载与本地复现。其中U-Net分割模型与HDF5数据读写脚本构成核心管道训练、测试与结果生成模块覆盖数据预处理、模型评估、输出提交的完整链路配套实验Notebook则逐步演示函数调试、病灶体积计算和模型结果检查适合对照学习代码细节。多份TXT说明文件进一步梳理使用步骤与注意事项可帮助初学者少走弯路。目前已有114人学习该资源对小白友好拿到后即可按说明运行并理解整个智能诊断流程。1. 从一张患者表格里预测淋巴结转移这题不靠深度学习靠基本功第七届“泰迪杯”数据挖掘挑战赛 B 题给出的是典型的医疗表格数据场景用一组直肠癌患者的临床特征和检查记录预测患者是否存在淋巴结转移。这是一道标准的数据挖掘监督二分类任务难倒初学者的往往不是模型本身而是数据处理和评估设计。这题对刚起步的人最友好的地方在于它不要求你上深度网络把逻辑回归、随机森林、XGBoost 这些传统但可靠的模型吃透就够了。如果你至今还没完整跑过一个竞赛项目这道题是从读数据、清洗、建模到提交一条龙走通的最佳练手题。2. 读懂任务与准备数据建模之前先回答三个问题拿到题目包之后先别急着跑代码。泰迪杯的 B 题包一般会提供训练集、测试集和一份变量说明文档你要在半小时内回答自己三个问题这是一道什么任务、评估指标是什么、特征来自哪里。这三个问题决定了你后面所有代码的走向。2.1 这是一个二分类任务标签定义与评估口径直肠癌淋巴结转移预测的本质是二分类每一行是一个患者的记录特征列是各种临床指标、影像检查结果与病理参数标签是“是否有淋巴结转移”通常用 0 和 1 表达。先确认标签是不是二值化的——有些临床数据会把转移数量分级但本题一般聚焦“有/无”的智能诊断以二分类处理即可。模型输出的是一个 0 到 1 之间的概率超过阈值判为“有”否则判为“无”。评估指标是第二个要确认的事。别把准确率当成唯一标准医疗场景里漏诊和误诊的代价完全不同。漏掉一个真正有转移的患者后续治疗方案会偏差误诊又让患者承受不必要的术后辅助治疗。所以这类题目通常把 AUCROC 曲线下面积当作重要参考——它不依赖固定阈值能更全面衡量模型把正样本排在负样本前面的能力。AUC 越高代表模型对“有转移”和“无转移”的区分能力越强这个指标贯穿你的整个建模过程。最后是特征来源。直肠癌数据的特征一般来自三部分基础信息年龄、性别、BMI、实验室检查CEA、CA19-9 等肿瘤标志物、影像或病理参数T 分期、分化程度、病灶位置。这些信息对你的特征工程方向影响很大——有些特征表面无关实际上与淋巴结转移有隐含相关性有些特征强相关却因为缺失率高而难以利用。带着对特征来源的理解去读变量说明文档效率高得多。2.2 搭建可复现的数据挖掘环境Anaconda 与五个库我建议你用 Anaconda 创建一个独立环境避免依赖冲突而不是在自己的主环境里直接装包。常见做法是这样conda create -n teddy python3.9 -y conda activate teddy pip install pandas numpy scikit-learn matplotlib xgboost这五个库是这道题的最小依赖集pandas 和 numpy 负责任务的数据读入与处理scikit-learn 是建模主力xgboost 是梯度提升树的补充。如果你想最后再提一点分数可以加装 lightgbm但初期不需要。这些库不需要一次学透能跑通即可后面每个代码里我会说明核心调用的作用。环境一致性是个常被忽略的隐性要求。你用本地 Python 3.10 调好代码评测环境如果是 Python 3.8某些库版本不兼容可能直接跑挂。所以一上来就把环境锁死最终提交时附上依赖版本清单这是数据挖掘的基础职业习惯。另外整个分析过程养成用同一个 kernel 的习惯避免换环境后随机种子不一致导致结果复现不了。2.3 加载数据与第一轮探查用 pandas 把表格“看出问题”加载数据这一步看似简单实际上初学者最容易在这里卡住。文件编码常见为 UTF-8 或 GBK如果直接用默认编码读报 UnicodeDecodeError 是大概率事件。我一般先试 UTF-8报错就切 GBKimport pandas as pd train pd.read_csv(train.csv, encodingutf-8) test pd.read_csv(test.csv, encodingutf-8) print(训练集形状:, train.shape) print(测试集形状:, test.shape) print(train.head(8).T)最后一行把 DataFrame 转置打印是因为竞赛表格的特征列往往很多横着打会折行转置后每一列一个特征、每一行一个样本可读性好得多。如果不确定列名的实际大小写用print(train.columns.tolist())先看全部列名。接下来做第一轮结构探查print(train.info()) desc train.describe().T print(desc) print(train[label].value_counts(normalizeTrue))train.info()直接告诉你每列的类型和非空数量缺失情况一目了然describe()给出数值列的分布概况value_counts(normalizeTrue)输出标签比例让你立刻知道类别是否平衡。看分布时我有两个固定动作。第一看每列缺失率的形状——部分特征在训练集缺失 20%在测试集却只缺 5%这种“缺失模式漂移”会影响后面插补策略。第二看数值的量级差异——如果 CEA 范围是 0.1 到 100而年龄只有 30 到 80逻辑回归会因量纲差异收敛很慢树模型倒是无所谓。这里发现的问题直接决定第 3 章的特征处理怎么做。3. 数据清洗与特征工程小白也能直接上手的三个步骤数据处理是这题拿分的分水岭。模型选得再先进输入脏数据输出就是垃圾。我处理 B 题这类医疗数据的顺序是固定的先补缺失再修分布最后做特征减法每一步都有需要避开的坑。3.1 缺失值怎么补医学场景里没有“随便填空”这回事先统计缺失比例这是所有缺失值处理的前提missing train.isnull().mean().sort_values(ascendingFalse) missing missing[missing 0] print(missing)缺失比例超过 70% 的特征我的习惯是直接丢弃。因为补出来的信息基本都是强假设在交叉验证里很容易变成噪声。对中低缺失比例的特征怎么补要看特征类型连续特征用中位数类别特征用众数或新增一个“缺失”类别。中位数比均值安全因为医学指标经常呈右偏分布均值会被极端值拉走。num_cols train.select_dtypes(include[float64, int64]).columns cat_cols train.select_dtypes(include[object]).columns for col in num_cols: if col ! label: med train[col].median() train[col] train[col].fillna(med) test[col] test[col].fillna(med) for col in cat_cols: train[col] train[col].fillna(Missing) test[col] test[col].fillna(Missing)这里有个关键细节训练集和测试集必须统一填充值。如果两边各自算中位数再填充那两个中位数大概率不同模型在推理时面对的特征分布就和训练时不一致了。正确做法是先算训练集的中位数med再把同一个值同时写到两个集里。这行代码是初学者最容易漏掉的。更进一步缺失本身有时就是信息。医疗数据里“某指标没测”和“测了但值正常”在临床决策上含义完全不同所以值得加一列缺失标记for col in [CEA, CA199]: train[col _is_missing] train[col].isnull().astype(int) test[col _is_missing] test[col].isnull().astype(int)树模型能自动捕捉这种“有没有值”的模式逻辑回归不行所以你要显式把这个信息编码给模型。这些缺失标记列在后面的特征筛选中经常能排进前二十说明它们确实携带了预测信息。3.2 类别特征编码与连续特征标准化分开处理才不会出错拿到类别列之后初学阶段最常见的翻车是把所有字符串列一股脑做 LabelEncoder。LabelEncoder 的本质是给类别贴 0、1、2 有序数字对无序类别比如病灶位置上/中/下段这等于强行引入“位置编码 1 比位置编码 0 更重要”的序关系模型会学到不存在的语义。正确做法是独热编码train_encoded pd.get_dummies(train, columnscat_cols, drop_firstFalse) test_encoded pd.get_dummies(test, columnscat_cols, drop_firstFalse) train_encoded, test_encoded train_encoded.align( test_encoded, joinouter, axis1 ) test_encoded test_encoded.fillna(0)第三行代码很多人没意识到训练集和测试集的类别取值集合不一定一致测试集可能出现训练集没见过的类别直接 get_dummies 会导致两边列数不等。align的本质是以两个集的列并集为基准对齐再对缺失列补 0这样模型在推理时面对的特征维度就和训练时完全一致了。连续特征的处理取决于你最终选什么模型。逻辑回归或 SVM 需要标准化否则量纲差异会让收敛变慢树模型则完全不需要标准化对它们没有收益反而丢失了特征原始分布的数值含义。竞赛里最常见的过度操作就是不管三七二十一先StandardScaler一把梭——对树模型来说这是纯粹的画蛇添足。3.3 特征筛选用随机森林和相关性矩阵做减法特征不是越多越好。医疗数据里有些特征高度相关有些和标签几乎无关留着无关特征会让模型学到的噪声增加。我先算相关系数矩阵把与标签相关系数绝对值较低的数值特征列出来人工过一遍再决定删不删corr train_encoded.corr() print(corr[label].sort_values(ascendingFalse).head(20))接下来看特征之间的共线性。如果两个特征相关系数超过 0.9我保留树模型重要性更高或临床解释更强的那个另一个删掉。这个过程不需要很复杂的算法直接把结果打出来人工审查比任何自动筛选工具都可靠。树模型重要性筛选可以这样完成from sklearn.ensemble import RandomForestClassifier x_train train_encoded.drop(columns[label, id]) y_train train_encoded[label] rf RandomForestClassifier(n_estimators200, random_state42, n_jobs-1) rf.fit(x_train, y_train) importance pd.DataFrame({ feature: x_train.columns, importance: rf.feature_importances_ }).sort_values(importance, ascendingFalse) print(importance.head(20))看到输出后我一般保留累计重要性达到 85% 的前 N 个特征具体 N 看“肘部”——重要性从某个特征开始直线下降后面的就可以删。注意一个重要原则特征选择要放在交叉验证之前做但拿来做筛选的随机森林本身只用于挑选特征不用于最终建模避免“用同一份数据既选特征又评分数”带来的乐观偏差。这一步做完你的特征矩阵就干净了可以进入建模环节。4. 从逻辑回归到树模型跑通第一个能用的诊断模型特征准备好之后离第一个可提交的预测只差三步分数据、选模型、定指标。我建议新手直接用一套固定的模板把整个过程跑通不追求一步到位的调参。4.1 划分训练集与验证集stratify 是初学者必会的参数数据划分是模型评估可信度的地基。很多新手直接train_test_split(x, y)完事结果正样本在两个子集里的比例差了一倍后面所有指标全乱。加上stratify参数from sklearn.model_selection import train_test_split x_tr, x_val, y_tr, y_val train_test_split( x_train, y_train, test_size0.2, random_state42, stratifyy_train )stratifyy_train的作用是让训练集和验证集里的类别比例保持一致。如果标签不平衡比如正样本只占 20%两个子集里的正样本比例也会是 20%。随机划分在小样本场景下经常出现某一折恰好没有正样本的极端情况stratify 能从源头避免。为了让验证分数更稳我再进一步用分层 K 折交叉验证。交叉验证的价值在于告诉你“模型在数据的不同切分下表现是否稳定”from sklearn.model_selection import StratifiedKFold skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) for train_idx, val_idx in skf.split(x_train, y_train): # 每一折都训练和验证记录分数 pass如果某一折的 AUC 比其他折低 0.1 以上说明模型或特征里存在对特定样本敏感的隐患这个信息单次划分给不了你。我在实际做这道题时就是用这个循环发现某一折的 AUC 异常追查下去发现是一个离群患者样本在作怪。4.2 三个候选模型的完整训练与对比从逻辑回归到 XGBoost对这道题不建议一上来就调 XGBoost 超参而是先跑通三个难度递进的模型形成基准from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from xgboost import XGBClassifier from sklearn.metrics import roc_auc_score models { logistic: LogisticRegression(max_iter1000, random_state42), rf: RandomForestClassifier(n_estimators300, random_state42, n_jobs-1), xgb: XGBClassifier( n_estimators200, max_depth4, learning_rate0.1, subsample0.8, colsample_bytree0.8, random_state42 ) } auc_scores {} for name, model in models.items(): model.fit(x_tr, y_tr) y_prob model.predict_proba(x_val)[:, 1] auc_scores[name] roc_auc_score(y_val, y_prob) print(f{name}: AUC {auc_scores[name]:.4f})三个模型跑完你会得到基本判断如果逻辑回归 AUC 和 XGBoost 差距很大说明特征与标签的关系偏非线性树模型适合如果三个模型表现接近说明信息主要藏在特征本身而非模型复杂度就不需要在调参上花费太多时间。XGBoost 参数里我默认设了subsample0.8和colsample_bytree0.8这是控制随机性的两个关键参数前者让每棵树只用 80% 的样本后者让每棵树只考虑 80% 的特征两者都在降低过拟合风险。初学阶段不要动n_estimators的默认值先跑通再说。这里必须提醒一个坑predict_proba返回二维数组第一列是“无转移”概率第二列是“有转移”概率。初学者如果取了[:, 0]就等于拿着负类的概率去算 AUC结果完全相反。我每次写都会注意这个细节习惯用[:, 1]并通过测试集里有转移的样本数去反验一下。4.3 评价指标准确率好看不等于模型好用先看一组数字。假设数据里有 20% 正样本一个“所有人全部判无”的模型准确率恰好是 80%。如果你只看 accuracy这个模型直接及格了但它一个患者都没筛出来。所以我的评价至少要看四个指标指标含义什么时候重点看AUC正样本得分高于负样本的概率整体排序能力不依赖阈值F1精确率与召回率的调和平均阈值定了之后看分类效果召回率真阳性占实际阳性比例需要漏诊率低的时候特异度真阴性占实际阴性比例需要误诊率低的时候竞赛题目通常会在说明里写清评分标准。如果以 AUC 为主把重心放在模型排序能力上最后再定阈值如果以 F1 为主就要专门调整阈值和类别权重因为 F1 已经是“给定阈值下的分类质量”。最后值得做一个 ROC 曲线可视化看曲线形状是否平滑from sklearn.metrics import roc_curve fpr, tpr, thresholds roc_curve(y_val, y_prob) # 画图观察 fpr 与 tpr 的关系曲线平滑度反映模型稳定性曲线如果“走一段直线后突然陡升”说明模型在大尾部样本上的排序不稳定要回去检查是否有极端离群值在干扰。这一步看似简单却经常能提前暴露数据里的硬伤。5. 避坑医疗数据挖掘里最容易翻车的五件事这一章的每一条都是我和学员在类似题目上真实踩过的坑按“现象-原因-解决”的格式写清楚。新手最大的成本不是写代码是排查这些看似莫名其妙的分数异常。5.1 类不平衡模型干脆摆烂把所有病例都判成“无转移”现象所有模型预测的正样本概率集中在 0.3 以下AUC 看起来有 0.7但把阈值放到 0.5 时验证集上预测为正的样本不到十个而真实正样本上百个。混淆矩阵显示模型几乎放弃了正样本。原因在类别比例严重失衡时分类器默认追求整体准确率少数类的梯度贡献被多数类稀释。逻辑回归的决策边界会被多数类整体拉偏树模型也会倾向于让叶子节点把样本分到多数类因为多数类的样本量天然让节点更容易“纯”于多数类。解决先给少数类加权重不急着上采样或 SMOTE。lr_weighted LogisticRegression(class_weightbalanced, max_iter1000) neg_count (y_tr 0).sum() pos_count (y_tr 1).sum() xgb_weighted XGBClassifier( scale_pos_weightneg_count / pos_count, n_estimators200, max_depth4, learning_rate0.1, random_state42 )scale_pos_weight等于负样本数除以正样本数作用是把正样本的单条梯度放大到和负样本接近让模型不能忽视少数类。自己算neg_count / pos_count是一个通用起点后续在此基础上可以再放大或缩小。加了权重之后 AUC 可能不变甚至略降但 F1 和召回率通常明显改善因为模型开始“关注”正样本了。5.2 数据泄漏标准化和填充必须发生在划分之后现象本地验证 AUC 0.85提交到官方评测只有 0.68分数断崖式下跌而代码逻辑看起来完全一致。原因最经典的场景是你在划分数据集之前就调用了StandardScaler.fit()或者fillna(train.median())。无论 fit 标准化器还是算中位数都已经“看”了全体训练数据的信息这使得交叉验证里每一折模型提前偷看了验证折的统计量分数虚高。等真正预测未知数据模型只能用全体训练数据的统计量差的那零点几就是水分。解决把预处理拆成两个阶段。第一阶段只基于训练集 fit 得到填充值、编码映射、标准化参数第二阶段把这些参数同时应用到验证集和测试集。from sklearn.preprocessing import StandardScaler scaler StandardScaler() x_tr_scaled scaler.fit_transform(x_tr) x_val_scaled scaler.transform(x_val)注意x_val只能用transform不能用fit_transform。这个顺序一旦写反分数就不干净了。更稳妥的做法是把整个预处理封装成 scikit-learn 的 Pipeline从结构上避免泄露。初学阶段至少做到“所有从数据里学到的量只 fit 在训练折上”这是数据挖掘最重要的纪律。5.3 阈值调低之后 F1 涨了但这可能只是乐观错觉现象按 0.5 阈值提交 F1 只有 0.55。把阈值降到 0.3F1 升到 0.65觉得模型“变强了”。原因阈值降低改变了分类边界边界的移动是用“更积极的召回”换来的。F1 在低阈值下上升并不代表模型整体变好而是分类质量在另一个阈值下的最优解。问题出在你没有校准概率模型输出的 0.3 未必对应真实的 30% 可能性。解决画校准曲线或直接用CalibratedClassifierCV校一版概率from sklearn.calibration import CalibratedClassifierCV calibrated_model CalibratedClassifierCV( estimatorrf, methodisotonic, cv5 ) calibrated_model.fit(x_tr, y_tr) y_prob_cal calibrated_model.predict_proba(x_val)[:, 1]做完校准再看各个阈值下的 F1这时选的阈值才是可信的。校准曲线如果贴近对角线说明概率有真实频率含义如果严重偏离阈值策略就要保守不要轻易追高召回。这个坑在医疗数据里特别常见因为模型概率经常存在系统性偏移。5.4 训练集和测试集的特征分布不一致现象线上分数比本地验证低 0.15 以上。回看测试集某些连续特征的最小值比训练集 5% 分位数还低或某个类别出现在测试集但训练集从没见过。原因数据集的划分方式可能不是随机的比如按时间段或按医院划分造成时间漂移或中心效应。模型在训练环境的特征分布里学到的相关性在测试环境里不成立。淋巴结转移诊断模型特别害怕这种分布漂移。解决写一个几十行的 KS 检验把训练集和测试集每个特征的分布差异打出来from scipy.stats import ks_2samp for col in x_train.columns: stat, p ks_2samp(x_train[col], test_encoded[col]) if p 0.05: print(f{col}: KS{stat:.3f}, p{p:.4f}分布差异显著)KS 检验 p 值小于 0.05 的特征就是分布漂移的候选人。如果某一个漂移严重的特征恰好是模型重要性排前三的特征就要警惕线上分数落差。应对办法是对漂移严重的特征降低权重或删除或者对连续特征做分箱降低它对临界值的敏感度。这一步不能完全消除问题但能提前让你心里有数避免把希望全押在一个不稳定的特征上。5.5 只调阈值不保存模型和预处理对象复现全靠玄学现象调出一个高分模型后关掉 notebook第二天再打开重新跑一遍代码结果对不上之前的分数再也复现不出来。原因没有保存中间产物也没有固定随机种子。竞赛环境变动一个小版本或者 DataFrame 的列顺序在重读时发生变化都会导致结果漂移。更常见的是你从头到尾用了同一个 notebook但每一次运行的数据处理过程有随机性。解决每跑出一版好结果立刻把预处理对象和模型一起保存下来import joblib joblib.dump(scaler, scaler.pkl) joblib.dump(best_model, final_model.pkl)同时固定所有随机种子random_state在 sklearn 的每个模型上都设一致numpy 和 Python 内置的 random 也要设。这几行代码成本极低却能保证你三天后还能复现今天的分数。这算是一个数据挖掘实操的祖传习惯我每次都会被“忘了保存”坑一次所以现在写完模型第一件事就是落盘。6. 上分与交付模型保存、概率校准与提交前检查你已经跑通基线和权重调整接下来不要急着堆模型先做三件收尾的事判断模型状态、校准概率、按比赛格式提交。6.1 用学习曲线判断提升空间画学习曲线能回答一个关键问题——你有足够的数据吗这里用的是前面交叉验证的框架from sklearn.model_selection import learning_curve train_sizes, train_scores, val_scores learning_curve( models[xgb], x_train, y_train, cv5, train_sizes[0.3, 0.5, 0.7, 0.9, 1.0], scoringroc_auc )训练集 AUC 高、验证集 AUC 明显低是“高方差”状态这时调参效果有限应该降低模型复杂度或收集更多样本两条曲线都低且接近是“高偏差”状态加大模型没用要回去补特征工程。有了这个判断你不会把时间浪费在错误的优化方向上。我自己的习惯是先把学习曲线打出来确认模型不缺数据也不缺容量再做最后一步概率校准。这能避免把两三个小时浪费在对一个样本量不足的模型死磕调参上。6.2 提交前必须检查的三件事第一测试集预测时严格复用训练集上派生出的所有映射关系test_prob best_model.predict_proba(x_test_scaled)[:, 1] submission pd.DataFrame({id: test[id], probability: test_prob}) submission.to_csv(submission.csv, indexFalse)第二提交前检查列名是否符合比赛模板第几列是 ID、第几列是预测概率和题目给的那张样例提交表逐列对齐。概率范围必须落在 [0,1]如果出现 NaN 或负数多半是缺失值没有在测试集里处理好。行数必须与测试集完全一致我自己吃过几次“输出多两行”的亏原因不过是索引没重置写提交文件前reset_index(dropTrue)是必修课。第三最终提交的版本必须是你保存下来的模型文件推理出的结果而不是重新跑一遍的最新输出。竞赛里最后一天往往会改一堆参数你记忆里“最好的那个结果”和当前 notebook 的输出经常对不上。先落盘再做推理这是最不容易出错的工作流。走到这你已经完整地跑通了数据挖掘从读数据到建模到提交的全流程。回头看这题的高分秘诀不在哪个花哨模型而在处理数据的耐心和评估方法的严谨。愿你从这份小白的方案出发亲手调出属于你自己的第一版智能诊断模型也愿这些踩过的坑帮你在面对任何一张陌生表格时都能站稳脚跟。希望帮到你。本文还有配套的精品资源点击获取
返回列表