接了一个医学生毕设的数据分析项目,我是怎么把生存分析做成可复现交付的
这篇文章记录一个我实际接手过的医学数据分析项目。
项目来自一位医学生的毕业设计,主题并不是简单的“跑一个 Cox 回归”或者“画一张 KM 曲线”,而是要把肿瘤患者在医疗路径不同阶段的延迟真正拆开来分析,并且把整套分析流程做成可复现、可扩展、可继续交付的工程化项目。
我最后选择的做法,不是只交一个结果表,而是把它做成了一套完整的 R 分析框架:
- 上游有数据导入与清洗
- 中间有阈值搜索、KM、log-rank、Cox
- 下游有随机生存森林(RSF)
- 最终还能一键导出论文图件和结果表
对我来说,这类项目真正有价值的地方,不在于“模型多高级”,而在于:你能不能把一个医学问题翻译成一套稳定、清晰、别人接手后也能继续跑的分析流程。
一、这个项目在解决什么问题
项目的核心问题其实很明确:
肿瘤患者在不同医疗阶段发生的延迟,是否会影响生存结局?
这里面被重点分析的延迟有两类:
- 就医延迟:从症状出现或首次不适,到真正去医院就诊;
- 治疗延迟:从确诊,到第一次开始治疗。
另外还有一个诊断延迟变量,但在当前版本里,它主要作为协变量进入模型,用来控制混杂,而不是主分析对象。
这个项目最后不是只回答“有没有影响”,而是要把问题拆成几层:
- 延迟时间和生存结局到底有没有关联;
- 能不能为就医延迟、治疗延迟各自找到一个对生存最敏感的阈值;
- 按阈值分组之后,生存曲线有没有差异;
- 这种差异在 Cox 多变量模型里还能不能站得住;
- 在更灵活的非线性模型里,两类延迟的预测贡献谁更大。
这也是这个项目最有意思的地方:它不只是一个统计作业,而是一个完整的临床路径 + 生存结局 + 工程交付问题。
二、为什么这类项目不能只“跑一个模型”
医学毕设里最常见的误区,就是把项目理解成:
- 导入 Excel;
- 挑几个变量;
- 跑个单因素和多因素 Cox;
- 画张图;
- 结束。
但这个项目如果真这么做,最后大概率会踩很多坑。
1. 延迟变量本身不干净
医疗数据和 Kaggle 数据完全不是一个画风。
延迟时间字段里经常会混入:
- “1周”
- “4日多”
- “两周”
- “半天”
- 甚至夹杂临床文本记录
如果前面的解析逻辑不稳,后面的阈值搜索、KM 分组、Cox 回归都会跟着出问题。
2. 阈值不是随便拍脑袋定的
这次项目里,延迟不是直接按“30 天”“90 天”这种经验值切,而是希望找到对生存差异最敏感的 cutpoint。
这就要求阈值搜索过程本身可解释、可追溯,而不是“代码一跑,给你一个结果,你也不知道为什么是它”。
3. 结果不能只看显著性
临床项目里,很多变量会在单因素里有趋势,但一放进多变量模型里,效应就会减弱。
这不是模型错了,而是说明:
- 变量之间存在混杂;
- 疾病严重程度可能才是主导因素;
- 延迟变量可能更多是“伴随现象”,而不是唯一驱动因素。
所以这个项目最后必须同时交付:
- 描述性统计
- 阈值搜索依据
- KM / log-rank
- Cox
- 非线性预测模型(RSF)
- 图表与结果表
只有这样,结论才比较稳。
三、我最后是怎么搭这个项目结构的
为了让后续继续扩样本、补敏感性分析时不推倒重来,我把它做成了一个比较标准的项目结构。
1 | Tumor_Survival_Analysis/ |
这个结构有几个好处:
1. 数据层和结果层是分开的
原始数据、处理后的分析数据、最终结果表、论文图件,各自单独放,避免写到后面目录越来越乱。
2. 每一步都能单独跑
如果后面只是修正某个阈值逻辑,不需要把整套流程都重跑。
3. 对交付很友好
你给学生、老师或者下一个接手的人时,不需要解释半天:
- 哪个脚本干什么
- 哪个目录存什么
- 图是哪里出来的
别人看目录基本就能明白项目怎么走。
四、五个脚本分别负责什么
这一套里,我把任务拆成了五个核心脚本。
1. 01_import_clean.R:先把数据真正处理成“能分析”的样子
这一步主要做几件事:
- 读取原始 Excel 数据;
- 统一变量命名;
- 处理类型转换;
- 定义核心分析变量;
- 做基础质量检查;
- 输出后续分析使用的数据集。
这里面最重要的一点,是先把生存分析真正依赖的主变量整理好:
followup_dayseventtreat_delay_daysvisit_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 | 01_import_clean.R |
就能把新的分析数据、阈值、图表和结果表全部更新掉。
2. 做敏感性分析
后面很适合补几类内容:
- 在 FIGO 分期层内做分层分析;
- 对极端延迟值做截尾或变换;
- 比较不同阈值策略的一致性;
- 做重复切分或交叉验证,看 RSF 结果稳不稳。
3. 进一步做论文化整理
如果真的往毕业论文或小论文方向走,这个框架已经足够支撑:
- 主文放 KM、Cox、延迟对比主图;
- 补充材料放扫描表、审计文件、更多 RSF 输出;
- 讨论部分重点解释“延迟效应为什么在调整后减弱”。
九、对我来说,这个项目最像一次“分析工程化”练习
如果只从技术名词上看,这个项目包含:
- R 语言
- 生存分析
- Kaplan–Meier
- log-rank
- Cox 回归
- 随机生存森林
但真正让我觉得有意思的,不是学会了某个模型,而是把一个医学问题做成了下面这种状态:
- 数据输入是稳的;
- 阈值逻辑是可追溯的;
- 图表输出是统一的;
- 结果解释是分层次的;
- 后续扩样本时不需要推倒重来。
这其实比“会不会某个包函数”更重要。
因为真实项目里,别人买单的从来不是你会不会敲 coxph(),而是你能不能把一套分析真正交付出去。
十、最后总结一下
这次项目如果用一句话概括,我会说:
这不是一个“帮忙跑统计”的活,而是一个把医学问题翻译成可复现分析框架的项目。
当前版本下,我已经把它做成了一条比较完整的链路:
- 从数据清洗开始;
- 到治疗延迟阈值识别;
- 到 KM / Cox;
- 到 RSF;
- 再到就医延迟与治疗延迟的对比;
- 最后落到统一图表和结果文件交付。
如果后面继续接这类项目,我还是会坚持这个思路:
先把问题定义清楚,再把流程搭清楚,最后才是模型本身。
因为模型只是工具,真正稀缺的是把问题做成体系的能力。