Aplicar várias funções de resumo em várias variáveis ​​por grupo em uma chamada

91

Eu tenho o seguinte quadro de dados

x <- read.table(text = "  id1 id2 val1 val2
1   a   x    1    9
2   a   x    2    4
3   a   y    3    5
4   a   y    4    9
5   b   x    1    7
6   b   y    4    4
7   b   x    3    9
8   b   y    2    8", header = TRUE)

Quero calcular a média de val1 e val2 agrupados por id1 e id2 e, simultaneamente, contar o número de linhas para cada combinação id1-id2. Posso realizar cada cálculo separadamente:

# calculate mean
aggregate(. ~ id1 + id2, data = x, FUN = mean)

# count rows
aggregate(. ~ id1 + id2, data = x, FUN = length)

Para fazer os dois cálculos em uma chamada, tentei

do.call("rbind", aggregate(. ~ id1 + id2, data = x, FUN = function(x) data.frame(m = mean(x), n = length(x))))

No entanto, recebo uma saída distorcida junto com um aviso:

#     m   n
# id1 1   2
# id2 1   1
#     1.5 2
#     2   2
#     3.5 2
#     3   2
#     6.5 2
#     8   2
#     7   2
#     6   2
# Warning message:
#   In rbind(id1 = c(1L, 2L, 1L, 2L), id2 = c(1L, 1L, 2L, 2L), val1 = list( :
#   number of columns of result is not a multiple of vector length (arg 1)

Eu poderia usar o pacote plyr, mas meu conjunto de dados é muito grande e o plyr é muito lento (quase inutilizável) quando o tamanho do conjunto de dados aumenta.

Como posso usar aggregateou outras funções para realizar vários cálculos em uma chamada?

brócolis
fonte
Ao lado do aggregatemencionado nas respostas, existem também bye tapply.
Roman Luštrik de

Respostas:

152

Você pode fazer tudo em uma etapa e obter a rotulagem adequada:

> aggregate(. ~ id1+id2, data = x, FUN = function(x) c(mn = mean(x), n = length(x) ) )
#   id1 id2 val1.mn val1.n val2.mn val2.n
# 1   a   x     1.5    2.0     6.5    2.0
# 2   b   x     2.0    2.0     8.0    2.0
# 3   a   y     3.5    2.0     7.0    2.0
# 4   b   y     3.0    2.0     6.0    2.0

Isso cria um dataframe com duas colunas de id e duas colunas de matriz:

str( aggregate(. ~ id1+id2, data = x, FUN = function(x) c(mn = mean(x), n = length(x) ) ) )
'data.frame':   4 obs. of  4 variables:
 $ id1 : Factor w/ 2 levels "a","b": 1 2 1 2
 $ id2 : Factor w/ 2 levels "x","y": 1 1 2 2
 $ val1: num [1:4, 1:2] 1.5 2 3.5 3 2 2 2 2
  ..- attr(*, "dimnames")=List of 2
  .. ..$ : NULL
  .. ..$ : chr  "mn" "n"
 $ val2: num [1:4, 1:2] 6.5 8 7 6 2 2 2 2
  ..- attr(*, "dimnames")=List of 2
  .. ..$ : NULL
  .. ..$ : chr  "mn" "n"

Como apontado por @ lord.garbage abaixo, isso pode ser convertido em um dataframe com colunas "simples" usando do.call(data.frame, ...)

str( do.call(data.frame, aggregate(. ~ id1+id2, data = x, FUN = function(x) c(mn = mean(x), n = length(x) ) ) ) 
    )
'data.frame':   4 obs. of  6 variables:
 $ id1    : Factor w/ 2 levels "a","b": 1 2 1 2
 $ id2    : Factor w/ 2 levels "x","y": 1 1 2 2
 $ val1.mn: num  1.5 2 3.5 3
 $ val1.n : num  2 2 2 2
 $ val2.mn: num  6.5 8 7 6
 $ val2.n : num  2 2 2 2

Esta é a sintaxe para várias variáveis ​​no LHS:

aggregate(cbind(val1, val2) ~ id1 + id2, data = x, FUN = function(x) c(mn = mean(x), n = length(x) ) )
IRTFM
fonte
1
Muito obrigado. Como uma observação lateral, como faço para que o agregado some apenas uma coluna. Se eu tiver várias colunas numéricas, não quero somar colunas que não quero. Eu poderia, é claro, descartar as colunas depois que a agregação for concluída, mas os ciclos da CPU já teriam sido gastos nessa hora.
brócolis
Você apenas fornece os fatores a serem agrupados e as colunas a serem agregadas. Possivelmente, use indexação de coluna negativa nos dados ou coloque as colunas desejadas no LHS da fórmula. (Ver edição.)
IRTFM
2
Eu encontrei o bug que o user2659402 mencionou em sua atualização ao usar o RStudio 0.98.1014 em uma máquina com Windows 7. Se você enviar o quadro de dados para o console conforme mostrado, ele parecerá normal; no entanto, se você salvá-lo em d e tentar acessar d $ val1.mn, ele retornará NULL. d também aparecerá malformado se você executar o view (d). Usar o código na atualização corrigiu isso.
JHowIX
4
O motivo de você estar tendo dificuldade é que os "vals" estão sendo retornados como matrizes com duas colunas cada, em vez de colunas comuns. Experimente d$val1[ , ""mn"]e observe a estrutura com str.
IRTFM
5
Você pode vincular as colunas que contêm matrizes de volta ao quadro de dados: agg <- aggregate(cbind(val1, val2) ~ id1 + id2, data = x, FUN = function(x) c(mn = mean(x), n = length(x))) usando agg_df <- do.call(data.frame, agg). Veja também aqui .
lord.garbage
30

Dado isso na pergunta:

Eu poderia usar o pacote plyr, mas meu conjunto de dados é muito grande e o plyr é muito lento (quase inutilizável) quando o tamanho do conjunto de dados aumenta.

Em seguida, em data.table( 1.9.4+) você pode tentar:

> DT
   id1 id2 val1 val2
1:   a   x    1    9
2:   a   x    2    4
3:   a   y    3    5
4:   a   y    4    9
5:   b   x    1    7
6:   b   y    4    4
7:   b   x    3    9
8:   b   y    2    8

> DT[ , .(mean(val1), mean(val2), .N), by = .(id1, id2)]   # simplest
   id1 id2  V1  V2 N
1:   a   x 1.5 6.5 2
2:   a   y 3.5 7.0 2
3:   b   x 2.0 8.0 2
4:   b   y 3.0 6.0 2

> DT[ , .(val1.m = mean(val1), val2.m = mean(val2), count = .N), by = .(id1, id2)]  # named
   id1 id2 val1.m val2.m count
1:   a   x    1.5    6.5     2
2:   a   y    3.5    7.0     2
3:   b   x    2.0    8.0     2
4:   b   y    3.0    6.0     2

> DT[ , c(lapply(.SD, mean), count = .N), by = .(id1, id2)]   # mean over all columns
   id1 id2 val1 val2 count
1:   a   x  1.5  6.5     2
2:   a   y  3.5  7.0     2
3:   b   x  2.0  8.0     2
4:   b   y  3.0  6.0     2

Para comparação de tempos aggregate(usado na pergunta e nas 3 outras respostas) para data.tablever este benchmark (os casos agge agg.x).

Matt Dowle
fonte
12

Você pode adicionar uma countcoluna, agregar com sum, em seguida, escalar para obter mean:

x$count <- 1
agg <- aggregate(. ~ id1 + id2, data = x,FUN = sum)
agg
#   id1 id2 val1 val2 count
# 1   a   x    3   13     2
# 2   b   x    4   16     2
# 3   a   y    7   14     2
# 4   b   y    6   12     2

agg[c("val1", "val2")] <- agg[c("val1", "val2")] / agg$count
agg
#   id1 id2 val1 val2 count
# 1   a   x  1.5  6.5     2
# 2   b   x  2.0  8.0     2
# 3   a   y  3.5  7.0     2
# 4   b   y  3.0  6.0     2

Tem a vantagem de preservar os nomes das colunas e criar uma única countcoluna.

flodel
fonte
12

Usando o dplyrpacote, você pode conseguir isso usando summarise_all. Com esta função de resumo, você pode aplicar outras funções (neste caso meane n()) a cada uma das colunas não agrupadas:

x %>%
  group_by(id1, id2) %>%
  summarise_all(funs(mean, n()))

que dá:

     id1    id2 val1_mean val2_mean val1_n val2_n
1      a      x       1.5       6.5      2      2
2      a      y       3.5       7.0      2      2
3      b      x       2.0       8.0      2      2
4      b      y       3.0       6.0      2      2

Se você não quiser aplicar a (s) função (ões) a todas as colunas não agrupadas, especifique as colunas às quais elas devem ser aplicadas ou excluindo as não desejadas com um menos usando a summarise_at()função:

# inclusion
x %>%
  group_by(id1, id2) %>%
  summarise_at(vars(val1, val2), funs(mean, n()))

# exclusion
x %>%
  group_by(id1, id2) %>%
  summarise_at(vars(-val2), funs(mean, n()))
Jaap
fonte
10

Talvez você queira mesclar ?

x.mean <- aggregate(. ~ id1+id2, p, mean)
x.len  <- aggregate(. ~ id1+id2, p, length)

merge(x.mean, x.len, by = c("id1", "id2"))

  id1 id2 val1.x val2.x val1.y val2.y
1   a   x    1.5    6.5      2      2
2   a   y    3.5    7.0      2      2
3   b   x    2.0    8.0      2      2
4   b   y    3.0    6.0      2      2
Neilfws
fonte
4

Você também pode usar o plyr::each()para apresentar várias funções:

aggregate(cbind(val1, val2) ~ id1 + id2, data = x, FUN = plyr::each(avg = mean, n = length))
heschmat
fonte
1

Outra dplyropção é acrossque faz parte da versão dev atual

#devtools::install_github("tidyverse/dplyr")
library(dplyr)

x %>% 
  group_by(id1, id2) %>% 
  summarise(across(starts_with("val"), list(mean = mean, n = length)))

Resultado

# A tibble: 4 x 4
# Groups:   id1 [2]
  id1   id2   mean$val1 $val2 n$val1 $val2
  <fct> <fct>     <dbl> <dbl>  <int> <int>
1 a     x           1.5   6.5      2     2
2 a     y           3.5   7        2     2
3 b     x           2     8        2     2
4 b     y           3     6        2     2

packageVersion("dplyr")
[1]0.8.99.9000
Markus
fonte