这篇文章记录一个我实际接手过的医学数据分析项目。

项目来自一位医学生的毕业设计,主题并不是简单的“跑一个 Cox 回归”或者“画一张 KM 曲线”,而是要把肿瘤患者在医疗路径不同阶段的延迟真正拆开来分析,并且把整套分析流程做成可复现、可扩展、可继续交付的工程化项目。

我最后选择的做法,不是只交一个结果表,而是把它做成了一套完整的 R 分析框架:

  • 上游有数据导入与清洗
  • 中间有阈值搜索、KM、log-rank、Cox
  • 下游有随机生存森林(RSF)
  • 最终还能一键导出论文图件和结果表

对我来说,这类项目真正有价值的地方,不在于“模型多高级”,而在于:你能不能把一个医学问题翻译成一套稳定、清晰、别人接手后也能继续跑的分析流程。

一、这个项目在解决什么问题

项目的核心问题其实很明确:

肿瘤患者在不同医疗阶段发生的延迟,是否会影响生存结局?

这里面被重点分析的延迟有两类:

  1. 就医延迟:从症状出现或首次不适,到真正去医院就诊;
  2. 治疗延迟:从确诊,到第一次开始治疗。

另外还有一个诊断延迟变量,但在当前版本里,它主要作为协变量进入模型,用来控制混杂,而不是主分析对象。

这个项目最后不是只回答“有没有影响”,而是要把问题拆成几层:

  • 延迟时间和生存结局到底有没有关联;
  • 能不能为就医延迟、治疗延迟各自找到一个对生存最敏感的阈值;
  • 按阈值分组之后,生存曲线有没有差异;
  • 这种差异在 Cox 多变量模型里还能不能站得住;
  • 在更灵活的非线性模型里,两类延迟的预测贡献谁更大。

这也是这个项目最有意思的地方:它不只是一个统计作业,而是一个完整的临床路径 + 生存结局 + 工程交付问题。

二、为什么这类项目不能只“跑一个模型”

医学毕设里最常见的误区,就是把项目理解成:

  • 导入 Excel;
  • 挑几个变量;
  • 跑个单因素和多因素 Cox;
  • 画张图;
  • 结束。

但这个项目如果真这么做,最后大概率会踩很多坑。

1. 延迟变量本身不干净

医疗数据和 Kaggle 数据完全不是一个画风。

延迟时间字段里经常会混入:

  • “1周”
  • “4日多”
  • “两周”
  • “半天”
  • 甚至夹杂临床文本记录

如果前面的解析逻辑不稳,后面的阈值搜索、KM 分组、Cox 回归都会跟着出问题。

2. 阈值不是随便拍脑袋定的

这次项目里,延迟不是直接按“30 天”“90 天”这种经验值切,而是希望找到对生存差异最敏感的 cutpoint

这就要求阈值搜索过程本身可解释、可追溯,而不是“代码一跑,给你一个结果,你也不知道为什么是它”。

3. 结果不能只看显著性

临床项目里,很多变量会在单因素里有趋势,但一放进多变量模型里,效应就会减弱。

这不是模型错了,而是说明:

  • 变量之间存在混杂;
  • 疾病严重程度可能才是主导因素;
  • 延迟变量可能更多是“伴随现象”,而不是唯一驱动因素。

所以这个项目最后必须同时交付:

  • 描述性统计
  • 阈值搜索依据
  • KM / log-rank
  • Cox
  • 非线性预测模型(RSF)
  • 图表与结果表

只有这样,结论才比较稳。

三、我最后是怎么搭这个项目结构的

为了让后续继续扩样本、补敏感性分析时不推倒重来,我把它做成了一个比较标准的项目结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Tumor_Survival_Analysis/
├─ data/
│ └─ processed/
├─ scripts/
│ ├─ 01_import_clean.R
│ ├─ 02_cutpoint_km_cox.R
│ ├─ 03_rsf_model.R
│ ├─ 04_make_all_figures.R
│ └─ 05_visit_delay_analysis.R
├─ results/
│ ├─ rsf/
│ ├─ visit_delay/
│ └─ rsf_delays_compare/
└─ figures_all/

这个结构有几个好处:

1. 数据层和结果层是分开的

原始数据、处理后的分析数据、最终结果表、论文图件,各自单独放,避免写到后面目录越来越乱。

2. 每一步都能单独跑

如果后面只是修正某个阈值逻辑,不需要把整套流程都重跑。

3. 对交付很友好

你给学生、老师或者下一个接手的人时,不需要解释半天:

  • 哪个脚本干什么
  • 哪个目录存什么
  • 图是哪里出来的

别人看目录基本就能明白项目怎么走。

四、五个脚本分别负责什么

这一套里,我把任务拆成了五个核心脚本。

1. 01_import_clean.R:先把数据真正处理成“能分析”的样子

这一步主要做几件事:

  • 读取原始 Excel 数据;
  • 统一变量命名;
  • 处理类型转换;
  • 定义核心分析变量;
  • 做基础质量检查;
  • 输出后续分析使用的数据集。

这里面最重要的一点,是先把生存分析真正依赖的主变量整理好:

  • followup_days
  • event
  • treat_delay_days
  • visit_delay_days

如果这一步没打稳,后面所有统计结论都不可靠。

2. 02_cutpoint_km_cox.R:治疗延迟的阈值、分组、生存分析

这个脚本主要做三件事:

  • 找治疗延迟的最佳阈值;
  • 按阈值把人群分成延迟 / 不延迟两组;
  • 跑 KM、log-rank 和 Cox。

阈值选择上,我没有只依赖单一方法,而是做了一个比较稳妥的双保险:

  • 优先用 surv_cutpoint
  • 如果阈值不稳定,就自动回退到 log-rank 扫描阈值法

这样做的好处是:即使数据分布右偏、存在离群值,整个 cutpoint 过程依然有兜底,而且扫描过程还能输出出来,方便之后追溯。

3. 03_rsf_model.R:把问题放进非线性框架里再看一遍

单纯 Cox 的好处是好解释,但它毕竟还是偏线性、偏参数模型。

所以我又补了一个随机生存森林(RSF)模块,专门做两种建模方式对比:

  • Model A:把治疗延迟作为阈值分组变量;
  • Model B:把治疗延迟作为连续变量直接进模型。

这样一来,我既能回答“按阈值分层还有没有用”,也能回答“如果不强行分组,连续时间本身能不能提供更多预测信息”。

4. 04_make_all_figures.R:一键导出论文主图

这一步其实很像交付时的“收尾工程”,但我觉得它非常重要。

我把主线图都统一导出到了 figures_all/,包括:

  • 流程图
  • 延迟分布图
  • KM 曲线
  • Cox 森林图
  • RSF 变量重要性图

这样后面无论是写论文、做汇报,还是交给别人修改图注,都只需要围绕一个固定目录工作。

5. 05_visit_delay_analysis.R:把同样逻辑复用到“就医延迟”

这个脚本是我比较满意的一部分,因为它不是从头再写一遍,而是把治疗延迟那一套分析逻辑复用了过去。

它完成的事情包括:

  • 为就医延迟自动找阈值;
  • 跑 KM 和 Cox;
  • 建联合 RSF 模型;
  • 比较“就医延迟”和“治疗延迟”谁的预测贡献更大。

这一步很关键,因为项目主线不是只看某一个时间点,而是想比较医疗路径不同阶段的延迟,到底哪个阶段更值得关注

五、这个项目里我用到的核心分析思路

如果只从统计学层面看,这套流程可以概括成下面几层。

1. 先做阈值识别

对延迟这种变量,最常见的处理方式有两种:

  • 当作连续变量;
  • 切成组别变量。

如果直接分组,最关键的问题就是:阈值怎么来?

我这里采用的思路是:

  • 先用最大检验统计量方法找候选阈值;
  • 如果返回不稳定,就用 log-rank 扫描法枚举切点;
  • 同时设置最小分组比例,避免切出来的组太极端。

这样得到的阈值,比“经验拍一个 30 天”要扎实得多。

2. 再做 KM / log-rank

这一步的价值不是“图好看”,而是把延迟分组后的生存差异先直观展示出来。

如果连 KM 曲线都没有分离趋势,后面的多变量分析通常也很难讲。

3. 然后做 Cox 回归

我这里不会只报一个单因素结果,而是至少给两层:

  • 未调整 Cox
  • 多变量调整 Cox

因为医学项目里你必须知道:

  • 这个变量本身是不是有趋势;
  • 控制掉疾病严重程度后,它还能不能保留独立作用。

4. 最后再用 RSF 做非线性补充

RSF 不是为了替代 Cox,而是为了补充两个问题:

  • 延迟变量在非线性框架里还有没有预测贡献;
  • 如果把就医延迟和治疗延迟一起放进来,谁更重要。

这一步很适合作为博客或项目复盘里的“技术亮点”,因为它体现的不是简单套公式,而是完整地从解释型分析走到预测型分析

六、当前已经跑出来的结果,能说明什么

截至目前,治疗延迟这一条主线已经给出了比较清晰的结果。

1. 治疗延迟的最佳阈值是 28 天

也就是说,按当前数据来看,把患者分成:

  • ≤ 28 天
  • > 28 天

是比较敏感的一种切法。

2. KM 曲线显示延迟组整体生存更差,但更像“趋势性差异”

当前结果里,延迟组的生存曲线整体更差,log-rank 的 p 值大约在 0.059 左右。

这个结果很典型:

  • 方向上是有信号的;
  • 但还没有强到可以非常武断地下结论。

3. Cox 回归里,治疗延迟的效应在调整后变弱了

未调整模型下,治疗延迟对应的 HR 大约在 1.53,已经接近显著;

但把核心临床变量一起放进模型后,HR 大约降到 1.36,p 值也变大了。

这说明一个很现实的问题:

延迟变量本身可能和疾病严重程度纠缠在一起。

你不能只看单因素结果,就说“延迟一定直接导致更差预后”。

4. 真正的强预后因素还是疾病严重程度本身

在当前结果里,像 FIGO 分期、淋巴结转移这类变量,对生存结局的解释力明显更强。

这其实也符合临床常识。

延迟当然重要,但很多时候,真正决定患者远期结局的,还是肿瘤本身到了什么程度。

5. RSF 也支持了这个判断

RSF 模型里,重要性更靠前的通常还是分期、淋巴结状态等变量;

延迟变量并不是完全没用,但整体贡献会更靠后。

这并不意味着延迟不值得分析,反而说明:

  • 它更适合被理解为一个“次级风险因素”;
  • 对极端延迟个体可能更敏感;
  • 在预测层面和在因果解释层面,需要分开看。

七、这个项目里真正难的,不是统计学公式,而是“工程细节”

如果你真的做过这类项目,会发现最花时间的地方往往不是模型,而是下面这些东西。

1. 数据字段不规范

同一类信息可能出现:

  • 中英文混合命名;
  • 重复列名;
  • 文本、数字、临床标记混在一起;
  • 缺失和非法值穿插。

你不先把输入层做扎实,后面所有分析都可能是建立在错数据上的。

2. 阈值搜索必须能解释

很多人一看到 cutpoint,就默认它是模型“自动算出来的”。

但如果你不能说清楚:

  • 它是怎么选的;
  • 为什么不是别的值;
  • 当主方法失效时怎么兜底;

那这个阈值在答辩或写论文时会很虚。

3. 图表和表格要能直接交付

博客里大家喜欢只展示“分析过程”,但真实项目里不够。

交付时必须考虑:

  • 老师能不能直接看图;
  • 学生能不能直接拿去写论文;
  • 后续扩样本时能不能继续复用;
  • 新结果能不能替换老结果而不重构整个项目。

所以我最后把图件、表格、RDS、CSV 都做了固定输出。

八、如果后面继续扩样本,这个框架怎么往下接

我觉得这个项目有价值的一点,就是它不是“一次性脚本”。

如果后面样本从当前规模继续往上加,这套流程还可以自然往下接:

1. 直接重跑全链路

按顺序跑:

1
2
3
4
5
01_import_clean.R
02_cutpoint_km_cox.R
03_rsf_model.R
04_make_all_figures.R
05_visit_delay_analysis.R

就能把新的分析数据、阈值、图表和结果表全部更新掉。

2. 做敏感性分析

后面很适合补几类内容:

  • 在 FIGO 分期层内做分层分析;
  • 对极端延迟值做截尾或变换;
  • 比较不同阈值策略的一致性;
  • 做重复切分或交叉验证,看 RSF 结果稳不稳。

3. 进一步做论文化整理

如果真的往毕业论文或小论文方向走,这个框架已经足够支撑:

  • 主文放 KM、Cox、延迟对比主图;
  • 补充材料放扫描表、审计文件、更多 RSF 输出;
  • 讨论部分重点解释“延迟效应为什么在调整后减弱”。

九、对我来说,这个项目最像一次“分析工程化”练习

如果只从技术名词上看,这个项目包含:

  • R 语言
  • 生存分析
  • Kaplan–Meier
  • log-rank
  • Cox 回归
  • 随机生存森林

但真正让我觉得有意思的,不是学会了某个模型,而是把一个医学问题做成了下面这种状态:

  • 数据输入是稳的;
  • 阈值逻辑是可追溯的;
  • 图表输出是统一的;
  • 结果解释是分层次的;
  • 后续扩样本时不需要推倒重来。

这其实比“会不会某个包函数”更重要。

因为真实项目里,别人买单的从来不是你会不会敲 coxph(),而是你能不能把一套分析真正交付出去。

十、最后总结一下

这次项目如果用一句话概括,我会说:

这不是一个“帮忙跑统计”的活,而是一个把医学问题翻译成可复现分析框架的项目。

当前版本下,我已经把它做成了一条比较完整的链路:

  • 从数据清洗开始;
  • 到治疗延迟阈值识别;
  • 到 KM / Cox;
  • 到 RSF;
  • 再到就医延迟与治疗延迟的对比;
  • 最后落到统一图表和结果文件交付。

如果后面继续接这类项目,我还是会坚持这个思路:

先把问题定义清楚,再把流程搭清楚,最后才是模型本身。

因为模型只是工具,真正稀缺的是把问题做成体系的能力。