janitor adorn_totals() in R: Add Row and Column Totals
The adorn_totals() function in janitor appends a totals row, a totals column, or both to a data frame or tabyl object. It works inside a pipe, ignores non-numeric columns by default, and labels the totals line so the output ships straight to a report without a manual colSums() step.
adorn_totals(df) # default: row of totals adorn_totals(df, where = "col") # column of row sums adorn_totals(df, where = c("row", "col")) # both, with grand total adorn_totals(df, name = "Grand total") # custom label adorn_totals(df, fill = "-") # placeholder for text cols adorn_totals(df, na.rm = FALSE) # propagate NA in sums tabyl(mtcars, cyl, gear) |> adorn_totals() # tabyl pipe chain
Need explanation? Read on for examples and pitfalls.
What adorn_totals() does in one sentence
adorn_totals() sums each numeric column (and optionally each row) and appends the result as an extra row or column labelled Total. It is the standard finishing step for tabyl output, but it works on any data frame whose numeric columns make sense to add up.
The function preserves the input's class. A tabyl in, a tabyl out. A regular data frame in, a regular data frame out. That detail matters because downstream adorn_* helpers (percentages, rounding, titles) only behave correctly when the class survives the pipe.
Syntax
adorn_totals() takes a data frame plus four optional arguments that control placement, label, missing-value handling, and the placeholder for non-numeric cells. The numeric columns are detected automatically; the first column is assumed to be the identifier and is skipped.
The full signature:
adorn_totals(dat, where = "row", fill = "-", na.rm = TRUE, name = "Total", ..., na_rm = NULL)
Only dat is required. where accepts "row", "col", or c("row", "col"). fill decides what to print in non-numeric columns of the totals row. na.rm controls whether NA values are skipped when summing. name is the label that appears in the first column of the totals row.
adorn_totals() instead of rbind(df, colSums(df)) whenever the input has an identifier column. Manual colSums() fails on non-numeric columns and produces a vector you then have to coerce back to a data frame. adorn_totals() skips the identifier column, keeps the data frame class, and stays inside the pipe.Six common patterns
1. Add a totals row (the default)
The default where = "row" adds one extra row at the bottom. The first column shows the label "Total" because that column is non-numeric and the function treats it as the identifier.
2. Add a totals column (row sums)
With where = "col", each row gets a per-region annual total in a new rightmost column. The label of that column is whatever name is set to (default "Total").
3. Add both row and column totals at once
The bottom-right cell is the grand total. It equals both sum(q1, q2, q3) and sum(rowTotals); the two routes agree because na.rm = TRUE by default and there are no missing values here.
4. Customize the label with name
Use name when "Total" is too vague (multiple totals rows for grouped tables) or when a report style guide requires a specific label like "Subtotal" or "Grand total".
5. Handle non-numeric columns in the middle
The manager column is character, so its totals cell is filled with "-". Set fill = "" for a blank cell, or fill = NA_character_ to keep NA.
6. Chain with tabyl and other adorn_* helpers
Order matters in this chain. Compute totals BEFORE percentages so each row (including the totals row) divides by its own total and sums to 1.0. Reversing the order produces percentages that no longer sum cleanly.
Compare with alternatives
The base R way is rbind(df, c("Total", colSums(df[-1]))), and it is clumsier than it looks. The cast back to character mangles numeric columns, you lose the data frame class on a one-column input, and you have to write the label by hand. Three R 4.5 alternatives are worth knowing.
| Approach | Best for | Watch out for |
|---|---|---|
janitor::adorn_totals() |
Tabyl output, mixed-type frames, pipe chains | Treats first column as identifier; reorder if needed |
base::colSums() / rowSums() |
Quick vector of sums | Returns a vector, not a row; errors on non-numeric |
dplyr::bind_rows(df, summarise(df, across(where(is.numeric), sum))) |
When you already use dplyr | Verbose; misses the identifier label |
gt::summary_rows() |
Polished HTML/PDF tables | gt-only; not a data frame in, data frame out |
adorn_totals() twice on the same axis. A second call adds another "Total" row and the new totals include the previous totals row in the sum, double-counting your data. If you need to re-render totals after editing the frame, drop the prior totals row with filter(region != "Total") first.Common pitfalls
Pitfall 1: NA propagation surprises you. With the default na.rm = TRUE, a column that contains an NA still produces a clean sum of the non-missing values. If you want NA in, NA out (i.e., flag any column with missing data), set na.rm = FALSE.
Pitfall 2: the first column is treated as the identifier even when it is numeric. If your data frame leads with a numeric ID column (e.g., year), adorn_totals() will skip it. Move identifiers up front explicitly, or pass the columns to total via the ... argument so the function knows what to sum.
Pitfall 3: adorn_totals() does not respect dplyr groupings. Group-wise subtotals are not built in. To get totals per group, split the frame with group_split(), apply adorn_totals() to each piece, and bind_rows() the results, or switch to gt::summary_rows() for grouped report tables.
Try it yourself
Try it: Take mtcars, build a tabyl(cyl, gear) cross-tab, and add BOTH row and column totals with the label "All cars" instead of "Total". Save to ex_totals.
Click to reveal solution
Explanation: where = c("row", "col") adds totals on both axes in one call. The name argument relabels both the totals row identifier and the totals column header, keeping the table self-explanatory in a report.
Related janitor functions
adorn_totals() is one of seven adorn_* helpers that polish tabyl output. They are designed to stack inside a pipe; each one returns the same class so the next call works.
adorn_percentages(): divide counts by row, column, or grand totaladorn_pct_formatting(): turn proportions into "47.2%" stringsadorn_rounding(): round numeric columns for displayadorn_ns(): paste raw counts onto percentage cellsadorn_title(): attach a banner row above a tabyltabyl(): the upstream frequency builder; almost always feeds this functionclean_names(): standardize column names before any of the above
See the janitor reference on tidyverse.org for the full argument list and edge-case behaviour.
FAQ
How do I add a totals row without losing the data frame class?
Pipe through adorn_totals() directly; it preserves the input class. The pitfall is reaching for rbind(df, colSums(df)), which silently coerces everything to character when the first column is non-numeric. adorn_totals() keeps the column types intact, fills non-numeric cells with whatever you set in the fill argument, and returns a data frame (or tabyl) you can keep chaining on.
Why does adorn_totals() skip my first numeric column?
The first column is treated as the row identifier and is never summed. That assumption is what lets the function place the "Total" label correctly. If your first column is genuinely numeric (a year, an ID), either reorder so the identifier is first, or pass an explicit column selection through the ... argument to control which columns get totalled.
Can I get totals per group instead of one grand total?
Not in a single call. adorn_totals() operates on a whole frame, not on dplyr groupings. The idiom is group_split(df, grp) |> map(adorn_totals) |> bind_rows() for compact subtotal blocks, or move to gt::summary_rows() for nested totals in a presentation table. For analytic work, prefer summarize(df, total = sum(x), .by = grp).
Does adorn_totals() work on tibbles and data.tables?
Yes for tibbles (the result stays a tibble). For data.tables, the function will work but returns a data frame; copy back with as.data.table() if you need to continue with data.table syntax. Most janitor users pipe through tibbles or base data frames, where the class preservation is automatic.
How do I format the totals row with commas or currency?
Apply adorn_rounding() and adorn_pct_formatting() AFTER adorn_totals(), in that order. For currency, switch to scales::dollar() or format(big.mark = ",") applied with mutate(across(where(is.numeric), ...)). Janitor stops at counts and proportions; presentation formatting beyond rounding is delegated to scales or gt.