04. Tables

Author

Camille Seaberry

Modified

September 8, 2026

# subset of acs data to use for examples
acs_balt <- justviz::acs |>
    dplyr::filter(
        name %in%
            c(
                "United States",
                "Maryland",
                "Baltimore city",
                "Baltimore County",
                "Anne Arundel County",
                "Howard County"
            )
    ) |>
    dplyr::select(level, name, total_hh, homeownership, median_hh_income)

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:

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.

# by default just calling the variable `acs_balt` will print it,
# but I'll do it explicitly
print(acs_balt)
# 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.

knitr::kable(acs_balt)
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:

gt::gt(acs_balt)
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:

setNames(
    acs_balt,
    c(
        "Level",
        "Name",
        "Total households",
        "Homeownership rate",
        "Median household income"
    )
)
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

dplyr::rename(
    acs_balt,
    Level = level,
    Name = name,
    `Total households` = total_hh,
    `Homeownership rate` = homeownership,
    `Median household income` = median_hh_income
)
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.

knitr::kable(
    acs_balt,
    col.names = c(
        "Level",
        "Name",
        "Total households",
        "Homeownership rate",
        "Median household income"
    )
)
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.

gt::gt(acs_balt) |>
    gt::cols_label(
        level = "Level",
        name = "Name",
        total_hh = "Total households",
        homeownership = "Homeownership rate",
        median_hh_income = "Median household income"
    )
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.

knitr::kable(acs_fmttd, align = "llrrr")
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.

gt::gt(acs_balt) |>
    gt::fmt_number(columns = total_hh, decimals = 0) |>
    gt::fmt_percent(columns = homeownership, decimals = 0) |>
    gt::fmt_currency(columns = median_hh_income, decimals = 0)
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.

knitr::kable(
    acs_fmttd,
    align = "llrrr",
    col.names = c(
        "Level",
        "Name",
        "Total households",
        "Homeownership rate",
        "Median household income"
    ),
    caption = "Select housing and income indicators, Baltimore and nearby counties, 2024"
)
Select housing and income indicators, Baltimore and nearby counties, 2024
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.

Thomas, A. (2025). Are immigrants eligible for government assistance? In USAFacts. https://usafacts.org/articles/immigrant-program-eligibility/
Back to top