Introduction
This article follows gtsummary’s article of the same name.
A reproducible report quotes its tables in the text, and the numbers
in the text must match the numbers in the table.
inline_text() returns a formatted statistic from an
ltsummary table as a string, so a sentence written with inline R code in
a Quarto or R Markdown document updates when the data change.
Setup
The examples use the trial data set, 200 patients who
received one of two chemotherapies (Drug A or Drug B), with tumor
response and death as the outcomes.
Inline results from tbl_summary()
First build a summary table with tbl_summary() (the tbl_summary() tutorial covers the function
in detail).
tab1 <- tbl_summary(trial, by = trt, include = c(marker, stage))
tab1To report the median (Q1, Q3) of the marker level in each group, write the following inline:
The median (IQR) marker level in the Drug A and Drug B groups are
`r inline_text(tab1, variable = marker, column = "Drug A")`and`r inline_text(tab1, variable = marker, column = "Drug B")`, respectively.
Here is how the line appears in the report:
The median (IQR) marker level in the Drug A and Drug B groups are 0.72 (0.39, 1.19) and 0.68 (0.29, 1.25), respectively.
The column argument takes a level of the by
variable, as above, or the name of a column of the table body such as
stat_1. When the table has an overall column (from
add_overall(), or a table without by),
column defaults to it.
For a categorical variable, add the level argument:
`r inline_text(tab1, variable = stage, level = "T1", column = "Drug B")`resolves to “24 (23%)”
P-values
After add_p(), the p-value of a variable is available
through column = "p.value". It is formatted with
pvalue_fun, which by default prepends “p”:
tab2 <- tab1 |> add_p()
inline_text(tab2, variable = marker, column = "p.value")
#> [1] "p=0.5"
inline_text(tab2, variable = stage, column = "p.value", pvalue_fun = label_style_pvalue(digits = 2))
#> [1] "0.22"The marker levels did not differ between arms (
`r inline_text(tab2, variable = marker, column = "p.value")`) resolves to “The marker levels did not differ between arms (p=0.5)”.
Patterns
A cell shows one arrangement of the statistics computed for it;
pattern lets you quote them in another. With
column set, the pattern draws on the statistics behind that
cell: those named in tbl_summary(statistic =) plus the
missing-value counts N_obs, N_miss,
N_nonmiss, p_miss and
p_nonmiss.
inline_text(tab1, variable = marker, column = "Drug A", pattern = "{median} ng/mL (IQR {p25} to {p75}), {N_miss} missing")
#> [1] "0.72 ng/mL (IQR 0.39 to 1.19), 8 missing"
inline_text(tab1, variable = stage, level = "T1", column = "Drug B", pattern = "{n} of {N} patients ({p}%)")
#> [1] "24 of 105 patients (23%)"The statistics are formatted with the same digits as the table. To
quote a statistic that the table does not compute, such as the mean of a
variable shown as a median, include it in
tbl_summary(statistic =); inline_text()
refuses statistics it does not have rather than computing them on the
side.
Without column, the pattern draws on the columns of the
table row instead, which is the way to quote both arms and the p-value
in one phrase. The p-value sits on the variable’s label row, so pair it
with variable rather than level:
inline_text(tab2, variable = marker, pattern = "{stat_1} vs. {stat_2} ({p.value})")
#> [1] "0.72 (0.39, 1.19) vs. 0.68 (0.29, 1.25) (p=0.5)"
inline_text(tab2, variable = stage, level = "T4", pattern = "{stat_1} and {stat_2}")
#> [1] "18 (19%) and 32 (30%)"
inline_text(tab2, variable = stage, pattern = "{p.value} across stages")
#> [1] "p=0.2 across stages"Regression tables
For a table built with tbl_regression() or
tbl_uvregression(), inline_text() returns the
estimate, its confidence interval and the p-value in one string.
level picks a level of a categorical variable, and
pattern rearranges the pieces.
tab3 <- glm(response ~ age + grade, trial, family = binomial) |>
tbl_regression(exponentiate = TRUE)
inline_text(tab3, variable = age)
#> [1] "1.00 (95% CI 0.97, 1.03; p=0.9)"
inline_text(tab3, variable = grade, level = "III")
#> [1] "2.45 (95% CI 0.99, 6.41; p=0.058)"
inline_text(tab3, variable = age, pattern = "OR {estimate} (95% CI {conf.low} to {conf.high})")
#> [1] "OR 1.00 (95% CI 0.97 to 1.03)"Cross tables
A tbl_cross() table is addressed by level rather than by
variable and column. row_level is a level of the row
variable or the margin text, and col_level is a level of
the column variable, "stat_0" for the total column, or
"p.value" after add_p().
tab4 <- trial |>
tbl_cross(row = trt, col = response) |>
add_p()
inline_text(tab4, row_level = "Drug A", col_level = "1")
#> [1] "22"
inline_text(tab4, row_level = "Total", col_level = "1")
#> [1] "46"
inline_text(tab4, col_level = "p.value")
#> [1] "p=0.7"Survival tables
For a tbl_survfit() table, the column is chosen with
time or prob rather than column,
and level picks the stratum. variable defaults
to the first model of the table, which is what a single-model table
needs.
library(survival)
tab5 <- survfit(Surv(ttdeath, death) ~ trt, trial) |>
tbl_survfit(times = c(12, 24)) |>
add_p()
inline_text(tab5, time = 24, level = "Drug B")
#> [1] "63% (54%, 73%)"
inline_text(tab5, time = 24, level = "Drug B", pattern = "{estimate} (95% CI {conf.low}, {conf.high})")
#> [1] "63% (95% CI 54%, 73%)"
inline_text(tab5, column = p.value)
#> [1] "p=0.007"The median survival time comes from the same table built with
probs instead of times. An estimate the data
never reach is shown as the missing symbol, an em dash by
default, in the text just as in the table:
tab6 <- trial |>
tbl_survfit(y = Surv(ttdeath, death), include = trt, probs = 0.5)
inline_text(tab6, prob = 0.5, level = "Drug A")
#> [1] "23 (21, —)"
inline_text(tab6, prob = 0.5, level = "Drug B")
#> [1] "— (—, —)"Stacked and merged tables
A table built with tbl_stack(), tbl_merge()
or the tbl_strata() family is addressed by
variable, level and a column of
table_body. Merging suffixes the columns with the position
of the table they came from, so the statistic column of the second table
is stat_0_2.
tab7 <- tbl_merge(
list(
trial |> tbl_summary(include = c(age, grade), missing = "no"),
trial |> tbl_summary(include = c(age, grade), by = trt, missing = "no") |> add_p()
),
tab_spanner = c("**Overall**", "**By treatment**")
)
inline_text(tab7, variable = age, column = stat_0_1)
#> [1] "60 (53, 68)"
inline_text(tab7, variable = grade, level = "II", column = stat_2_2)
#> [1] "43 (41%)"
inline_text(tab7, variable = age, column = p.value_2)
#> [1] "0.043"Stacking keeps the column names of the inputs, so a variable that
appears in more than one of the stacked tables is no longer unique.
inline_text() then returns the first match and says so;
give the tables distinct variables, or reach for the merged table
instead, when the message appears.
tab8 <- tbl_stack(
list(
trial |> tbl_summary(include = age, missing = "no"),
trial |> tbl_summary(include = marker, missing = "no")
),
group_header = c("Age", "Marker"),
quiet = TRUE
)
inline_text(tab8, variable = marker, column = stat_0)
#> [1] "0.68 (0.36, 1.19)"Tips
- Variable and column names can be given bare or as strings:
inline_text(tab1, variable = marker, column = "Drug A")andinline_text(tab1, variable = "marker", column = stat_1)are both fine. -
inline_text()reads the formatted table, so changes made withmodify_fmt_fun()ortbl_summary(digits =)carry through to the text. - In Quarto, inline code is
`r inline_text(...)`with the knitr engine. Keep a table object in a chunk near the top of the document and refer to it throughout the text, so every number comes from one computation.