资讯动态

基于Python与Jupyter的直肠癌淋巴结转移智能诊断实战

发布时间:2026/10/1 17:10:29 来源:尧图企业网站定制
简介本资源面向计算机、人工智能及医学信息方向的本科生与研究生提供一套基于Python与Jupyter实现的直肠癌淋巴结转移智能诊断完整方案可用于毕业设计、课程设计或数据挖掘挑战赛复现。项目以U-Net为核心完成肿瘤区域预测并围绕淋巴结转移诊断展开实验与结果分析。压缩包共25个文件包含11个py脚本、4个ipynb交互式实验笔记、4个txt说明、4个png与1个gif展示图及1个md文档整体约2.37MB涵盖模型训练、测试、数据生成与结果可视化等模块。已有160人学习关注。读者可获得经过严格测试的源码、项目文档、实验记录与预测结果展示并借助使用说明快速理解代码结构、复现实验流程在此基础上延伸改进模型或迁移至相似医学影像任务。1. 从一份病理表格到可复现的预测直肠癌淋巴结转移智能诊断在做什么拿到「基于pythonJupyter开发的直肠癌淋巴结转移的智能诊断」这个题目时很多人第一反应是把它当成一次普通的 python数据分析与数据挖掘实战 作业读个 CSV、调个模型、打印准确率就交差。但真正做过医学数据挖掘的人都知道直肠癌淋巴结转移预测的难点从来不在模型本身而在数据。影像组学特征、病理 T 分期、分化程度、脉管侵犯这些字段往往来自不同科室、不同年份、不同采集设备缺失值和类别不平衡是常态。这个方向要解决的核心问题很具体在术前无创或微创条件下用结构化临床特征和影像特征判断患者淋巴结是否存在转移N0 vs N从而辅助医生决定是否需要新辅助治疗或扩大清扫范围。它适合三类人想找一个真实场景练手 python数据分析与数据挖掘 的学生、准备数据挖掘挑战赛的参赛者、以及需要快速验证特征组合的临床科研人员。Jupyter Notebook 在这里不是噱头而是把数据探索、特征工程、建模、结果展示串成一条可复现链路的最佳载体——每个 cell 的输出都能被审阅、被质疑、被复现这正是医学建模最需要的透明度。2. 数据到手先别建模直肠癌淋巴结转移数据的清洗与特征分层2.1 先搞清楚字段语义再谈缺失值处理拿到一份直肠癌数据集第一步不是df.isnull().sum()而是逐列确认语义。常见的字段包括年龄、性别、CEA、CA19-9、肿瘤下缘距肛缘距离、T 分期、N 分期这是标签必须剔除、分化程度、脉管侵犯、神经侵犯、肿瘤最大径、以及若干影像组学特征如纹理、形状、一阶统计量。这里有个血泪经验很多公开数据集里 N 分期字段同时出现在特征列和标签列如果不剔除模型准确率能飙到 0.99但那是标签泄漏不是模型聪明。import pandas as pd import numpy as np # 读取原始数据注意编码医学数据常见 GBK 或 UTF-8-sig df pd.read_csv(rectal_cancer_ln.csv, encodingutf-8-sig) # 打印字段名和类型先人工确认哪些是标签、哪些是 ID print(df.dtypes) print(df.columns.tolist()) # 明确标签列N0 为无转移N 为有转移 label_col N_stage # 剔除明显泄漏字段N 分期本身、病理报告编号、患者姓名等 leak_cols [N_stage, pathology_id, patient_name] feature_cols [c for c in df.columns if c not in leak_cols] # 把标签二值化N0 - 0N1/N2 - 1 df[label] df[label_col].apply(lambda x: 0 if str(x).strip() in [N0, 0] else 1) print(df[label].value_counts())这段代码的逻辑很直白先看类型再剔泄漏最后构造二分类标签。参数上唯一需要注意的是encoding医学数据导出常带 BOM用utf-8-sig能避免第一列列名多一个不可见字符。value_counts()的输出决定了后面要不要做重采样——如果 N 只占 20%直接训练会让模型偏向多数类。2.2 缺失值不是填得越满越好要按缺失机制分策略直肠癌数据里CEA、CA19-9 这类血液指标缺失通常是因为没做检查属于随机缺失而影像组学特征缺失往往和图像质量有关属于非随机缺失。对随机缺失中位数填充加缺失指示列是稳妥做法对非随机缺失直接填充会引入偏差更合理的做法是保留缺失指示让树模型自己去学「缺失本身是否有信息」。from sklearn.impute import SimpleImputer # 数值型特征中位数填充同时保留缺失指示 num_cols df[feature_cols].select_dtypes(include[np.number]).columns.tolist() cat_cols [c for c in feature_cols if c not in num_cols] # 为每个数值列增加缺失指示捕捉非随机缺失信息 for c in num_cols: if df[c].isnull().sum() 0: df[c _is_missing] df[c].isnull().astype(int) imputer SimpleImputer(strategymedian) df[num_cols] imputer.fit_transform(df[num_cols]) # 类别型特征单独填 Unknown不要用众数硬填 for c in cat_cols: df[c] df[c].fillna(Unknown) print(处理后缺失总数:, df[num_cols cat_cols].isnull().sum().sum())这里的关键参数是strategymedian相比均值中位数对影像组学特征里的极端值更稳健。缺失指示列看起来多余但在 200 例左右的小样本里它经常能贡献几个百分点的 AUC。类别特征填Unknown而不是众数是因为众数填充会把大量样本压到同一个类别掩盖真实分布。2.3 类别不平衡与特征共线性建模前必须处理直肠癌淋巴结转移数据里N 比例通常在 30% 到 45% 之间不算极端不平衡但如果你的数据集 N 低于 25%就要考虑class_weightbalanced或 SMOTE。另一个容易被忽略的是共线性肿瘤最大径和 T 分期高度相关多个影像组学纹理特征之间相关系数可能超过 0.9。树模型对共线性不敏感但逻辑回归会翻车。import seaborn as sns import matplotlib.pyplot as plt # 计算数值特征相关系数矩阵 corr df[num_cols].corr().abs() # 找出相关系数大于 0.9 的特征对 high_corr [(c1, c2) for c1 in corr.columns for c2 in corr.columns if c1 c2 and corr.loc[c1, c2] 0.9] print(高相关特征对:, high_corr) # 对每对高相关特征保留与标签相关性更高的那个 label_corr df[num_cols [label]].corr()[label].abs() drop_cols [] for c1, c2 in high_corr: drop_cols.append(c1 if label_corr[c1] label_corr[c2] else c2) drop_cols list(set(drop_cols)) print(建议剔除:, drop_cols)这段代码先算相关矩阵再按「与标签相关性更低者剔除」的原则去冗余。参数 0.9 是经验阈值医学数据里可以放宽到 0.85。注意不要用 PCA 去共线性因为主成分在临床解释上几乎没法讲清楚评审和医生都不买账。3. 在 Jupyter 里跑通建模链路从特征筛选到交叉验证3.1 用嵌套交叉验证代替单次 train_test_split小样本医学数据最忌讳一次train_test_split定终身。200 例数据换个随机种子 AUC 能差 0.08这就是玄学来源。正确做法是嵌套交叉验证外层 5 折评估泛化内层 3 折调参。Jupyter 里跑这个会有点慢但结果可信。from sklearn.model_selection import StratifiedKFold, GridSearchCV, cross_val_score from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import roc_auc_score, make_scorer # 构造特征矩阵注意排除标签和缺失指示以外的辅助列 X df[num_cols cat_cols [c for c in df.columns if c.endswith(_is_missing)]] X pd.get_dummies(X, columnscat_cols, drop_firstTrue) y df[label] # 逻辑回归管道标准化 L2 正则 lr_pipe Pipeline([ (scaler, StandardScaler()), (clf, LogisticRegression(max_iter2000, class_weightbalanced)) ]) lr_params {clf__C: [0.01, 0.1, 1, 10]} # 随机森林管道 rf_pipe Pipeline([ (clf, RandomForestClassifier(random_state42, class_weightbalanced)) ]) rf_params {clf__n_estimators: [200, 500], clf__max_depth: [3, 5, None]} # 外层 5 折内层 3 折 outer_cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) inner_cv StratifiedKFold(n_splits3, shuffleTrue, random_state42) for name, pipe, params in [(LR, lr_pipe, lr_params), (RF, rf_pipe, rf_params)]: grid GridSearchCV(pipe, params, cvinner_cv, scoringroc_auc, n_jobs-1) scores cross_val_score(grid, X, y, cvouter_cv, scoringroc_auc) print(f{name} 外层 AUC: {scores.mean():.3f} ± {scores.std():.3f})逻辑说明StratifiedKFold保证每折里 N 比例一致避免某折全是 N0。class_weightbalanced让少数类权重自动上调。scoringroc_auc比准确率更适合医学场景因为医生更关心排序能力而非硬分类。参数上逻辑回归的C越小正则越强小样本建议从 0.01 开始试随机森林max_depth限制在 3 到 5防止过拟合。3.2 特征重要性要看稳定不要只看一次输出随机森林的feature_importances_每次跑都不一样直接拿去写论文会被质疑。更稳的做法是用 permutation importance并在交叉验证的每一折上算看均值和标准差。from sklearn.inspection import permutation_importance # 用外层第一折的训练集拟合一个 RF做 permutation importance skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) train_idx, test_idx next(iter(skf.split(X, y))) rf RandomForestClassifier(n_estimators500, max_depth5, class_weightbalanced, random_state42) rf.fit(X.iloc[train_idx], y.iloc[train_idx]) result permutation_importance(rf, X.iloc[test_idx], y.iloc[test_idx], n_repeats30, random_state42, scoringroc_auc) imp_df pd.DataFrame({ feature: X.columns, importance_mean: result.importances_mean, importance_std: result.importances_std }).sort_values(importance_mean, ascendingFalse) print(imp_df.head(15))n_repeats30表示每个特征重复打乱 30 次次数越多均值越稳但计算量线性增长。输出里importance_std大的特征说明重要性不稳定写报告时要谨慎。常见做法是只保留重要性均值大于 0 且标准差小于均值一半的特征再跑一次建模对比。3.3 模型解释SHAP 值让医生看得懂医学场景里黑箱模型很难被接受。SHAP 能把每个样本的预测拆解到每个特征上医生能看到「这个患者因为 CEA 高、脉管侵犯阳性所以被判定为高风险」。import shap # 用训练好的 RF 模型做 SHAP 解释 explainer shap.TreeExplainer(rf) shap_values explainer.shap_values(X.iloc[test_idx]) # 二分类输出取正类N的 SHAP 值 shap.summary_plot(shap_values[1], X.iloc[test_idx], plot_typedot)TreeExplainer对树模型是精确计算速度快。shap_values[1]对应 N 类。summary_plot 里每个点是一个患者横轴是 SHAP 值颜色是特征取值高低。如果 CEA 高值集中在右侧说明高 CEA 推动模型判为 N符合临床认知。这一步在 Jupyter 里直接出图比任何文字解释都有说服力。4. 预测结果展示与实验记录让 Jupyter Notebook 自己说话4.1 ROC、校准曲线、决策曲线三件套只放一张 ROC 曲线是不够的。医学预测模型还要看校准度预测概率和实际发生率是否一致和临床净收益决策曲线分析。这三张图放在 Jupyter 里就是一份完整的实验报告。from sklearn.calibration import calibration_curve from sklearn.metrics import roc_curve import matplotlib.pyplot as plt # 用测试折的预测概率 y_prob rf.predict_proba(X.iloc[test_idx])[:, 1] fpr, tpr, _ roc_curve(y.iloc[test_idx], y_prob) auc roc_auc_score(y.iloc[test_idx], y_prob) fig, axes plt.subplots(1, 3, figsize(15, 4)) # ROC axes[0].plot(fpr, tpr, labelfAUC{auc:.3f}) axes[0].plot([0, 1], [0, 1], --, colorgray) axes[0].set_xlabel(1 - Specificity) axes[0].set_ylabel(Sensitivity) axes[0].legend() # 校准曲线 prob_true, prob_pred calibration_curve(y.iloc[test_idx], y_prob, n_bins5) axes[1].plot(prob_pred, prob_true, o-) axes[1].plot([0, 1], [0, 1], --, colorgray) axes[1].set_xlabel(Predicted probability) axes[1].set_ylabel(Observed fraction) # 决策曲线阈值从 0.1 到 0.9计算净收益 thresholds np.arange(0.1, 0.9, 0.05) net_benefit [] for t in thresholds: tp ((y_prob t) (y.iloc[test_idx] 1)).sum() fp ((y_prob t) (y.iloc[test_idx] 0)).sum() n len(test_idx) net_benefit.append(tp / n - fp / n * (t / (1 - t))) axes[2].plot(thresholds, net_benefit, o-) axes[2].axhline(0, colorgray, linestyle--) axes[2].set_xlabel(Threshold probability) axes[2].set_ylabel(Net benefit) plt.tight_layout() plt.show()校准曲线的n_bins5是因为小样本分太多箱每箱样本太少曲线会剧烈抖动。决策曲线的公式是标准净收益计算t/(1-t)是阈值 odds。如果曲线在大部分阈值区间高于 0 和两条默认线全治、全不治说明模型有临床实用价值。4.2 用 Jupyter 的 nbconvert 固化实验记录Jupyter 最大的优势是可复现但前提是你得把 notebook 导出成带输出的 HTML 或 PDF否则别人打开你的.ipynb看不到图。常见做法是在命令行跑jupyter nbconvert --to html --execute rectal_ln_diagnosis.ipynb \ --output rectal_ln_report.html--execute会重新跑一遍所有 cell确保输出和代码一致。如果某个 cell 依赖本地路径记得在 notebook 开头用%cd或绝对路径否则换台机器就翻车。导出的 HTML 可以直接作为项目文档的一部分比截图靠谱。5. 避坑与排查直肠癌淋巴结转移建模里最容易翻车的五件事5.1 现象AUC 高得离谱超过 0.98原因标签泄漏。N 分期字段、病理报告结论、甚至某些影像组学特征是在已知淋巴结转移后提取的天然带标签信息。解决逐列核对字段来源凡是术后或病理确认后才产生的字段一律剔除再用df.corr()[label]检查有没有单特征相关性超过 0.7 的异常列。5.2 现象换随机种子 AUC 波动超过 0.1原因样本量太小单次划分不稳定。解决改用嵌套交叉验证报告均值和标准差而不是单次结果。如果标准差仍然很大考虑增加数据或做 bootstrap 置信区间不要只报一个最好看的数。5.3 现象Jupyter 里ModuleNotFoundError: No module named sklearn原因Jupyter 内核和安装库的 Python 环境不是同一个。常见于 miniconda 装完后又用系统 Python 装了包。解决在 notebook 里跑import sys; print(sys.executable)确认内核路径然后用!{sys.executable} -m pip install scikit-learn装到当前内核。不要直接!pip install那可能装到另一个环境。5.4 现象类别特征 one-hot 后维度爆炸模型跑不动原因分化程度、肿瘤位置等字段类别太多pd.get_dummies直接展开成几百列。解决先做类别合并把出现次数少于 10 的类别归为Other再 one-hot。或者改用目标编码但目标编码必须在交叉验证内部做否则泄漏。5.5 现象校准曲线严重偏离对角线原因用了class_weightbalanced或 SMOTE 后预测概率被系统性抬高。解决如果最终要输出概率训练时不要用重采样改用scale_pos_weight或直接在阈值上调整。已经用了重采样的用 Platt scaling 或 isotonic regression 做后校准校准集必须独立于训练集。6. 把模型推到能用的边缘阈值选择与外部验证的一个技巧模型跑出 AUC 0.85 只是起点真正决定它能不能进临床的是阈值。默认 0.5 在医学场景里几乎总是错的因为漏诊一个 N 患者的代价远大于误判一个 N0。我一般会这样做先画出决策曲线找到净收益最高的阈值区间再结合临床可接受的敏感度下限比如要求敏感度不低于 0.90反推阈值。# 在测试集上找敏感度 0.90 的最小阈值 from sklearn.metrics import confusion_matrix best_thr 1.0 for t in np.arange(0.05, 0.95, 0.01): pred (y_prob t).astype(int) tn, fp, fn, tp confusion_matrix(y.iloc[test_idx], pred).ravel() sensitivity tp / (tp fn) if sensitivity 0.90: best_thr t break print(f满足敏感度0.90的最小阈值: {best_thr:.2f}) # 用该阈值重新评估 pred_final (y_prob best_thr).astype(int) tn, fp, fn, tp confusion_matrix(y.iloc[test_idx], pred_final).ravel() print(f敏感度{tp/(tpfn):.3f}, 特异度{tn/(tnfp):.3f})这个循环从低到高扫阈值第一个让敏感度达标的就是候选。注意这只是内部测试集的结果真正要上线还得做外部验证——换一家医院、换一个时间段的数据重新跑一遍。外部验证时不要重新调参直接用之前定好的模型和阈值否则等于又调了一次。还有一个容易被忽略的技巧把缺失指示特征单独拿出来看 SHAP 值。如果CEA_is_missing的 SHAP 值很高说明「没查 CEA」这件事本身就和淋巴结转移相关可能是病情紧急直接手术没来得及查。这种发现往往比模型本身更有临床讨论价值。我自己在这个方向踩过最大的坑是早期太迷信准确率把大量时间花在调参上结果换一批数据 AUC 掉了 0.15。后来才明白医学数据挖掘里特征质量决定上限模型只是逼近上限的工具。现在我的习惯是拿到数据先花 60% 时间做字段核对和缺失分析建模只占 20%剩下 20% 留给校准和阈值讨论。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑