¿Cómo resumir datos por grupo en R? [cerrado]

181

Tengo un marco de datos R como este:

        age group
1   23.0883     1
2   25.8344     1
3   29.4648     1
4   32.7858     2
5   33.6372     1
6   34.9350     1
7   35.2115     2
8   35.2115     2
9   35.2115     2
10  36.7803     1
...

Necesito obtener el marco de datos en la siguiente forma:

group mean     sd
1     34.5     5.6
2     32.3     4.2
...

El número de grupo puede variar, pero sus nombres y cantidad se pueden obtener llamando levels(factor(data$group))

¿Qué manipulaciones se deben hacer con los datos para obtener el resultado?

Yuriy Petrovskiy
fuente
Las comas en el marco de datos de resultados significan algo especial, ¿o es solo el punto decimal?
mpiktas
@mpiktas Gracias por señalar. Corregido Estos fueron problemas locales (soy ruso): utilizamos comas para la separación decimal.
Yuriy Petrovskiy
3
Yo sospechaba eso. Toda Europa usa comas, excepto los británicos.
mpiktas
44
A pesar de no ser británico, prefiero el punto por el separador decimal.
Roman Luštrik
1
Consulte aggregate, tapplyy luego stackoverflow.com para cualquier pregunta de codificación posterior de este tipo.
conjugateprior

Respuestas:

140

Aquí está la variante de una línea de plyr usando ddply :

dt <- data.frame(age=rchisq(20,10),group=sample(1:2,20,rep=T))
ddply(dt,~group,summarise,mean=mean(age),sd=sd(age))

Aquí hay otra variante de una línea que usa el nuevo paquete data.table .

dtf <- data.frame(age=rchisq(100000,10),group=factor(sample(1:10,100000,rep=T)))
dt <- data.table(dtf)
dt[,list(mean=mean(age),sd=sd(age)),by=group]

Este es más rápido, aunque esto se nota solo en la tabla con 100k filas. Tiempos en mi Macbook Pro con procesador 2.53 Ghz Core 2 Duo y R 2.11.1:

> system.time(aa <- ddply(dtf,~group,summarise,mean=mean(age),sd=sd(age)))
utilisateur     système      écoulé 
      0.513       0.180       0.692 
> system.time(aa <- dt[,list(mean=mean(age),sd=sd(age)),by=group])
utilisateur     système      écoulé 
      0.087       0.018       0.103 

Es posible ahorrar más si utilizamos setkey:

> setkey(dt,group)
> system.time(dt[,list(mean=mean(age),sd=sd(age)),by=group])
utilisateur     système      écoulé 
      0.040       0.007       0.048 
mpiktas
fuente
2
@chl, me dio la oportunidad de probar este nuevo paquete data.table . Se ve muy prometedor.
mpiktas
77
+6000 para data.table. Realmente es mucho más rápido que ddply, incluso para mí en conjuntos de datos de menos de 100k (tengo uno con solo 20k filas). Debe tener algo que ver con las funciones que estoy aplicando, pero ddply tomará unos minutos y data.table unos segundos.
atomicules
Error tipográfico simple: creo que quisiste decir en dt <- data.table(dtf)lugar de dt <- data.table(dt)en el segundo bloque de código. De esa manera, está creando la tabla de datos desde un marco de datos en lugar de desde la dtfunción del statspaquete. Intenté editarlo, pero no puedo hacer ediciones con menos de seis caracteres.
Christopher Bottoms
En mi opinión (no humilde en este caso) data.tablees la mejor manera de agregar datos y esta respuesta es excelente, pero solo rasca la superficie. Además de ser sintácticamente superior, también es extremadamente flexible y tiene muchas características avanzadas que incluyen uniones y mecanismos internos. Consulte las preguntas frecuentes, la página de github o el curso para obtener más información.
geneorama
98

Una posibilidad es usar la función de agregado . Por ejemplo,

aggregate(data$age, by=list(data$group), FUN=mean)[2]

le da la segunda columna del resultado deseado.

ocram
fuente
1
No enlace a su servidor de ayuda local :-) +1 pero vea mis comentarios a la respuesta de @ steffen.
chl
data.frame(group=levels(factor(data$group)),mean=(aggregate(data$age, by=list(data$group), FUN=mean)$x),sd=(aggregate(data$age, by=list(data$group), FUN=sd)$x))Hice la cosa llamando pero no estoy seguro de que sea la forma correcta. No estoy seguro de lo que sucederá, entonces los resultados de las columnas unidas estarán en un orden diferente (creo que es posible). ¿Cuál es tu opinión?
Yuriy Petrovskiy
99
@Yuriy Las filas no deben estar fuera de servicio, pero aquí hay una manera de hacerlo con una llamada a aggregate():aggregate(age ~ group, data=dat, FUN = function(x) c(M=mean(x), SD=sd(x)))
cerrado el
@lockedoff: ¡Gracias por haber completado mi respuesta!
ocram
27

Como está manipulando un marco de datos, el dplyrpaquete es probablemente la forma más rápida de hacerlo.

library(dplyr)
dt <- data.frame(age=rchisq(20,10), group=sample(1:2,20, rep=T))
grp <- group_by(dt, group)
summarise(grp, mean=mean(age), sd=sd(age))

o equivalente, usando el operador dplyr/ magrittrpipe:

library(dplyr)
dt <- data.frame(age=rchisq(20,10), group=sample(1:2,20, rep=T))
group_by(dt, group) %>%
 summarise(mean=mean(age), sd=sd(age))

EDITAR el uso completo del operador de tubería:

library(dplyr)
data.frame(age=rchisq(20,10), group=sample(1:2,20, rep=T)) %>%
  group_by(group) %>%
  summarise(mean=mean(age), sd=sd(age))
Bastiaan Quast
fuente
3
+1 para dplyr. Ha hecho que muchas tareas de R sean simples y muchos de estos métodos sean obsoletos.
gregmacfarlane
Lamentablemente
dagcilibili
¿Cargaste dplyr o magrittr?
Bastiaan Quast
muchas gracias @bquast por señalar hacia la solución, se llamó a la función de resumen en plyrlugar de la dplyrque estaba causando el problema.
dagcilibili
12

¡Genial, gracias por agregar la solución dplyr!

Resulta que entonces, dplyr y data.table están muy cerca:

library(plyr)
library(dplyr)
library(data.table)
library(rbenchmark)

dtf <- data.frame(age=rchisq(100000,10),group=factor(sample(1:10,100000,rep=T)))
dt <- data.table(dtf)

setkey(dt,group)

a<-benchmark(ddply(dtf,~group,plyr:::summarise,mean=mean(age),sd=sd(age)),
         dt[,list(mean=mean(age),sd=sd(age)),by=group],
         group_by(dt, group) %>% summarise(mean=mean(age),sd=sd(age) ),
         group_by(dtf, group) %>% summarise(mean=mean(age),sd=sd(age) )
)

a[, c(1,3,4)]

data.table sigue siendo el más rápido, seguido de cerca por dplyr (), que curiosamente parece más rápido en data.frame que en data.table:

                                                              test elapsed relative
1 ddply(dtf, ~group, plyr:::summarise, mean = mean(age), sd = sd(age))   1.689    4.867
2               dt[, list(mean = mean(age), sd = sd(age)), by = group]   0.347    1.000
4   group_by(dtf, group) %>% summarise(mean = mean(age), sd = sd(age))   0.369    1.063
3    group_by(dt, group) %>% summarise(mean = mean(age), sd = sd(age))   0.580    1.671
Matifou
fuente
Al principio pensé que necesitabas mover setkey al punto de referencia, pero resulta que eso no toma casi nada de tiempo.
kasterma
10

Además de las sugerencias existentes, es posible que desee consultar la describe.byfunción en el psychpaquete.

Proporciona una serie de estadísticas descriptivas que incluyen la media y la desviación estándar basadas en una variable de agrupación.

Jeromy Anglim
fuente
es agradable, pero algo complicado de exportar a LaTeX IME.
richiemorrisroe
10

He encontrado que la función summaryByen el paquete doBy es la más conveniente para esto:

library(doBy)

age    = c(23.0883, 25.8344, 29.4648, 32.7858, 33.6372,
           34.935,  35.2115, 35.2115,  5.2115, 36.7803)
group  = c(1, 1, 1, 2, 1, 1, 2, 2, 2, 1)
dframe = data.frame(age=age, group=group)

summaryBy(age~group, data=dframe, FUN=c(mean, sd))
# 
#   group age.mean    age.sd
# 1     1 30.62333  5.415439
# 2     2 27.10507 14.640441
gung
fuente
9

Usa el sqldfpaquete. Esto le permite ahora usar SQL para resumir los datos. Una vez que lo cargues, puedes escribir algo como:

sqldf('  select group,avg(age) from data group by group  ')
KalEl
fuente
8

Editado: según las sugerencias de chl

La función que está buscando se llama "tapply", que aplica una función por grupo especificado por un factor.

# create some artificial data
set.seed(42)
groups <- 5

agedat <- c()
groupdat <- c()

for(group in 1:groups){
    agedat <- c(agedat,rnorm(100,mean=0 + group,1/group))
    groupdat <- c(groupdat,rep(group,100))
}
dat <- data.frame("age"=agedat,"group"=factor(groupdat))

# calculate mean and stdev age per group
res <- rbind.data.frame(group=1:5, with(dat, tapply(age, group, function(x) c(mean(x), sd(x)))))
names(res) <- paste("group",1:5)
row.names(res)[2:3] <- c("mean","sd")

Realmente sugiero trabajar a través de un tutorial básico de R que explique todas las estructuras de datos y métodos comúnmente utilizados. De lo contrario, se quedará atascado cada pulgada durante la programación. Consulte esta pregunta para obtener una colección de recursos gratuitos disponibles.

steffen
fuente
2
@steffen +1 pero no hay necesidad de un forbucle aquí, puede construir su marco de datos en línea, IMO. Para la tapplyllamada, use function(x) c(mean(x),sd(x)))y cbindel resultado ya que el OP solicitó ambas estadísticas. Además, ddplydesde el paquete plyr podría hacerlo sin problemas.
chl
@steffen El problema es que necesito exactamente la estructura de tabla que describí. No hay problema con obtener medios y SD. El problema es con la estructura.
Yuriy Petrovskiy
@chl: Gracias por tu comentario, no sabía sobre plyr :). Agregué cbind, pero dejé el resto intacto. Que otro tome el crédito, esta respuesta seguirá siendo un ejemplo menos óptimo.
steffen
@Yuriy: Agregado cbind. Si ya sabía cómo aplicar funciones por grupo, puede reformular su pregunta (solo por claridad;)).
steffen
@steffen cbind("mean"=mperage,"stdev"=stperage) gives no 'group' column. Will be joining by cbind (grupo = niveles (factor (datos $ grupo)), "mean" = mperage, "stdev" = stperage) `¿correcto?
Yuriy Petrovskiy
7

Aquí hay un ejemplo con la función aggregates()que hice yo mismo hace algún tiempo:

# simulates data
set.seed(666)
( dat <- data.frame(group=gl(3,6), level=factor(rep(c("A","B","C"), 6)), 
                    y=round(rnorm(18,10),1)) )

> dat
   group level    y
1      1     A 10.8
2      1     B 12.0
3      1     C  9.6
4      1     A 12.0
5      1     B  7.8
6      1     C 10.8
7      2     A  8.7
8      2     B  9.2
9      2     C  8.2
10     2     A 10.0
11     2     B 12.2
12     2     C  8.2
13     3     A 10.9
14     3     B  8.3
15     3     C 10.1
16     3     A  9.9
17     3     B 10.9
18     3     C 10.3

# aggregates() function
aggregates <- function(formula, data=NULL, FUNS){ 
    if(class(FUNS)=="list"){ 
        f <- function(x) sapply(FUNS, function(fun) fun(x)) 
    }else{f <- FUNS} 
    temp <- aggregate(formula, data, f) 
    out <- data.frame(temp[,-ncol(temp)], temp[,ncol(temp)]) 
    colnames(out)[1] <- colnames(temp)[1] 
return(out) 
} 

# example 
FUNS <- function(x) c(mean=round(mean(x),0), sd=round(sd(x), 0)) 
( ag <- aggregates(y~group:level, data=dat, FUNS=FUNS) ) 

Da el siguiente resultado:

> ag
  group level mean sd
1     1     A   11  1
2     2     A    9  1
3     3     A   10  1
4     1     B   10  3
5     2     B   11  2
6     3     B   10  2
7     1     C   10  1
8     2     C    8  0
9     3     C   10  0

Tal vez pueda obtener el mismo resultado a partir de la función R split ():

> with(dat, sapply( split(y, group:level), FUNS ) )
     1:A 1:B 1:C 2:A 2:B 2:C 3:A 3:B 3:C
mean  11  10  10   9  11   8  10  10  10
sd     1   3   1   1   2   0   1   2   0

Permítanme volver a la salida de la aggregatesfunción. Puedes transformarlo en una hermosa mesa usando reshape(), xtabs()y ftable():

rag <- reshape(ag, varying=list(3:4), direction="long", v.names="y") 
rag$time <- factor(rag$time) 
ft <- ftable(xtabs(y~group+level+time, data=rag)) 
attributes(ft)$col.vars <- list(c("mean","sd")) 

Esto da:

> ft 
             mean sd
group level         
1     A        11  1
      B        10  3
      C        10  1
2     A         9  1
      B        11  2
      C         8  0
3     A        10  1
      B        10  2
      C        10  0

Hermosa, ¿no es así? Puede exportar esta tabla a un pdf con la textplot()función del gplotspaquete.

Vea aquí las soluciones de otros.

Stéphane Laurent
fuente