Skip to contents

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

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

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))
tab1

To 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") and inline_text(tab1, variable = "marker", column = stat_1) are both fine.
  • inline_text() reads the formatted table, so changes made with modify_fmt_fun() or tbl_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.