04. Tables
This is a brief look at using tables as part of your data visualization toolbox. We’re doing them early in the semester because they can sometimes be a better solution than a traditional visual.
Situations where a table might be a good route:
- You only have a few numbers to display or you’re tight on space, i.e. your data or your needs don’t warrant the sacrifices you’d make for a chart (space in a document, abstraction of data)
- Your audience needs access to multiple specific values—common with scientific results, public health data, etc.
- Your data has more dimensions that you can easily visualize
- You want to give an overview of data that your reader will encounter throughout a long document (i.e. “at a glance” type profiles)
A table can be a good counterpart to a chart as well: research papers often have a boxplot or bar chart to display results of an experiment, then a table of specific diagnostics like p-values. My organization puts out a lot of reports and things that are used by grant writers, so when there’s a set of data that we know grant writers will want lots of specific numbers, we’ll opt for a table over a chart, or do both to meet multiple sets of needs.
There are a few sets of tools for making tables in R, and more than with charts, what you use will depend on the format of your output. We’ll only explore a few.
Some basic tools
print
R’s print function will print out most types of objects, including data frames. You probably want to do more than just print your data frame for any documents that anyone besides you and your other programmer colleagues, but for just looking at some data in a tabular format, this can be fine.
# A tibble: 6 × 5
level name total_hh homeownership median_hh_income
<fct> <chr> <dbl> <dbl> <dbl>
1 us United States 129227496 0.65 80734
2 state Maryland 2362928 0.68 103678
3 county Anne Arundel County 224748 0.75 124911
4 county Baltimore County 332801 0.66 91768
5 county Baltimore city 255668 0.48 62177
6 county Howard County 121065 0.71 149763
kable
The knitr package has a function kable that handles some basic table formatting and can prep tables for different types of output, mainly markdown (what we’re generally writing out to in this class), HTML, and PDF (using LaTeX formatting). The package kableExtra will take that output and do additional formatting.
| level | name | total_hh | homeownership | median_hh_income |
|---|---|---|---|---|
| us | United States | 129227496 | 0.65 | 80734 |
| state | Maryland | 2362928 | 0.68 | 103678 |
| county | Anne Arundel County | 224748 | 0.75 | 124911 |
| county | Baltimore County | 332801 | 0.66 | 91768 |
| county | Baltimore city | 255668 | 0.48 | 62177 |
| county | Howard County | 121065 | 0.71 | 149763 |
In your console, kable will output a markdown table, which with the default format, will look like this:
|level |name | total_hh| homeownership| median_hh_income|
|:------|:-------------------|---------:|-------------:|----------------:|
|us |United States | 129227496| 0.65| 80734|
|state |Maryland | 2362928| 0.68| 103678|
|county |Anne Arundel County | 224748| 0.75| 124911|
|county |Baltimore County | 332801| 0.66| 91768|
|county |Baltimore city | 255668| 0.48| 62177|
|county |Harford County | 101795| 0.80| 112265|You can see the formatting used in markdown tables: column names are separated by a row of dashes, and the alignment of each column is set by the side that has a colon.
gt package
gt is a relatively newer package from the RStudio devs that follows the grammar of graphics idea used in ggplot. Similar to how we assign visual encodings to different aspects of the data, with gt we assign pieces of the table to different aspects of the data. I’ve had a hard time getting the hang of it for more complex tables, but it is very capable. Unlike some other packages that are focused on one or two output formats, gt can export to many types of documents, and can also export high-res images of tables. Its starting function is also called gt:
| level | name | total_hh | homeownership | median_hh_income |
|---|---|---|---|---|
| us | United States | 129227496 | 0.65 | 80734 |
| state | Maryland | 2362928 | 0.68 | 103678 |
| county | Anne Arundel County | 224748 | 0.75 | 124911 |
| county | Baltimore County | 332801 | 0.66 | 91768 |
| county | Baltimore city | 255668 | 0.48 | 62177 |
| county | Howard County | 121065 | 0.71 | 149763 |
Column names
If all you do is print a data frame, it will just print out the column names as they are. If you’re only printing the data into your notebook, you probably don’t care. Some easy ways to change all the column names:
Changing the data frame’s names directly
Base R’s setNames requires a character vector of the same length as the number of columns:
| Level | Name | Total households | Homeownership rate | Median household income |
|---|---|---|---|---|
| us | United States | 129227496 | 0.65 | 80734 |
| state | Maryland | 2362928 | 0.68 | 103678 |
| county | Anne Arundel County | 224748 | 0.75 | 124911 |
| county | Baltimore County | 332801 | 0.66 | 91768 |
| county | Baltimore city | 255668 | 0.48 | 62177 |
| county | Howard County | 121065 | 0.71 | 149763 |
dplyr::rename takes a format like new_name = old_name, where both new & old column names are bare column names (not a string in quotes), as long as those names are syntactically valid. If they’re not valid, wrap them in backticks. One advantage of dplyr::rename is that you don’t have to rename or rewrite every column, just the ones you want to change.
If you have a function you want to use for batch renaming multiple columns, dplyr::rename_with is very handy
| Level | Name | Total households | Homeownership rate | Median household income |
|---|---|---|---|---|
| us | United States | 129227496 | 0.65 | 80734 |
| state | Maryland | 2362928 | 0.68 | 103678 |
| county | Anne Arundel County | 224748 | 0.75 | 124911 |
| county | Baltimore County | 332801 | 0.66 | 91768 |
| county | Baltimore city | 255668 | 0.48 | 62177 |
| county | Howard County | 121065 | 0.71 | 149763 |
With kable
knitr::kable has an argument to take column names. Like with setNames, you’ll need to give all column names, even ones you aren’t changing.
| Level | Name | Total households | Homeownership rate | Median household income |
|---|---|---|---|---|
| us | United States | 129227496 | 0.65 | 80734 |
| state | Maryland | 2362928 | 0.68 | 103678 |
| county | Anne Arundel County | 224748 | 0.75 | 124911 |
| county | Baltimore County | 332801 | 0.66 | 91768 |
| county | Baltimore city | 255668 | 0.48 | 62177 |
| county | Howard County | 121065 | 0.71 | 149763 |
With gt
gt::gt relabels columns in another function. It uses a syntax similar to dplyr::rename, except because of how gt thinks about names vs labels, you’ll use old_name = label instead.
| Level | Name | Total households | Homeownership rate | Median household income |
|---|---|---|---|---|
| us | United States | 129227496 | 0.65 | 80734 |
| state | Maryland | 2362928 | 0.68 | 103678 |
| county | Anne Arundel County | 224748 | 0.75 | 124911 |
| county | Baltimore County | 332801 | 0.66 | 91768 |
| county | Baltimore city | 255668 | 0.48 | 62177 |
| county | Howard County | 121065 | 0.71 | 149763 |
Formatting
One thing all 3 tables have in common is that the numbers are unformatted, e.g. homeownership is written as 0.65 instead of 65%. knitr::kable has some arguments for rounding numbers and for using the base format function, but no easy options for specifically formatting percentages, etc. (Some examples are in the function docs.) I like to be in control of formatting anyway, so when I’m using kable, I usually format all my columns myself—that’s why our first programming exercise was writing formatting functions.
Here I’ll write some formatter functions like we did the first week and use those on the corresponding columns, then show what it looks like with kable.
# remember that the scales::label_* functions return formatter functions to reuse
comma <- scales::label_comma(accuracy = 1) # round to nearest whole number
percent <- scales::label_percent(accuracy = 1)
dollar <- scales::label_currency(accuracy = 1)
acs_fmttd <- acs_balt |>
dplyr::mutate(total_hh = comma(total_hh)) |>
dplyr::mutate(homeownership = percent(homeownership)) |>
dplyr::mutate(median_hh_income = dollar(median_hh_income))
knitr::kable(acs_fmttd)| level | name | total_hh | homeownership | median_hh_income |
|---|---|---|---|---|
| us | United States | 129,227,496 | 65% | $80,734 |
| state | Maryland | 2,362,928 | 68% | $103,678 |
| county | Anne Arundel County | 224,748 | 75% | $124,911 |
| county | Baltimore County | 332,801 | 66% | $91,768 |
| county | Baltimore city | 255,668 | 48% | $62,177 |
| county | Howard County | 121,065 | 71% | $149,763 |
A hiccup! kable automatically aligns columns based on their data types, where strings or similar column types are left-aligned, and numbers are right-aligned. By formatting numbers in this way, the numeric columns are now strings, not actual numeric type values. The align argument takes alignments (left, center, right) all stuck together as one string.
| level | name | total_hh | homeownership | median_hh_income |
|---|---|---|---|---|
| us | United States | 129,227,496 | 65% | $80,734 |
| state | Maryland | 2,362,928 | 68% | $103,678 |
| county | Anne Arundel County | 224,748 | 75% | $124,911 |
| county | Baltimore County | 332,801 | 66% | $91,768 |
| county | Baltimore city | 255,668 | 48% | $62,177 |
| county | Howard County | 121,065 | 71% | $149,763 |
gt on the other hand has a lot of formatting functions built in. You call those functions with some arguments that fit the format type, and then the columns the formatter should apply to.
| level | name | total_hh | homeownership | median_hh_income |
|---|---|---|---|---|
| us | United States | 129,227,496 | 65% | $80,734 |
| state | Maryland | 2,362,928 | 68% | $103,678 |
| county | Anne Arundel County | 224,748 | 75% | $124,911 |
| county | Baltimore County | 332,801 | 66% | $91,768 |
| county | Baltimore city | 255,668 | 48% | $62,177 |
| county | Howard County | 121,065 | 71% | $149,763 |
Putting it all together + next steps
We could do a lot more, but this is all we have time for this semester. For your tables in this class, if you can put together the data subsetting, formatting, and renaming to get something like this, you’re all set. For more complex documents or presentations, definitely dig into the docs for kableExtra or gt, among other packages.
| Level | Name | Total households | Homeownership rate | Median household income |
|---|---|---|---|---|
| us | United States | 129,227,496 | 65% | $80,734 |
| state | Maryland | 2,362,928 | 68% | $103,678 |
| county | Anne Arundel County | 224,748 | 75% | $124,911 |
| county | Baltimore County | 332,801 | 66% | $91,768 |
| county | Baltimore city | 255,668 | 48% | $62,177 |
| county | Howard County | 121,065 | 71% | $149,763 |
Lastly, check out Thomas (2025) for a great example of what you can do with a table. It’s an incredible information-dense overview of a notoriously complicated set of procedures, and the table then leads into more detailed charts to explore different aspects of the issue (immigrants’ eligibility for government assistance program). I can’t think of a chart that would do this better.