96

I have a data frame where I would like to add an additional row that totals up the values for each column. For example, Let's say I have this data:

x <- data.frame(Language=c("C++", "Java", "Python"), 
                Files=c(4009, 210, 35), 
                LOC=c(15328,876, 200), 
                stringsAsFactors=FALSE)    

Data looks like this:

  Language Files   LOC
1      C++  4009 15328
2     Java   210   876
3   Python    35   200

My instinct is to do this:

y <- rbind(x, c("Total", colSums(x[,2:3])))

And this works, it computes the totals:

> y
  Language Files   LOC
1      C++  4009 15328
2     Java   210   876
3   Python    35   200
4    Total  4254 16404

The problem is that the Files and LOC columns have all been converted to strings:

> y$LOC
[1] "15328" "876"   "200"   "16404"

I understand that this is happening because I created a vector c("Total", colSums(x[,2:3]) with inputs that are both numbers and strings, and it's converting all the elements to a common type so that all of the vector elements are the same. Then the same thing happens to the Files and LOC columns.

What's a better way to do this?

12 Answers 12

127

See adorn_totals() from the janitor package:

library(janitor)
x %>%
  adorn_totals("row")

#>  Language Files   LOC
#>       C++  4009 15328
#>      Java   210   876
#>    Python    35   200
#>     Total  4254 16404

The numeric columns remain of class numeric.

Disclaimer: I created this package, including adorn_totals() which is made for precisely this task.

Sign up to request clarification or add additional context in comments.

1 Comment

Note that one problem with this (the only one) is that it's now hard to sort by row totals, which I usually want to do. The 'Total' row ends up on top.
85

A tidyverse way to do this would be to use bind_rows (or eventually add_row) and summarise to compute the sums. Here the issue is that we want sums for all but one, so a trick would be:

summarise_all(x, ~if(is.numeric(.)) sum(.) else "Total")

In one line:

x %>%
  bind_rows(summarise_all(., ~if(is.numeric(.)) sum(.) else "Total"))

Edit with dplyr >=1.0

One can also use across(), which is slightly more verbose in this case:

x %>%
  bind_rows(summarise(.,
                      across(where(is.numeric), sum),
                      across(where(is.character), ~"Total")))

8 Comments

Thanks, you were right: My solution was not the required answer. Your answer is a correct one. I voted you up and deleted my entry.
Nice, I appreciate keeping it in the tidyverse, seems silly to load another package just for this.
Nice answer, how is about only sum for a certain no. of columns as other columsn might not need sum but Average instead.
I am afraid that if you want to use different functions on different columns, you would need to run manually summarise(var1=mean(var1), var2= sum(var2), var = "Total")
Nice, solution. Wondering how to write right code with across instead of summarise_all?
|
30

Here's a way that gets you what you want, but there may very well be a more elegant solution.

rbind(x, data.frame(Language = "Total", t(colSums(x[, -1]))))

For the record, I prefer Chase's answer if you don't absolutely need the Language column.

Comments

26

Do you need the Language column in your data, or is it more appropriate to think of that column as the row.names? That would change your data.frame from 4 observations of 3 variables to 4 observations of 2 variables (Files & LOC).

x <- data.frame(Files = c(4009, 210, 35), LOC = c(15328,876, 200),
                row.names = c("C++", "Java", "Python"), stringsAsFactors = FALSE)    
x["Total" ,] <- colSums(x)


> x
       Files   LOC
C++     4009 15328
Java     210   876
Python    35   200
Total   4254 16404

3 Comments

Personally, I don't recommend storing data in rownames - that's what variables are for!
In general, I agree. I also tend to follow @csgillespie's advice of not mixing raw data and summary statistics in the same object. As the OP pointed out however, it isn't really an issue in this instance since the question revolves around the presentation of data, not any further analysis.
What is the tidyverse equivalent?
14

Extending the answer of Nicolas Ratto, if you were to have a lot more columns you could use

x %>% add_row(Language = "Total", summarise(., across(where(is.numeric), sum)))

3 Comments

This solution is good but what we don't know at time of execution what's the name of the first column?
@Angelo Not sure how robust this is, or if there is a far simpler way, but it seems to work for this example at least x %>% add_row(!!rlang::as_name(names(.)[1]) := "Total", summarise(., across(where(is.numeric), sum)))
In fact no need for rlang::as_name() i.e. !!names(.)[1] := "Total" works
11

Try this

y[4,] = c("Total", colSums(y[,2:3]))

Comments

7

If (1) we don't need the "Language" heading on the first column then we can represent it using row names and if (2) it is ok to label the last row as "Sum" rather than "Total" then we can use addmargins like this:

rownames(x) <- x$Language
addmargins(as.table(as.matrix(x[-1])), 1)

giving:

       Files   LOC
C++     4009 15328
Java     210   876
Python    35   200
Sum     4254 16404

If we do want the first column labelled "Language" and the total row labelled "Total" then its a bit longer:

rownames(x) <- x$Language
Total <- sum
xa <- addmargins(as.table(as.matrix(x[-1])), 1, FUN = Total)
data.frame(Language = rownames(xa), as.matrix(xa[]), row.names = NULL)

giving:

  Language Files   LOC
1      C++  4009 15328
2     Java   210   876
3   Python    35   200
4    Total  4254 16404

Comments

5

Try this

library(tibble)
x %>% add_row( Language="Total",Files = sum(.$Files),LOC = sum(.$LOC) )

Comments

4
df %>% bind_rows(purrr::map_dbl(.,sum))

1 Comment

Good and elegant solution, but you have to drop the first column, and then pass it to map_dbl. A way to do that is to use the [] operator. x %>% bind_rows(x[,-1] %>% map_dbl(.,sum))
1

Are you sure you really want to have the column totals in your data frame? To me, the data frame's interpretation now depends on the row. For example,

  • Rows 1-(n-1): how many files are associated with a particular language
  • Row n: how many files are associated with all languages

This gets more confusing if you start to subset your data. For example, suppose you want to know which languages have more than 100 Files:

> x = data.frame(Files=c(4009, 210, 35), 
                LOC=c(15328,876, 200), 
                row.names=c("C++", "Java", "Python"), 
                stringsAsFactors=FALSE)    
> x["Total" ,] = colSums(x)
> x[x$Files > 100,]
       Files   LOC
C++    4009 15328
Java    210   876
Total  4254 16404#But this refers to all languages!

The Total row is now wrong!

Personally I would work out the column sums and store them in a separate vector.

1 Comment

Typically I wouldn't do this for analysis, but this is for presentation. This is the last step before I generate a table in a LaTeX document with Sweave.
1

Since you mention this is a last step before exporting for presentation, you may have column names that will include spaces in them for clarity (i.e. "Grand Total"). If so, the following will insure that the created data.frame will rbind to the original dataset without an error caused by mismatched column names:

dfTotals <- data.frame(Language="Total",t(colSums(x[,-1]))))

colnames(dfTotals) <- names(x)  

rbind(x, dfTotals)

Comments

0

Your original instinct would work if you coerced your columns to numeric:

y$LOC <- as.numeric(y$LOC)
y$Files <- as.numeric(y$Files)

And then apply colSums() and rbind().

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.