Суммировать несколько столбцов по группам с помощью

Я хотел бы суммировать отдельные столбцы по группе, и я подумал, что нужно использовать tapply. Однако я не могу заставить tapply работать. Можно ли использовать tapply для суммирования нескольких столбцов? Если нет, почему бы и нет?

Я много раз искал интернет и нашел множество подобных вопросов еще в 2008 году. Однако на каждый из этих вопросов не был дан прямой ответ. Вместо этого ответы неизменно предполагают использование другой функции.

Ниже приведен пример набора данных, для которого я хочу суммировать яблоки по состоянию, вишни по штату и сливы по штату. Ниже я собрал множество альтернатив tapply, которые делайте работу.

Внизу я показываю простую модификацию исходного кода tapply, который позволяет tapply, чтобы выполнить требуемую операцию.

Тем не менее, возможно, я пропустил простой способ выполнить желаемую операцию с tapply. Я не ищу альтернативные функции, хотя возможны дополнительные варианты.

Учитывая простоту моей модификации исходного кода tapply, мне интересно, почему это или что-то подобное, еще не реализовано.

Спасибо за любой совет. Если мой вопрос будет дубликат, я буду рад опубликовать свой вопрос как ответ на этот другой вопрос.

Вот пример набора данных:

df.1 <- 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)

Это не работает:

tapply(df.1, df.1$state, function(x) {colSums(x[,3:5])})

Страницы помощи говорят:

tapply(X, INDEX, FUN = NULL, ..., simplify = TRUE)

X       an atomic object, typically a vector.

Меня смутила фраза typically a vector, которая заставила меня задаться вопросом: можно использовать фрейм данных. Я никогда не понимал, что означает atomic object.

Вот несколько альтернатив tapply, которые работают. Первый вариант - это работа, которая объединяет tapply с apply.

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

Вот исходный код для tapply, за исключением того, что я изменил строку:

nx <- length(X)

в

nx <- ifelse(is.vector(X), length(X), dim(X)[1])

Эта измененная версия tapply выполняет требуемую операцию:

my.tapply <- function (X, INDEX, FUN = NULL, ..., simplify = TRUE)
{
    FUN <- if (!is.null(FUN)) match.fun(FUN)
    if (!is.list(INDEX)) INDEX <- list(INDEX)
    nI <- length(INDEX)
    if (!nI) stop("'INDEX' is of length zero")
    namelist <- vector("list", nI)
    names(namelist) <- names(INDEX)
    extent <- integer(nI)
    nx     <- ifelse(is.vector(X), length(X), dim(X)[1])  # replaces nx <- length(X)
    one <- 1L
    group <- rep.int(one, nx) #- to contain the splitting vector
    ngroup <- one
    for (i in seq_along(INDEX)) {
    index <- as.factor(INDEX[[i]])
    if (length(index) != nx)
        stop("arguments must have same length")
    namelist[[i]] <- levels(index)#- all of them, yes !
    extent[i] <- nlevels(index)
    group <- group + ngroup * (as.integer(index) - one)
    ngroup <- ngroup * nlevels(index)
    }
    if (is.null(FUN)) return(group)
    ans <- lapply(X = split(X, group), FUN = FUN, ...)
    index <- as.integer(names(ans))
    if (simplify && all(unlist(lapply(ans, length)) == 1L)) {
    ansmat <- array(dim = extent, dimnames = namelist)
    ans <- unlist(ans, recursive = FALSE)
    } else {
    ansmat <- array(vector("list", prod(extent)),
            dim = extent, dimnames = namelist)
    }
    if(length(index)) {
        names(ans) <- NULL
        ansmat[index] <- 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

Ответ 1

tapply работает с вектором, для data.frame вы можете использовать by (который является оберткой для tapply, посмотрите на код):

> by(df.1[,c(3:5)], df.1$state, FUN=colSums)
df.1$state: AA
  apples cherries    plums 
     111      222      333 
------------------------------------------------------------------------------------- 
df.1$state: BB
  apples cherries    plums 
    -111     -222     -333 

Ответ 2

Вы ищете by. Он использует INDEX так, как вы предполагали tapply, по строке.

by(df.1, df.1$state, function(x) colSums(x[,3:5]))

Проблема с использованием tapply заключается в том, что вы индексировали столбцы data.frame по столбцам. (Потому что data.frame на самом деле просто столбцы list). Таким образом, tapply жаловался, что ваш индекс не соответствует длине вашего data.frame, который равен 5.

Ответ 3

Я посмотрел исходный код для by, как предположил EDi. Этот код был значительно более сложным, чем мое изменение в одной строке в tapply. Я обнаружил, что my.tapply не работает с более сложным сценарием ниже, где apples и cherries суммируются с помощью state и county. Если я получу my.tapply для работы с этим случаем, я могу опубликовать код здесь позже:

df.2 <- read.table(text = '

    state   county   apples   cherries   plums
       AA        1        1          2       3
       AA        1        1          2       3
       AA        2       10         20      30
       AA        2       10         20      30
       AA        3      100        200     300
       AA        3      100        200     300

       BB        7       -1         -2      -3
       BB        7       -1         -2      -3
       BB        8      -10        -20     -30
       BB        8      -10        -20     -30
       BB        9     -100       -200    -300
       BB        9     -100       -200    -300

', header = TRUE, stringsAsFactors = FALSE)

# my function works

   tapply(df.2$apples  , list(df.2$state, df.2$county), function(x) {sum(x)})
my.tapply(df.2$apples  , list(df.2$state, df.2$county), function(x) {sum(x)})

# my function works

   tapply(df.2$cherries, list(df.2$state, df.2$county), function(x) {sum(x)})
my.tapply(df.2$cherries, list(df.2$state, df.2$county), function(x) {sum(x)})

# my function does not work

my.tapply(df.2[,3:4], list(df.2$state, df.2$county), function(x) {colSums(x)})