ltsummary makes the descriptive tables that clinical reports start
with, using the same function names and arguments as gtsummary, and
renders them with the lt package. If you have used gtsummary, the only
new function you need is as_lt(), and you rarely need to
call it yourself. This vignette walks through a typical table from the
first draft to the sentence in the results section that quotes it.
The package ships trial, a simulated data set of 200
patients with the same columns as gtsummary’s example data: treatment
arm, age, a tumor marker, stage, grade, response, death and survival
time.
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.00A first table
tbl_summary() summarizes every column it is given.
Continuous variables get the median and quartiles, categorical variables
get counts and percentages, and variables coded 0/1, TRUE/FALSE or
yes/no are shown on one line. Column labels come from the
label attribute of the data when there is one.
tbl_summary(trial, include = c(age, grade, response))Add by to split the table by the levels of a variable.
Rows with a missing by value are dropped with a
message.
tbl_summary(trial, by = trt, include = c(age, marker, grade, response))Changing the defaults
Most arguments accept a formula list: the left-hand side selects
variables and the right-hand side gives the value. Variables can be
selected by name, with a character vector, or with
all_continuous(), all_categorical() and
all_dichotomous().
tbl_summary(
trial,
by = trt,
include = c(age, marker, grade),
statistic = list(
all_continuous() ~ "{mean} ({sd})",
all_categorical() ~ "{n} / {N} ({p}%)"
),
digits = list(age ~ 1, marker ~ c(2, 3)),
label = list(grade ~ "Tumor grade"),
missing = "no"
)Statistics are written with the name in braces. Continuous variables
can use {mean}, {sd}, {var},
{median}, {min}, {max},
{sum} and any percentile from {p0} to
{p100}; categorical variables can use {n},
{N} and {p}. The name of a function of one
argument also works, so "{IQR}" or
"{my_trimmed_mean}" are valid.
The number of decimal places is guessed from the spread of each
variable. Pass digits to override it with a single number
for every statistic, a vector matched to the statistics in order, or a
named list such as list(mean = 1, sd = 2). Functions such
as label_style_sigfig() are accepted too.
type changes how a variable is summarized. The usual
reason is to show every level of a 0/1 variable, or to summarize a
numeric variable with few distinct values as continuous.
tbl_summary(
trial,
include = c(response, grade),
type = list(response ~ "categorical"),
value = list(grade ~ "III"),
label = list(grade ~ "Grade III")
)A "continuous2" variable gets one line per
statistic:
tbl_summary(
trial,
include = c(age, marker),
type = all_continuous() ~ "continuous2",
statistic = all_continuous() ~ c("{median} ({p25}, {p75})", "{mean} ({sd})", "{min}, {max}")
)Adding columns
add_overall() adds a column for all patients,
add_n() adds the number of non-missing observations,
add_p() adds p-values, and add_stat_label()
writes the statistic next to each variable label so the footnote is no
longer needed.
tbl <- trial |>
tbl_summary(by = trt, include = c(age, marker, grade, response)) |>
add_overall() |>
add_n() |>
add_p() |>
add_stat_label()
tbladd_difference() reports the difference between two
groups with its confidence interval instead of a p-value alone,
add_ci() puts an interval next to every statistic, and
add_stat() adds a column computed by a function you
write.
By default add_p() compares continuous variables with
the Wilcoxon rank sum test (or the Kruskal-Wallis test for more than two
groups) and categorical variables with Pearson’s chi-squared test,
falling back to Fisher’s exact test when an expected cell count is below
5. The test argument takes a formula list of test names or
functions, and test.args passes extra arguments through;
?tests lists what is built in.
trial |>
tbl_summary(by = trt, include = c(age, grade)) |>
add_p(
test = list(all_continuous() ~ "t.test", all_categorical() ~ "fisher.test"),
test.args = list(age ~ list(var.equal = TRUE)),
pvalue_fun = label_style_pvalue(digits = 2)
)Headers, footnotes and formatting
Header text can reference the column’s statistics: {N}
is the total, {n} the number in the column’s group,
{p} the proportion and {level} the group
label. Text supports a small markdown subset, **bold**,
_italic_ and line breaks.
tbl |>
modify_header(label = "**Variable**", all_stat_cols() ~ "**{level}** \nn = {n} ({style_percent(p)}%)") |>
modify_spanning_header(c(stat_1, stat_2) ~ "**Treatment arm**") |>
modify_caption("**Table 1. Baseline characteristics (N = {N})**") |>
modify_footnote_header("Randomized 1:1", columns = c(stat_1, stat_2)) |>
modify_abbreviation(c("Q1 = first quartile", "Q3 = third quartile")) |>
modify_source_note("Simulated data.") |>
bold_labels() |>
italicize_levels() |>
bold_p(t = 0.1)show_header_names() prints the column names and the
values available to each header, which is the quickest way to see what
{n} and {level} will resolve to.
Cells can be formatted after the fact with
modify_fmt_fun(), missing values replaced with
modify_missing_symbol(), columns hidden with
modify_column_hide() or merged with
modify_column_merge(), and rows removed with
remove_row_type().
tbl |>
remove_row_type(type = "missing") |>
modify_column_hide(stat_0) |>
modify_fmt_fun(p.value = label_style_pvalue(digits = 3))Quoting the table in text
inline_text() pulls a formatted cell out of the table.
Select the variable, the column (a by level or a column
name such as p.value) and, for categorical variables, the
level.
inline_text(tbl, variable = age, column = "Drug A")
#> [1] "59 (51, 66)"
inline_text(tbl, variable = grade, level = "II", column = "Drug B")
#> [1] "43 (41%)"
inline_text(tbl, variable = age, column = p.value)
#> [1] "p=0.043"A pattern combines the statistics behind a cell in any
arrangement:
inline_text(tbl, variable = age, column = "Drug A", pattern = "median {median} (IQR {p25} to {p75})")
#> [1] "median 59 (IQR 51 to 66)"Without column, the pattern works on the columns of the
row instead, which is how to quote two arms and their p-value in one go.
column defaults to the overall column when the table has
one, so this form needs a table without add_overall(). The
p-value sits on the variable’s label row, so pair it with
variable alone; a level row has the counts but
no p-value.
tbl_arms <- trial |>
tbl_summary(by = trt, include = c(age, grade)) |>
add_p()
inline_text(tbl_arms, variable = age, pattern = "{stat_1} vs. {stat_2} ({p.value})")
#> [1] "59 (51, 66) vs. 62 (54, 71) (p=0.043)"
inline_text(tbl_arms, variable = grade, level = "III", pattern = "{stat_1} vs. {stat_2}")
#> [1] "30 (32%) vs. 25 (24%)"In a Quarto or R Markdown document these calls go in inline code, so
the sentence “Median age was
`r inline_text(tbl, variable = age, column = "Drug A")`
years in the Drug A arm” updates with the data.
Under the hood, and getting out
as_lt() converts the table to an lt object. Printing
does this for you, but calling it yourself gives access to the lt verbs,
for example to set the table width or to save a static copy.
as.data.frame() returns the formatted cells as a plain
data frame, with the header labels as column names. The underlying
object is a list with a table_body data frame and a
table_styling list, the same structure gtsummary uses, so
code that works with x$table_body carries over.
as.data.frame(tbl)[1:4, 1:4]
#> **Characteristic** **N** **Overall** \nN = 200
#> 1 Age, Median (Q1, Q3) 189 60 (53, 68)
#> 2 Unknown <NA> 11
#> 3 Marker Level (ng/mL), Median (Q1, Q3) 190 0.68 (0.36, 1.19)
#> 4 Unknown <NA> 10
#> **Drug A** \nN = 95
#> 1 59 (51, 66)
#> 2 4
#> 3 0.72 (0.39, 1.19)
#> 4 8Further reading
- The tbl_summary() tutorial goes through every argument with an example.
- The tbl_regression()
tutorial does the same for regression models, with
tbl_regression()andtbl_uvregression(). - The inline_text() tutorial covers quoting cells and patterns in report text, for every kind of table.
- The modifier functions article covers the formatting functions one by one.
- The FAQ and
gallery collects recipes, including cross tables with
tbl_cross(), survival curves withtbl_survfit(), and tables combined withtbl_merge()andtbl_strata(). - The Quarto and R Markdown article explains the output formats.
- Migrating from gtsummary lists what carries over, what is planned and where behavior differs.
- Definition of an ltsummary object documents the object for people who build on it.