Definition of an ltsummary object
Source:vignettes/articles/ltsummary-definition.Rmd
ltsummary-definition.RmdThis article is for people who want to contribute to ltsummary, or who want to understand the object well enough to modify it for their own needs. For an introduction to the package, start with the tbl_summary() tutorial.
The internal structures are documented for information. They may change, and a change to them is not treated as a breaking change.
Introduction
Every ltsummary table shares a few characteristics, and the object
mirrors gtsummary’s on purpose: code that reads
x$table_body from a gtsummary table reads an ltsummary
table unchanged. The example table below is used throughout.
library(ltsummary)
tbl_summary_ex <- trial |>
tbl_summary(by = trt, include = c(age, grade, response)) |>
add_p() |>
bold_labels()Structure of an ltsummary object
Every ltsummary object is a list with, at minimum, these elements:
A table built by tbl_summary() also carries
.$stats, .$df_by, .$inputs and
.$call_list.
names(tbl_summary_ex)
#> [1] "table_body" "table_styling" "stats" "df_by"
#> [5] "inputs" "call_list"A table built by tbl_stack(), tbl_merge()
or the tbl_strata() family carries .$tbls, the
list of tables it was built from, and tbl_strata() adds
.$df_strata recording the strata and their headers.
table_body
.$table_body is the data frame that is printed as the
output. To work with every function of the package (the selectors in
particular), it must include the columns "label",
"row_type" and "variable". The
"label" column is printed and the other two are hidden.
tbl_summary_ex$table_body
#> variable test_name var_type var_label row_type
#> 1 age wilcox.test continuous Age label
#> 2 age wilcox.test continuous Age missing
#> 3 grade chisq.test.no.correct categorical Grade label
#> 4 grade chisq.test.no.correct categorical Grade level
#> 5 grade chisq.test.no.correct categorical Grade level
#> 6 grade chisq.test.no.correct categorical Grade level
#> 7 response chisq.test.no.correct dichotomous Tumor Response label
#> 8 response chisq.test.no.correct dichotomous Tumor Response missing
#> label stat_1 stat_2 estimate parameter statistic
#> 1 Age 59 (51, 66) 62 (54, 71) -3.000035 NA 3.699000e+03
#> 2 Unknown 4 7 NA NA NA
#> 3 Grade <NA> <NA> NA 2 2.391539e+00
#> 4 I 25 (26%) 37 (35%) NA NA NA
#> 5 II 40 (42%) 43 (41%) NA NA NA
#> 6 III 30 (32%) 25 (24%) NA NA NA
#> 7 Tumor Response 22 (24%) 24 (24%) NA 1 3.142673e-03
#> 8 Unknown 2 5 NA NA NA
#> conf.low conf.high p.value
#> 1 -6.999936 -4.69388e-05 0.04315754
#> 2 NA NA NA
#> 3 NA NA 0.30247116
#> 4 NA NA NA
#> 5 NA NA NA
#> 6 NA NA NA
#> 7 NA NA 0.95529437
#> 8 NA NA NAThe p.value, statistic,
estimate and confidence interval columns are numeric; they
are formatted when the table is printed, following the instructions in
.$table_styling.
Combining tables adds hidden columns with reserved names.
tbl_stack() (and tbl_uvregression(), which is
built on it) adds tbl_id1 holding the position of the table
each row came from, and a second stacking adds tbl_id2
outside it; group_header adds groupname_col,
whose values become the group rows; and
tbl_strata_nested_stack() adds tbl_indent_id1,
which is 0 for the rows of a stratum’s own table and the
nesting depth for the row headers above them. They are ordinary columns
of table_body, so a rows = predicate can
select on them, which is how the row headers of a nested stack are
styled:
modify_bold(x, columns = label, rows = tbl_indent_id1 > 0)table_styling
.$table_styling is a list of data frames describing how
.$table_body is printed, formatted and styled. The data
frames are header, spanning_header,
footnote_header, footnote_body,
footnote_spanning_header, abbreviation,
source_note, text_format, indent,
fmt_missing, fmt_fun, cols_merge
and post_fmt_fun; caption and
horizontal_line_above are single values. Apart from
header, every data frame is append-only: each
modify_*() call adds rows, and the rows are resolved in
order when the table is printed, so a later instruction for the same
cells wins.
names(tbl_summary_ex$table_styling)
#> [1] "header" "spanning_header"
#> [3] "footnote_header" "footnote_body"
#> [5] "footnote_spanning_header" "abbreviation"
#> [7] "source_note" "text_format"
#> [9] "indent" "fmt_missing"
#> [11] "fmt_fun" "cols_merge"
#> [13] "post_fmt_fun" "caption"
#> [15] "horizontal_line_above"header
One row per column of .$table_body, holding the styling
that applies to the whole column or its header.
| Column | Description |
|---|---|
column |
column name from .$table_body
|
hide |
logical, whether the column is hidden in the output |
align |
alignment of the column, "left",
"right" or "center"
|
interpret_label |
how the label is interpreted, "md",
"html" or "none"
|
label |
header label displayed when the column is shown |
modify_stat_* |
statistics available to modify_header()
and friends: modify_stat_level, modify_stat_N,
modify_stat_n, modify_stat_p,
modify_stat_N_event for regression tables, and
modify_stat_time or modify_stat_prob for
survival tables |
tbl_summary_ex$table_styling$header
#> column hide align interpret_label label
#> 1 variable TRUE center md variable
#> 2 test_name TRUE center md test_name
#> 3 var_type TRUE center md var_type
#> 4 var_label TRUE center md var_label
#> 5 row_type TRUE center md row_type
#> 6 label FALSE left md **Characteristic**
#> 7 stat_1 FALSE center md **Drug A** \nN = 95
#> 8 stat_2 FALSE center md **Drug B** \nN = 105
#> 9 estimate TRUE center md **Difference**
#> 10 parameter TRUE center md **Parameter**
#> 11 statistic TRUE center md **Statistic**
#> 12 conf.low TRUE center md **95% CI**
#> 13 conf.high TRUE center md conf.high
#> 14 p.value FALSE center md **p-value**
#> modify_stat_level modify_stat_N modify_stat_n modify_stat_p
#> 1 <NA> 200 NA NA
#> 2 <NA> 200 NA NA
#> 3 <NA> 200 NA NA
#> 4 <NA> 200 NA NA
#> 5 <NA> 200 NA NA
#> 6 <NA> 200 NA NA
#> 7 Drug A 200 95 0.475
#> 8 Drug B 200 105 0.525
#> 9 <NA> 200 NA NA
#> 10 <NA> 200 NA NA
#> 11 <NA> 200 NA NA
#> 12 <NA> 200 NA NA
#> 13 <NA> 200 NA NA
#> 14 <NA> 200 NA NA
#> modify_stat_N_event
#> 1 NA
#> 2 NA
#> 3 NA
#> 4 NA
#> 5 NA
#> 6 NA
#> 7 NA
#> 8 NA
#> 9 NA
#> 10 NA
#> 11 NA
#> 12 NA
#> 13 NA
#> 14 NAspanning_header
Spanning headers, one row per column covered. level is
the row of spanning headers, 1 being closest to the column
headers; the current lt release renders level 1 only.
| Column | Description |
|---|---|
level |
integer, the level of the spanning header |
column |
column name from .$table_body
|
spanning_header |
the spanning header text |
text_interpret |
how the text is interpreted, "md",
"html" or "none"
|
remove |
logical, whether the spanning header is removed from the column |
footnote_header,
footnote_body and
footnote_spanning_header
Footnotes on column headers, on body cells and on spanning headers.
footnote_body has a rows column, and
footnote_spanning_header a level column.
| Column | Description |
|---|---|
column |
column name from .$table_body
|
rows |
expression selecting rows of .$table_body
(body footnotes only) |
footnote |
the footnote text |
text_interpret |
how the text is interpreted, "md",
"html" or "none"
|
replace |
logical, whether the footnote replaces existing
footnotes at the location (TRUE) or is added to them
(FALSE) |
remove |
logical, whether every footnote at the location is removed |
tbl_summary_ex$table_styling$footnote_header
#> column footnote text_interpret
#> 1 stat_1 Median (Q1, Q3); n (%) md
#> 2 stat_2 Median (Q1, Q3); n (%) md
#> 3 p.value Wilcoxon rank sum test; Pearson's Chi-squared test md
#> 4 estimate Wilcoxon rank sum test; Pearson's Chi-squared test md
#> 5 parameter Wilcoxon rank sum test; Pearson's Chi-squared test md
#> 6 statistic Wilcoxon rank sum test; Pearson's Chi-squared test md
#> 7 conf.low Wilcoxon rank sum test; Pearson's Chi-squared test md
#> replace remove
#> 1 TRUE FALSE
#> 2 TRUE FALSE
#> 3 TRUE FALSE
#> 4 TRUE FALSE
#> 5 TRUE FALSE
#> 6 TRUE FALSE
#> 7 TRUE FALSEabbreviation
Abbreviations are added one at a time and coalesced into a single
note when the table is rendered. The prefix and separators chosen in
modify_abbreviation() are stored in
.$table_styling$abbreviation_format.
| Column | Description |
|---|---|
column |
optional column name from .$table_body;
when present, the abbreviation is printed only when the column appears
in the rendered table |
abbreviation |
the abbreviation,
e.g. "CI = Confidence Interval"
|
text_interpret |
how the text is interpreted, "md",
"html" or "none"
|
source_note
Source notes are numbered in the order they are added, which is how
remove_source_note() refers to them.
| Column | Description |
|---|---|
id |
integer identifying the source note |
source_note |
the source note text |
text_interpret |
how the text is interpreted, "md",
"html" or "none"
|
remove |
logical, whether the source note is removed |
text_format
Bold and italic styling of cells.
| Column | Description |
|---|---|
column |
column name from .$table_body
|
rows |
expression selecting rows of
.$table_body
|
format_type |
"bold" or "italic"
|
undo_text_format |
logical, whether the formatting is removed rather than added |
tbl_summary_ex$table_styling$text_format
#> column rows format_type undo_text_format
#> 1 label ~, row_t.... bold FALSEindent
Indentation of cells, in spaces. The level and missing rows of a
tbl_summary() table are indented by 4 spaces in the label
column.
| Column | Description |
|---|---|
column |
column name from .$table_body
|
rows |
expression selecting rows of
.$table_body
|
n_spaces |
integer, the number of spaces of indentation |
fmt_missing
By default NA values are printed as blanks. Rows of
fmt_missing replace them with a symbol in the selected
cells.
| Column | Description |
|---|---|
column |
column name from .$table_body
|
rows |
expression selecting rows of
.$table_body
|
symbol |
string printed in place of NA
|
fmt_fun and
post_fmt_fun
Numeric columns are formatted with the functions in
fmt_fun. The functions in post_fmt_fun run
afterwards on the formatted text.
| Column | Description |
|---|---|
column |
column name from .$table_body
|
rows |
expression selecting rows of
.$table_body
|
fmt_fun |
list of formatting functions |
str(tbl_summary_ex$table_styling$fmt_fun)
#> 'data.frame': 3 obs. of 3 variables:
#> $ column : chr "p.value" "parameter" "statistic"
#> $ rows :List of 3
#> ..$ : NULL
#> ..$ : NULL
#> ..$ : NULL
#> ..- attr(*, "class")= chr "AsIs"
#> $ fmt_fun:List of 3
#> ..$ :function (x)
#> ..$ :function (x)
#> ..$ :function (x)
#> ..- attr(*, "class")= chr "AsIs"cols_merge
Instructions for merging columns into a single column. The merged
text replaces the column named in column, which is the
first column of the pattern.
| Column | Description |
|---|---|
column |
column name from .$table_body
|
rows |
expression selecting rows of
.$table_body
|
pattern |
glue pattern directing how to merge the columns |
caption
A string printed as the table title, with a
"text_interpret" attribute of "md",
"html" or "none".
horizontal_line_above
Reserved for an expression identifying a row above which a horizontal
line is drawn. Not used by tbl_summary().
The rows column
Every rows column holds one-sided formulas. The
expression is captured when the modify_*() function is
called and evaluated in .$table_body when the table is
printed, so an instruction such as
rows = row_type == "label" still applies after rows are
added or removed. resolve_rows() turns each formula into
row numbers inside finalize_styling(), which also applies
the last-wins rule and drops instructions for hidden columns.
tbl_summary_ex$table_styling$text_format$rows
#> [[1]]
#> ~row_type == "label"
#> <environment: base>stats
In place of the .$cards element of a gtsummary object, a
tbl_summary() table stores its computed statistics in
.$stats, a list with one long data frame per function that
computed something (tbl_summary, add_overall,
add_n, add_p). The columns are the variable
and level, the by level, the column of
.$table_body the statistic feeds, the statistic name,
label, value and formatted value, and any warning or error raised while
computing it. inline_text() and
add_stat_label() read this element.
head(tbl_summary_ex$stats$tbl_summary, 8)
#> variable variable_level group_level column context stat_name stat_label
#> 1 age <NA> Drug A stat_1 missing N_obs No. obs.
#> 2 age <NA> Drug A stat_1 missing N_miss N Missing
#> 3 age <NA> Drug A stat_1 missing N_nonmiss N Non-missing
#> 4 age <NA> Drug A stat_1 missing p_miss % Missing
#> 5 age <NA> Drug A stat_1 missing p_nonmiss % Non-missing
#> 6 age <NA> Drug A stat_1 summary median Median
#> 7 age <NA> Drug A stat_1 summary p25 Q1
#> 8 age <NA> Drug A stat_1 summary p75 Q3
#> stat stat_fmt warning error
#> 1 95 95 <NA> <NA>
#> 2 4 4 <NA> <NA>
#> 3 91 91 <NA> <NA>
#> 4 0.042105.... 4.2 <NA> <NA>
#> 5 0.957894.... 96 <NA> <NA>
#> 6 59 59 <NA> <NA>
#> 7 51 51 <NA> <NA>
#> 8 66 66 <NA> <NA>inputs and call_list
.$inputs holds the resolved arguments of
tbl_summary(): the data, the by variable, and
one named list per formula-list argument, with a value for every
included variable. add_overall() uses it to rebuild the
table without by. .$call_list records the
calls made on the table, in order, which is how
add_stat_label() knows it has already run.
Constructing an ltsummary object
table_body
Start from a data frame with the columns "label",
"row_type" and "variable". Only
"label" is printed; "row_type" typically
decides whether the label is indented, and "variable" is
used by inline_text() and the selectors.
tbl_summary_ex$table_body[c("variable", "row_type", "label")]
#> variable row_type label
#> 1 age label Age
#> 2 age missing Unknown
#> 3 grade label Grade
#> 4 grade level I
#> 5 grade level II
#> 6 grade level III
#> 7 response label Tumor Response
#> 8 response missing UnknownThe other columns are created by the author and are printed, or not,
according to .$table_styling.
table_styling
A few functions help construct and modify the styling:
as_ltsummary(table_body)takes a data frame and returns the skeleton of an ltsummary object, with the fulltable_stylinglist.update_table_styling()(internal) re-synchronizestable_stylingwith the columns oftable_bodyafter columns are added or removed. New columns are hidden by default.modify_table_styling()records printing instructions for one or more columns.modify_table_body()applies a function totable_bodyand runsupdate_table_styling()afterwards, so the styling stays valid.
A small regression-style table built from scratch:
df <- data.frame(
variable = c("age", "grade", "grade", "grade"),
row_type = c("label", "label", "level", "level"),
label = c("Age", "Grade", "II", "III"),
estimate = c(0.12, NA, 1.4, 2.1),
p.value = c(0.03, NA, 0.2, 0.001)
)
as_ltsummary(df) |>
modify_header(label = "**Characteristic**", estimate = "**Beta**", p.value = "**p-value**") |>
modify_fmt_fun(estimate = label_style_sigfig(), p.value = label_style_pvalue()) |>
modify_indent(columns = label, rows = row_type == "level") |>
bold_labels()Printing an ltsummary object
Every ltsummary object is printed by print.ltsummary().
The object is first converted to an lt table with as_lt(),
which uses .$table_styling to build a list of lt calls
executed on .$table_body, and the lt table is then printed
like any other. as_lt(return_calls = TRUE) returns the
calls.
as_lt(tbl_summary_ex, return_calls = TRUE)[c("lt", "lt_label")]
#> $lt
#> lt::lt(list(label = c("Age", "Unknown", "Grade", "I", "II", "III",
#> "Tumor Response", "Unknown"), stat_1 = c("59 (51, 66)", "4",
#> "", "25 (26%)", "40 (42%)", "30 (32%)", "22 (24%)", "2"), stat_2 = c("62 (54, 71)",
#> "7", "", "37 (35%)", "43 (41%)", "25 (24%)", "24 (24%)", "5"),
#> p.value = c("0.043", "", "0.3", "", "", "", ">0.9", "")),
#> auto_format = FALSE, auto_label = FALSE)
#>
#> $lt_label
#> lt::lt_label(tbl, label = "<strong>Characteristic</strong>",
#> stat_1 = "<strong>Drug A</strong><br>N = 95", stat_2 = "<strong>Drug B</strong><br>N = 105",
#> p.value = "<strong>p-value</strong>")The conversion is the only place where the package calls lt, so a
change in lt’s interface is absorbed in one file,
R/as_lt.R.