Skip to content

Support for constrained longitudinal data analysis (cLDA) parametrization #551

Open

Description

@DavidJesse21

Typically, MMRMs are parametrized to directly model the change from baseline as the outcome and include the baseline value itself as a covariate in the model.
An alternative is to have the baseline value as part of the response vector.
Then, in randomized clinical trials, we could assume two treatment groups (e.g. active and control) to have the same mean (or distribution more generally) at baseline, hence in a regression model share a common intercept.
This is known as constrained longitudinal data analysis (cLDA) in the literature (https://www.jstor.org/stable/25053123).
However, fitting a discrete-time cLDA using mmrm is difficult because the time/visits must be encoded as factors.
I try to illustrate the issue below with the fev_data example from the mmrm package.

The following code chunk fits an MMRM with the baseline visit as part of the response vector, but it has separate intercepts (estimates of the baseline mean) for the two groups:

library(mmrm)
library(emmeans)
library(data.table)

dt = as.data.table(fev_data)

# Drop columns not required for this example/illustration
for (j in c("RACE", "SEX", "WEIGHT", "VISITN2")) set(dt, j = j, value = NULL)

# Preparation of data for cLDA model
dt_bl = copy(dt)
dt_bl = dt_bl[VISITN == 1]
dt_bl[, `:=`(FEV1 = FEV1_BL, VISITN = 0, AVISIT = "VIS0")]
dt = rbindlist(list(dt, dt_bl))
dt[, AVISIT := factor(AVISIT, levels = sort(levels(AVISIT)))]
setorder(dt, USUBJID, AVISIT)

# Usual approach: Includes separate intercepts (baseline estimates)
m1 = mmrm(
  FEV1 ~ 1 + AVISIT + ARMCD:AVISIT + us(AVISIT | USUBJID),
  data = dt
)
summary(m1)

One approach to circumvent this could be to do some manual work, e.g. by specifying each term in the model formula explicitly.
This works in principle for estimating the model, but working with it afterwards (e.g. using emmeans) is difficult.

# Construct formula by hand
form_mmrm = as.formula(paste(
  "FEV1 ~ 1 + AVISIT +",
  paste(
    sprintf(
      'I((ARMCD == "TRT") & (AVISIT == "%s"))',
      dt[, levels(AVISIT)][-1]
    ),
    collapse = " + "
  ),
  "+ us(AVISIT | USUBJID)"
))

# Estimates the correct model, but difficulties arise when trying to work with emmeans afterwards.
m2 = mmrm(form_mmrm, data = dt)
summary(m2)
emmeans(
  m2, ~ ARMCD | AVISIT, data = dt,
  params = c("AVISIT", "ARMCD", '"TRT"', '"VIS1"', '"VIS2"', '"VIS3"', '"VIS4"')
)

I know that this is not an issue/shortcoming of mmrm per se but more related to the R formula syntax and/or ecosystem in general.
Nonetheless, I thought since this problem/model is quite specific but may be of interest for many users of mmrm it could be worth to think about a solution for this within the mmrm package.

Activity

  1. danielinteractive commented on Nov 24, 2025

    @danielinteractive
    Collaborator

    Thanks @DavidJesse21 - do you have an idea of an interface you would like to have here in mmrm?

  2. DavidJesse21 commented on Nov 25, 2025

    @DavidJesse21
    Author

    Hi @danielinteractive , thank you for the quick response.
    I am not sure what could be a "clean"/convenient solution as I am not familiar with the internals of mmrm.
    One initial idea that came to my mind is to somehow specify in the formula to remove certain terms/columns in the design matrix, e.g. like FEV1 ~ 1 + AVISIT + ARMCD:AVISIT - AVISITVIS0:ARMCDTRT + us(AVISIT | USUBJID).

    Edit:
    Another idea for the interface (again via the formula): FEV1 ~ 1 + AVISIT + constrained(ARMCD:AVISIT) + us(AVISIT | USUBJID).
    Here, constrained() could/should, for example, signal that the first category of AVISIT should be ignored in this interaction.
    Perhaps, constrained() would then need to require further arguments to control what is happening here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions