ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

出血性脑卒中诊疗建模实战:从临床定义到XGBoost预测

出血性脑卒中诊疗建模实战:从临床定义到XGBoost预测 简介围绕2023年中国研究生数学建模竞赛E题资源包提供完整Python解决方案面向参赛学生、指导教师以及从事出血性脑卒中临床数据分析的研究者。压缩包共34个文件整体仅3.25MB内容组织清晰3个py脚本对应数据预处理与模型主流程10个xlsx和3个xls存放患者临床信息、影像血肿与水肿体积、形状及灰度分布等核心数据6个xml为PyCharm工程配置4个docx和1个pdf为赛题背景、概念解析与建模思路说明另有CSV便于统计验证README指引快速上手数据表覆盖患者列表、影像血肿水肿体积及形态特征等多个维度可直接用于特征工程和模型验证。资源内附AI_CDM_for_ICH主工程通过main.py、1_a.py等脚本从患者流水号与影像检查时间检索入手结合临床与影像数据逐步复现血肿水肿分析及智能诊疗建模的完整流水线配套文档对赛题背景、解题思路及代码使用进行了说明便于二次开发。已有392人学习目录结构符合工程化习惯适合希望借鉴赛题思路、减少数据整理时间并快速上手的建模爱好者。1. 出血性脑卒中临床智能诊疗建模2023年研究生数学建模E题为什么难在“数据”而不是“模型”2023年中国研究生数学建模竞赛E题“出血性脑卒中临床智能诊疗建模”是当年华为杯里少数把真实临床决策与数据建模绑在一起的题目。它不要求你搭建一套深度学习阅片系统而是把CT影像报告、入院量表、治疗方案和90天mRS评分整理成结构化数据用python完成血肿扩张预测和功能预后预测两个任务。很多抱着“医疗AI等于看片子”思路入场的人第一周就翻车了——真正决定分数上限的不是模型多前沿而是数据清洗、特征构造和对临床定义的理解。这篇文章适合想用python完整复现一次医疗建模赛、又需要一份能跑通的数据集与源码参考的从业者和参赛者。2. 把E题拆成两个核心建模任务血肿扩张与mRS预后预测出血性脑卒中ICH的临床智能诊疗落到赛题里其实就是两个预测任务而且二者有明确的因果链条入院时血肿是否继续扩张直接影响患者90天后的神经功能恢复。把这件事想清楚你就知道数据字典里那些表格为什么要这样设计也自然知道该按什么顺序建模型。2.1 血肿扩张HE预测标签的临床定义决定模型上限血肿扩张Hematoma ExpansionHE的临床定义在影像上非常明确从首次CT到复查CT血肿体积增加超过6mL或相对增加超过33%。目前大多数临床研究采用INTERACT2研究口径也就是“绝对增加≥6mL 或 相对增加≥33%”。这个定义直接决定了你的标签长什么样也决定了哪些字段可以进入特征矩阵——注意只能用首次影像及入院基线信息来预测扩张用复查影像去预测扩张属于时间穿越这点在后面的避坑章节展开。拿到数据后第一步不是跑模型而是把标签按临床口径构造出来import pandas as pd import numpy as np # 字段名以赛题数据字典为准这里示意常见的表格结构 img_first pd.read_csv(data/影像信息_首次.csv) img_recheck pd.read_csv(data/影像信息_复查.csv) # 以患者ID关联首次与复查影像 merged img_first.merge(img_recheck, onpatient_id, suffixes(_first, _recheck)) # 按INTERACT2口径构造HE标签 merged[vol_diff] merged[血肿体积_recheck] - merged[血肿体积_first] merged[vol_ratio] merged[血肿体积_recheck] / merged[血肿体积_first] merged[HE] ((merged[vol_diff] 6) | (merged[vol_ratio] 1.33)).astype(int) # 检查正负样本比例这是后面所有模型的前提 print(merged[HE].value_counts()) print(HE比例: %.2f%% % (merged[HE].mean() * 100))逻辑说明先用merge把首次与复查两次影像记录关联到同一行这是医疗多表数据最常见的操作然后同时计算绝对增量与相对增量任何一个满足条件都判定为HE阳性。正负样本比例之所以要第一时间看是因为HE在临床真实数据中阳性率通常只有15%到25%如果你发现这个比例异常高先检查merge是否出错、是否把同一个患者的两次随访记录当成两行独立样本了。参数说明6mL和33%这两个阈值不要自己改赛题如果明确用了其他定义以赛题为准。2.2 90天mRS预后预测序数标签不能当普通多分类做mRS改良Rankin量表是评估脑卒中患者神经功能恢复程度的金标准分0到6共7个等级0代表无症状6代表死亡。建模时最容易犯的错误是把它当成普通多分类问题用softmax输出7类概率。mRS是一个典型的有序标签Ordinal Label类别之间不只是“不一样”还有明确的严重程度递进。把序数信息丢掉等于让模型重复学习“相邻类别之间的边界”数据量不够时学得一塌糊涂。常见做法是先做标签分布分析再决定建模口径import pandas as pd prognosis pd.read_csv(data/预后信息表.csv) # 查看mRS标签分布 mrs_counts prognosis[mRS_90].value_counts().sort_index() print(mrs_counts) # 临床研究常用二分口径mRS 0-2 为功能良好3-6 为功能不良 prognosis[good_prognosis] (prognosis[mRS_90] 2).astype(int) print(良好预后比例: %.2f%% % prognosis[good_prognosis].mean() * 100)逻辑说明先统计每个mRS等级的样本量如果某些等级样本极少比如mRS0只有十几个人强行做7分类会因为类别不均衡产生大量无效模型。此时赛题一般允许两种处理一是保留序数信息做有序回归二是把标签合并为“良好/不良”二分类。二分类是比赛中性价比最高的做法因为样本量够、指标稳定、解释容易。参数说明mRS2作为良好预后是临床论文的通用口径如果赛题报告里写了不同的cutoff按其要求执行。2.3 评价指标对齐临床口径与竞赛评分口径先定死建模之前确定评价指标是为了避免“模型调了一周最后发现提交格式不对”的尴尬。不同任务有不同评价习惯对参赛者来说必须同时对齐两个口径临床研究论文用什么赛题评分用什么。任务临床常用指标竞赛/复现常用口径注意点血肿扩张HE预测AUC、灵敏度/特异度AUC、F1、召回率阳性样本少别只看准确率mRS预后预测加权Kappa、有序回归准确率、加权Kappa普通准确率会被多数类主导提交文件CSV列ID预测值按竞赛要求有的题要求概率有的要求标签对于HE预测临床最关心的是“能不能把会扩张的病人筛出来”所以召回率比精确率更重要对于mRS预测加权KappaQuadratic Weighted Kappa能惩罚“预测成相差两级的错误”比平白准确率更贴临床。我一般会同时算AUC、F1、加权Kappa三个指标最后以竞赛给出的主指标为准做模型选择。如果你发现模型AUC很高但准确率很拉别慌这通常是样本不均衡下的正常现象。3. 数据处理与特征构建把临床表格变成一张干净的宽表E题的数据集通常分成多张表患者基本信息、入院首次影像信息、复查影像信息、治疗方案、90天随访结果。建模前要把它们合并成一张“宽表”这个环节的坑比模型调参多得多。3.1 多表合并先画一张患者ID地图再谈特征工程第一件事是搞清楚每张表的粒度。患者基本信息表是一人一行治疗方案表也是一人一行或一人多行影像信息表可能是一人两行首次复查随访表是一人一行。合并要以患者ID为主键先把粒度统一到“一人一行”再开始建特征。如果同一患者有多条治疗记录需要按优先级取第一条或用聚合函数压缩成“是否溶栓”“是否介入取栓”等布尔字段。import pandas as pd # 读入全部原始表 base pd.read_csv(data/患者基本信息.csv) # 一人一行ID,年龄,性别,既往史... img pd.read_csv(data/影像信息_首次.csv) # 一人一行血肿体积,位置,密度... treat pd.read_csv(data/治疗方案.csv) # 一人一行或多行 # 治疗方案如果存在多行先聚合再合并 treat_group treat.groupby(patient_id).agg({ 是否溶栓: max, 是否介入: max, 治疗时间: first }).reset_index() # 用left join保留全部患者 train base.merge(img, onpatient_id, howleft) train train.merge(treat_group, onpatient_id, howleft) print(train.shape) print(train.head())逻辑说明groupby聚合是处理多行治疗记录的安全方式“max”对0/1布尔字段意味着“只要有任意一次溶栓就标记为1”符合临床含义“first”取首次治疗时间不引入未来信息。left join保证所有患者都保留在原表里如果用inner join会悄悄丢掉缺失影像记录的患者导致样本偏倚。参数说明合并完成后务必检查train的行数是否等于base的行数多一行或少一行都说明ID有重复或join逻辑有误。3.2 缺失值不等于空值医疗表格的缺失要留痕迹医疗数据集的缺失值往往是信息本身。比如NIHSS评分缺失可能是因为患者入院时昏迷无法完成神经功能评估这时候缺失是一个重要的预测因子而不是需要抹掉的无用数据。所以我不直接dropna或随便填均值而是同时生成一个“是否缺失”的布尔特征再用中位数填充原列。# 统计缺失率决定哪些字段要做留痕处理 miss_ratio train.isnull().mean().sort_values(ascendingFalse) print(miss_ratio[miss_ratio 0]) # 对数值型特征做缺失留痕 中位数填充 for col in train.columns: if train[col].dtype in [float64, int64] and train[col].isnull().any(): train[col _missing] train[col].isnull().astype(int) train[col] train[col].fillna(train[col].median())逻辑说明对每个有缺失的数值列先生成一个_missing标志列再原地用中位数填充。树模型对缺失值本身有一定容忍度但逻辑回归会把NaN当成错误输入所以填充是必须的而_missing列保留的“缺失位置”信息对医疗数据往往有真实的预测价值。参数说明代码里用train[col].dtype in [...]判断数值列是因为性别、既往史这类object列不能直接fillna中位数需要单独用众数或其他策略。这里先只看数值列后续再补做分类型字段的编码。3.3 影像特征入库血肿体积、位置密度与时间窗把影像报告转成结构化特征时最核心的是三类体积、形态、时间。体积通常直接给mL数但数值跨度很大几毫升到一百多毫升直接丢进模型会被极端值带偏形态特征是位置基底节、脑叶、丘脑、脑干等和是否破入脑室时间特征是发病到首次CT的间隔它直接关系到溶栓时间窗的判定。# 体积取对数压缩极端值 train[log_volume] np.log1p(train[血肿体积]) # 位置特征做onehot编码字段默认是字符串 location_dummies pd.get_dummies(train[血肿位置], prefixloc) train pd.concat([train, location_dummies], axis1) # 发病到首次CT时间差单位小时 train[onset_to_ct] ( pd.to_datetime(train[首次CT时间]) - pd.to_datetime(train[发病时间]) ).dt.total_seconds() / 3600 # 是否处于4.5小时溶栓时间窗内 train[in_window] (train[onset_to_ct] 4.5).astype(int)逻辑说明log1p对血肿体积做对数压缩避免体积从个位数到上百的巨大跨度影响线性模型get_dummies把位置字符串转成0/1哑变量时间差是连续特征保留原始小时数比只做成布尔值信息量更大但“是否在溶栓窗内”这种布尔特征对临床路径有直接意义两者都保留。参数说明4.5小时是急性缺血性卒中溶栓的通用时间窗但在ICH赛题中出血性卒中是否溶栓存在争议具体以数据字典和治疗方案表为准这里只是示范。4. 建模与调参用逻辑回归和XGBoost把医疗预测稳稳落地医疗赛题的样本量通常只有几百到一两千人深度学习容易过拟合真正稳的是逻辑回归和树模型。我的建议是先用逻辑回归打出基线看系数理解变量方向再上XGBoost捕捉非线性关系最后针对不均衡样本调阈值。这条路径在绝大多数医疗表格竞赛中都能稳定拿到中上分数。4.1 基线模型逻辑回归的系数是性价比最高的“可解释性”逻辑回归在医疗建模中不可替代的原因是它给出的是对数几率比log-odds每个特征系数在论文里可以直接解读成“年龄每增加一岁HE风险增加X%”。比赛评分不只看AUC报告的可解释性也占很大权重逻辑回归的系数表就是最方便的可解释性材料。from sklearn.model_selection import StratifiedKFold from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline from sklearn.metrics import roc_auc_score features [年龄, 性别, GCS评分, NIHSS评分, log_volume, onset_to_ct] X train[features] y train[HE] # 分层5折交叉验证保证每折阳性比例一致 cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) auc_scores [] for train_idx, valid_idx in cv.split(X, y): X_tr, X_va X.iloc[train_idx], X.iloc[valid_idx] y_tr, y_va y.iloc[train_idx], y.iloc[valid_idx] model make_pipeline(StandardScaler(), LogisticRegression(C1.0, max_iter1000)) model.fit(X_tr, y_tr) proba model.predict_proba(X_va)[:, 1] auc_scores.append(roc_auc_score(y_va, proba)) print(LR 5折AUC: %.3f ± %.3f % (np.mean(auc_scores), np.std(auc_scores)))逻辑说明StratifiedKFold保证每一折训练集和验证集中HE阳性比例与原数据集一致避免某一折恰好全是阴性样本导致AUC失真。StandardScaler放在Pipeline里先标准化再训练逻辑回归的梯度下降对特征尺度敏感血肿体积几十毫升、年龄几十岁量纲不同会导致收敛变慢或系数不可比。参数说明C1.0是正则化强度的默认值医疗小样本上C在0.1到1之间效果差别不大但C太大容易过拟合建议做一轮网格搜索候选值[0.01, 0.1, 1, 10]。4.2 上XGBoost树模型接管非线性与缺失值逻辑回归的局限是只能建模线性关系——年龄与HE风险可能是U型关系GCS评分与预后的关系也不是单调的。XGBoost这类梯度提升树不需要特征缩放能自动学习特征交互也不需要提前填充缺失值虽然我们已经填充了。在小数据集上关键是用早停防止过拟合。import xgboost as xgb from sklearn.metrics import roc_auc_score # XGBoost使用同样的交叉验证结构 xgb_model xgb.XGBClassifier( n_estimators300, max_depth3, learning_rate0.05, subsample0.8, colsample_bytree0.8, eval_metricauc, early_stopping_rounds20, random_state42 ) # 以第一折为例展示训练过程完整跑5折时每折单独早停 xgb_model.fit( X_tr, y_tr, eval_set[(X_va, y_va)], verboseFalse ) proba_xgb xgb_model.predict_proba(X_va)[:, 1] print(XGBoost这一折AUC: %.3f % roc_auc_score(y_va, proba_xgb))逻辑说明n_estimators300是树的数量上限配合early_stopping_rounds20当验证集AUC连续20轮不提升就停止这是防止过拟合最有效的手段。max_depth3在几百个样本的数据集上是安全深度深度到5或6训练集AUC能到0.99但验证集会崩。subsample0.8表示每棵树随机抽80%样本训练colsample_bytree0.8表示每棵树随机抽80%特征两相结合相当于给模型加随机性降低方差。参数说明learning_rate0.05比较保守配合大n_estimators能慢慢逼近最优值如果想提速度可以调到0.1并把n_estimators降到150但AUC通常会掉1到2个点。4.3 阈值优化血肿扩张预测拼的是召回率不是默认0.5分类模型默认把0.5当阈值这在正负样本均衡时没问题但HE阳性率只有20%左右默认阈值会把大量阳性病例误判为阴性。临床场景里漏掉一个真正会扩张的病人比多筛出几个假阳性严重得多所以要按Precision-Recall曲线找最优阈值让模型在召回率和精确率之间找到赛题需要的平衡点。from sklearn.metrics import precision_recall_curve import numpy as np # 用验证集概率计算PR曲线 precision, recall, thresholds precision_recall_curve(y_va, proba_xgb) # F1最大化法选择阈值兼顾精确与召回 f1_scores 2 * precision * recall / (precision recall 1e-9) best_idx np.argmax(f1_scores) best_thr thresholds[best_idx] print(最优阈值: %.3f % best_thr) print(此阈值下召回率: %.3f, 精确率: %.3f % (recall[best_idx], precision[best_idx])) # 实际预测时用最优阈值替代默认0.5 y_pred (proba_xgb best_thr).astype(int)逻辑说明precision_recall_curve会返回每个候选阈值下的精确率与召回率f1_scores是两者的调和平均最大化F1提供的是一个不需要额外指定业务权重的阈值选择方案。如果需要更极端地偏向召回率可以在f1_scores的计算里给recall加权比如f1 (12) * precision * recall / (2 * precision recall)这相当于认为漏诊的成本是误报的两倍。参数说明这个阈值的可迁移性有限——它是在某一折验证集上算出来的最终确定模型后应把全部训练数据重新拟合一次再用该阈值预测测试集。用交叉验证每一折单独选阈值再取平均比固定某一折的阈值更稳。5. 医疗建模排坑数据泄漏、标签噪声与不均衡的五个现场医疗结构化数据的坑大部分不在模型而在数据语义。下面五个问题我在历年数学建模赛和临床数据项目中都踩过每条按“现象→原因→解决”写清楚。5.1 训练时AUC接近0.95提交后分数暴跌你用了复查信息现象模型在交叉验证里AUC 0.94看起来无敌提交测试集后发现分数远低于预期甚至不如全体预测为0的基线。原因特征矩阵中包含了只有“事后”才知道的信息最常见的就是把复查影像表里的数据复查血肿体积、复查密度当作预测HE的特征。用复查结果预测复查结果属于典型的数据泄漏——训练时模型相当于直接看到了答案。解决画一条时间轴把所有特征按“入院时可得”与“入院后可知”分类。HE预测只能用入院首次影像、入院生命体征、既往病史、治疗方案中在影像复查前已确定的部分复查影像信息坚决不进入HE模型。对mRS预后预测治疗信息可以参与建模因为治疗发生在90天随访之前但要在报告里明确说明时间线逻辑。5.2 mRS多分类训练集准确率很高验证集一塌糊涂标签噪声被模型背下来了现象mRS七分类模型在训练集准确率超过70%但验证集加权Kappa只有0.3左右。原因mRS评分本身是医生主观判断同一份病历不同医生可能给出相差1分的评分某些边缘病例mRS 2与3之间标签本身就存在噪声。模型在训练集上把这些噪声标签当作确定真值硬学导致过拟合到错误模式。解决改用加权Kappa作为主指标它允许相邻等级的误差有较小惩罚更符合临床实际此外做一个敏感性分析——把训练集中mRS在2和3之间的样本标签随机互换一部分观察模型Kappa变化幅度如果波动巨大说明模型在依赖噪声特征。最直接的兜底方案是放弃七分类合并为0-2与3-6的二分类牺牲粒度但大幅降低标签噪声的干扰。5.3 HE阳性率只有18%模型全预测0也有82%准确率被准确率骗了现象模型在测试集上准确率82%你以为做得不错一看AUC只有0.52——模型根本没学到东西只是把所有样本都预测成了多数类0。原因分类器默认阈值0.5在样本极度不均衡时失效模型输出的概率普遍低于0.5所以全部被判为阴性。此时准确率完全被多数类主导不能反映真实预测能力。解决放弃准确率改看AUC、F1和召回率。优先在模型层面设置class_weightbalanced或scale_pos_weight让模型对少数类的错分给予更大惩罚如果效果还不够再用4.3节的阈值优化方法直接调决策边界而不是修改训练数据分布。我不建议在小样本医疗数据上盲目使用SMOTE过采样——合成样本对HE这种强临床特征的场景容易引入不存在的组合AUC反而下降。5.4 同一个患者的多条随访记录把训练集和验证集污染了现象单折验证AUC很高但换一个random_state后结果剧烈波动折间标准差超过0.1。原因如果数据集中同一个患者存在多次影像记录或多次随访记录而你按“记录行”而不是“患者ID”做交叉验证切分同名ID的行会同时出现在训练集和验证集中。模型在训练时已经见过这位患者验证时自然表现好这是另一种更隐蔽的泄漏。解决使用GroupKFold按patient_id分组切分保证同一患者的所有记录只在训练集或验证集里出现一次。from sklearn.model_selection import GroupKFold gkf GroupKFold(n_splits5) for train_idx, valid_idx in gkf.split(X, y, groupstrain[patient_id]): # 每组切分中同一个patient_id绝不会横跨train/valid assert len(set(train.loc[train_idx, patient_id]) set(train.loc[valid_idx, patient_id])) 0逻辑说明GroupKFold的groups参数传入患者ID后切分算法保证同一组内的样本不会被拆开。上面代码里的assert是一个一次性验证确保患者ID在两个集合里没有交集防止数据准备阶段的隐藏问题。参数说明n_splits5在几百个患者的数据集上比较合适如果患者总数不足200改为4折或3折否则每折训练样本太少模型不稳定。5.5 血肿体积出现几千毫升的样本单位的坑比算法深现象某列血肿体积的中位数是25最大值却是2400模型系数被这个离群值带偏去掉后AUC从0.60涨到0.72。原因数据录入可能混用了单位一部分记录是mL一部分是mm³或者存在直接录入错误的脏数据更隐蔽的是某些表用逗号作为小数点分隔符被pandas读成了字符串与数值混存。解决先对每个数值列做describe检查max/min是否落在临床合理区间。血肿体积超过80mL在临床上已经很危重超过150mL的存活率很低出现几百上千的值要逐条核对原始记录。处理上可以三位截断如保留99%分位数以下的数据或者直接log变换降低影响。这个动作必须在特征工程之前完成因为log变换后的离群值仍然可能偏离正常范围。6. 收尾验证提交文件与SHAP可解释性分析模型调完后把结果转换成竞赛要求的提交格式并做一份可解释性分析这一步决定你最终能拿到多少印象分。6.1 按赛题口径生成提交文件常见提交CSV只包含两列patient_id与预测值。需要注意赛题要求的是预测概率还是预测标签HE任务通常提交概率或其二分类结果mRS任务可能要求预测0-6整数等级。稳妥做法是先写一个唯一ID的DataFrame再按顺序merge预测结果确保行对齐不出错。# 预测测试集并生成提交文件 test pd.read_csv(data/test.csv) X_test test[features] proba_test final_model.predict_proba(X_test)[:, 1] sub pd.DataFrame({ patient_id: test[patient_id], HE概率: np.round(proba_test, 4) }) sub.to_csv(submit_he.csv, indexFalse, encodingutf-8)6.2 SHAP让黑盒模型能在论文里说话如果你是用了XGBoost这类黑盒模型必须用SHAP解释特征贡献。它输出每个样本中每个特征的贡献值可以直接画出全局特征重要性排序和个体决策解释图这是医疗建模报告里最容易被评委认可的部分。import shap explainer shap.TreeExplainer(xgb_model) shap_values explainer.shap_values(X_va) # 全局特征重要性排序 shap.summary_plot(shap_values, X_va, feature_namesfeatures)参数说明TreeExplainer专为树模型设计计算速度比KernelExplainer快一到两个数量级几百个样本也能跑动summary_plot输出的是每个特征在全部样本上的SHAP值分布横坐标表示对预测结果的影响方向与大小。最终报告里我会把SHAP图中的age、血肿体积、GCS评分三个特征单独拎出来做局部依赖图解释其与HE风险的关系这部分内容在答辩时比单纯放AUC曲线更能体现你对临床问题的理解。做完这些整个E题的复现链路就完整了从临床定义构造标签到多表合并与缺失处理到逻辑回归与XGBoost基线再到阈值优化、防泄漏验证和SHAP解释。我这些年参赛养成的习惯是拿到任何医疗数据集第一件事不是训练而是先画时间轴和数据字典——哪张表是入院前的哪张表是出院后的哪张表是随访的三者的顺序一旦搞错后面所有调参都是白费力气。希望这份完整的复现路径能帮你少走一轮弯路。本文还有配套的精品资源点击获取
返回列表