knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
library(tfrmt)
library(dplyr)
library(gt)
library(tidyr)

Demography Table

For this demography table we are going to use data_demog, an example analysis results dataset found in the package, which is based on the CDISC pilot data. This dataset has two different row label columns, rowlbl1 and rowlbl2 because we are building a table with group and row labels. There are also two order columns which will be used to set the row order of the output. There is a single column to define our table's columns (multiple column columns are used when there is column spanning). Finally there is a param column, a value column and an additional grouping column, grp, which we can use for more complex formatting.

head(data_demog)

The mock we are going to match looks like this:

tfrmt(
  # specify columns in the data
  group = c(rowlbl1,grp),
  label = rowlbl2,
  column = column, 
  param = param,
  value = value,
  sorting_cols = c(ord1, ord2),

  # Specify body plan
  body_plan = body_plan(
    frmt_structure(group_val = ".default", label_val = ".default", frmt_combine("{n} {pct}", 
                                                                                n = frmt("xxx"),
                                                                                pct = frmt_when("==100" ~ "",
                                                                                                "==0" ~ "",
                                                                                                TRUE ~ frmt("(xx.x %)")))),
    frmt_structure(group_val = ".default", label_val = "n", frmt("xxx")),
    frmt_structure(group_val = ".default", label_val = c("Mean", "Median", "Min","Max"), frmt("xxx.x")),
    frmt_structure(group_val = ".default", label_val = "SD", frmt("xxx.xx")),
    frmt_structure(group_val = ".default", label_val = ".default", p = frmt("")),
    frmt_structure(group_val = ".default", label_val = c("n","<65 yrs","<12 months","<25"), p = frmt_when(">0.99" ~ ">0.99",
                                                                                 "<0.001" ~ "<0.001",
                                                                                 TRUE ~ frmt("x.xxx", missing = "")))
  ),

  # Specify row group plan
  row_grp_plan = row_grp_plan(
    row_grp_structure(group_val = ".default", element_block(post_space = " ")),
    label_loc = element_row_grp_loc(location = "column")
  ),

  # Specify column styling plan
  col_style_plan = col_style_plan(
    col_style_structure(align = c(".",","," "), col = c("Placebo", "Xanomeline Low Dose",
                                                        "Xanomeline High Dose", "Total", "p-value")),
    col_style_structure(align = "left", col = c("rowlbl1","rowlbl2"))
  ),

  # remove extra cols
  col_plan = col_plan(-grp, 
                      -starts_with("ord") )
) %>% 
  print_mock_gt(data_demog %>% select(-value)) %>% 
  tab_options(
    container.width = 900
  )

For this table, we have three columns for each of the treatment groups, a total column for all groups combined, and a p-value column. The table also contains a mix of categorical and continuous analysis.

The first thing we are going to do when building out the tfrmt is specify all our columns

tfrmt(
  # specify columns in the data
  group = c(rowlbl1,grp),
  label = rowlbl2,
  column = column, 
  param = param,
  value = value,
  sorting_cols = c(ord1, ord2)) %>% 
  print_to_gt(data_demog) %>% 
  tab_options(
    container.width = 900
  )

While this makes a table, it isn't a very nice table and definitely doesn't match the mock. So let's start with formatting all the numbers. To do this we are going to build a body_plan to add to our tfrmt. This will be a fairly quick explanation of body_plans but if you would like more information see vignettes("Body Plan")

Body plans are made up of a series of frmt_stuctures where each frmt_stucture represents the formatting of a cell within the table. The order of the frmt_structures matter; they are always applied latest to oldest. This means the first frmt_stucture in the body_plan should be the most generic. You can use the groups, labels and parameters to specify which formatting applies to which values.

To start, we are going to use all the rows that are "n (%)" as the default. This way we don't need to list out every row that is an "n (%)" row. These rows are made up of two different values, so we will need to use frmt_combine. Next, we can format the continuous variables, which is just a straightforward one value per row so we can just use the label to filter and frmt to define the look. Finally, we want to format the p-values. This is a bit more complicated, since the p-value sits in the same row as other parameters; therefore the group and label value are not specific enough and we need something more granular. As such, we will need to specify the parameter in the frmt_structure like so: frmt_structure(group_val = ".default", label_val = ".default", p = frmt("x.xx"). Further, we also need to make sure it never displays a rounded p-value of 0 or 1. So we can use frmt_when to specify the formatting based on the value.

tfrmt(
  # specify columns in the data
  group = c(rowlbl1,grp),
  label = rowlbl2,
  column = column, 
  param = param,
  value = value,
  sorting_cols = c(ord1, ord2),
  # specify value formatting 
  body_plan = body_plan(
    frmt_structure(group_val = ".default", label_val = ".default", frmt_combine("{n} ({pct} %)", 
                                                                                n = frmt("xxx"),
                                                                                pct = frmt("xx.x"))),
    frmt_structure(group_val = ".default", label_val = "n", frmt("xxx")),
    frmt_structure(group_val = ".default", label_val = c("Mean", "Median", "Min","Max"), frmt("xxx.x")),
    frmt_structure(group_val = ".default", label_val = "SD", frmt("xxx.xx")),
    frmt_structure(group_val = ".default", label_val = ".default", p = frmt_when(">0.99" ~ ">0.99",
                                                                                 "<0.001" ~ "<0.001",
                                                                                 TRUE ~ frmt("x.xxx", missing = "")))
  )) %>% 
  print_to_gt(data_demog) %>% 
  tab_options(
    container.width = 900
  )

Now that all the numbers look correct, we can drop the order columns and the grp column (note that while we do not want to display the grp column, it plays a role behind the scenes, which will be addressed in the next step). To do this we use a col_plan which uses tidy-select nomenclature to drop/move columns.

tfrmt(
  # specify columns in the data
  group = c(rowlbl1,grp),
  label = rowlbl2,
  column = column, 
  param = param,
  value = value,
  sorting_cols = c(ord1, ord2),
  # specify value formatting 
  body_plan = body_plan(
    frmt_structure(group_val = ".default", label_val = ".default", frmt_combine("{n} {pct}", 
                                                                                n = frmt("xxx"),
                                                                                pct = frmt_when("==100" ~ "",
                                                                                                "==0" ~ "",
                                                                                                TRUE ~ frmt("(xx.x %)")))),
    frmt_structure(group_val = ".default", label_val = "n", frmt("xxx")),
    frmt_structure(group_val = ".default", label_val = c("Mean", "Median", "Min","Max"), frmt("xxx.x")),
    frmt_structure(group_val = ".default", label_val = "SD", frmt("xxx.xx")),
    frmt_structure(group_val = ".default", label_val = ".default", p = frmt("")),
    frmt_structure(group_val = ".default", label_val = c("n","<65 yrs","<12 months","<25"), p = frmt_when(">0.99" ~ ">0.99",
                                                                                 "<0.001" ~ "<0.001",
                                                                                 TRUE ~ frmt("x.xxx", missing = "")))
  ),
  # remove extra cols
  col_plan = col_plan(-grp, 
                      -starts_with("ord") )) %>% 
  print_to_gt(data_demog) %>% 
  tab_options(
    container.width = 900
  )

Now this table looks just about right. There are two problems, (1) alignment and (2) spacing between the continuous and categorical values. To take care of the alignment we are going to add a col_style_plan which accepts a series of col_style_structures. This allows columns to be aligned differently if needed. For this table, we want all the columns to align on either ".", "," or " " so our col_style_structure looks like col_style_structure(align = c(".",","," "), col = vars(everything())). After the alignment is sorted we can move on to the spacing. In order to match the spacing of the mock we need to use the extra grp column from our data. If we look at our data, we can see we want a space any time either of the groups change.

data_demog %>% 
  distinct(rowlbl1,grp)

This means that we can use a row_grp_plan with just a ".default" as the group value and it should handle all of the spacing. In addition to the spacing, row_grp_plan will let us move the spanning group labels to a separate column by changing the label_loc to "column".

tfrmt(
  # specify columns in the data
  group = c(rowlbl1,grp),
  label = rowlbl2,
  column = column, 
  param = param,
  value = value,
  sorting_cols = c(ord1, ord2),
  # specify value formatting 
  body_plan = body_plan(
    frmt_structure(group_val = ".default", label_val = ".default", frmt_combine("{n} {pct}", 
                                                                                n = frmt("xxx"),
                                                                                pct = frmt_when("==100" ~ "",
                                                                                                "==0" ~ "",
                                                                                                TRUE ~ frmt("(xx.x %)")))),
    frmt_structure(group_val = ".default", label_val = "n", frmt("xxx")),
    frmt_structure(group_val = ".default", label_val = c("Mean", "Median", "Min","Max"), frmt("xxx.x")),
    frmt_structure(group_val = ".default", label_val = "SD", frmt("xxx.xx")),
    frmt_structure(group_val = ".default", label_val = ".default", p = frmt("")),
    frmt_structure(group_val = ".default", label_val = c("n","<65 yrs","<12 months","<25"), p = frmt_when(">0.99" ~ ">0.99",
                                                                                 "<0.001" ~ "<0.001",
                                                                                 TRUE ~ frmt("x.xxx", missing = "")))
  ),
  # remove extra cols
  col_plan = col_plan(-grp, 
                      -starts_with("ord") ),
  # Specify column styling plan
  col_style_plan = col_style_plan(
    col_style_structure(align = c(".",","," "), col = c("Placebo", "Xanomeline Low Dose",
                                                        "Xanomeline High Dose", "Total", "p-value")),
    col_style_structure(align = "left", col = c("rowlbl1","rowlbl2"))
  ),

    # Specify row group plan
  row_grp_plan = row_grp_plan(
    row_grp_structure(group_val = ".default", element_block(post_space = " ")),
    label_loc = element_row_grp_loc(location = "column")
  )

  ) %>% 
  print_to_gt(data_demog) %>% 
  tab_options(
    container.width = 900
  )

AE table

For the adverse events (AE) table, we will use the data_ae analysis results data, which is also based on the CDISC pilot data. This dataset has two different row label columns, AEBODSYS and AETERM, for system organ class and preferred term, respectively. There are also two order columns which will be used to set the row order of the output. Because this table has column spanners, we have two column variables, col2 and col1 to define the hierarchy of columns. Finally, there is a param column and a value column. For brevity, we will subset to AEs with >10% prevalence in the High Dose group.

Expand for the code used to produce this subset

data_ae2 <- data_ae %>% 
  group_by(AEBODSYS, AETERM) %>% 
  mutate(pct_high = value[col2=="Xanomeline High Dose" & param=="pct"]) %>% 
  ungroup %>% 
  filter(pct_high >10) %>% 
  select(-pct_high)

head(data_ae2)

The mock we are going to match looks like this:

fmt_spec <- tfrmt(
  group = AEBODSYS,
  label = AETERM,
  param = param,
  column = c(col2, col1),
  value = value,
  row_grp_plan = row_grp_plan(),
  body_plan = body_plan(
    frmt_structure(group_val = ".default", label_val = ".default",
            frmt_combine("{n} {pct}",
                        n = frmt("XXX"),
                        pct = frmt_when(
                          "==100" ~ "",
                          "==0" ~ "",
                          TRUE ~ frmt("(xx.x %)")))),
    frmt_structure(group_val = ".default", label_val = ".default", 
                   AEs = frmt("[XXX]")),
    frmt_structure(group_val = ".default", label_val = ".default", 
                   pval = frmt_when(">0.99" ~ ">0.99",
                                    "<0.001" ~ "<0.001",
                                    "<0.05" ~ frmt("x.xxx*"),
                                    TRUE ~ frmt("x.xxx", missing = "--")))
  ), col_plan = col_plan(-starts_with("ord")))

data_ae2 %>%  
  select(-value) %>% 
  arrange(ord1, ord2 ) %>% 
  print_mock_gt(fmt_spec, . )  %>% 
  tab_options(
    container.width = 1000
  )

For this table we have three treatment group columns (Placebo, Low, and High Dose) which each have the following values reported: # of subjects with at least one AE (n), percent of subjects with at least one AE (pct), and # of AEs (AEs). We also have two p-value columns (Low Dose vs. Placebo, High Dose vs. Placebo).

Like the demography example, the first thing we are going to do when building out the tfrmt is specify all our columns. Note that col2 contains our spanning labels and col1 contains our lower level column headers:

tfrmt(
  # specify columns in the data
  group = AEBODSYS,
  label = AETERM,
  column = c(col2, col1), 
  param = param,
  value = value,
  sorting_cols = c(ord1, ord2)) %>% 
  print_to_gt(data_ae2) %>% 
  tab_options(
    container.width = 1000
  )

Next, we need to format the values using the body_plan. Recall that our body plan will be made up of a series of frmt_stuctures where each frmt_stucture represents the formatting of a cell within the table. Our AE table boils down to the following values: # of subjects with at least one AE (n), percent of subjects with at least one AE (pct), # of AEs (AEs), and p-value (pval). Because our n and pct will be combined using frmt_combine, we will have 3 frmt_structure objects. Note the use of frmt_when to format the p-values.

tfrmt(
  # specify columns in the data
  group = AEBODSYS,
  label = AETERM,
  column = c(col2, col1), 
  param = param,
  value = value,
  sorting_cols = c(ord1, ord2),
  # specify value formatting 
  body_plan = body_plan(
    frmt_structure(group_val = ".default", label_val = ".default",
                   frmt_combine("{n} {pct}",
                                n = frmt("XXX"),
                                pct = frmt_when(
                                  "==100" ~ "",
                                  "==0" ~ "",
                                  TRUE ~ frmt("(xx.x %)")))),
    frmt_structure(group_val = ".default", label_val = ".default", 
                   AEs = frmt("[XXX]")),
    frmt_structure(group_val = ".default", label_val = ".default", 
                   pval = frmt_when(">0.99" ~ ">0.99",
                                    "<0.001" ~ "<0.001",
                                    "<0.05" ~ frmt("x.xxx*"),
                                    TRUE ~ frmt("x.xxx", missing ="--")))
  )) %>% 
  print_to_gt(., data_ae2) %>% 
  tab_options(
    container.width = 1000
  )

Almost there! Our AE table contains data for both Preferred Terms and System Organ Classes. Therefore, we do not want a typical group-level header. Instead, we want to display the System Organ Class label inline with its data, and nest the Preferred Term data underneath. Fortunately, we are able to achieve this formatting with a row_grp_plan:

tfrmt(
  # specify columns in the data
  group = AEBODSYS,
  label = AETERM,
  column = c(col2, col1), 
  param = param,
  value = value,
  sorting_cols = c(ord1, ord2),
  # specify value formatting 
  body_plan = body_plan(
    frmt_structure(group_val = ".default", label_val = ".default",
                   frmt_combine("{n} {pct}",
                                n = frmt("XXX"),
                                pct = frmt_when(
                                  "==100" ~ "",
                                  "==0" ~ "",
                                  TRUE ~ frmt("(xx.x %)")))),
    frmt_structure(group_val = ".default", label_val = ".default", 
                   AEs = frmt("[XXX]")),
    frmt_structure(group_val = ".default", label_val = ".default", 
                   pval = frmt_when(">0.99" ~ ">0.99",
                                    "<0.001" ~ "<0.001",
                                    "<0.05" ~ frmt("x.xxx*"),
                                    TRUE ~ frmt("x.xxx", missing ="--")))
  ),
  # Nest Preferred terms under SOC
  row_grp_plan = row_grp_plan(label_loc = element_row_grp_loc(location = "indented"))
  ) %>% 
  print_to_gt(data_ae2) %>% 
  tab_options(
    container.width = 1000
  )

Our column alignment looks good as-is, except for the p-values. We can use the col_style_plan to tweak those.

tfrmt(
  # specify columns in the data
  group = AEBODSYS,
  label = AETERM,
  column = c(col2, col1), 
  param = param,
  value = value,
  sorting_cols = c(ord1, ord2),
  # specify value formatting 
  body_plan = body_plan(
    frmt_structure(group_val = ".default", label_val = ".default",
                   frmt_combine("{n} {pct}",
                                n = frmt("XXX"),
                                pct = frmt_when(
                                  "==100" ~ "",
                                  "==0" ~ "",
                                  TRUE ~ frmt("(xx.x %)")))),
    frmt_structure(group_val = ".default", label_val = ".default", 
                   AEs = frmt("[XXX]")),
    frmt_structure(group_val = ".default", label_val = ".default", 
                   pval = frmt_when(">0.99" ~ ">0.99",
                                    "<0.001" ~ "<0.001",
                                    "<0.05" ~ frmt("x.xxx*"),
                                    TRUE ~ frmt("x.xxx", missing ="--")))
  ),
  # Nest Preferred terms under SOC
  row_grp_plan = row_grp_plan(label_loc = element_row_grp_loc(location = "indented")),
  # alignment

  # Specify column styling plan
  col_style_plan = col_style_plan(
    col_style_structure(align = c(".",","," "), col = vars(starts_with("p_")))
  )
  ) %>% 
  print_to_gt(data_ae2) %>% 
  tab_options(
    container.width = 1000
  )

Notice that we still have our order columns and the column labels could benefit from some renaming. We can add a col_plan to help with the ordering:

tfrmt(
  # specify columns in the data
  group = AEBODSYS,
  label = AETERM,
  column = c(col2, col1), 
  param = param,
  value = value,
  sorting_cols = c(ord1, ord2),
  # specify value formatting 
  body_plan = body_plan(
    frmt_structure(group_val = ".default", label_val = ".default",
                   frmt_combine("{n} {pct}",
                                n = frmt("XXX"),
                                pct = frmt_when(
                                  "==100" ~ "",
                                  "==0" ~ "",
                                  TRUE ~ frmt("(xx.x %)")))),
    frmt_structure(group_val = ".default", label_val = ".default", 
                   AEs = frmt("[XXX]")),
    frmt_structure(group_val = ".default", label_val = ".default", 
                   pval = frmt_when(">0.99" ~ ">0.99",
                                    "<0.001" ~ "<0.001",
                                    "<0.05" ~ frmt("x.xxx*"),
                                    TRUE ~ frmt("x.xxx", missing ="--")))
  ),
  # Nest Preferred terms under SOC
  row_grp_plan = row_grp_plan(label_loc = element_row_grp_loc(location = "indented")),

  # Specify column styling plan
  col_style_plan = col_style_plan(
    col_style_structure(align = c(".",","," "), col = vars(p_low, p_high))
  ),
  # columns
  col_plan = col_plan(
    -starts_with("ord")
  )
  ) %>% 
  print_to_gt(data_ae2)  %>% 
  tab_options(
    container.width = 1000
  )

For better control over our column labels, we can make use of col_plan's span_structures to define the column labels and spanners order and names:

tfrmt(
  # specify columns in the data
  group = AEBODSYS,
  label = AETERM,
  column = c(col2, col1), 
  param = param,
  value = value,
  sorting_cols = c(ord1, ord2),
  # specify value formatting 
  body_plan = body_plan(
    frmt_structure(group_val = ".default", label_val = ".default",
                   frmt_combine("{n} {pct}",
                                n = frmt("XXX"),
                                pct = frmt_when(
                                  "==100" ~ "",
                                  "==0" ~ "",
                                  TRUE ~ frmt("(xx.x %)")))),
    frmt_structure(group_val = ".default", label_val = ".default", 
                   AEs = frmt("[XXX]")),
    frmt_structure(group_val = ".default", label_val = ".default", 
                   pval = frmt_when(">0.99" ~ ">0.99",
                                    "<0.001" ~ "<0.001",
                                    "<0.05" ~ frmt("x.xxx*"),
                                    TRUE ~ frmt("x.xxx", missing ="--")))
  ),
  # Nest Preferred terms under SOC
  row_grp_plan = row_grp_plan(label_loc = element_row_grp_loc(location = "indented")),

  # Specify column styling plan
  col_style_plan = col_style_plan(
    col_style_structure(align = c(".",","," "), col = c(p_low, p_high))
  ),

  # columns
  col_plan = col_plan(
    ## defines the spanning column order, and then beneath them the order of their contents
    -starts_with("ord"),
    span_structure(
      col2 = c(
        "Xanomeline High Dose (N=84)" = `Xanomeline High Dose`,
        "Xanomeline Low Dose (N=84)" = `Xanomeline Low Dose`,
        "Placebo (N=86)" = Placebo
      ),
      col1 = c(`n (%)` = `n_pct` ,
               `[AEs]` = `AEs`)
    ),
    span_structure(
      col2 = c("Fisher's Exact p-values" = fisher_pval),
      col1 = c(
        # add a line break to help with table formatting
        `Placebo vs.\n Low Dose` = `p_low` ,
       `Placebo vs.\n High Dose` = `p_high` 
      )
    ))
  ) %>% 
  print_to_gt(data_ae2)  %>% 
  tab_options(
    container.width = 1000
  )

Our AE table is now complete!

Efficacy

For this example, we will use the data_efficacy dataset, an example analysis results dataset found in the package, which is based on the CDISC pilot data for the ADAS-Cog(11) score. The goal is to recreate table 14-3.01 from the CDISC pilot.

This data is relatively simple in that it contains only 1 group column and column column, but it adds complexity in that multiple analyses are stacked together - summary statistics of different values at different time points, and the results of several different ANCOVA models. Multiple treatment groups as well as the contrasts between groups are included.

head(data_efficacy)

The mock we are going to match looks like this:

tfrmt(
  group = group,
  label = label,
  column = column,
  param = param,
  value = value,
  sorting_cols = c(ord1, ord2),
  row_grp_plan = row_grp_plan(
    row_grp_structure(group_val = list(group="Change from Baseline"), element_block(post_space = " ")),
    row_grp_structure(group_val = list(group="p-value (Dose Response)"), element_block(post_space = " ")),
    row_grp_structure(group_val = list(group="p-value (Xan - Placebo)"), element_block(post_space = " "))
  ),
  body_plan = body_plan(
    frmt_structure(group_val = ".default", label_val = "n", frmt("xx")), 
    frmt_structure(group_val = ".default", label_val = "Median (Range)", frmt_combine("{median} ({min};{max})",
                                                                                      median = frmt("xx.x"),
                                                                                      min = frmt("xx"),
                                                                                      max = frmt("xx"), missing = " ")),
    frmt_structure(group_val = ".default", label_val = "Mean (SD)", frmt_combine("{mean} ({sd})",
                                                                                      mean = frmt("xx.x"),
                                                                                      sd = frmt("xx.xx"), missing = " ")),
    frmt_structure(group_val = ".default", label_val = "Diff of LS Means (SE)", frmt_combine("{diff} ({diff_se})",
                                                                                 diff = frmt("xx.x"),
                                                                                 diff_se = frmt("xx.xx"), missing = " ")),
    frmt_structure(group_val = ".default", label_val = "95% CI", frmt_combine("({diff_lcl};{diff_ucl})",
                                                                              diff_lcl = frmt("xx.x"),
                                                                              diff_ucl = frmt("xx.x"), missing = " ")),
    frmt_structure(group_val = ".default", label_val = ".default", p.value = frmt_when("<0.001" ~ "<0.001",
                                                                                       ">0.99" ~ ">0.99",
                                                                                       TRUE ~ frmt("x.xxx", missing = " ")))
  ),
  col_plan = col_plan(
    group, label,
    contains("Placebo"),
    contains("Low"),
    contains("High")
  )
) %>%
  print_mock_gt(data_efficacy %>% select(-value))%>% 
  tab_options(
    container.width = 800
  )

Let's first see how the table looks without any special formatting.

tfrmt(
  group = group,
  label = label,
  column = column,
  param = param,
  value = value 
) %>%
  print_to_gt(data_efficacy) %>% 
  tab_options(
    container.width = 800
  ) 

Through judicious use of body_plan's frmt, frmt_combine, and frmt_when, we can conditionally format each of the different pieces of results. For the summary statistics, we have the number of observations (n), the mean and standard deviation (mean, sd), and the median and range (median, min, max). For the models, we have the p-value (p.value) and the least squares mean difference (diff) as well as its associated standard error (diff_se) and 95% confidence interval (diff_lcl, diff_ucl).

The label column indicates which row the various measures belong on. First, let's format the stand-alone values: n and p-value. Notice that n always sits on a row labelled "n"; therefore we can reference it by label_val or param name in the frmt_structure. Our p-values have several different label values so it is more convenient to format them according to their param name in the frmt_structure.

tfrmt(
  group = group,
  label = label,
  column = column,
  param = param,
  value = value, 
  body_plan = body_plan(
    frmt_structure(group_val = ".default", label_val = "n", 
                   frmt("xx")),  # we could also do: label_val = ".default", n = frmt("xx")
    frmt_structure(group_val = ".default", label_val = ".default", 
                   p.value = frmt_when("<0.001" ~ "<0.001",
                                       ">0.99" ~ ">0.99",
                                       TRUE ~ frmt("x.xxx", missing = " ")))
  )
) %>%
  print_to_gt(data_efficacy) %>% 
  tab_options(
    container.width = 800
  )

Next, the remaining param values are combined in twos or threes. Therefore, we use the frmt_combine utility to achieve desired formatting:

tfrmt(
  group = group,
  label = label,
  column = column,
  param = param,
  value = value, 
  body_plan = body_plan(
    frmt_structure(group_val = ".default", label_val = "n", 
                   frmt("xx")),  # we could also do: label_val = ".default", n = frmt("xx")
    frmt_structure(group_val = ".default", label_val = ".default", 
                   p.value = frmt_when("<0.001" ~ "<0.001",
                                       ">0.99" ~ ">0.99",
                                       TRUE ~ frmt("x.xxx", missing = " "))),
    frmt_structure(group_val = ".default", label_val = "Median (Range)", 
                   frmt_combine("{median} ({min};{max})",
                                median = frmt("xx.x"),
                                min = frmt("xx"),
                                max = frmt("xx"), missing = " ")),
    frmt_structure(group_val = ".default", label_val = "Mean (SD)",
                   frmt_combine("{mean} ({sd})",
                                mean = frmt("xx.x"),
                                sd = frmt("xx.xx"), missing = " ")),
    frmt_structure(group_val = ".default", label_val = "Diff of LS Means (SE)", 
                   frmt_combine("{diff} ({diff_se})",
                                diff = frmt("xx.x"),
                                diff_se = frmt("xx.xx"), missing = " ")),
    frmt_structure(group_val = ".default", label_val = "95% CI", 
                   frmt_combine("({diff_lcl};{diff_ucl})",
                                diff_lcl = frmt("xx.x"),
                                diff_ucl = frmt("xx.x"), missing = " "))
  )
) %>%
  print_to_gt(data_efficacy) %>% 
  tab_options(
    container.width = 800
  )

Now that our values are all formatted correctly, we can make sure the table is sorted appropriately by passing our order columns to sorting_cols. We can also drop these order columns from the final display using col_plan.

tfrmt(
  group = group,
  label = label,
  column = column,
  param = param,
  value = value,
  sorting_cols = c(ord1, ord2),
  body_plan = body_plan(
    frmt_structure(group_val = ".default", label_val = "n", 
                   frmt("xx")),  # we could also do: label_val = ".default", n = frmt("xx")
    frmt_structure(group_val = ".default", label_val = ".default", 
                   p.value = frmt_when("<0.001" ~ "<0.001",
                                       ">0.99" ~ ">0.99",
                                       TRUE ~ frmt("x.xxx", missing = " "))),
    frmt_structure(group_val = ".default", label_val = "Median (Range)", 
                   frmt_combine("{median} ({min};{max})",
                                median = frmt("xx.x"),
                                min = frmt("xx"),
                                max = frmt("xx"), missing = " ")),
    frmt_structure(group_val = ".default", label_val = "Mean (SD)", 
                   frmt_combine("{mean} ({sd})",
                                mean = frmt("xx.x"),
                                sd = frmt("xx.xx"), missing = " ")),
    frmt_structure(group_val = ".default", label_val = "Diff of LS Means (SE)", 
                   frmt_combine("{diff} ({diff_se})",
                                diff = frmt("xx.x"),
                                diff_se = frmt("xx.xx"), missing = " ")),
    frmt_structure(group_val = ".default", label_val = "95% CI", 
                   frmt_combine("({diff_lcl};{diff_ucl})",
                                diff_lcl = frmt("xx.x"),
                                diff_ucl = frmt("xx.x"), missing = " "))
  ),
  col_plan = col_plan(
    group, label, Placebo, contains("Low"), contains("High"), -starts_with("ord")
  )
) %>%
  print_to_gt(data_efficacy) %>% 
  tab_options(
    container.width = 800
  )

Notice that our row labels are not quite right. First, we have a bit of a hierarchy with the label values nested under the group values, and it would be nice to add some indentation to make the nesting more obvious. Also, in some cases, the group values also contain summary data, which means the ARD contains a matching group and label value. For summary rows, we want to suppress the printing of the extra group-level header, and display the summary data in-line. The row_grp_plan can help us with both via the row_grp_loc argument:

tfrmt(
  group = group,
  label = label,
  column = column,
  param = param,
  value = value,
  sorting_cols = c(ord1, ord2),
  body_plan = body_plan(
    frmt_structure(group_val = ".default", label_val = "n", 
                   frmt("xx")),  # we could also do: label_val = ".default", n = frmt("xx")
    frmt_structure(group_val = ".default", label_val = ".default", 
                   p.value = frmt_when("<0.001" ~ "<0.001",
                                       ">0.99" ~ ">0.99",
                                       TRUE ~ frmt("x.xxx", missing = " "))),
    frmt_structure(group_val = ".default", label_val = "Median (Range)", 
                   frmt_combine("{median} ({min};{max})",
                                median = frmt("xx.x"),
                                min = frmt("xx"),
                                max = frmt("xx"), missing = " ")),
    frmt_structure(group_val = ".default", label_val = "Mean (SD)", 
                   frmt_combine("{mean} ({sd})",
                                mean = frmt("xx.x"),
                                sd = frmt("xx.xx"), missing = " ")),
    frmt_structure(group_val = ".default", label_val = "Diff of LS Means (SE)", 
                   frmt_combine("{diff} ({diff_se})",
                                diff = frmt("xx.x"),
                                diff_se = frmt("xx.xx"), missing = " ")),
    frmt_structure(group_val = ".default", label_val = "95% CI", 
                   frmt_combine("({diff_lcl};{diff_ucl})",
                                diff_lcl = frmt("xx.x"),
                                diff_ucl = frmt("xx.x"), missing = " "))
  ),
  col_plan = col_plan(
    group, label, Placebo, contains("Low"), contains("High"), -starts_with("ord")
  ),
  row_grp_plan = row_grp_plan(
    label_loc = element_row_grp_loc(location = "indented")
  )
) %>%
  print_to_gt(data_efficacy) %>% 
  tab_options(
    container.width = 800
  )

Almost done! Notice that the spec also contains empty rows after different groups of data. We can mimic this behavior by passing row_grp_structure objects in our row_grp_plan. These objects define "blocks" of rows and describe how to format them. In this case, we want to add a post space after specific blocks of data. We can reference the locations of each block based on the values of the group variable.

tfrmt(
  group = group,
  label = label,
  column = column,
  param = param,
  value = value,
  sorting_cols = c(ord1, ord2),
  body_plan = body_plan(
    frmt_structure(group_val = ".default", label_val = "n", 
                   frmt("xx")),  # we could also do: label_val = ".default", n = frmt("xx")
    frmt_structure(group_val = ".default", label_val = ".default", 
                   p.value = frmt_when("<0.001" ~ "<0.001",
                                       ">0.99" ~ ">0.99",
                                       TRUE ~ frmt("x.xxx", missing = " "))),
    frmt_structure(group_val = ".default", label_val = "Median (Range)", 
                   frmt_combine("{median} ({min};{max})",
                                median = frmt("xx.x"),
                                min = frmt("xx"),
                                max = frmt("xx"), missing = " ")),
    frmt_structure(group_val = ".default", label_val = "Mean (SD)", 
                   frmt_combine("{mean} ({sd})",
                                mean = frmt("xx.x"),
                                sd = frmt("xx.xx"), missing = " ")),
    frmt_structure(group_val = ".default", label_val = "Diff of LS Means (SE)", 
                   frmt_combine("{diff} ({diff_se})",
                                diff = frmt("xx.x"),
                                diff_se = frmt("xx.xx"), missing = " ")),
    frmt_structure(group_val = ".default", label_val = "95% CI", 
                   frmt_combine("({diff_lcl};{diff_ucl})",
                                diff_lcl = frmt("xx.x"),
                                diff_ucl = frmt("xx.x"), missing = " "))
  ),
  col_plan = col_plan(
    group, label, Placebo, contains("Low"), contains("High"), -starts_with("ord")
  ),
  row_grp_plan = row_grp_plan(
    row_grp_structure(group_val = list(group="Change from Baseline"), 
                      element_block(post_space = " ")),
    row_grp_structure(group_val = list(group="p-value (Dose Response)"),
                      element_block(post_space = " ")),
    row_grp_structure(group_val = list(group="p-value (Xan - Placebo)"), 
                      element_block(post_space = " ")),
    label_loc = element_row_grp_loc(location = "indented")
  )
) %>%
  print_to_gt(data_efficacy) %>% 
  tab_options(
    container.width = 800
  )

There we have it, our efficacy table is complete!



GSK-Biostatistics/tlang documentation built on Dec. 11, 2024, 11:16 a.m.