title: sum multiple columns by group with tapply
body: <p>I wanted to sum individual columns by group and my first thought was to use <code>tapply</code>.
  However, I cannot get <code>tapply</code> to work.  Can <code>tapply</code> be used to sum multiple columns?
  If not, why not?</p>
  
  <p>I have searched the internet extensively and found numerous similar questions posted
  as far back as 2008.  However, none of those questions have been answered directly.
  Instead, the responses invariably suggest using a different function.</p>
  
  <p>Below is an example data set for which I wish to sum apples by state, cherries by state 
  and plums by state.  Below that I have compiled numerous alternatives to <code>tapply</code> that
  do work.</p>
  
  <p>At the bottom I show a simple modification to the <code>tapply</code> source code that allows
  <code>tapply</code> to perform the desired operation.</p>
  
  <p>Nevertheless, perhaps I am overlooking a simple way to perform the desired operation
  with <code>tapply</code>.  I am not looking for alternative functions, although additional alternatives are welcome.</p>
  
  <p>Given the simplicity of my modification to the <code>tapply</code> source code I wonder why it, or
  something similar, has not already been implemented.</p>
  
  <p>Thank you for any advice.  If my question is a duplicate I will be happy to post my
  question as an answer to that other question.</p>
  
  <p>Here is the example data set:</p>
  
  <pre><code>df.1 &lt;- read.table(text = '
  
      state   county   apples   cherries   plums
         AA        1        1          2       3
         AA        2       10         20      30
         AA        3      100        200     300
         BB        7       -1         -2      -3
         BB        8      -10        -20     -30
         BB        9     -100       -200    -300
  
  ', header = TRUE, stringsAsFactors = FALSE)
  </code></pre>
  
  <p>This does not work:</p>
  
  <pre><code>tapply(df.1, df.1$state, function(x) {colSums(x[,3:5])})
  </code></pre>
  
  <p>The help pages says: </p>
  
  <pre><code>tapply(X, INDEX, FUN = NULL, ..., simplify = TRUE)
  
  X       an atomic object, typically a vector.
  </code></pre>
  
  <p>I was confused by the phrase <code>typically a vector</code> which made me wonder whether
  a data frame could be used.  I have never been clear on what <code>atomic object</code> means.</p>
  
  <p>Here are several alternatives to <code>tapply</code> that do work.  The first alternative is a work-around that combines <code>tapply</code> with <code>apply</code>.</p>
  
  <pre><code>apply(df.1[,c(3:5)], 2, function(x) tapply(x, df.1$state, sum))
  
  #    apples cherries plums
  # AA    111      222   333
  # BB   -111     -222  -333
  
  with(df.1, aggregate(df.1[,3:5], data.frame(state), sum))
  
  #   state apples cherries plums
  # 1    AA    111      222   333
  # 2    BB   -111     -222  -333
  
  t(sapply(split(df.1[,3:5], df.1$state), colSums))
  
  #    apples cherries plums
  # AA    111      222   333
  # BB   -111     -222  -333
  
  t(sapply(split(df.1[,3:5], df.1$state), function(x) apply(x, 2, sum)))
  
  #    apples cherries plums
  # AA    111      222   333
  # BB   -111     -222  -333
  
  aggregate(df.1[,3:5], by=list(df.1$state), sum)
  
  #   Group.1 apples cherries plums
  # 1      AA    111      222   333
  # 2      BB   -111     -222  -333
  
  by(df.1[,3:5], df.1$state, colSums)
  
  # df.1$state: AA
  #   apples cherries    plums 
  #      111      222      333 
  # ------------------------------------------------------------ 
  # df.1$state: BB
  #   apples cherries    plums 
  #     -111     -222     -333
  
  with(df.1, 
       aggregate(x = list(apples   = apples, 
                          cherries = cherries,
                          plums    = plums), 
                 by = list(state   = state), 
                 FUN = function(x) sum(x)))
  
  #   state apples cherries plums
  # 1    AA    111      222   333
  # 2    BB   -111     -222  -333
  
  lapply(split(df.1, df.1$state), function(x) {colSums(x[,3:5])} )
  
  # $AA
  #   apples cherries    plums 
  #      111      222      333 
  #
  # $BB
  #   apples cherries    plums 
  #     -111     -222     -333
  </code></pre>
  
  <p>Here is the source code for <code>tapply</code> except that I changed the line:</p>
  
  <pre><code>nx &lt;- length(X)
  </code></pre>
  
  <p>to:</p>
  
  <pre><code>nx &lt;- ifelse(is.vector(X), length(X), dim(X)[1])
  </code></pre>
  
  <p>This modified version of <code>tapply</code> performs the desired operation:</p>
  
  <pre><code>my.tapply &lt;- function (X, INDEX, FUN = NULL, ..., simplify = TRUE)
  {
      FUN &lt;- if (!is.null(FUN)) match.fun(FUN)
      if (!is.list(INDEX)) INDEX &lt;- list(INDEX)
      nI &lt;- length(INDEX)
      if (!nI) stop("'INDEX' is of length zero")
      namelist &lt;- vector("list", nI)
      names(namelist) &lt;- names(INDEX)
      extent &lt;- integer(nI)
      nx     &lt;- ifelse(is.vector(X), length(X), dim(X)[1])  # replaces nx &lt;- length(X)
      one &lt;- 1L
      group &lt;- rep.int(one, nx) #- to contain the splitting vector
      ngroup &lt;- one
      for (i in seq_along(INDEX)) {
      index &lt;- as.factor(INDEX[[i]])
      if (length(index) != nx)
          stop("arguments must have same length")
      namelist[[i]] &lt;- levels(index)#- all of them, yes !
      extent[i] &lt;- nlevels(index)
      group &lt;- group + ngroup * (as.integer(index) - one)
      ngroup &lt;- ngroup * nlevels(index)
      }
      if (is.null(FUN)) return(group)
      ans &lt;- lapply(X = split(X, group), FUN = FUN, ...)
      index &lt;- as.integer(names(ans))
      if (simplify &amp;&amp; all(unlist(lapply(ans, length)) == 1L)) {
      ansmat &lt;- array(dim = extent, dimnames = namelist)
      ans &lt;- unlist(ans, recursive = FALSE)
      } else {
      ansmat &lt;- array(vector("list", prod(extent)),
              dim = extent, dimnames = namelist)
      }
      if(length(index)) {
          names(ans) &lt;- NULL
          ansmat[index] &lt;- ans
      }
      ansmat
  }
  
  my.tapply(df.1$apples, df.1$state, function(x) {sum(x)})
  
  #  AA   BB 
  # 111 -111
  
  my.tapply(df.1[,3:4] , df.1$state, function(x) {colSums(x)})
  
  # $AA
  #   apples cherries 
  #      111      222 
  #
  # $BB
  #   apples cherries 
  #     -111     -222
  </code></pre>