☰
基于Python与Jupyter的直肠癌淋巴结转移智能诊断实战
2026/10/1 17:10:25 网站建设 项目流程

简介:本资源面向计算机、人工智能及医学信息方向的本科生与研究生,提供一套基于Python与Jupyter实现的直肠癌淋巴结转移智能诊断完整方案,可用于毕业设计、课程设计或数据挖掘挑战赛复现。项目以U-Net为核心完成肿瘤区域预测,并围绕淋巴结转移诊断展开实验与结果分析。压缩包共25个文件,包含11个py脚本、4个ipynb交互式实验笔记、4个txt说明、4个png与1个gif展示图及1个md文档,整体约2.37MB,涵盖模型训练、测试、数据生成与结果可视化等模块。已有160人学习关注。读者可获得经过严格测试的源码、项目文档、实验记录与预测结果展示,并借助使用说明快速理解代码结构、复现实验流程,在此基础上延伸改进模型或迁移至相似医学影像任务。

1. 从一份病理表格到可复现的预测:直肠癌淋巴结转移智能诊断在做什么

拿到「基于python+Jupyter开发的直肠癌淋巴结转移的智能诊断」这个题目时,很多人第一反应是把它当成一次普通的 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", encoding="utf-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 -> 0,N1/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(strategy="median") 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())

这里的关键参数是strategy="median",相比均值,中位数对影像组学特征里的极端值更稳健。缺失指示列看起来多余,但在 200 例左右的小样本里,它经常能贡献几个百分点的 AUC。类别特征填"Unknown"而不是众数,是因为众数填充会把大量样本压到同一个类别,掩盖真实分布。

2.3 类别不平衡与特征共线性,建模前必须处理

直肠癌淋巴结转移数据里,N+ 比例通常在 30% 到 45% 之间,不算极端不平衡,但如果你的数据集 N+ 低于 25%,就要考虑class_weight="balanced"或 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, columns=cat_cols, drop_first=True) y = df["label"] # 逻辑回归管道:标准化 + L2 正则 lr_pipe = Pipeline([ ("scaler", StandardScaler()), ("clf", LogisticRegression(max_iter=2000, class_weight="balanced")) ]) lr_params = {"clf__C": [0.01, 0.1, 1, 10]} # 随机森林管道 rf_pipe = Pipeline([ ("clf", RandomForestClassifier(random_state=42, class_weight="balanced")) ]) rf_params = {"clf__n_estimators": [200, 500], "clf__max_depth": [3, 5, None]} # 外层 5 折,内层 3 折 outer_cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) inner_cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=42) for name, pipe, params in [("LR", lr_pipe, lr_params), ("RF", rf_pipe, rf_params)]: grid = GridSearchCV(pipe, params, cv=inner_cv, scoring="roc_auc", n_jobs=-1) scores = cross_val_score(grid, X, y, cv=outer_cv, scoring="roc_auc") print(f"{name} 外层 AUC: {scores.mean():.3f} ± {scores.std():.3f}")

逻辑说明:StratifiedKFold保证每折里 N+ 比例一致,避免某折全是 N0。class_weight="balanced"让少数类权重自动上调。scoring="roc_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_splits=5, shuffle=True, random_state=42) train_idx, test_idx = next(iter(skf.split(X, y))) rf = RandomForestClassifier(n_estimators=500, max_depth=5, class_weight="balanced", random_state=42) rf.fit(X.iloc[train_idx], y.iloc[train_idx]) result = permutation_importance(rf, X.iloc[test_idx], y.iloc[test_idx], n_repeats=30, random_state=42, scoring="roc_auc") imp_df = pd.DataFrame({ "feature": X.columns, "importance_mean": result.importances_mean, "importance_std": result.importances_std }).sort_values("importance_mean", ascending=False) print(imp_df.head(15))

n_repeats=30表示每个特征重复打乱 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_type="dot")

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, label=f"AUC={auc:.3f}") axes[0].plot([0, 1], [0, 1], "--", color="gray") 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_bins=5) axes[1].plot(prob_pred, prob_true, "o-") axes[1].plot([0, 1], [0, 1], "--", color="gray") 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, color="gray", linestyle="--") axes[2].set_xlabel("Threshold probability") axes[2].set_ylabel("Net benefit") plt.tight_layout() plt.show()

校准曲线的n_bins=5是因为小样本分太多箱每箱样本太少,曲线会剧烈抖动。决策曲线的公式是标准净收益计算,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_weight="balanced"或 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/(tp+fn):.3f}, 特异度={tn/(tn+fp):.3f}")

这个循环从低到高扫阈值,第一个让敏感度达标的就是候选。注意这只是内部测试集的结果,真正要上线还得做外部验证——换一家医院、换一个时间段的数据重新跑一遍。外部验证时不要重新调参,直接用之前定好的模型和阈值,否则等于又调了一次。

还有一个容易被忽略的技巧:把缺失指示特征单独拿出来看 SHAP 值。如果CEA_is_missing的 SHAP 值很高,说明「没查 CEA」这件事本身就和淋巴结转移相关,可能是病情紧急直接手术没来得及查。这种发现往往比模型本身更有临床讨论价值。

我自己在这个方向踩过最大的坑,是早期太迷信准确率,把大量时间花在调参上,结果换一批数据 AUC 掉了 0.15。后来才明白,医学数据挖掘里特征质量决定上限,模型只是逼近上限的工具。现在我的习惯是:拿到数据先花 60% 时间做字段核对和缺失分析,建模只占 20%,剩下 20% 留给校准和阈值讨论。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询