Fit and display models in a for-loop

There are situations where fitting a series of models in a for-loop in R is beneficial, especially when making minor modifications such as replacing one predictor variable with another. Instead of manually fitting each model separately, a for-loop can be structured to dynamically iterate over a vector of predictors. While implementing this in R is straightforward, a greater challenge arises when rendering the model outputs into a Quarto-generated Word (.docx) document.

I frequently use Quarto and the gtsummary package to generate Word output to share results with my team. This includes tables, listings, and figures that allow team members to review the findings and provide feedback.

There are several ways to incorporate iterative model results into a Quarto-rendered Word document. One approach is to store model outputs in a list and manually create subheadings in the Quarto document to display each result. Another option is to structure the for-loop to generate separate Word files, each corresponding to a model with its respective modification. The method I have settled on involves printing a Quarto heading and output directly within the for-loop. This results in a well orgarnized document with headings, a table of contents, and a table of each model’s ouptu.

However, this requires additional modifications to how model output is saved and rendered in Quarto. The key components of this method include setting an R chunk to render with “asis” formatting, saving each model output as a Word object, and using the cat() function to generate a heading and display the Word output within the loop.

This approach ensures that results are structured dynamically while keeping the document organized and readable.

Specify the Quarto yaml header options

---
title: ".docx"

format:
  docx:
    toc: true

execute: 
  echo: false
---

Load packages

library(tidyverse)
library(gtsummary)

Create a vector of all variables iterate through

moderators <-  c("moderator1", "moderator2", "moderator3")

Create a new R chunk with options specifif to the for-loop

In a new R chunk in a Quarto document, set results: to ‘asis’ . “#| results: ‘asis’” since this approach will rely on printing a gtsummary table to openxml so that it will show up as a neatly formatted table in Word.

# results: 'asis'
# echo: false

Set up a for loop for each new predictor to be added

I will usually start a for-loop by specifying the values to iteratre through and setting up the beginning and closing curly braces.

for (i in 1:length(moderators)) {

}

Insert a heading

After setting the for loop, I add a cat() command. The purpose of this line is to create a subheading “##” followed by the moderator, and then a new line “”. This separates each table’s output to make navigating the results easier. If the table of contents in the Quarto yaml header is set to true, then these should also represent sections that one can use to navigate in the output Word document.

for (i in 1:length(moderators)) {
  cat("## ", moderators[i], "\n")  # Display a subheading in the rendered document 

}

Set up a formula

The next part of the loop is to specify formulat. One way to do this is to use the str_c() function to concatenate different elements of the model formula. In this example, I fit a linear mixed effect model, where each moderator is dynamically included as a predictor and combined as an interaction with predictors x3 and x4, and includes a random effect for the subject.

for (i in 1:length(moderators)) {
  cat("## ", moderators[i], "\n")

  formula <- as.formula(
    str_c(
        "y ~ x1 + x2 + ",           # dependent variable and covariates
        moderators[i],              # moderator to iterate over
        " + x3 + x4 + ",            # predictors to be combined with moderators
        "x3:", moderators[i],       # interaction between predictor 3 and moderator
        " + x4:", moderators[i],    # interaction between predictor 4 and moderator
        " + ", 
        "(1 | subject_id)"          # clustering term/random effect
    )
  )



}

Fit the model

Once the formula is specified, the model can be fit. In this case, I am using the lmerTest package because we are interesting in retrieving a p value from the results section. The lme4 package, for reasons beyond the scope of this write-up, does not provide p-values.

for (i in 1:length(moderators)) {
  cat("## ", moderators[i], "\n")

  formula <- as.formula(
    str_c(
        "y ~ x1 + x2 + ",           
        moderators[i],              
        " + x3 + x4 + ",            
        "x3:", moderators[i],       
        " + x4:", moderators[i],    
        " + ", 
        "(1 | subject_id)"          
    )
  )

  # Fit the model
  model <- lmerTest::lmer(formula, data = modeling_data_set)

}

Generate a table to display

After the model is fit, I like to use the tbl_regression() function from the gtsummary package to display model output. In order for the tables to display properly, they need to go through a series of transformations. Initially, the tbl_regression() function creates a gtsummary object, that is then converted to a gt object. Finally the gt object is then converted to a Word object. If we were to print the gt::as_word() object at this point in the R console, it would be in openxml format and would be very difficult to read.

for (i in 1:length(moderators)) {
  cat("## ", moderators[i], "\n")

  formula <- as.formula(
    str_c(
        "y ~ x1 + x2 + ",           
        moderators[i],              
        " + x3 + x4 + ",            
        "x3:", moderators[i],       
        " + x4:", moderators[i],    
        " + ", 
        "(1 | subject_id)"          
    )
  )

  # Fit the model
  model <- lmerTest::lmer(formula, data = modeling_data_set)

  # Generate a table to display
  tab <- model %>% 
    tbl_regression(., estimate_fun = ~style_sigfig(., digits = 4)) %>%
    as_gt() %>% 
    gt::as_word()
}