Aplicar varias funciones de resumen en varias variables por grupo en una llamada

91

Tengo el siguiente marco de datos

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)

Quiero calcular la media de val1 y val2 agrupados por id1 e id2, y simultáneamente contar el número de filas para cada combinación id1-id2. Puedo realizar cada cálculo por separado:

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

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

Para hacer ambos cálculos en una llamada, intenté

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

Sin embargo, obtengo una salida confusa junto con una advertencia:

#     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)

Podría usar el paquete plyr, pero mi conjunto de datos es bastante grande y plyr es muy lento (casi inutilizable) cuando aumenta el tamaño del conjunto de datos.

¿Cómo puedo usar aggregateu otras funciones para realizar varios cálculos en una llamada?

brócoli
fuente
Además de aggregatemencionado en las respuestas, también hay byy tapply.
Roman Luštrik

Respuestas:

152

Puede hacerlo todo en un solo paso y obtener el etiquetado adecuado:

> 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

Esto crea un marco de datos con dos columnas de identificación y dos columnas 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 lo señala @ lord.garbage a continuación, esto se puede convertir en un marco de datos con columnas "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 es la sintaxis para múltiples variables en el LHS:

aggregate(cbind(val1, val2) ~ id1 + id2, data = x, FUN = function(x) c(mn = mean(x), n = length(x) ) )
IRTFM
fuente
1
Muchas gracias. Como nota al margen, ¿cómo consigo un agregado para resumir solo una columna? Si tengo varias columnas numéricas, no quiero que sume las columnas que no quiero. Por supuesto, podría tirar las columnas después de que se realiza la agregación, pero los ciclos de la CPU ya se habrían gastado.
brócoli
Solo le da los factores para agrupar y las columnas para agregar. Posiblemente use la indexación de columnas negativas en los datos o coloque las columnas que desee en el lado izquierdo de la fórmula. (Ver edición)
IRTFM
2
Encontré el error que mencionó user2659402 en su actualización mientras usaba RStudio 0.98.1014 en una máquina con Windows 7. Si envía el marco de datos a la consola como se muestra, parece normal, sin embargo, si lo guarda en d, y luego intenta acceder a d $ val1.mn, devuelve NULL. d también aparece con formato incorrecto si ejecuta view (d). Usar el código en la actualización lo solucionó.
JHowIX
4
La razón por la que tiene dificultades es que los "vals" se devuelven como matrices con dos columnas cada una, en lugar de como columnas normales. Intenta d$val1[ , ""mn"]mirar la estructura con str.
IRTFM
5
Puede vincular las columnas que contienen matrices nuevamente en el marco de datos: 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). Consulte también aquí .
lord.garbage
30

Dado esto en la pregunta:

Podría usar el paquete plyr, pero mi conjunto de datos es bastante grande y plyr es muy lento (casi inutilizable) cuando aumenta el tamaño del conjunto de datos.

Luego, en data.table( 1.9.4+) podrías intentar:

> 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 comparar tiempos aggregate(usado en la pregunta y las otras 3 respuestas) para data.tablever este punto de referencia (los casos aggy agg.x).

Matt Dowle
fuente
12

Puede agregar una countcolumna, agregarla sumy luego reducirla para obtener 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

Tiene la ventaja de conservar los nombres de sus columnas y crear una sola countcolumna.

Flodel
fuente
12

Usando el dplyrpaquete, podría lograr esto usando summarise_all. Con esta función de resumen puede aplicar otras funciones (en este caso meany n()) a cada una de las columnas que no se agrupan:

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

lo que da:

     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

Si no desea aplicar la (s) función (es) a todas las columnas que no se agrupan, especifique las columnas a las que deben aplicarse o excluyendo las no deseadas con un signo menos usando la summarise_at()función:

# 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
fuente
10

¿Quizás quieras fusionarte ?

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
fuente
4

También puede utilizar plyr::each()para introducir varias funciones:

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

Otra dplyropción es acrosscuál es parte de la versión de desarrollo actual.

#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
fuente