Report-Ready Tables
Maya runs a neighbourhood bakery, and she is applying for a small-business loan. The bank wants her Q1 numbers, so she opens R, where her till export sits as a tidy data frame: five products, the units each one sold, the revenue, and the profit margin. She copies the raw printout straight into the application.
It comes out monospaced and unlabeled: 0.62 where she means 62%, 5520 with no dollar sign, column names like margin that only an analyst would love. The data is perfectly correct. It just does not read like something you hand to a bank. A report table fixes exactly that: a title, human column names, dollars and percentages, and a line saying where the numbers came from.
Toggle the table below between the raw print and the report version. Same five numbers, two very different first impressions.
By the end of this lesson you will be able to:
- Build a report-ready table from a data frame with gt: a title, human column labels, and a source note
- Format the numbers in a table: currency, percentages, and thousands separators
- Shape the data first, sorting it and adding a share column, so the table tells a story
- Pick the right tool for where the table will live: gt, flextable, or kableExtra
Prerequisites: you can run R and load a package with library(), and you have wrangled a data frame with dplyr (filter, mutate, arrange) in the Data Wrangling with dplyr course. Every new term is defined as it appears.
A raw print is for you. A report table is for them.
When you type a data frame's name in R, you get a printout built for you, mid-analysis: fixed-width, no styling, raw numbers. That is the right tool while you work. It is the wrong thing to put in front of a reader, who needs to glance at the table and understand it without you standing beside them.
Here is Maya's data. Each lesson runs in a fresh R session, so we build it right here (run this once), then look at the raw print:
The very fastest upgrade is kable() from the knitr package: one line, and you get a clean, aligned table instead of console output. It is great for a quick look or a plain document, though it stops short of titles and number formatting:
To go all the way to a report table, you follow the same short recipe every time. Each step adds one kind of polish:
gt: building a table one layer at a time
The gt package builds report tables the way ggplot2 builds plots: you start with the data, then add one layer at a time with the pipe |>, which feeds the table on its left into the next function. The name stands for "grammar of tables," and each function adds exactly one kind of polish.
Read this top to bottom. gt() turns the data frame into a table object; tab_header() adds a title and subtitle; cols_label() renames the columns to something a human reads; tab_source_note() adds the small print at the bottom:
library(gt)
sales |>
gt() |>
tab_header(
title = "Bakery Q1 sales by product",
subtitle = "Five best-selling items, January to March 2026"
) |>
cols_label(
product = "Product",
units = "Units sold",
revenue = "Revenue",
margin = "Margin"
) |>
tab_source_note("Source: point-of-sale export, Q1 2026.")
gt renders rich HTML, the kind a browser or a PDF report shows, which is more than the in-browser R session here can draw. So the result is pictured for you in the widget below: the same five rows, now with a title, labeled columns, and a source line. That is the gt output you would see in a real report.
tab_header() when you want a title, cols_label() when you want better names, a fmt_ verb when you want to format numbers. Each line does one job.Add the missing format
Maya's table now has a title and labels, but the money column is still raw: revenue reads 5520, not $5,520. gt formats numbers with verbs that all start with fmt_, and the one for money is fmt_currency(). Fill in the blank with the right verb so the revenue column shows as dollars.
Show answer
sales |>
gt() |>
tab_header(title = "Bakery Q1 sales by product") |>
fmt_currency(columns = revenue, currency = "USD", decimals = 0)Sort the rows, then format the numbers
A good report table does two jobs before any styling: it puts the rows in a meaningful order, and it shows numbers in a form a person reads at a glance. Both happen with tools you already know plus one new helper package.
First, shape the data with dplyr: sort so the best seller is on top, and add a share column, each product's slice of total revenue, so the reader sees who matters most:
Now format. The scales package turns bare numbers into the strings a reader expects. dollar() adds the currency symbol and a thousands separator, percent() turns a proportion into a percentage, and comma() adds separators to plain counts:
Inside gt you do not call scales directly; you use the matching fmt_ verbs, which apply the same formatting to whole columns at once:
library(gt)
report |>
gt() |>
fmt_number(columns = units, decimals = 0) |> # 1,840 with a separator
fmt_currency(columns = revenue, currency = "USD", decimals = 0) |>
fmt_percent(columns = c(margin, share), decimals = 0) # 0.62 becomes 62%
The widget shows the payoff: the sorted, formatted table a reader can act on.
What does fmt_percent expect?
Maya's margin column stores proportions: 0.62, 0.55, and so on. She wants the table to show 62%, 55%. She writes fmt_percent(columns = margin). What appears in the table?
Where will this table live?
gt makes beautiful HTML and PDF tables, but it is not the only option, and it cannot do everything. The right package depends on where the table is going. Pick by destination:
| Package | Best for | The trade-off |
|---|---|---|
kable |
a fast console or Markdown table | almost no styling |
gt |
HTML and PDF reports, Quarto | no native Word output |
flextable |
Word and PowerPoint | a different function vocabulary to learn |
kableExtra |
R Markdown HTML and LaTeX | built around kable, not data-first |
When the table has to land in a Word document, flextable is the tool. It writes a true, editable Office table, something gt cannot do. Run this in your own R session (it produces a Word file, so it needs a desktop R install):
library(flextable)
# Word and PowerPoint: flextable writes a real, editable Office table.
# install.packages("flextable") first, then in your own R session:
ft <- flextable::flextable(sales)
ft <- flextable::set_caption(ft, "Bakery Q1 sales by product")
ft <- flextable::colformat_double(ft, j = "revenue", prefix = "$", digits = 0)
flextable::save_as_docx(ft, path = "sales.docx")
And when you are writing an R Markdown report and want styled HTML or LaTeX, kableExtra adds polish on top of kable:
# R Markdown HTML or LaTeX: kableExtra styles a kable.
# install.packages("kableExtra") first, then in your own R session:
kableExtra::kbl(sales, col.names = c("Product", "Units", "Revenue", "Margin")) |>
kableExtra::kable_styling(bootstrap_options = c("striped", "hover"))
Pick the package
Maya's loan officer asks her to email the sales table inside a Word document she can edit and paste into the bank's own report. Which package should Maya use to build that table?
Build the finished report frame
Bring the pieces together. Starting from sales, build Maya's report-ready data in one pipe: sort by revenue (highest first), add a share column for each product's fraction of total revenue, then format that share as a rounded percentage. Fill in the function that turns a proportion like 0.26 into "26%".
Show answer
sales |>
arrange(desc(revenue)) |>
mutate(
share = revenue / sum(revenue),
share = percent(share, accuracy = 1)
)
#> product units revenue margin share
#> 1 Croissant 1840 5520 0.62 26%
#> 2 Sourdough 1210 4840 0.55 23%
#> 3 Cinnamon Roll 1100 4400 0.58 20%
#> 4 Baguette 1530 3825 0.51 18%
#> 5 Blueberry Muffin 970 2910 0.48 14%References
A few authoritative places to take this further:
- gt: the official documentation - the package you used here, with a gallery and the full list of
fmt_andtab_functions. - flextable user guide - building tables for Word and PowerPoint, the destinations gt cannot reach.
- kableExtra documentation - styling HTML and LaTeX tables on top of
kable, for R Markdown reports. - scales: formatting numbers and labels -
dollar(),percent(),comma()and the rest of the number-formatting helpers used in this lesson.
Lesson 1 complete
You can now turn a raw data frame into a table a reader will actually read. You built one with gt (title, labels, source note), formatted the numbers as currency and percentages (and saw why a proportion, not a pre-multiplied percent, is what fmt_percent wants), shaped the data first with dplyr so the rows tell a story, and learned to pick the package by where the table is headed: gt for reports, flextable for Word, kableExtra for R Markdown.
Next, Lesson 2: Summary tables and number formatting. You will build one-line summary tables and regression tables with gtsummary, and go deeper on formatting numbers, percentages and units so a whole table reads cleanly, the finishing skills that make a report look effortless.