Skip to contents

Introduction

This article follows gtsummary’s article of the same name.

tbl_regression() takes a fitted regression model and returns a table of its results that is ready for publication: one row per term, with the estimate, its confidence interval and a p-value, and a reference row for each categorical variable. Like tbl_summary(), it has sensible defaults and every part of the table can be changed afterwards.

Behind the scenes, tbl_regression() tidies lm(), glm() and survival::coxph() models in base R. Other model classes are tidied with broom::tidy() when broom is installed, or with a function you pass in tidy_fun; the supported models section says what that covers.

Setup

Install ltsummary from GitHub and load it.

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

Example data set

The examples use the trial data set that ships with the package: 200 patients who received one of two chemotherapies, with tumor response and death as the outcomes. Each variable has a label attribute, which the tables use as the variable label; a variable without one is labelled with its name.

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

Basic usage

Start with a logistic regression model for tumor response, with age and stage as the predictors.

m1 <- glm(response ~ age + stage, trial, family = binomial)

summary(m1)$coefficients
#>                 Estimate Std. Error     z value   Pr(>|z|)
#> (Intercept) -0.838671134 1.00695498 -0.83287848 0.40491327
#> age         -0.001505552 0.01544471 -0.09748011 0.92234513
#> stageT2      0.029387942 0.46434355  0.06328922 0.94953620
#> stageT3     -0.263766551 0.48759757 -0.54095132 0.58854114
#> stageT4     -1.017557569 0.54665522 -1.86142476 0.06268422

One call turns the model into a table.

tbl_regression(m1, exponentiate = TRUE)

Note the defaults, each of which can be changed:

  • The model is a logistic regression and the coefficients are exponentiated, so the header says OR and the abbreviation is explained below the table.
  • Variable types are detected: the categorical variable gets a label row, one row per level and a reference row for the reference level.
  • The estimates and confidence limits are rounded with style_ratio(), the p-values with style_pvalue().
  • The label attributes of the data are printed, and the levels are indented.

Customizing the output

There are four ways to customize a regression table:

  1. The arguments of tbl_regression().
  2. The add_*() functions, which add columns and statistics.
  3. The ltsummary functions that modify the appearance of the table.
  4. The functions of the lt package, applied after as_lt().

tbl_regression() arguments

Argument Description
label variable labels printed in the table
exponentiate exponentiate the coefficients and their confidence limits
include variables to include in the table
show_single_row dichotomous variables to print on a single row instead of three
conf.level confidence level of the interval
intercept whether to include the intercept
estimate_fun function that rounds and formats the estimates
pvalue_fun function that rounds and formats the p-values
tidy_fun function that tidies the model, for classes without a built-in tidier
add_estimate_to_reference_rows whether to show 0 (or 1, when exponentiated) on the reference rows
conf.int whether to show the confidence interval

An example that uses several of them:

lm(marker ~ age + trt + grade, trial) |>
  tbl_regression(
    label = list(grade = "Tumor grade", marker = "Marker"),
    show_single_row = trt,
    conf.level = 0.9,
    estimate_fun = label_style_sigfig(digits = 3)
  )

Functions that add information

Function Description
add_global_p() one p-value per variable, from a type III test of all its coefficients
add_n() number of observations in the model, or at each level
add_nevent() number of events, for logistic, Poisson and Cox models
add_glance_table() model statistics such as R², AIC and the log-likelihood as rows at the bottom
add_glance_source_note() the same statistics as a source note
add_q() q-values adjusted for multiple comparisons
tbl_regression(m1, exponentiate = TRUE) |>
  add_global_p() |>
  add_nevent(location = "level") |>
  add_glance_source_note(include = c(nobs, AIC))

add_global_p() replaces the p-values of the individual levels with one test per variable, the same test car::Anova(type = "III") reports, so a categorical variable is judged as a whole. Pass keep = TRUE to keep the level p-values next to it.

Functions that format the table

The modifier functions of tbl_summary() work on regression tables too; the modifier functions article covers them one by one.

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
sort_p() sort the variables by p-value
filter_p() keep the variables below a p-value threshold
remove_row_type() remove reference rows, header rows or levels

lt functions

The table is rendered by the lt package. as_lt() converts it, and the lt functions apply from there.

tbl_regression(m1, exponentiate = TRUE) |>
  as_lt() |>
  lt::lt_note("Data are simulated")

Example

A table with odds ratios, global p-values, p-values rounded to two decimal places and bold below 0.10, bold labels and italic levels:

tbl_regression(m1, exponentiate = TRUE, pvalue_fun = label_style_pvalue(digits = 2)) |>
  add_global_p() |>
  bold_p(t = 0.10) |>
  bold_labels() |>
  italicize_levels()

Univariable regression

tbl_uvregression() fits one model per variable and stacks the results. It is a wrapper around tbl_regression(), so it takes nearly the same arguments, and its result can be modified in the same ways. With y, every variable in include is the predictor of its own model; with x, every variable is the outcome.

trial |>
  tbl_uvregression(
    method = glm,
    y = response,
    include = c(age, grade),
    method.args = list(family = binomial),
    exponentiate = TRUE,
    pvalue_fun = label_style_pvalue(digits = 2)
  ) |>
  add_global_p() |>
  add_nevent() |>
  add_q() |>
  bold_p() |>
  bold_p(t = 0.10, q = TRUE) |>
  bold_labels()

The formula argument adjusts every model for the same covariates, e.g. formula = "{y} ~ {x} + age".

Quoting the table in text

inline_text() returns a term’s estimate, interval and p-value as a string, so a sentence in a Quarto or R Markdown document updates with the data. The default pattern is "{estimate} ({conf.level*100}% CI {conf.low}, {conf.high}; {p.value})"; the columns of the row are available for other patterns.

tbl <- tbl_regression(m1, exponentiate = TRUE)
inline_text(tbl, variable = age)
#> [1] "1.00 (95% CI 0.97, 1.03; p>0.9)"
inline_text(tbl, variable = stage, level = "T4", pattern = "OR {estimate}, {p.value}")
#> [1] "OR 0.36, p=0.063"

What the tidier does

tbl_regression() reads everything it needs from the fitted model: the coefficients from summary(), the intervals from confint() (see the reference page for which interval each model class gets), and the mapping from coefficients to variables from the model matrix. The rules for the less common cases:

  • A factor, character or logical variable gets a label row, a reference row and one row per other level. The reference level is the first level for the default treatment contrasts, the last level for contr.SAS and contr.sum; Helmert, polynomial and custom contrasts have no reference level, so those variables show their coefficient names.
  • Interaction terms are labelled by their parts, Age * Drug B.
  • A term built from a function, such as log(ttdeath) or I(age^2), keeps the label attribute of its variable. A term that spans several columns, such as splines::ns(age, 3), gets a label row and one row per column.
  • The N on label rows is the number of observations in the model; on level rows it is the number of observations at that level.
lm(marker ~ age * trt + log(ttdeath), trial) |>
  tbl_regression() |>
  add_n(location = c("label", "level"))

Supported models

lm(), glm() (with every family) and survival::coxph() are tidied in base R and tested against gtsummary. Any other model class works through broom::tidy() when broom is installed, or through a function passed in tidy_fun that returns a data frame with a term column and the estimate, std.error, statistic, p.value, conf.low and conf.high columns. For those classes the variable metadata comes from the model matrix when the class provides one (model.matrix() and model.frame() methods), and each coefficient is otherwise shown on its own row.