rshinygtsummarysurvival

"Object of type 'symbol' is not subsettable" error when rendering survival table with gtsummary in shiny


I am trying to build a simple app in shiny to create from a database a table with survival % at a timepoint, comparing 2 groups (female vs male, drug a vs drug b, etc).

The app has a sidebar with inputs (time var. and status var.) to calculate the survival. For a descriptive table without group comparison (just survival at 24 months in %) , it works fine (code below with dataframe example).

library(shiny)
library(gtsummary)
library(survival)
library(dplyr)
library(DT)
library(cards)
library(cardx)
library(gt)

# Create a made-up dataframe
set.seed(123)
data <- data.frame(
  gender = sample(c("male", "female"), 100, replace = TRUE),
  age = sample(18:70, 100, replace = TRUE),
  stage = sample(1:4, 100, replace = TRUE),
  OS = runif(100, 2, 70),
  status = sample(0:1, 100, replace = TRUE),
  ECOG24 = sample(0:1, 100, replace = TRUE)
)

# Define UI
ui <- fluidPage(
  navbarPage("Clinical Data App",
             tabPanel("Survival Analysis",
                      sidebarLayout(
                        sidebarPanel(
                          selectInput("time_var", "Select Time Variable:",
                                      choices = names(data), selected = "OS"),
                          selectInput("status_var", "Select Status Variable:",
                                      choices = names(data), selected = "status"),
                          numericInput("timepoint", "Enter Timepoint:", 24, min = 1, max = 70)
                        ),
                        mainPanel(
                          tabsetPanel(
                            tabPanel("Survival Table", gt_output("surv_table"))
                          )
                        )
                      )
             )
  )
)

# Define server logic
server <- function(input, output, session) {
  
  output$surv_table <- render_gt({
    # Validate inputs
    req(input$time_var, input$status_var, input$timepoint)
    
    # Create survival object
    surv_obj <- Surv(time = data[[input$time_var]], event = data[[input$status_var]])
    
    # Build tbl_survival using the input values
    tbl_survival <- 
      survfit(surv_obj ~ 1, data) |> 
      cardx::ard_survival_survfit(times = c(input$timepoint)) |>  
      cards::update_ard_fmt_fn(
        stat_names = c("estimate", "conf.low", "conf.high"),
        fmt_fn = label_style_sigfig(digits = 2, scale = 100)
      ) |> 
      tbl_ard_summary(
        label = list(time = paste0(input$timepoint, " months Survival Probability")),
        statistic = time ~ "{estimate}%"
      )
    
    # Convert to gt table before rendering
    gt_table <- as_gt(tbl_survival)
    
    # Render the table
    gt_table
  })
}

# Run the application 
shinyApp(ui = ui, server = server)

I am expecting a table created with this code:

survfit(Surv(OS, status) ~ gender, data) |> 
  cardx::ard_survival_survfit(times = c(24)) |>  
  cards::update_ard_fmt_fn(
    stat_names = c("estimate", "conf.low", "conf.high"),
    fmt_fn = label_style_sigfig(digits = 2, scale = 100)
  ) |> 
  tbl_ard_summary(
    by = gender,
    label = list(time = paste0("24 ", " months Survival Probability")),
    statistic = time ~ "{estimate}%"
  )

The problem is in shiny when I add a variable to compare groups (for example gender).

with this code:

ui <- fluidPage(
  navbarPage("Clinical Data App",
             tabPanel("Survival Analysis",
                      sidebarLayout(
                        sidebarPanel(
                          selectInput("time_var", "Select Time Variable:",
                                      choices = names(data)),
                          selectInput("status_var", "Select Status Variable:",
                                      choices = names(data)),
                          selectInput("group_var", "Select Grouping Variable:",
                                      choices = c("None", names(data))),
                          numericInput("timepoint", "Enter Timepoint:", 12, min = 1, max = 70)
                        ),
                        mainPanel(
                          tabsetPanel(
                            tabPanel("Survival Table", gt_output("surv_table"))
                          )
                        )
                      )
             )
  )
)

# Define server logic
server <- function(input, output, session) {
  
  output$surv_table <- render_gt({
    # Validate inputs
    req(input$time_var, input$status_var, input$timepoint)
    
    # Create the survival object
    surv_obj <- Surv(time = data[[input$time_var]], event = data[[input$status_var]])
    print("Survival Object Created:")
    print(surv_obj)
    
    # Determine grouping formula
    if (input$group_var == "None") {
      formula <- surv_obj ~ 1
    } else {
      formula <- as.formula(paste("surv_obj ~", input$group_var))
    }
    
    # Print the formula for debugging
    print("Survival Formula:")
    print(formula)
    
    # Fit the survival model with the correct data
    fit <- survfit(formula, data = data)
    print("Survival Model Fitted:")
    print(summary(fit))
    
    # Build tbl_survival using the input value
    tbl_survival <- 
      fit |> 
      cardx::ard_survival_survfit(times = input$timepoint) |> 
      cards::update_ard_fmt_fn(
        stat_names = c("estimate", "conf.low", "conf.high"),
        fmt_fn = label_style_sigfig(digits = 2, scale = 100)
      ) |> 
      dplyr::mutate(context = "categorical") |> # forcing the summary to a "category-like" format
      tbl_ard_summary(
        by = if (input$group_var == "None") NULL else input$group_var,
        label = list(time = paste0(input$timepoint, " months Survival Probability")),
        statistic = time ~ "{estimate}% (95% CI {conf.low}%, {conf.high}%)"
      )
    
    print("Summary Table:")
    print(tbl_survival)
    
    # Convert to gt table before rendering
    gt_table <- as_gt(tbl_survival)
    
    # Render the table
    gt_table
  })
}

# Run the application 
shinyApp(ui = ui, server = server)

I get the error "object of type 'symbol' is not subsettable" (i tried many different alternatives and always the same error).

When debugging, the formula works fine (summary(fit) is ok).

Also, same coding in simple r (no shiny) using a fixed grouping variable (for example gender), it works fine. (see code above)

So, the problem should be in the construction of the table with gt_summary, I assume...

Do you have any ideas? Thanks!


Solution

  • You can track the error down to this part:

    fit |> 
      cardx::ard_survival_survfit(times = c(24)) 
    

    You can use Shiny inputs to create the formula object passed to the survfit function to create the fit object. This object is created without errors, but then it includes a "call" element containing the formula, written like this:

    str(fit$call)
    # language survfit(formula = formula, data = data)
    

    And this "call" element is then used by the cardx/cards functions to calculate further estimates, but it cannot find the real formula.

    If you look at the "call" element from the following fit, directly created without variables/inputs (like in your second code block), the whole formula is there:

    fit2 <- survfit(Surv(OS, status) ~ gender, data)
    str(fit2$call)
    # language survfit(formula = Surv(OS, status) ~ gender, data = data)
    

    So we need a way to both create the fit object with the formula generated dynamically from input variables, and then pass the whole formula in the fit$call element. I was able to do this by using the quote(), deparse() and deparse1() functions to recreate the formula and replace it in the fit$call element, as a call object:

    # don't create the surv_obj object yet
     surv_obj <- quote(Surv(time = data[[input$time_var]], event = data[[input$status_var]]))
    
        # Determine grouping formula
        if (input$group_var == "None") {
          formula <- as.formula(paste0(deparse(surv_obj), "~ gender"))
        } else {
          formula <- as.formula(paste0(deparse(surv_obj), "~", input$group_var))
        }
        
        # Print the formula for debugging
        print("Survival Formula:")
        print(formula)
        
        # Fit the survival model with the correct data
        fit <- survfit(formula = formula, data = data)
        formula2 = deparse1(formula)
        fit$call <- as.call(str2lang(paste("survfit(formula = ", formula2, ", data = data)")))
        print("Survival Model Fitted:")
        print(summary(fit))
    

    This is kinf of hacky, but then the cardx::ard_survival_survfit() functions does not show any error, and your app works well!

    EDIT Here's the full working code:

    ui <- fluidPage(
      navbarPage("Clinical Data App",
                 tabPanel("Survival Analysis",
                          sidebarLayout(
                            sidebarPanel(
                              selectInput("time_var", "Select Time Variable:",
                                          choices = names(data), selected = "OS"),
                              selectInput("status_var", "Select Status Variable:",
                                          choices = names(data), selected = "status"),
                              selectInput("group_var", "Select Grouping Variable:",
                                          choices = c("None", names(data)), selected = "gender"),
                              numericInput("timepoint", "Enter Timepoint:", 24, min = 1, max = 70)
                            ),
                            mainPanel(
                              tabsetPanel(
                                tabPanel("Survival Table", gt_output("surv_table"))
                              )
                            )
                          )
                 )
      )
    )
    
    # Define server logic
    server <- function(input, output, session) {
      
      output$surv_table <- render_gt({
        # Validate inputs
        req(input$time_var, input$status_var, input$timepoint)
        
        # Create the survival object
        surv_obj <- quote(Surv(time = data[[input$time_var]], event = data[[input$status_var]]))
    
        # Determine grouping formula
        if (input$group_var == "None") {
          formula <- as.formula(paste0(deparse(surv_obj), "~ gender"))
        } else {
          formula <- as.formula(paste0(deparse(surv_obj), "~", input$group_var))
        }
        
        # Print the formula for debugging
        print("Survival Formula:")
        print(formula)
        
        # Fit the survival model with the correct data
        fit <- survfit(formula = formula, data = data)
        formula2 = deparse1(formula)
        fit$call <- as.call(str2lang(paste("survfit(formula = ", formula2, ", data = data)")))
        print("Survival Model Fitted:")
        print(summary(fit))
        
        # Build tbl_survival using the input value
        tbl_survival <- 
          fit |> 
          cardx::ard_survival_survfit(times = input$timepoint) |> 
          cards::update_ard_fmt_fn(
            stat_names = c("estimate", "conf.low", "conf.high"),
            fmt_fn = label_style_sigfig(digits = 2, scale = 100)
          ) |> 
          dplyr::mutate(context = "categorical") |> # forcing the summary to a "category-like" format
          tbl_ard_summary(
            by = if (input$group_var == "None") NULL else input$group_var,
            label = list(time = paste0(input$timepoint, " months Survival Probability")),
            statistic = time ~ "{estimate}% (95% CI {conf.low}%, {conf.high}%)"
          )
        
        print("Summary Table:")
        print(tbl_survival)
        
        # Convert to gt table before rendering
        gt_table <- as_gt(tbl_survival)
        
        # Render the table
        gt_table
      })
    }
    
    # Run the application 
    shinyApp(ui = ui, server = server)