Once you have a table, tsg can save it to a file in
several formats. The steps are always the same: generate the table,
optionally add a title and notes, then call the appropriate
write_*() function.
Which format should I use?
| Format | Best for |
|---|---|
Excel (.xlsx) |
Sharing with colleagues, further editing |
| HTML | Embedding in a website or report |
| Print-ready documents | |
Word (.docx) |
Inserting into a Word report or document |
Excel is the most fully-featured output and requires no additional
packages beyond tsg. The other formats need a few extra
packages — details in their sections below.
All examples use the person_record sample dataset
included with the package.
Use write_xlsx() to save any table to an
.xlsx file.
Chain add_table_title(),
add_table_subtitle(), add_source_note(), and
add_footnote() before saving. These appear as styled rows
above and below the table in the Excel file.
person_record |>
generate_crosstab(marital_status, sex) |>
add_table_title("Marital Status by Sex") |>
add_table_subtitle("National Sample Survey, 2024") |>
add_source_note("Source: person_record dataset.") |>
add_footnote("Missing values are excluded from the denominator.")
#> # A tibble: 6 × 6
#> category total frequency_1 frequency_2 percent_1 percent_2
#> <int+lbl> <int> <int> <int> <dbl> <dbl>
#> 1 1 [Single/never married] 1544 859 685 55.6 44.4
#> 2 2 [Married] 769 387 382 50.3 49.7
#> 3 3 [Common law/live-in] 424 211 213 49.8 50.2
#> 4 4 [Widowed] 138 40 98 29.0 71.0
#> 5 6 [Separated] 43 19 24 44.2 55.8
#> 6 0 [Total] 2918 1516 1402 52.0 48.0person_record |>
generate_crosstab(marital_status, sex) |>
add_table_title("Marital Status by Sex") |>
add_table_subtitle("National Sample Survey, 2024") |>
add_source_note("Source: person_record dataset.") |>
add_footnote("Missing values are excluded from the denominator.") |>
write_xlsx(path = "table-marital-sex.xlsx")Shortcut: You can also pass the title, subtitle, and notes directly as arguments to
write_xlsx()instead of chaining the helper functions.
person_record |>
generate_frequency(sex, add_cumulative = TRUE, add_cumulative_percent = TRUE)
#> # A tibble: 3 × 5
#> category frequency percent cumulative cumulative_percent
#> <int+lbl> <int> <dbl> <int> <dbl>
#> 1 1 [Male] 1516 52.0 1516 52.0
#> 2 2 [Female] 1402 48.0 2918 100
#> 3 0 [Total] 2918 100 NA NAPipe a group_by() before
generate_frequency() to stratify the output. The result is
a single flat table with group labels in the category column.
person_record |>
group_by(sex) |>
generate_frequency(employed)
#> # A tibble: 8 × 4
#> sex category frequency percent
#> <int+lbl> <int+lbl> <int> <dbl>
#> 1 1 [Male] 1 [Yes] 599 39.5
#> 2 1 [Male] 2 [No] 508 33.5
#> 3 1 [Male] 8 [Not reported] 409 27.0
#> 4 1 [Male] 0 [Total] 1516 100
#> 5 2 [Female] 1 [Yes] 323 23.0
#> 6 2 [Female] 2 [No] 678 48.4
#> 7 2 [Female] 8 [Not reported] 401 28.6
#> 8 2 [Female] 0 [Total] 1402 100person_record |>
generate_crosstab(employed, sex)
#> # A tibble: 4 × 6
#> category total frequency_1 frequency_2 percent_1 percent_2
#> <int+lbl> <int> <int> <int> <dbl> <dbl>
#> 1 1 [Yes] 922 599 323 65.0 35.0
#> 2 2 [No] 1186 508 678 42.8 57.2
#> 3 8 [Not reported] 810 409 401 50.5 49.5
#> 4 0 [Total] 2918 1516 1402 52.0 48.0person_record |>
generate_crosstab(employed, sex, percent_by_column = TRUE)
#> # A tibble: 4 × 7
#> category frequency_total frequency_1 frequency_2 percent_total percent_1
#> <int+lbl> <int> <int> <int> <dbl> <dbl>
#> 1 1 [Yes] 922 599 323 31.6 39.5
#> 2 2 [No] 1186 508 678 40.6 33.5
#> 3 8 [Not report… 810 409 401 27.8 27.0
#> 4 0 [Total] 2918 1516 1402 100 100
#> # ℹ 1 more variable: percent_2 <dbl>Pass a named list to write_xlsx(). Each
element becomes a separate worksheet. The name of each list element
becomes the sheet name.
Set separate_files = TRUE and provide a folder path
instead of a file name. The folder is created if it does not exist.
When you have many tables, it is more practical to keep all titles,
subtitles, and notes in one place — a reference spreadsheet — rather
than scattering them across your analysis script. tsg
supports this with the table_list_reference argument.
The template has one row per table with these columns:
| Column | What it contains |
|---|---|
table_id |
A unique identifier that must match the name of the
list element in write_xlsx() |
table_number |
Display number shown in the index sheet |
table_name |
Short label shown in the index sheet |
title |
Full table title |
subtitle |
Optional subtitle |
footnotes |
Optional footnote text |
source_note |
Optional source line printed below the table |
Edit the template in Excel, or build it in R:
table_ref <- tibble::tibble(
table_id = c("table_sex", "table_emp", "table_ct"),
table_number = 1:3,
table_name = c("Sex", "Employment", "Employment × Sex"),
title = c(
"Distribution by Sex",
"Employment Status",
"Employment Status by Sex"
),
subtitle = c(NA, NA, "Cross-tabulation"),
footnotes = NA,
source_note = "Source: person_record dataset."
)The table_id values in your reference must match the
names of your list. write_xlsx() looks up each table,
applies its metadata, and builds the index sheet automatically.
tables <- list(
table_sex = person_record |> generate_frequency(sex),
table_emp = person_record |> generate_frequency(employed),
table_ct = person_record |> generate_crosstab(employed, sex)
)
write_xlsx(
tables,
path = "report.xlsx",
include_table_list = TRUE,
table_list_reference = table_ref
)Required package:
gt— install withinstall.packages("gt")
person_record |>
generate_frequency(sex) |>
add_table_title("Distribution by Sex") |>
write_html(path = "table-sex.html")Cross-tabulations with grouped column headers are fully supported:
person_record |>
generate_crosstab(marital_status, sex) |>
add_table_title("Marital Status by Sex") |>
write_html(path = "crosstab.html")When data is a named list, all tables are written to a
single self-contained HTML file by default. Each table is wrapped in its
own section. Set include_table_list = TRUE to add a
clickable table of contents.
tables <- list(
"Sex" = person_record |> generate_frequency(sex),
"Marital Status"= person_record |> generate_frequency(marital_status),
"Sex × Marital" = person_record |> generate_crosstab(sex, marital_status)
)
write_html(tables, path = "report.html", include_table_list = TRUE)Set separate_files = TRUE to write each table to its own
.html file in a folder.
Required packages:
gtandwebshot2— install withinstall.packages(c("gt", "webshot2")).
webshot2also requires a Chromium browser; runwebshot2::install_phantomjs()or ensure Chrome/Chromium is available.
person_record |>
generate_frequency(sex) |>
add_table_title("Distribution by Sex") |>
write_pdf(path = "table-sex.pdf")When data is a list, the default is one PDF file per
table inside the specified folder:
To combine all tables into a single PDF file, set
separate_files = FALSE (requires the qpdf
package):
Required packages:
officerandflextable— install withinstall.packages(c("officer", "flextable"))
person_record |>
generate_frequency(sex) |>
add_table_title("Distribution by Sex") |>
add_source_note("Source: person_record dataset") |>
write_docx(path = "table-sex.docx")Cross-tabulations are fully supported:
person_record |>
generate_crosstab(marital_status, sex) |>
add_table_title("Marital Status by Sex") |>
add_footnote("Missing values excluded from the denominator.") |>
write_docx(path = "crosstab.docx")When data is a named list, the default is a single
.docx file with one table per page:
tables <- list(
"Sex" = person_record |>
generate_frequency(sex) |>
add_table_title("Distribution by Sex"),
"Marital Status"= person_record |>
generate_frequency(marital_status) |>
add_table_title("Distribution by Marital Status"),
"Sex × Marital" = person_record |>
generate_crosstab(sex, marital_status) |>
add_table_title("Sex by Marital Status")
)
write_docx(tables, path = "report.docx")Set separate_files = TRUE to write each table to its own
.docx file:
| Format | Function | Key packages |
|---|---|---|
| Excel | write_xlsx() |
(none beyond tsg) |
| HTML | write_html() |
gt |
write_pdf() |
gt, webshot2 (+ qpdf for
combined) |
|
| Word | write_docx() |
officer, flextable |