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.
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
labelattribute (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 thetbl_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.00Basic 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:
- The arguments of
tbl_summary(). - The
add_*()functions, which add columns of statistics. - The ltsummary functions that modify the appearance of the table.
- 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.
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:
The tidyselect-style helpers
everything(),starts_with(),contains(),all_of()and friends, exclusion with-, and ranges such asage:grade. The package implements these in base R; see the syntax reference for the supported forms.-
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().