Skip to contents

Introduction

This article follows gtsummary’s article of the same name.

tbl_summary() calculates descriptive statistics for continuous, categorical and dichotomous variables and presents them in a summary table ready for publication, the kind that opens a clinical paper as Table 1.

This tutorial walks through tbl_summary() and the functions that add to and modify the table it returns. If you have used gtsummary, everything here will look familiar: the function names, arguments and defaults are the same, and the migration article lists the few places where the two packages differ.

Setup

Install ltsummary from GitHub and load it.

# pak::pak("tgerke/ltsummary")
library(ltsummary)

Example data set

The examples use the trial data set that ships with the package.

  • It contains data from 200 patients who received one of two types of chemotherapy (Drug A or Drug B). The outcomes are tumor response and death.
  • Each variable has a label attribute (for example, attr(trial$trt, "label") is "Chemotherapy Treatment"). The labels are used in the table by default. A data frame without labels prints the variable names instead, and labels can be supplied in the tbl_summary() call.
Variable Class Label
trt character Chemotherapy Treatment
age numeric Age
marker numeric Marker Level (ng/mL)
stage factor T Stage
grade factor Grade
response integer Tumor Response
death integer Patient Died
ttdeath numeric Months to Death/Censor
head(trial)
#>      trt age marker stage grade response death ttdeath
#> 1 Drug A  66  0.629    T3     I        0     1   20.96
#> 2 Drug B  72  0.622    T3   III        0     0   24.00
#> 3 Drug A  72  2.389    T4    II        0     1   23.36
#> 4 Drug A  73  1.591    T2    II        0     1   15.89
#> 5 Drug B  43  0.744    T2    II        1     1   22.71
#> 6 Drug A  81     NA    T1   III        0     0   24.00

Basic usage

The default output of tbl_summary() is meant to be publication ready. The function takes a data frame as its only required input and returns descriptive statistics for every column.

trial |> tbl_summary(include = c(trt, age, grade))

Note the defaults, each of which can be changed:

  • Variable types are detected, so each variable gets an appropriate statistic.
  • The label attributes of the data are printed.
  • Missing values are counted in an “Unknown” row.
  • Variable levels are indented, and footnotes describe the statistics.

For this study the statistics should be split by treatment group, which the by argument does. To compare the groups, add add_p(), which picks a test for each variable type.

trial |>
  tbl_summary(by = trt, include = c(age, grade)) |>
  add_p()

Customizing the output

There are four ways to customize a summary table:

  1. The arguments of tbl_summary().
  2. The add_*() functions, which add columns of statistics.
  3. The ltsummary functions that modify the appearance of the table.
  4. The functions of the lt package, applied after as_lt().

tbl_summary() arguments

Argument Description
label variable labels printed in the table
type variable type (continuous, categorical, …)
statistic summary statistics presented
digits number of digits the statistics are rounded to
missing whether to show a row with the number of missing observations
missing_text label of the missing row
missing_stat statistic shown on the missing row
sort order of categorical levels, alphanumeric or by frequency
percent column, row or cell percentages
include variables to include in the table

An example that uses several of them:

trial |>
  tbl_summary(
    by = trt,
    include = c(age, grade),
    statistic = list(
      all_continuous() ~ "{mean} ({sd})",
      all_categorical() ~ "{n} / {N} ({p}%)"
    ),
    digits = all_continuous() ~ 2,
    label = list(grade = "Tumor Grade"),
    missing_text = "(Missing)"
  )

Arguments such as statistic accept a single formula, a list of formulas, or a named list. The table below shows equivalent ways to request the mean for the continuous variables age and marker. Every argument that accepts formulas accepts each of these forms; the syntax reference has the details.

Select with helpers Select by variable name Select with a named list
all_continuous() ~ "{mean}" c("age", "marker") ~ "{mean}" list(age = "{mean}", marker = "{mean}")
list(all_continuous() ~ "{mean}") c(age, marker) ~ "{mean}"
list(c(age, marker) ~ "{mean}")

Functions that add information

Function Description
add_p() add p-values comparing the groups
add_difference() add the difference between two groups, with a confidence interval
add_ci() add a confidence interval around each statistic
add_overall() add a column with the overall statistics
add_n() add a column with the number of observations (or missing) for each variable
add_stat_label() add a label describing the statistic shown in each row
add_stat() add a column computed by a function you write

Functions that format the table

Function Description
modify_header() update column headers
modify_footnote_header() update column header footnotes
modify_footnote_body() update table body footnotes
modify_spanning_header() update spanning headers
modify_caption() update the table caption
bold_labels() bold variable labels
bold_levels() bold variable levels
italicize_labels() italicize variable labels
italicize_levels() italicize variable levels
bold_p() bold significant p-values

An example that combines the add_*() and modify_*() functions:

trial |>
  tbl_summary(by = trt, include = c(age, grade)) |>
  add_p(pvalue_fun = label_style_pvalue(digits = 2)) |>
  add_overall() |>
  add_n() |>
  modify_header(label ~ "**Variable**") |>
  modify_spanning_header(c("stat_1", "stat_2") ~ "**Treatment Received**") |>
  modify_footnote_header("Median (Q1, Q3) or Frequency (%)", columns = all_stat_cols()) |>
  modify_caption("**Table 1. Patient Characteristics**") |>
  bold_labels()

The modifier functions article goes through every modify_*() function with an example.

lt functions

The table is rendered by the lt package. To use lt’s own functions, convert the table with as_lt() once the ltsummary modifications are done; the result accepts any lt::lt_*() function. Here a note is added and the table width is set.

trial |>
  tbl_summary(by = trt, include = c(age, grade), missing = "no") |>
  add_n() |>
  as_lt() |>
  lt::lt_note("Data are simulated") |>
  lt::lt_width("70%")

Select helpers

Variables can be selected in several ways, which makes the arguments flexible. To show age and the marker level to one decimal place, pass digits = c(age, marker) ~ 1; quoted names work as well, digits = c("age", "marker") ~ 1.

Beyond naming the variables, you can use:

  1. The tidyselect-style helpers everything(), starts_with(), contains(), all_of() and friends, exclusion with -, and ranges such as age:grade. The package implements these in base R; see the syntax reference for the supported forms.

  2. The ltsummary selectors, which select variables by summary type. This is how to report the mean and standard deviation for every continuous variable, statistic = all_continuous() ~ "{mean} ({sd})".

    Dichotomous variables are included in all_categorical() by default.

Multi-line continuous summaries

Continuous variables can be summarized on several lines, a format some journals ask for. Set the summary type to "continuous2" and pass a vector of statistics, one per line.

trial |>
  tbl_summary(
    by = trt,
    include = age,
    type = all_continuous() ~ "continuous2",
    statistic = all_continuous() ~ c(
      "{N_nonmiss}",
      "{median} ({p25}, {p75})",
      "{min}, {max}"
    ),
    missing = "no"
  ) |>
  add_p(pvalue_fun = label_style_pvalue(digits = 2))

Advanced customization

This section applies to every ltsummary object.

An ltsummary object has two important components:

Internal object Description
.$table_body data frame that is printed as the output table
.$table_styling instructions for styling .$table_body when printed

When a table is printed in the console or knit into a document, .$table_body is formatted following the instructions in .$table_styling. The default printer converts the object to an lt table with as_lt(), which runs a sequence of lt calls on .$table_body. Here are the first few calls saved with tbl_summary():

tbl_summary(trial) |>
  as_lt(return_calls = TRUE) |>
  head(n = 4)
#> $lt
#> lt::lt(list(label = c("Chemotherapy Treatment", "Drug A", "Drug B", 
#> "Age", "Unknown", "Marker Level (ng/mL)", "Unknown", "T Stage", 
#> "T1", "T2", "T3", "T4", "Grade", "I", "II", "III", "Tumor Response", 
#> "Unknown", "Patient Died", "Months to Death/Censor"), stat_0 = c("", 
#> "95 (48%)", "105 (53%)", "60 (53, 68)", "11", "0.68 (0.36, 1.19)", 
#> "10", "", "51 (26%)", "54 (27%)", "45 (23%)", "50 (25%)", "", 
#> "62 (31%)", "83 (42%)", "55 (28%)", "46 (24%)", "7", "93 (47%)", 
#> "24.0 (15.5, 24.0)")), auto_format = FALSE, auto_label = FALSE)
#> 
#> $lt_label
#> lt::lt_label(tbl, label = "<strong>Characteristic</strong>", 
#>     stat_0 = "<strong>N = 200</strong>")
#> 
#> $lt_align
#> lt::lt_align(tbl, columns = "label", align = "left")
#> 
#> $lt_align_2
#> lt::lt_align(tbl, columns = "stat_0", align = "center")

The lt functions are called in the order they appear, beginning with lt::lt(). To skip one of them, exclude it in as_lt(). In the example below the alignment call is dropped, so lt’s default alignment applies, and then an lt function adds a note.

tbl_summary(trial, by = trt, include = c(age, grade)) |>
  as_lt(include = -lt_align) |>
  lt::lt_note("Data are simulated")

The object definition article describes both components in detail.

Default options

tbl_summary() and its companions have sensible defaults for rounding and presentation. A few of them can be changed globally with options, which is useful in a document that reports every table the same way:

Option Effect
ltsummary.big.mark thousands separator used by style_number() and the statistics built on it
ltsummary.decimal.mark decimal mark; when set to "," the default big mark becomes a thin space
ltsummary.cat_threshold numeric variables with fewer distinct values than this are categorical (default 10)
ltsummary.default_con_type summary type of numeric variables, "continuous" (default) or "continuous2"
ltsummary.print_engine "lt" (default) or "data.frame", the print method used at the console

A numeric variable with four distinct values is categorical by default and continuous once the threshold is lowered:

df_scores <- data.frame(score = rep(1:4, 50))
tbl_summary(df_scores)

options(ltsummary.cat_threshold = 3L)
tbl_summary(df_scores)
options(ltsummary.cat_threshold = NULL)

Themes cover the same ground more completely: set_ltsummary_theme() with the theme_ltsummary_*() constructors changes the default statistics, tests, marks, rendering and language in one step, and a theme outranks the options above. See ?set_ltsummary_theme; the migration article maps each theme_gtsummary_*() function to its counterpart.

with_ltsummary_theme(
  theme_ltsummary_journal("lancet", set_theme = FALSE),
  tbl_summary(trial, include = c(marker, grade), missing = "no")
)

What comes next

The inline_text() tutorial shows how to quote the table in the text of a report, the modifier functions article goes through the formatting functions one by one, and the FAQ and gallery collects recipes for common requests, including cross tables with tbl_cross(), survival curves with tbl_survfit(), and tables combined with tbl_merge() and tbl_strata().