Answers to the questions that come up most, each with a table showing the result. This article follows gtsummary’s article of the same name, so the questions are gtsummary’s.
Frequently asked questions
Headers, labels and formatting
Adding and modifying statistics
How do I report the mean and standard deviation instead of the median?
How do I report a statistic that ltsummary does not compute?
How do I include a column for missing values of a grouping variable?
How do I add a column with the confidence interval around the mean?
How do I summarize a numeric variable with few distinct values as continuous?
How do I show a chosen level of a categorical variable on one row?
Statistical tests
Cross and survival tables
Combining tables
Summary tables
Add a spanning header over the group columns for clarity, and modify
the column headers to show the group size. bold_labels()
formats the labels in bold; italicize_labels() is the
italic counterpart, and the two combine.
trial |>
tbl_summary(
by = trt,
include = c(age, grade),
missing = "no",
statistic = all_continuous() ~ "{median} ({p25}, {p75})"
) |>
modify_header(all_stat_cols() ~ "**{level}** \nN = {n} ({style_percent(p)}%)") |>
add_n() |>
bold_labels() |>
modify_spanning_header(all_stat_cols() ~ "**Chemotherapy Treatment**")Show continuous summary statistics on multiple lines. The levels are
italicized with italicize_levels();
bold_levels() makes them bold, and both can be used
together.
trial |>
tbl_summary(
by = trt,
include = c(age, marker),
type = all_continuous() ~ "continuous2",
statistic =
all_continuous() ~ c("{N_nonmiss}",
"{mean} ({sd})",
"{median} ({p25}, {p75})",
"{min}, {max}"),
missing = "no"
) |>
italicize_levels()Change the function that formats the p-values and update the variable labels. Here the table is split by tumor response, so the response variable is recoded with descriptive levels first.
trial$response <- factor(trial$response, labels = c("No Tumor Response", "Tumor Responded"))
trial |>
tbl_summary(
by = response,
include = c(age, grade),
missing = "no",
label = list(age ~ "Patient Age", grade ~ "Tumor Grade")
) |>
add_p(pvalue_fun = label_style_pvalue(digits = 2))Include the patients with a missing tumor response as a column of
their own. tbl_summary() drops rows with a missing
by value, so the missing values become a level first.
trial$response <- addNA(trial$response)
levels(trial$response)[is.na(levels(trial$response))] <- "Missing Response Status"
trial |>
tbl_summary(
by = response,
include = c(age, grade),
label = list(age ~ "Patient Age", grade ~ "Tumor Grade")
)Show the statistic next to each variable label instead of in a footnote, or in a column of its own.
trial |>
tbl_summary(by = trt, include = c(age, grade), missing = "no") |>
add_stat_label(label = all_continuous() ~ "Median (IQR)")
trial |>
tbl_summary(by = trt, include = c(age, grade), missing = "no") |>
add_stat_label(location = "column")Report the mean and standard deviation for the continuous variables, and counts with percentages in the form n / N.
trial |>
tbl_summary(
by = trt,
include = c(age, marker, grade),
statistic = list(
all_continuous() ~ "{mean} ({sd})",
all_categorical() ~ "{n} / {N} ({p}%)"
),
missing = "no"
)Any function of one argument can be a statistic: name it in braces.
The function is called on the non-missing values of the variable. Here
the geometric mean of the marker level and the interquartile range of
age are reported, and add_stat_label() names them.
geo_mean <- function(x) exp(mean(log(x)))
trial |>
tbl_summary(
by = trt,
include = c(age, marker),
statistic = list(age ~ "{median} ({IQR})", marker ~ "{geo_mean}"),
missing = "no"
) |>
add_stat_label(label = list(age ~ "Median (IQR)", marker ~ "Geometric mean"))Set the number of decimal places per variable, per statistic, or with a formatting function. A vector is matched to the statistics in order; a named list changes one statistic and leaves the others at their defaults.
trial |>
tbl_summary(
by = trt,
include = c(age, marker, grade),
statistic = all_continuous() ~ "{mean} ({sd})",
digits = list(
age ~ c(1, 2),
marker ~ label_style_sigfig(digits = 3),
grade ~ list(p = 1)
),
missing = "no"
)Report row percentages, and show only the percentage for a dichotomous variable.
trial |>
tbl_summary(
by = trt,
include = c(grade, response),
percent = "row",
statistic = list(grade ~ "{n} ({p}%)", response ~ "{p}%"),
digits = response ~ 1,
missing = "no"
) |>
modify_header(all_stat_cols() ~ "**{level}**") |>
modify_footnote_header("Row percentages", columns = all_stat_cols())A numeric variable with fewer than ten distinct values is summarized as categorical by default. Set its type to summarize it as continuous; the reverse works as well.
trial$grade_num <- as.integer(trial$grade)
trial |>
tbl_summary(
include = c(grade_num, age),
type = list(grade_num ~ "continuous"),
label = list(grade_num ~ "Grade (numeric)")
)
trial <- ltsummary::trialShow one level of a categorical variable on a single row with the
value argument, which turns the variable into a dichotomous
one.
trial |>
tbl_summary(
by = trt,
include = c(grade, stage),
value = list(grade ~ "III", stage ~ "T4"),
label = list(grade ~ "Grade III", stage ~ "Stage T4")
)Use a decimal comma throughout. When the decimal mark is a comma, the thousands separator defaults to a thin space, as in gtsummary.
options(ltsummary.decimal.mark = ",")
trial |>
tbl_summary(
by = trt,
include = c(age, marker),
statistic = all_continuous() ~ "{mean} ({sd})",
missing = "no"
) |>
add_p()
options(ltsummary.decimal.mark = NULL)Statistical tests
Choose the tests by variable or by summary type. Arguments are passed
to a test with test.args; all_tests() selects
every variable compared with a given test. The tests reference lists the built-in
tests with the code each one runs.
trial |>
tbl_summary(by = trt, include = c(age, marker, grade), missing = "no") |>
add_p(
test = list(all_continuous() ~ "t.test", all_categorical() ~ "fisher.test"),
test.args = all_tests("t.test") ~ list(var.equal = TRUE)
)Paired t-test and McNemar’s test. The data are expected in long format, one row per participant and treatment, with complete pairs only.
# imagine that each patient received Drug A and Drug B: `id` links the two rows
trial_paired <- trial[c("trt", "marker", "response")]
trial_paired$id <- ave(seq_len(nrow(trial_paired)), trial_paired$trt, FUN = seq_along)
# delete incomplete pairs first
trial_paired <- trial_paired[complete.cases(trial_paired), ]
trial_paired <- trial_paired[trial_paired$id %in% names(which(table(trial_paired$id) == 2)), ]
trial_paired |>
tbl_summary(
by = trt,
include = c(marker, response),
label = list(marker ~ "Marker Level (ng/mL)", response ~ "Tumor Response"),
missing = "no"
) |>
add_p(
test = list(marker ~ "paired.t.test", response ~ "mcnemar.test"),
group = id
)Run a test that is not built in by writing a function that returns a
list or one-row data frame with a p.value element. A
method element is shown in the footnote. The tests reference
describes the arguments the function receives.
ansari <- function(data, variable, by, ...) {
res <- ansari.test(data[[variable]] ~ as.factor(data[[by]]))
list(p.value = res$p.value, method = "Ansari-Bradley test")
}
trial |>
tbl_summary(by = trt, include = c(age, marker), missing = "no") |>
add_p(test = all_continuous() ~ ansari)Add a 95% confidence interval around the mean as an extra column.
add_ci() puts an interval next to every statistic column;
for categorical variables it is the interval of each proportion.
trial |>
tbl_summary(
include = c(age, marker),
statistic = all_continuous() ~ "{mean} ({sd})",
missing = "no"
) |>
modify_header(stat_0 = "**Mean (SD)**") |>
remove_footnote_header(stat_0) |>
add_ci()Report the difference between two treatment groups, as randomized trials often do. Here the difference in tumor response and in marker level, each with its confidence interval and p-value.
trial |>
tbl_summary(
by = trt,
include = c(response, marker),
statistic = list(
all_continuous() ~ "{mean} ({sd})",
all_categorical() ~ "{p}%"
),
missing = "no"
) |>
add_difference() |>
add_n() |>
modify_header(all_stat_cols() ~ "**{level}**")Compare each group with a single reference group with
add_difference_row(): below each variable it adds one row
per non-reference level, holding the difference against the reference,
its confidence interval and a p-value.
trial |>
tbl_summary(by = grade, include = c(age, response), missing = "no") |>
add_difference_row(reference = "I")Cross and survival tables
Cross-tabulate two variables with tbl_cross(). Margins
are added by default; add_p() compares the two variables,
and source_note = TRUE moves the p-value below the
table.
trial |>
tbl_cross(row = stage, col = trt, percent = "cell") |>
add_p(source_note = TRUE) |>
bold_labels()Summarize survival curves with tbl_survfit(): the
survival probability at chosen times, or the survival time at chosen
quantiles. Pass a data frame and it fits one survfit()
model per variable; add_p() adds the log-rank test and
add_n() and add_nevent() the counts.
library(survival)
trial |>
tbl_survfit(
y = Surv(ttdeath, death),
include = c(trt, grade),
times = c(12, 24),
label_header = "**{time} Month**"
) |>
add_p() |>
add_n() |>
add_nevent()
trial |>
tbl_survfit(y = Surv(ttdeath, death), include = trt, probs = 0.5, label_header = "**Median Survival**")Combining tables
Stratify a summary table by a second variable with
tbl_strata(): one table per stratum, merged side by side
under a spanning header, or stacked with
.combine_with = "tbl_stack".
tbl_strata_nested_stack() shows the strata as indented row
headers instead.
trial |>
tbl_strata(
strata = grade,
.tbl_fun = ~ .x |> tbl_summary(by = trt, include = age, missing = "no"),
.header = "**Grade {strata}**, N = {n}"
)
trial |>
tbl_strata_nested_stack(
strata = trt,
.tbl_fun = ~ .x |> tbl_summary(include = c(age, grade), missing = "no") |>
modify_header(all_stat_cols() ~ "**Summary Statistics**"),
row_header = "{strata}, n={n}"
) |>
modify_bold(columns = label, rows = tbl_indent_id1 > 0)Summarize a continuous variable by two categorical variables with
tbl_continuous(): one forms the rows, the other the
columns, and each cell holds a statistic of the continuous variable.
add_p() compares it across the row variable with a two-way
ANOVA.
trial |>
tbl_continuous(variable = age, by = trt, include = grade) |>
add_p()