Skip to contents

这一步要解决什么问题

这篇教程是 leo.ukb 生存分析部分的第二步。主 Cox 分析之后,最常见的 下一个问题通常是:

exposure 和结局的关联,会不会在不同人群中表现得不一样?如果不一样, 是在哪个尺度上不一样?

这个问题实际上会对应三类相关但不同的输出:

  1. 正式的乘性交互检验
  2. 用来展示各层 HR 模式的 subgroup 表
  3. 用于 binary-binary 对比的加性交互结果

Step 2 的重点不是把这三者混在一起,而是学会什么时候该用哪一个、结果 该怎么读。

先看数据和变量角色

这里继续使用 survival::lung,但这次我们会把数据框整理成更适合教学的 结构,而不只是为了跑模型。

lung_df <- dplyr::transmute(
  survival::lung,
  outcome = as.integer(status == 2),
  outcome_censor = time / 365.25,
  age = age,
  age_high = factor(
    ifelse(age >= 65, "Older", "Younger"),
    levels = c("Younger", "Older")
  ),
  sex = factor(sex, levels = c(1, 2), labels = c("Male", "Female")),
  ecog_group3 = factor(
    dplyr::case_when(
      ph.ecog == 0 ~ "ECOG0",
      ph.ecog == 1 ~ "ECOG1",
      ph.ecog >= 2 ~ "ECOG2plus",
      TRUE ~ NA_character_
    ),
    levels = c("ECOG0", "ECOG1", "ECOG2plus")
  ),
  ecog_binary = factor(
    ifelse(ph.ecog >= 2, "Poor", "Good"),
    levels = c("Good", "Poor")
  )
)

正式建模前,先像真实研究一样看变量角色和分布。

dplyr::glimpse(lung_df)
#> Rows: 228
#> Columns: 7
#> $ outcome        <int> 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
#> $ outcome_censor <dbl> 0.8377823, 1.2457221, 2.7652293, 0.5749487, 2.4175222, 
#> $ age            <dbl> 74, 68, 56, 57, 60, 74, 68, 71, 53, 61, 57, 68, 68, 60,
#> $ age_high       <fct> Older, Older, Younger, Younger, Younger, Older, Older, 
#> $ sex            <fct> Male, Male, Male, Male, Male, Male, Female, Female, Mal…
#> $ ecog_group3    <fct> ECOG1, ECOG0, ECOG0, ECOG1, ECOG0, ECOG1, ECOG2plus, EC…
#> $ ecog_binary    <fct> Good, Good, Good, Good, Good, Good, Poor, Poor, Good, P…
Step 2 教学数据中的变量角色
Variable Type Teaching role
outcome 二分类事件指标 结局事件
outcome_censor 按年计的随访时间 结局时间
age 连续 exposure 乘性交互示例中的主 exposure
sex 二分类 modifier 乘性交互中的二分类 effect modifier
ecog_group3 ECOG 体能状态(三组) subgroup 展示中的多分类 modifier
age_high 二分类年龄分组 加性交互教学用的年龄二分类变量
ecog_binary ECOG 体能状态(二分) 加性交互教学用的二分化 ECOG 体能状态变量

交互分析之前,最好还要看一下各组的事件数和 binary-binary 联合分布。

knitr::kable(
  dplyr::count(lung_df, ecog_group3, outcome),
  caption = "多分类 subgroup 内的事件分布"
)
多分类 subgroup 内的事件分布
ecog_group3 outcome n
ECOG0 0 26
ECOG0 1 37
ECOG1 0 31
ECOG1 1 82
ECOG2plus 0 6
ECOG2plus 1 45
NA 1 1
knitr::kable(
  dplyr::count(lung_df, age_high, ecog_binary),
  caption = "加性交互教学示例中的四格分布"
)
加性交互教学示例中的四格分布
age_high ecog_binary n
Younger Good 107
Younger Poor 20
Younger NA 1
Older Good 69
Older Poor 31

第二张表尤其重要,因为加性交互需要 binary-binary 结构,而且四个联合组 都要有样本。

建模前先决定你要回答哪个尺度上的问题

最容易避免混乱的方法,是先决定科学问题,再决定函数。

decision_table <- data.frame(
  Goal = c(
    "正式乘性交互检验",
    "subgroup 展示",
    "加性交互"
  ),
  Exposure_types = c(
    "连续、二分类、多分类都可以",
    "连续、二分类、多分类都可以",
    "当前建议二分类"
  ),
  Modifier_types = c(
    "连续、二分类、多分类都可以",
    "二分类或多分类",
    "当前建议二分类"
  ),
  Main_function = c(
    "leo_cox_interaction()",
    "leo_cox_subgroup()",
    "leo_cox_add_interaction()"
  ),
  Main_question = c(
    "效应是否在 HR 尺度上发生改变?",
    "每一层内部的 HR 模式是什么?",
    "联合效应是否超过相加预期?"
  )
)

knitr::kable(
  decision_table,
  caption = "Step 2 里三类工具分别适合回答什么问题"
)
Step 2 里三类工具分别适合回答什么问题
Goal Exposure_types Modifier_types Main_function Main_question
正式乘性交互检验 连续、二分类、多分类都可以 连续、二分类、多分类都可以 leo_cox_interaction() 效应是否在 HR 尺度上发生改变?
subgroup 展示 连续、二分类、多分类都可以 二分类或多分类 leo_cox_subgroup() 每一层内部的 HR 模式是什么?
加性交互 当前建议二分类 当前建议二分类 leo_cox_add_interaction() 联合效应是否超过相加预期?

可以把这张表记成一句话:

  • 乘性交互最通用
  • subgroup 表要求 modifier 是分类变量,因为它需要显式分层
  • 加性交互最严格,当前最适合 binary-binary 对比

如果 modifier 是连续变量,leo_cox_interaction() 仍然可以做正式检验; 但 leo_cox_subgroup() 通常要等你先定义一个有临床意义的分组之后才适合用。

乘性交互:最通用的正式检验

在 Cox 模型里,乘性交互问的是:

exposure 和结局的关联,会不会随着 modifier 不同而在 HR 尺度上改变?

示例 1:连续 exposure × 二分类 modifier

这里先问:age 和结局的关联,会不会因 sex 不同而改变?

res_inter_sex <- leo_cox_interaction(
  df = lung_df,
  y_out = c("outcome", "outcome_censor"),
  x_exp = "age",
  x_inter = "sex",
  x_cov = list(
    "Crude" = NULL,
    "Model A" = c("ecog_group3")
  ),
  verbose = FALSE
)

res_inter_sex$result
#>     Model Exposure Interaction   N Case N Control N Person-years Interaction DF
#> 1   Crude      age         sex 227    164        63     190.3409              1
#> 2 Model A      age         sex 227    164        63     190.3409              1
#>   P for interaction Exposure class Interaction class
#> 1             0.602     Continuous            Binary
#> 2             0.180     Continuous            Binary

这张表要这样读:

  • Exposure class = Continuous 表明 age 被当成连续 exposure
  • Interaction class = Binary 表明 sex 是二分类 modifier
  • P for interaction 是正式的异质性检验

在这个教学示例里,校正后的 P for interaction = 0.18,说明没有清楚证据 支持 age 的效应因 sex 不同而在乘法 HR 尺度上发生变化。

这并不等于“什么都没有”,而是表示:

当前数据没有足够强的证据支持 sex 对 age 效应存在乘性交互。

示例 2:连续 exposure × 多分类 modifier

同一个函数也可以处理多分类 modifier。

res_inter_ecog <- leo_cox_interaction(
  df = lung_df,
  y_out = c("outcome", "outcome_censor"),
  x_exp = "age",
  x_inter = "ecog_group3",
  x_cov = list("Crude" = NULL),
  verbose = FALSE
)

res_inter_ecog$result
#>   Model Exposure Interaction   N Case N Control N Person-years Interaction DF
#> 1 Crude      age ecog_group3 227    164        63     190.3409              2
#>   P for interaction Exposure class      Interaction class
#> 1             0.037     Continuous Categorical (3 levels)

这里 modifier 是三分类变量,P for interaction = 0.037
这表示:

age 和结局的关联,在不同 ECOG 组之间似乎并不完全一样, 至少在乘法 HR 尺度上存在异质性证据。

既然 modifier 是分类变量,下一步就很自然进入 subgroup 展示。

subgroup 分析:在正式检验之后展示 HR 模式

leo_cox_subgroup() 不是用来替代 interaction test 的。
它更像是正式检验之后的展示层,用来回答:

每一层内部的 HR 到底长什么样?

res_sub_ecog <- leo_cox_subgroup(
  df = lung_df,
  y_out = c("outcome", "outcome_censor"),
  x_exp = "age",
  x_subgroup = "ecog_group3",
  x_cov = list("Crude" = NULL),
  verbose = FALSE
)

res_sub_ecog$result
#>      Subgroup     Level   N Model Exposure Outcome    Exposure level Case N
#> 1 ecog_group3     ECOG0  63 Crude      age outcome Per unit increase     37
#> 2 ecog_group3     ECOG1 113 Crude      age outcome Per unit increase     82
#> 3 ecog_group3 ECOG2plus  51 Crude      age outcome Per unit increase     45
#>   Control N Person-years    HR       95% CI P value P for interaction
#> 1        26     60.69268 1.059 1.013, 1.107   0.011             0.037
#> 2        31     97.28131 1.004 0.980, 1.029   0.731             0.037
#> 3         6     32.36687 0.994 0.958, 1.030   0.733             0.037

这张表告诉我们的描述性模式是:

  • ECOG0 组中,age 每增加 1 岁,HR 为 1.059
  • ECOG1 组中,HR 接近于 1.004
  • ECOG2plus 组中,HR 也接近于 0.994
  • 同时表里附的 P for interaction = 0.037 提醒我们: 这些 subgroup HR 要放在正式 interaction test 的背景下解读

所以更稳妥的论文表述应当是:

age 与结局的关联在不同 ECOG 组之间存在差异(P for interaction = 0.037)。 在 subgroup 分析中,该关联在 ECOG0 组更强,而在 ECOG1ECOG2plus 中更接近于零效应。

这比“一个 subgroup 显著、另一个不显著”要科学得多。

加性交互:回答的是另一类问题

乘性交互和加性交互不是一回事。
加性交互问的是:

两个 binary 因素一起出现时,它们的联合效应是否超过相加预期?

这也是为什么当前教学工作流里,加性交互最适合 binary-binary。

这里我们用:

  • ecog_binary:ECOG 体能状态较差 vs 较好
  • age_high:Older vs Younger

注意:这里把 age 二分类,只是为了教学加性交互。
真实研究中,这类 cutpoint 应该在分析前基于临床意义预先定义,而不是为了 让函数能跑才临时分组。

res_add <- leo_cox_add_interaction(
  df = lung_df,
  y_out = c("outcome", "outcome_censor"),
  x_exp = "ecog_binary",
  x_inter = "age_high",
  x_cov = list("Crude" = NULL),
  verbose = FALSE
)

res_add$result
#>   Model    Exposure Interaction   N Case N Control N Person-years
#> 1 Crude ecog_binary    age_high 227    164        63     190.3409
#>                      Reference group Recode applied Multiplicative interaction
#> 1 ecog_binary=Good, age_high=Younger          FALSE                      0.454
#>   Multiplicative 95% CI P for interaction   RERI   RERI 95% CI     AP
#> 1          0.226, 0.911             0.029 -1.475 -3.199, 0.249 -0.739
#>       AP 95% CI     S     S 95% CI
#> 1 -1.723, 0.245 0.403 0.154, 1.056

这张表可以分层来读。

先看结构列:

  • Reference group:哪一个联合组被当成参考组
  • Recode applied:函数是否为了更清楚的参考结构做了内部重编码

再看核心指标:

  • Multiplicative interaction:乘法尺度上的交互
  • RERI:relative excess risk due to interaction
  • AP:attributable proportion due to interaction
  • S:synergy index

这些指标的零值或无交互值并不一样:

  • Multiplicative interaction 的无交互值是 1
  • RERI 的无交互值是 0
  • AP 的无交互值是 0
  • S 的无交互值是 1

在这个教学示例里:

  • Multiplicative interaction = 0.454,且 P for interaction = 0.029
  • RERI = -1.475AP = -0.739,点估计都指向负向加性交互
  • RERIAPS 的置信区间都跨过了各自的无交互值

这里最容易误读的是第一行。Multiplicative interaction 不是联合组本身的 HR,而是:

HR11 / (HR10 × HR01)

所以它小于 1 的意思不是“联合组一定是保护性的”或“联合组 HR 一定小于 1”, 而是:

联合效应低于按两个单独效应直接相乘时的预期。

举个最直观的例子:

  • 如果 ecog_binary 单独 HR 是 2
  • age_high 单独 HR 是 3
  • 那么在“没有乘法交互”时,联合组预期 HR 是 2 × 3 = 6
  • 如果乘法交互项是 0.454,联合组 HR 大约就是 2 × 3 × 0.454 = 2.724

也就是说,联合组风险仍然可能高于参考组,只是没有高到“纯乘法预期”那么强。 这更接近乘法尺度上的负向交互或拮抗,而不是“联合组 HR < 1”。

所以更谨慎的解释应当是:

该 binary-binary 对比在乘法尺度上显示出交互证据,但在加法尺度上, 虽然点估计提示可能存在负向交互,置信区间仍较宽,尚不足以支持清楚的 加性交互结论。

这正好说明了一个教学重点:

同一组数据在乘法尺度和加法尺度上的答案,可能并不完全一致。

常见结果组合应该怎么解读

实务上可以按下面 4 种情况来理解:

  1. 乘性交互显著,加性交互不显著

    说明在 HR 尺度上有异质性证据,但加法尺度证据仍不稳定。

  2. 加性交互显著,乘性交互不显著

    说明联合效应超过相加预期,但未见清楚的乘法尺度偏离。

  3. 两种尺度都显著

    说明在两种尺度上都存在明确的交互证据。

  4. 两种尺度都不显著

    说明当前数据没有清楚支持效应修饰。

但无论哪种情况,都要记住一句话:

“一个 subgroup 显著、另一个 subgroup 不显著” 本身不能证明存在交互。

在论文里应该怎么写

下面给出 3 个和上面输出对应的写法模板。

对于没有明确乘性交互的情况:

调整后未观察到 age 与结局的关联在不同 sex 间存在显著差异 (P for interaction = 0.18)。

对于存在乘性交互并辅以 subgroup 表的情况:

age 与结局的关联在不同 ECOG 组之间存在差异 (P for interaction = 0.037)。在 subgroup 分析中, ECOG0 组中 age 每增加 1 岁的 HR 为 1.05995% CI 1.013, 1.107),而在 ECOG1ECOG2plus 中接近零效应。

对于 binary-binary 的加性交互:

对于 ECOG 体能状态较差与高年龄的 binary 对比,乘法尺度交互项估计为 0.45495% CI 0.226, 0.911),提示联合效应低于按两个单独 HR 直接 相乘时的预期;这并不表示联合组 HR 必然低于 1。加法尺度指标仍较不稳定 (例如 RERI = -1.475, 95% CI -3.199, 0.249)。

这种写法的好处是:

  • 先交代是哪一个尺度
  • 再交代方向和大小
  • 最后再决定能否下比较强的结论

做完 Step 2 以后你应该带走什么

Step 2 最重要的 5 个结论是:

  1. 乘性交互是 Cox 工作流里最通用的正式检验
  2. subgroup 表是展示层,modifier 最好是分类变量
  3. 加性交互当前最适合 binary-binary 对比
  4. 同一组数据在乘法尺度和加法尺度上可能给出不同答案
  5. 结果解读必须和真正检验的尺度对应,而不能只看 subgroup 的单个 P value

接下来的 Step 3 会换一个问题:

exposure 和结局的关联,是否有一部分可能通过 mediator 传递?