Skip to contents

ltsummary mirrors gtsummary 2.6.1 on purpose: the same function names, the same argument names and defaults, the same output strings. Most gtsummary code runs after changing the library() call, and as_gt() becomes as_lt(). This article shows the same table built with both packages, lists the few functions that stay out of scope, and explains the few places where behavior differs.

Both packages ship a trial data set with the same columns, but the simulated values differ, so the examples below use gtsummary’s data for both tables. Both packages also export tbl_summary(), add_p() and the rest, so the calls are namespaced here; in your own scripts you would load one package or the other.

trial <- gtsummary::trial

lt_tbl <- trial |>
  ltsummary::tbl_summary(by = trt, include = c(age, grade, response)) |>
  ltsummary::add_overall() |>
  ltsummary::add_p() |>
  ltsummary::bold_labels()

gt_tbl <- trial |>
  gtsummary::tbl_summary(by = trt, include = c(age, grade, response)) |>
  gtsummary::add_overall() |>
  gtsummary::add_p() |>
  gtsummary::bold_labels()

The formatted cells are the same, down to the markdown markers that as.data.frame() adds for bold text:

identical(as.data.frame(lt_tbl), as.data.frame(gt_tbl))
#> [1] TRUE
all.equal(lt_tbl$table_body$p.value, unname(gt_tbl$table_body$p.value))
#> [1] TRUE

And the rendered tables look alike. ltsummary:

lt_tbl

gtsummary:

gt_tbl
Characteristic Overall
N = 200
1
Drug A
N = 98
1
Drug B
N = 102
1
p-value2
Age 47 (38, 57) 46 (37, 60) 48 (39, 56) 0.7
    Unknown 11 7 4
Grade


0.9
    I 68 (34%) 35 (36%) 33 (32%)
    II 68 (34%) 32 (33%) 36 (35%)
    III 64 (32%) 31 (32%) 33 (32%)
Tumor Response 61 (32%) 28 (29%) 33 (34%) 0.5
    Unknown 7 3 4
1 Median (Q1, Q3); n (%)
2 Wilcoxon rank sum test; Pearson’s Chi-squared test

The test suite checks this agreement over a matrix of tbl_summary() settings, the add_*() functions, the style_*() formatters and inline_text() whenever gtsummary and broom are installed, so differences are treated as bugs unless a change in gtsummary is deliberate.

What carries over

The functions below exist in both packages with the same arguments. Arguments that gtsummary has deprecated are dropped rather than mirrored.

gtsummary ltsummary
tbl_summary() same
add_overall(), add_n(), add_p(), add_stat_label(), add_stat() same
add_difference(), add_difference_row(), add_ci() same, with the methods that can be computed in base R; see ?tests and ?add_ci
tbl_continuous() and its add_p(), add_overall() and inline_text() methods same, with the tests that can be computed in base R; see ?add_p_continuous
tbl_cross() and its add_p() and inline_text() methods same
tbl_hierarchical(), tbl_hierarchical_count(), sort_hierarchical(), filter_hierarchical() and their add_overall() and add_difference() methods same, counted directly in base R
tbl_likert() and its add_n() method same
tbl_wide_summary() same
tbl_custom_summary(), ratio_summary(), proportion_summary() same; the deprecated continuous_summary() is not mirrored
tbl_survfit() and its add_p(), add_n(), add_nevent() and inline_text() methods same, as one plain function for survfit objects, lists and data frames
modify_header(), modify_spanning_header(), remove_spanning_header(), show_header_names() same
modify_footnote_header(), modify_footnote_body(), modify_footnote_spanning_header() and the remove_footnote_*() functions same
modify_abbreviation(), remove_abbreviation() same
modify_caption(), modify_source_note(), remove_source_note() same
modify_column_hide(), modify_column_unhide(), modify_column_alignment(), modify_indent(), modify_column_merge(), remove_column_merge(), modify_fmt_fun(), modify_post_fmt_fun(), modify_missing_symbol() same
bold_labels(), bold_levels(), italicize_labels(), italicize_levels(), bold_p(), modify_bold(), modify_italic(), remove_bold(), remove_italic() same
remove_row_type(), add_variable_group_header(), separate_p_footnotes(), tbl_butcher() same
tbl_stack(), tbl_merge() same
tbl_split_by_rows(), tbl_split_by_columns() same; the split list has class "ltsummary_split" (also "tbl_split")
tbl_strata(), tbl_strata2(), tbl_strata_nested_stack() same, for data frames; the deprecated .quiet is not mirrored
tbl_regression(), tbl_uvregression() same, for lm, glm and coxph models; other classes through tidy_fun or broom::tidy()
add_global_p(), add_nevent(), add_n() on regression tables, add_glance_table(), add_glance_source_note(), glance_fun_s3(), add_significance_stars(), add_vif() same; the variance inflation factors are computed in base R
add_q(), sort_p(), filter_p(), combine_terms() same; combine_terms()’s deprecated quiet is not mirrored
all_interaction(), all_intercepts(), all_contrasts() same
style_number(), style_percent(), style_pvalue(), style_sigfig(), style_ratio() and the label_style_*() factories same
all_continuous(), all_continuous2(), all_categorical(), all_dichotomous(), all_stat_cols(), all_tests() same
inline_text() same
as_gt() as_lt()
as_tibble(), as.data.frame() as.data.frame()
modify_table_body(), modify_table_styling(), as_gtsummary() same; as_gtsummary() is as_ltsummary()
modify_footnote(), modify_column_indent(), tbl_split(), continuous_summary() (deprecated in gtsummary) not mirrored; use the replacements gtsummary recommends

With 0.8.0 the mirrored surface follows gtsummary 2.6.1: every function outside the deprecated rows above and the list below exists here with the same arguments.

Not planned: tbl_svysummary() and the survey functions, the ARD-based tbl_ard_*() builders and their brdg_*()/pier_*() infrastructure, the comparison methods that need emmeans or lme4 (emmeans, lme4, ancova_lme4), regression models that need lme4, mice or other packages to tidy, the tidy_robust()-style wrappers around broom.helpers, as_flex_table(), as_hux_table(), as_kable(), as_kable_extra() and save_flex_docx().

Where behavior differs

Rendering. lt builds the HTML table in the browser from a JSON specification with a 10 KB JavaScript file. The table reflows and styles with CSS, and hovering a formatted cell shows the unrounded value. There is no LaTeX or RTF output; lt::lt_export() writes static HTML, PDF or PNG files through a headless browser, and Quarto can turn lt’s static HTML into Word and PDF tables. The Quarto and R Markdown article covers each output format.

Spanning headers. The current lt release renders a single row of spanning headers. modify_spanning_header(level = 2) is accepted and stored, with a message, but only level 1 is shown.

Combined tables. tbl_stack()’s group headers render as full-width lt separator rows, the counterpart of gt’s row groups, and bold_labels() on such a table puts the bold on the label column (in gtsummary it targets the hidden group column and nothing visible changes). tbl_merge() writes its spanning headers at level 1, replacing any level-1 spanner the inputs carried, so they render within lt’s single spanner row. tbl_strata() accepts data frames, not survey designs.

Markdown. gtsummary passes header and footnote text through gt’s markdown renderer. ltsummary converts a subset: **bold**, _italic_ or *italic*, and line breaks. Every default header in gtsummary falls within the subset. For links, code or anything else, pass HTML and set text_interpret = "html"; text_interpret = "none" shows the text verbatim.

Regression models. gtsummary tidies models with broom.helpers; ltsummary tidies lm, glm and coxph models in base R and reproduces broom.helpers’ rules for variables, levels, reference rows and interactions. The intervals are the ones broom reports: t-based for lm, profile likelihood for glm (stats::confint(), from MASS on R older than 4.4) and Wald for coxph. add_global_p() computes car::Anova(type = "III")’s tests in base R, and add_glance_table() computes broom::glance()’s statistics in base R for the three model classes. Other model classes are tidied by broom::tidy() when broom is installed, or by a tidy_fun you supply, and get one row per coefficient when the class has no model matrix. tbl_regression(), tbl_uvregression() and glance_fun_s3() are plain functions rather than S3 generics, so that gtsummary’s generics of the same names keep working when both packages are attached.

Column selection. gtsummary uses tidyselect. ltsummary implements the common forms in base R: bare names, strings, c() including mixed selections such as c(age:grade, -marker), - for exclusion, ranges, character vectors held in a variable, the all_*() selectors and everything(), all_of(), any_of(), starts_with(), ends_with(), contains(), matches(), where() and last_col(). Boolean operators (!, &, |) are not supported.

Themes and languages. The theme functions exist with the ltsummary name, the one place a function name differs between the packages:

gtsummary ltsummary
set_gtsummary_theme(), get_gtsummary_theme(), reset_gtsummary_theme(), check_gtsummary_theme(), with_gtsummary_theme(), without_gtsummary_theme() set_ltsummary_theme() and so on; the deprecated quiet argument is not mirrored
theme_gtsummary_compact(), theme_gtsummary_journal(), theme_gtsummary_language(), theme_gtsummary_continuous2(), theme_gtsummary_mean_sd(), theme_gtsummary_eda() theme_ltsummary_compact() and so on
theme_gtsummary_printer() not mirrored; use options(ltsummary.print_engine =) or the pkgwide-str:print_engine element

The element names inside a theme are gtsummary’s ("tbl_summary-arg:statistic", "pkgwide-str:language", …), so a theme list written for gtsummary passes to set_ltsummary_theme() element by element; elements for functions ltsummary does not have (the survey family, the print-engine addl_cmds) are rejected by name, and "as_lt-lst:table_style" styles the rendered table in their place. See decisions/0015 for the accepted list.

A few defaults can also be set with options (ltsummary.big.mark, ltsummary.decimal.mark, ltsummary.cat_threshold, ltsummary.default_con_type, ltsummary.print_engine); the tbl_summary() tutorial lists them. A theme outranks the options, and an argument passed explicitly outranks both.

Comparison methods. The built-in tests come from the stats package, and three groups of gtsummary’s methods are re-implemented in base R: ancova (an lm() fit, so add_p(adj.vars =) and add_difference(adj.vars =) work), smd, which reproduces smd::smd() including its standard error, and the effect sizes cohens_d, hedges_g, paired_cohens_d and paired_hedges_g, which reproduce the estimates and noncentral t intervals of effectsize. The standardized mean difference uses conf.level, where gtsummary always reports 95% limits. The methods that need emmeans or lme4 are not available; see ?tests for the full list.

Survival tables. tbl_survfit() is one function rather than a generic. The formula and data of each survfit() call are evaluated where tbl_survfit() is called, so a formula stored in a variable works and a data frame local to the calling function is found, where gtsummary evaluates them from its own namespace. add_n() counts sum(x$n) from the survfit object rather than the rows of model.frame(), which is the same number unless the call used subset.

Footnote symbols. gtsummary 2.6.0 adds modify_footnote_symbol(), remove_footnote_symbol() and the pkgwide-chr:footnote_symbol theme element. lt numbers footnotes itself, so there is no equivalent, and the element is rejected by name.

Custom tests. A custom test function may return a one-row data frame, as in gtsummary, or a list with scalar elements.

Objects

An ltsummary object is a list with the same shape as a gtsummary object: table_body is a data frame of formatted cells with variable, var_type, row_type and label columns, and table_styling is a list of data frames recording headers, footnotes, formatting and so on. Code that inspects x$table_body works unchanged. The ARD that gtsummary stores in x$cards is replaced by x$stats, a list holding one simpler long-format data frame per function that computed something, which is what inline_text() and add_stat_label() read.

Objects from both packages share the tbl_summary class. Each package dispatches its methods through its own generics, so loading both at once, as this article does, is safe.