You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

离散时间模型中基于R公式实现cLDA约束的方法问询

问题:离散时间下无需数据重编码实现约束纵向数据分析(cLDA)

我从事临床试验重复测量数据的纵向模型研究,患者被随机分配到不同治疗组,在多个预设时间点接受测量。示例数据集采用mmrm包中的FEV数据集,其中ARMCD代表治疗组,AVISIT代表离散时间点:

library(tidyverse)
data("fev_data", package = "mmrm")
data <- fev_data %>%
  as_tibble() %>%
  mutate(ARMCD = as.character(ARMCD)) %>%
  select(-VISITN, -VISITN2)
data
#> # A tibble: 800 × 8
#>    USUBJID AVISIT ARMCD RACE                      SEX    FEV1_BL  FEV1 WEIGHT
#>    <fct>   <fct>  <chr> <fct>                     <fct>    <dbl> <dbl>  <dbl>
#>  1 PT1     VIS1   TRT   Black or African American Female    25.3  NA    0.677
#>  2 PT1     VIS2   TRT   Black or African American Female    25.3  40.0  0.801
#>  3 PT1     VIS3   TRT   Black or African American Female    25.3  NA    0.709
#>  4 PT1     VIS4   TRT   Black or African American Female    25.3  20.5  0.809
#>  5 PT2     VIS1   PBO   Asian                     Male      45.0  NA    0.465
#>  6 PT2     VIS2   PBO   Asian                     Male      45.0  31.5  0.233
#>  7 PT2     VIS3   PBO   Asian                     Male      45.0  36.9  0.360
#>  8 PT2     VIS4   PBO   Asian                     Male      45.0  48.8  0.507
#>  9 PT3     VIS1   PBO   Black or African American Female    43.5  NA    0.682
#> 10 PT3     VIS2   PBO   Black or African American Female    43.5  36.0  0.892
#> # ℹ 790 more rows
#> # ℹ Use `print(n = ...)` to see more rows

临床研究中常采用约束纵向数据分析(cLDA),要求基线时合并所有治疗组。如果时间是连续型且基线为AVISIT == 0,可以通过以下公式轻松实现cLDA:

formula <- FEV1 ~ AVISIT + AVISIT:ARMCD

但这种方法不适用于离散时间,因为生成的模型矩阵会包含AVISITVIS1:ARMCDTRT项,导致基线时PBO和TRT组被区分开:

colnames(model.matrix(formula, data = data))
#> [1] "(Intercept)"         "AVISITVIS2"          "AVISITVIS3"          "AVISITVIS4"         
#> [5] "AVISITVIS1:ARMCDTRT" "AVISITVIS2:ARMCDTRT" "AVISITVIS3:ARMCDTRT" "AVISITVIS4:ARMCDTRT"

我见过有人用手动修改数据的方式实现cLDA:

count(data, ARMCD, AVISIT)
#> # A tibble: 8 × 3
#>   ARMCD AVISIT     n
#>   <chr> <fct>  <int>
#> 1 PBO   VIS1     105
#> 2 PBO   VIS2     105
#> 3 PBO   VIS3     105
#> 4 PBO   VIS4     105
#> 5 TRT   VIS1      95
#> 6 TRT   VIS2      95
#> 7 TRT   VIS3      95
#> 8 TRT   VIS4      95
clda <- mutate(data, ARMCD = ifelse(AVISIT == "VIS1", "PBO", ARMCD))
count(clda, ARMCD, AVISIT)
#> # A tibble: 7 × 3
#>   ARMCD AVISIT     n
#>   <chr> <fct>  <int>
#> 1 PBO   VIS1     200
#> 2 PBO   VIS2     105
#> 3 PBO   VIS3     105
#> 4 PBO   VIS4     105
#> 5 TRT   VIS2      95
#> 6 TRT   VIS3      95
#> 7 TRT   VIS4      95

但这种方式并没有改变模型的项:

colnames(model.matrix(formula, data = clda))
#> [1] "(Intercept)"         "AVISITVIS2"          "AVISITVIS3"          "AVISITVIS4"         
#> [5] "AVISITVIS1:ARMCDTRT" "AVISITVIS2:ARMCDTRT" "AVISITVIS3:ARMCDTRT" "AVISITVIS4:ARMCDTRT"

更糟的是,模型矩阵不再是满秩:

as.integer(Matrix::rankMatrix(model.matrix(formula, data = clda)))
#> [1] 7

有没有无需重编码数据或手动生成模型矩阵,仅通过公式和/或对比就能实现cLDA的方法?手动生成自定义模型矩阵后调用lm()或brms::brm()的方式太底层,还会丢失brms自带的便捷后处理工具。


解决方案

方法1:通过自定义对比矩阵实现cLDA

可以为AVISIT:ARMCD交互项设置自定义对比,强制基线时(VIS1)的治疗组差异为0。

# 生成交互项的所有水平
int_levels <- expand.grid(AVISIT = levels(data$AVISIT), ARMCD = unique(data$ARMCD))
int_levels <- paste(int_levels$AVISIT, int_levels$ARMCD, sep = ":")

# 构造对比矩阵:仅保留VIS2-VIS4的组间差异,VIS1差异约束为0
contrasts_mat <- matrix(0, nrow = length(int_levels), ncol = 3)
colnames(contrasts_mat) <- c("VIS2:TRT-PBO", "VIS3:TRT-PBO", "VIS4:TRT-PBO")

# 为各时间点组间差异赋值
contrasts_mat[int_levels == "VIS2:TRT", 1] <- 1
contrasts_mat[int_levels == "VIS2:PBO", 1] <- -1

contrasts_mat[int_levels == "VIS3:TRT", 2] <- 1
contrasts_mat[int_levels == "VIS3:PBO", 2] <- -1

contrasts_mat[int_levels == "VIS4:TRT", 3] <- 1
contrasts_mat[int_levels == "VIS4:PBO", 3] <- -1

# 将对比矩阵应用到交互项
contrasts(interaction(data$AVISIT, data$ARMCD)) <- contrasts_mat

然后使用包含主效应和交互项的公式拟合模型:

formula_clda <- FEV1 ~ AVISIT + AVISIT:ARMCD
model <- lm(formula_clda, data = data)
summary(model)

这种方式不修改原始数据,模型矩阵保持满秩,同时实现了基线合并的cLDA约束。

方法2:调整公式结构,显式约束基线

重新构造公式,将基线作为共享截距,仅对后续时间点设置组间差异:

# 生成时间点指示变量和治疗组指示变量
data <- data %>%
  mutate(
    is_vis2 = as.integer(AVISIT == "VIS2"),
    is_vis3 = as.integer(AVISIT == "VIS3"),
    is_vis4 = as.integer(AVISIT == "VIS4"),
    trt = as.integer(ARMCD == "TRT")
  )

# cLDA公式:截距代表所有组的基线均值,后续时间点分别设置组间差异
formula_clda <- FEV1 ~ 1 + is_vis2 + is_vis3 + is_vis4 + is_vis2:trt + is_vis3:trt + is_vis4:trt
model <- lm(formula_clda, data = data)
summary(model)

公式含义:

  • 截距:所有治疗组在VIS1(基线)的均值
  • is_vis2/is_vis3/is_vis4:PBO组对应时间点相对于基线的变化
  • is_vis2:trt等:TRT组相对于PBO组在对应时间点的额外变化

该方式兼容brms,可直接使用其后续处理工具:

library(brms)
brm_model <- brm(formula_clda, data = data, family = gaussian())
summary(brm_model)

方法3:用emmeans施加事后约束

如果已拟合包含完整交互项的模型,可通过emmeans设置自定义对比,强制基线组间差异为0:

library(emmeans)
# 拟合完整交互模型
full_model <- lm(FEV1 ~ AVISIT * ARMCD, data = data)

# 定义对比:基线差异约束为0,保留后续时间点组间差异
clda_contrasts <- list(
  vis2_diff = c(0, 1, 0, 0, 0, -1, 0, 0),
  vis3_diff = c(0, 0, 1, 0, 0, 0, -1, 0),
  vis4_diff = c(0, 0, 0, 1, 0, 0, 0, -1),
  vis1_constraint = c(1, 0, 0, 0, -1, 0, 0, 0)
)

# 应用对比并输出结果
emmeans(full_model, ~ AVISIT * ARMCD) %>%
  contrast(clda_contrasts)

此方法适合已拟合模型后需要施加cLDA约束的场景,无需修改原始数据。


内容的提问来源于stack exchange,提问作者landau

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 12:57:33