Funciones de agrupación (tapply, by, agregada) y la familia * apply


1041

Cada vez que quiero hacer algo "map" py en R, generalmente trato de usar una función en la applyfamilia.

Sin embargo, nunca he entendido bien las diferencias entre ellos - cómo { sapply, lapply, etc.} se aplica a la función del / de entrada agrupados de entrada, lo que la salida se verá así, o incluso lo que puede ser la entrada - así que a menudo solo revísalos hasta que consiga lo que quiero.

¿Alguien puede explicar cómo usar cuál cuando?

Mi comprensión actual (probablemente incorrecta / incompleta) es ...

  1. sapply(vec, f): la entrada es un vector. La salida es un vector / matriz, donde iestá el elemento f(vec[i]), dándole una matriz si ftiene una salida de elementos múltiples

  2. lapply(vec, f): igual que sapply, pero la salida es una lista?

  3. apply(matrix, 1/2, f): la entrada es una matriz. la salida es un vector, donde el elemento ies f (fila / col i de la matriz)
  4. tapply(vector, grouping, f): la salida es una matriz / matriz, donde un elemento en la matriz / matriz es el valor de funa agrupación gdel vector y gse empuja a los nombres de fila / columna
  5. by(dataframe, grouping, f): deja gser una agrupación. aplicar fa cada columna del grupo / marco de datos. imprima bastante la agrupación y el valor de fen cada columna.
  6. aggregate(matrix, grouping, f): similar a by, pero en lugar de imprimir bastante la salida, el agregado pega todo en un marco de datos.

Cuestión lado: Todavía tengo plyr o la forma no se aprende - habría plyro reshapereemplazar todos ellos por completo?


33
a su pregunta secundaria: para muchas cosas, plyr es un reemplazo directo de *apply()y by. plyr (al menos para mí) parece mucho más coherente en el sentido de que siempre sé exactamente qué formato de datos espera y qué es lo que escupirá. Eso me ahorra mucha molestia.
JD Long

12
Además, recomendaría agregar: doByy las capacidades de selección y aplicación de data.table.
Iterator

77
sapplyes solo lapplycon la adición de simplify2arrayen la salida. applycoacciona al vector atómico, pero la salida puede ser vector o lista. bydivide los marcos de datos en subcuadros de datos, pero no se usa fen columnas por separado. Solo si hay un método para la clase 'data.frame' podría faplicarse por columna by. aggregatees genérico, por lo que existen diferentes métodos para diferentes clases del primer argumento.
IRTFM

8
Nemónico: l es para 'lista', s es para 'simplificar', t es para 'por tipo' (cada nivel de la agrupación es un tipo)
Lutz Prechelt

También existen algunas funciones en el paquete Rfast, como: eachcol.apply, apply.condition y más, que son más rápidas que los equivalentes de R
Stefanos

Respuestas:


1330

R tiene muchas * funciones de aplicación que se describen hábilmente en los archivos de ayuda (por ejemplo ?apply). Sin embargo, hay suficientes de ellos para que el uso inicial de los R tenga dificultades para decidir cuál es el adecuado para su situación o incluso para recordarlos a todos. Pueden tener un sentido general de que "Debería estar usando una función * apply aquí", pero puede ser difícil mantenerlos todos rectos al principio.

A pesar del hecho (señalado en otras respuestas) de que gran parte de la funcionalidad de la familia * apply está cubierta por el plyrpaquete extremadamente popular , las funciones básicas siguen siendo útiles y vale la pena conocerlas.

Esta respuesta está destinada a actuar como una especie de señal para los nuevos useRs para ayudarlos a dirigirlos a la función * * correcta para su problema particular. ¡Tenga en cuenta que esto no pretende simplemente regurgitar o reemplazar la documentación de R! La esperanza es que esta respuesta lo ayude a decidir qué función de aplicación * se adapta a su situación y luego depende de usted investigarla más a fondo. Con una excepción, no se abordarán las diferencias de rendimiento.

  • aplicar : cuando desea aplicar una función a las filas o columnas de una matriz (y análogos de dimensiones superiores); generalmente no es aconsejable para marcos de datos, ya que primero se convertirá en una matriz.

    # Two dimensional matrix
    M <- matrix(seq(1,16), 4, 4)
    
    # apply min to rows
    apply(M, 1, min)
    [1] 1 2 3 4
    
    # apply max to columns
    apply(M, 2, max)
    [1]  4  8 12 16
    
    # 3 dimensional array
    M <- array( seq(32), dim = c(4,4,2))
    
    # Apply sum across each M[*, , ] - i.e Sum across 2nd and 3rd dimension
    apply(M, 1, sum)
    # Result is one-dimensional
    [1] 120 128 136 144
    
    # Apply sum across each M[*, *, ] - i.e Sum across 3rd dimension
    apply(M, c(1,2), sum)
    # Result is two-dimensional
         [,1] [,2] [,3] [,4]
    [1,]   18   26   34   42
    [2,]   20   28   36   44
    [3,]   22   30   38   46
    [4,]   24   32   40   48
    

    Si desea que los medios de fila / columna o sumas de dinero por una matriz 2D, asegúrese de investigar la altamente optimizado, rápido como un rayo colMeans, rowMeans, colSums, rowSums.

  • lapply : cuando desea aplicar una función a cada elemento de una lista y recuperar una lista.

    Este es el caballo de batalla de muchas de las otras * funciones de aplicación. Despegue su código y con frecuencia lo encontrará lapplydebajo.

    x <- list(a = 1, b = 1:3, c = 10:100) 
    lapply(x, FUN = length) 
    $a 
    [1] 1
    $b 
    [1] 3
    $c 
    [1] 91
    lapply(x, FUN = sum) 
    $a 
    [1] 1
    $b 
    [1] 6
    $c 
    [1] 5005
    
  • sapply : cuando desea aplicar una función a cada elemento de una lista a la vez, pero desea recuperar un vector , en lugar de una lista.

    Si te encuentras escribiendo unlist(lapply(...)), detente y considera sapply.

    x <- list(a = 1, b = 1:3, c = 10:100)
    # Compare with above; a named vector, not a list 
    sapply(x, FUN = length)  
    a  b  c   
    1  3 91
    
    sapply(x, FUN = sum)   
    a    b    c    
    1    6 5005 
    

    En usos más avanzados sapply, intentará forzar el resultado a una matriz multidimensional, si corresponde. Por ejemplo, si nuestra función devuelve vectores de la misma longitud, sapplylos usará como columnas de una matriz:

    sapply(1:5,function(x) rnorm(3,x))

    Si nuestra función devuelve una matriz bidimensional, sapplyhará esencialmente lo mismo, tratando cada matriz devuelta como un único vector largo:

    sapply(1:5,function(x) matrix(x,2,2))

    A menos que especifiquemos simplify = "array", en cuyo caso usará las matrices individuales para construir una matriz multidimensional:

    sapply(1:5,function(x) matrix(x,2,2), simplify = "array")

    Por supuesto, cada uno de estos comportamientos depende de que nuestra función devuelva vectores o matrices de la misma longitud o dimensión.

  • vapply : cuando desee usarlo, sapplypero tal vez necesite exprimir un poco más la velocidad de su código.

    Para vapply, que, básicamente, da R un ejemplo de qué tipo de cosa que su función devolverá, que puede ahorrar algunos valores devueltos coaccionar tiempo para encajar en un único vector atómica.

    x <- list(a = 1, b = 1:3, c = 10:100)
    #Note that since the advantage here is mainly speed, this
    # example is only for illustration. We're telling R that
    # everything returned by length() should be an integer of 
    # length 1. 
    vapply(x, FUN = length, FUN.VALUE = 0L) 
    a  b  c  
    1  3 91
    
  • mapply : para cuando tiene varias estructuras de datos (por ejemplo, vectores, listas) y desea aplicar una función a los primeros elementos de cada uno, y luego a los segundos elementos de cada uno, etc., coaccionando el resultado a un vector / matriz como en sapply.

    Esto es multivariante en el sentido de que su función debe aceptar múltiples argumentos.

    #Sums the 1st elements, the 2nd elements, etc. 
    mapply(sum, 1:5, 1:5, 1:5) 
    [1]  3  6  9 12 15
    #To do rep(1,4), rep(2,3), etc.
    mapply(rep, 1:4, 4:1)   
    [[1]]
    [1] 1 1 1 1
    
    [[2]]
    [1] 2 2 2
    
    [[3]]
    [1] 3 3
    
    [[4]]
    [1] 4
    
  • Mapa : un contenedor mapplycon el SIMPLIFY = FALSEque se garantiza que devolverá una lista.

    Map(sum, 1:5, 1:5, 1:5)
    [[1]]
    [1] 3
    
    [[2]]
    [1] 6
    
    [[3]]
    [1] 9
    
    [[4]]
    [1] 12
    
    [[5]]
    [1] 15
    
  • rapply : para cuando desea aplicar una función a cada elemento de una estructura de lista anidada , de forma recursiva.

    Para darle una idea de lo poco común que rapplyes, ¡lo olvidé la primera vez que publiqué esta respuesta! Obviamente, estoy seguro de que muchas personas lo usan, pero YMMV. rapplyse ilustra mejor con una función definida por el usuario para aplicar:

    # Append ! to string, otherwise increment
    myFun <- function(x){
        if(is.character(x)){
          return(paste(x,"!",sep=""))
        }
        else{
          return(x + 1)
        }
    }
    
    #A nested list structure
    l <- list(a = list(a1 = "Boo", b1 = 2, c1 = "Eeek"), 
              b = 3, c = "Yikes", 
              d = list(a2 = 1, b2 = list(a3 = "Hey", b3 = 5)))
    
    
    # Result is named vector, coerced to character          
    rapply(l, myFun)
    
    # Result is a nested list like l, with values altered
    rapply(l, myFun, how="replace")
    
  • tapply : para cuando desea aplicar una función a subconjuntos de un vector y los subconjuntos están definidos por algún otro vector, generalmente un factor.

    Las ovejas negras de la * familia aplican, de algún tipo. El uso del archivo de ayuda de la frase "matriz irregular" puede ser un poco confuso , pero en realidad es bastante simple.

    Un vector:

    x <- 1:20

    Un factor (¡de la misma longitud!) Que define grupos:

    y <- factor(rep(letters[1:5], each = 4))

    Sume los valores xdentro de cada subgrupo definido por y:

    tapply(x, y, sum)  
     a  b  c  d  e  
    10 26 42 58 74 
    

    Se pueden manejar ejemplos más complejos donde los subgrupos están definidos por las combinaciones únicas de una lista de varios factores. tapplyes similar en espíritu a la fracción de aplicar-combinar funciones que son comunes en R ( aggregate, by, ave, ddply, etc.) Por lo tanto su estado ovejas negro.


32
Cree que encontrará que byes puramente dividido y aggregateestá tapplyen sus núcleos. Creo que las ovejas negras son un excelente tejido.
IRTFM

21
Fantástica respuesta! Esto debería ser parte de la documentación oficial de R :). Una pequeña sugerencia: ¿quizás agregar algunas viñetas sobre el uso aggregatey bytambién? (Por fin entiendo ellos después de su descripción !, pero son bastante comunes, por lo que podría ser útil para separar y tienen algunos ejemplos específicos para esas dos funciones.)
grautur

3
@grautur Estaba podando activamente las cosas de esta respuesta para evitar que fuera (a) demasiado larga y (b) una reescritura de la documentación. Decidí que aggregate, si bien , byetc. se basan en * aplicar funciones, la forma en que enfoca su uso es lo suficientemente diferente desde la perspectiva de los usuarios que deberían resumirse en una respuesta separada. Puedo intentarlo si tengo tiempo, o tal vez alguien más me gane y gane mi voto positivo.
joran

44
también, ?Mapcomo pariente demapply
baptiste

3
@jsanders: no estaría de acuerdo con eso en absoluto. data.frames son una parte absolutamente central de R y, como listobjeto, se manipulan con frecuencia utilizando lapplyparticularmente. También actúan como contenedores para agrupar vectores / factores de muchos tipos en un conjunto de datos rectangular tradicional. Si bien data.tabley plyrpodrían agregar un cierto tipo de sintaxis que algunos podrían encontrar más cómodo, se están extendiendo y actuando sobre data.frames respectivamente.
thelatemail

191

En la nota al margen, así es como las diversas plyrfunciones corresponden a las *applyfunciones base (de la introducción al documento de plyr de la página web de plyr http://had.co.nz/plyr/ )

Base function   Input   Output   plyr function 
---------------------------------------
aggregate        d       d       ddply + colwise 
apply            a       a/l     aaply / alply 
by               d       l       dlply 
lapply           l       l       llply  
mapply           a       a/l     maply / mlply 
replicate        r       a/l     raply / rlply 
sapply           l       a       laply 

Uno de los objetivos de plyres proporcionar convenciones de nomenclatura consistentes para cada una de las funciones, codificando los tipos de datos de entrada y salida en el nombre de la función. También proporciona consistencia en la salida, ya que la salida de dlply()es fácilmente transitable ldply()para producir resultados útiles, etc.

Conceptualmente, el aprendizaje plyrno es más difícil que comprender las *applyfunciones básicas .

plyry las reshapefunciones han reemplazado a casi todas estas funciones en mi uso diario. Pero, también del documento de Introducción a Plyr:

Funciones relacionadas tapplyy sweepno tienen una función correspondiente en plyr, y siguen siendo útiles. mergees útil para combinar resúmenes con los datos originales.


13
Cuando comencé a aprender R desde cero, encontré que plyr MUCHO más fácil de aprender que la *apply()familia de funciones. Para mí, ddply()era muy intuitivo ya que estaba familiarizado con las funciones de agregación de SQL. ddply()se convirtió en mi martillo para resolver muchos problemas, algunos de los cuales podrían haberse resuelto mejor con otros comandos.
JD Long

1
Supongo que pensé que el concepto detrás de las plyrfunciones es similar a las *applyfunciones, por lo que si puedes hacer una, puedes hacer la otra, pero las plyrfunciones son más fáciles de recordar. ¡Pero estoy totalmente de acuerdo con el ddply()martillo!
JoFrhwld

1
El paquete plyr tiene la join()función que realiza tareas similares a fusionar. Quizás sea más importante mencionarlo en el contexto de plyr.
marbel

No olvidemos pobre, olvidadoeapply
JDL

Gran respuesta en general, pero creo que minimiza la utilidad vapplyy las desventajas de sapply. Una de las principales ventajas vapplyes que impone el tipo y la longitud de salida, por lo que terminará con la salida esperada exacta o un error informativo. Por otro lado, sapplyintentaremos simplificar el resultado siguiendo reglas que no siempre son obvias, y de lo contrario recurrirán a una lista. Por ejemplo, tratar de predecir el tipo de salida de esto producirá: sapply(list(1:5, 6:10, matrix(1:4, 2)), function(x) head(x, 1)). ¿Qué hay de sapply(list(matrix(1:4, 2), matrix(1:4, 2)), ...)?
Alexey Shiklomanov

133

De la diapositiva 21 de http://www.slideshare.net/hadley/plyr-one-data-analytic-strategy :

aplicar, sapply, lapply, por, agregado

(Con suerte, está claro que applycorresponde a @ Hadley aaplyy aggregatecorresponde a @ Hadley, ddplyetc. La diapositiva 20 de la misma diapositiva se aclarará si no la obtiene de esta imagen).

(a la izquierda se ingresa, en la parte superior se emite)


44
¿hay un error tipográfico en la diapositiva? La celda superior izquierda debería estar visible
JHowIX

100

Primero comience con la excelente respuesta de Joran : dudoso cualquier cosa puede mejorar eso.

Entonces, los siguientes mnemónicos pueden ayudar a recordar las distinciones entre cada uno. Si bien algunos son obvios, otros pueden ser menos: para estos, encontrará justificación en las discusiones de Joran.

Mnemotécnica

  • lapplyes una aplicación de lista que actúa en una lista o vector y devuelve una lista.
  • sapplyes simple lapply (la función predeterminada es devolver un vector o matriz cuando sea posible)
  • vapplyes una aplicación verificada (permite especificar previamente el tipo de objeto de retorno)
  • rapplyes una solicitud recursiva para listas anidadas, es decir, listas dentro de listas
  • tapplyes una aplicación etiquetada donde las etiquetas identifican los subconjuntos
  • apply es genérico : aplica una función a las filas o columnas de una matriz (o, más generalmente, a las dimensiones de una matriz)

Construyendo el fondo correcto

Si usar la applyfamilia todavía te resulta un poco extraño, entonces es posible que te estés perdiendo un punto de vista clave.

Estos dos artículos pueden ayudar. Proporcionan los antecedentes necesarios para motivar las técnicas de programación funcional que proporciona la applyfamilia de funciones.

Los usuarios de Lisp reconocerán el paradigma de inmediato. Si no está familiarizado con Lisp, una vez que comprenda FP, habrá obtenido un poderoso punto de vista para usar en R, y applytendrá mucho más sentido.


51

Desde que me di cuenta de que (los muy excelentes) respuestas de esta entrada falta de byy aggregateexplicaciones. Aquí está mi contribución.

POR

Sin byembargo, la función, como se indica en la documentación, puede ser un "contenedor" para tapply. El poder de bysurge cuando queremos calcular una tarea que tapplyno puede manejar. Un ejemplo es este código:

ct <- tapply(iris$Sepal.Width , iris$Species , summary )
cb <- by(iris$Sepal.Width , iris$Species , summary )

 cb
iris$Species: setosa
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  2.300   3.200   3.400   3.428   3.675   4.400 
-------------------------------------------------------------- 
iris$Species: versicolor
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  2.000   2.525   2.800   2.770   3.000   3.400 
-------------------------------------------------------------- 
iris$Species: virginica
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  2.200   2.800   3.000   2.974   3.175   3.800 


ct
$setosa
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  2.300   3.200   3.400   3.428   3.675   4.400 

$versicolor
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  2.000   2.525   2.800   2.770   3.000   3.400 

$virginica
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  2.200   2.800   3.000   2.974   3.175   3.800 

Si imprimimos estos dos objetos, cty cb"esencialmente" tenemos los mismos resultados y las únicas diferencias están en cómo se muestran y los diferentes classatributos, respectivamente bypara cby arraypara ct.

Como he dicho, el poder de bysurge cuando no podemos usarlo tapply; El siguiente código es un ejemplo:

 tapply(iris, iris$Species, summary )
Error in tapply(iris, iris$Species, summary) : 
  arguments must have same length

R dice que los argumentos deben tener las mismas longitudes, digamos "queremos calcular el summaryde todas las variables a lo irislargo del factor Species": pero R simplemente no puede hacer eso porque no sabe cómo manejarlo.

Con la byfunción R, despache un método específico para la data frameclase y luego deje que la summaryfunción funcione incluso si la longitud del primer argumento (y el tipo también) son diferentes.

bywork <- by(iris, iris$Species, summary )

bywork
iris$Species: setosa
  Sepal.Length    Sepal.Width     Petal.Length    Petal.Width          Species  
 Min.   :4.300   Min.   :2.300   Min.   :1.000   Min.   :0.100   setosa    :50  
 1st Qu.:4.800   1st Qu.:3.200   1st Qu.:1.400   1st Qu.:0.200   versicolor: 0  
 Median :5.000   Median :3.400   Median :1.500   Median :0.200   virginica : 0  
 Mean   :5.006   Mean   :3.428   Mean   :1.462   Mean   :0.246                  
 3rd Qu.:5.200   3rd Qu.:3.675   3rd Qu.:1.575   3rd Qu.:0.300                  
 Max.   :5.800   Max.   :4.400   Max.   :1.900   Max.   :0.600                  
-------------------------------------------------------------- 
iris$Species: versicolor
  Sepal.Length    Sepal.Width     Petal.Length   Petal.Width          Species  
 Min.   :4.900   Min.   :2.000   Min.   :3.00   Min.   :1.000   setosa    : 0  
 1st Qu.:5.600   1st Qu.:2.525   1st Qu.:4.00   1st Qu.:1.200   versicolor:50  
 Median :5.900   Median :2.800   Median :4.35   Median :1.300   virginica : 0  
 Mean   :5.936   Mean   :2.770   Mean   :4.26   Mean   :1.326                  
 3rd Qu.:6.300   3rd Qu.:3.000   3rd Qu.:4.60   3rd Qu.:1.500                  
 Max.   :7.000   Max.   :3.400   Max.   :5.10   Max.   :1.800                  
-------------------------------------------------------------- 
iris$Species: virginica
  Sepal.Length    Sepal.Width     Petal.Length    Petal.Width          Species  
 Min.   :4.900   Min.   :2.200   Min.   :4.500   Min.   :1.400   setosa    : 0  
 1st Qu.:6.225   1st Qu.:2.800   1st Qu.:5.100   1st Qu.:1.800   versicolor: 0  
 Median :6.500   Median :3.000   Median :5.550   Median :2.000   virginica :50  
 Mean   :6.588   Mean   :2.974   Mean   :5.552   Mean   :2.026                  
 3rd Qu.:6.900   3rd Qu.:3.175   3rd Qu.:5.875   3rd Qu.:2.300                  
 Max.   :7.900   Max.   :3.800   Max.   :6.900   Max.   :2.500     

funciona de hecho y el resultado es muy sorprendente. Es un objeto de clase byque a lo largo Species(por ejemplo, para cada uno de ellos) calcula el summaryde cada variable.

Tenga en cuenta que si el primer argumento es a data frame, la función despachada debe tener un método para esa clase de objetos. Por ejemplo, si usamos este código con la meanfunción, tendremos este código que no tiene ningún sentido:

 by(iris, iris$Species, mean)
iris$Species: setosa
[1] NA
------------------------------------------- 
iris$Species: versicolor
[1] NA
------------------------------------------- 
iris$Species: virginica
[1] NA
Warning messages:
1: In mean.default(data[x, , drop = FALSE], ...) :
  argument is not numeric or logical: returning NA
2: In mean.default(data[x, , drop = FALSE], ...) :
  argument is not numeric or logical: returning NA
3: In mean.default(data[x, , drop = FALSE], ...) :
  argument is not numeric or logical: returning NA

AGREGAR

aggregatepuede verse como otra forma diferente de uso tapplysi la usamos de esa manera.

at <- tapply(iris$Sepal.Length , iris$Species , mean)
ag <- aggregate(iris$Sepal.Length , list(iris$Species), mean)

 at
    setosa versicolor  virginica 
     5.006      5.936      6.588 
 ag
     Group.1     x
1     setosa 5.006
2 versicolor 5.936
3  virginica 6.588

Las dos diferencias inmediatas son que el segundo argumento de aggregate debe ser una lista mientras que tapply puede (no obligatorio) ser una lista y que la salida de aggregatees un marco de datos mientras que el de tapplyes un array.

El poder de esto aggregatees que puede manejar fácilmente subconjuntos de datos con subsetargumentos y que también tiene métodos para tsobjetos formula.

Estos elementos hacen que aggregatesea ​​más fácil trabajar con eso tapplyen algunas situaciones. Aquí hay algunos ejemplos (disponibles en la documentación):

ag <- aggregate(len ~ ., data = ToothGrowth, mean)

 ag
  supp dose   len
1   OJ  0.5 13.23
2   VC  0.5  7.98
3   OJ  1.0 22.70
4   VC  1.0 16.77
5   OJ  2.0 26.06
6   VC  2.0 26.14

Podemos lograr lo mismo con, tapplypero la sintaxis es un poco más difícil y la salida (en algunas circunstancias) menos legible:

att <- tapply(ToothGrowth$len, list(ToothGrowth$dose, ToothGrowth$supp), mean)

 att
       OJ    VC
0.5 13.23  7.98
1   22.70 16.77
2   26.06 26.14

Hay otros momentos en que no podemos usar byo tapplyy tenemos que usar aggregate.

 ag1 <- aggregate(cbind(Ozone, Temp) ~ Month, data = airquality, mean)

 ag1
  Month    Ozone     Temp
1     5 23.61538 66.73077
2     6 29.44444 78.22222
3     7 59.11538 83.88462
4     8 59.96154 83.96154
5     9 31.44828 76.89655

No podemos obtener el resultado anterior con tapplyuna sola llamada, pero tenemos que calcular la media Monthpara cada elemento y luego combinarlos (también tenga en cuenta que tenemos que llamar a na.rm = TRUE, porque los formulamétodos de la aggregatefunción tienen por defecto el na.action = na.omit):

ta1 <- tapply(airquality$Ozone, airquality$Month, mean, na.rm = TRUE)
ta2 <- tapply(airquality$Temp, airquality$Month, mean, na.rm = TRUE)

 cbind(ta1, ta2)
       ta1      ta2
5 23.61538 65.54839
6 29.44444 79.10000
7 59.11538 83.90323
8 59.96154 83.96774
9 31.44828 76.90000

mientras que con bysimplemente no podemos lograr eso, de hecho, la siguiente llamada de función devuelve un error (pero lo más probable es que esté relacionado con la función suministrada mean):

by(airquality[c("Ozone", "Temp")], airquality$Month, mean, na.rm = TRUE)

Otras veces, los resultados son los mismos y las diferencias son solo en el objeto de clase (y luego cómo se muestra / imprime y no solo - por ejemplo, cómo subconjunto):

byagg <- by(airquality[c("Ozone", "Temp")], airquality$Month, summary)
aggagg <- aggregate(cbind(Ozone, Temp) ~ Month, data = airquality, summary)

El código anterior logra el mismo objetivo y resultados, en algunos puntos qué herramienta usar es solo una cuestión de gustos y necesidades personales; Los dos objetos anteriores tienen necesidades muy diferentes en términos de subconjunto.


Como he dicho, el poder de by surge cuando no podemos usar tapply; el siguiente código es un ejemplo: ESTA SON LAS PALABRAS QUE HAS USADO ARRIBA. Y ha dado un ejemplo de cálculo del resumen. Bueno, digamos que las estadísticas de resumen solo se pueden calcular que necesitarán limpieza: por ejemplo, data.frame(tapply(unlist(iris[,-5]),list(rep(iris[,5],ncol(iris[-5])),col(iris[-5])),summary))este es un uso de tapply . With the right splitting there is nothing you cant do with tapply . The only thing is it returns a matrix. Please be careful by saying we cant use tapply`
Onyambu

35

Hay muchas respuestas excelentes que analizan las diferencias en los casos de uso para cada función. Ninguna de las respuestas discute las diferencias en el rendimiento. Eso es razonable porque varias funciones esperan diversas entradas y producen varias salidas, aunque la mayoría de ellas tienen un objetivo común general para evaluar por series / grupos. Mi respuesta se centrará en el rendimiento. Debido a lo anterior, la creación de entrada a partir de los vectores se incluye en el tiempo, tampoco applyse mide la función.

He probado dos funciones diferentes sumy lengtha la vez. El volumen probado es 50M en entrada y 50K en salida. También he incluido dos paquetes actualmente populares que no se usaban ampliamente en el momento en que se hicieron las preguntas, data.tabley dplyr. Definitivamente vale la pena mirar ambos si buscas un buen rendimiento.

library(dplyr)
library(data.table)
set.seed(123)
n = 5e7
k = 5e5
x = runif(n)
grp = sample(k, n, TRUE)

timing = list()

# sapply
timing[["sapply"]] = system.time({
    lt = split(x, grp)
    r.sapply = sapply(lt, function(x) list(sum(x), length(x)), simplify = FALSE)
})

# lapply
timing[["lapply"]] = system.time({
    lt = split(x, grp)
    r.lapply = lapply(lt, function(x) list(sum(x), length(x)))
})

# tapply
timing[["tapply"]] = system.time(
    r.tapply <- tapply(x, list(grp), function(x) list(sum(x), length(x)))
)

# by
timing[["by"]] = system.time(
    r.by <- by(x, list(grp), function(x) list(sum(x), length(x)), simplify = FALSE)
)

# aggregate
timing[["aggregate"]] = system.time(
    r.aggregate <- aggregate(x, list(grp), function(x) list(sum(x), length(x)), simplify = FALSE)
)

# dplyr
timing[["dplyr"]] = system.time({
    df = data_frame(x, grp)
    r.dplyr = summarise(group_by(df, grp), sum(x), n())
})

# data.table
timing[["data.table"]] = system.time({
    dt = setnames(setDT(list(x, grp)), c("x","grp"))
    r.data.table = dt[, .(sum(x), .N), grp]
})

# all output size match to group count
sapply(list(sapply=r.sapply, lapply=r.lapply, tapply=r.tapply, by=r.by, aggregate=r.aggregate, dplyr=r.dplyr, data.table=r.data.table), 
       function(x) (if(is.data.frame(x)) nrow else length)(x)==k)
#    sapply     lapply     tapply         by  aggregate      dplyr data.table 
#      TRUE       TRUE       TRUE       TRUE       TRUE       TRUE       TRUE 

# print timings
as.data.table(sapply(timing, `[[`, "elapsed"), keep.rownames = TRUE
              )[,.(fun = V1, elapsed = V2)
                ][order(-elapsed)]
#          fun elapsed
#1:  aggregate 109.139
#2:         by  25.738
#3:      dplyr  18.978
#4:     tapply  17.006
#5:     lapply  11.524
#6:     sapply  11.326
#7: data.table   2.686

¿Es normal que dplyr sea inferior a las funciones de applt?
Mostafa

1
@DimitriPetrenko No lo creo, no estoy seguro de por qué está aquí. Es mejor probar con sus propios datos, ya que hay muchos factores que entran en juego.
jangorecki

28

A pesar de todas las excelentes respuestas aquí, hay 2 funciones básicas más que merecen ser mencionadas, la outerfunción útil y la eapplyfunción oscura

exterior

outerEs una función muy útil oculta como una más mundana. Si lees la ayuda para outersu descripción dice:

The outer product of the arrays X and Y is the array A with dimension  
c(dim(X), dim(Y)) where element A[c(arrayindex.x, arrayindex.y)] =   
FUN(X[arrayindex.x], Y[arrayindex.y], ...).

lo que hace que parezca que esto solo es útil para cosas de tipo álgebra lineal. Sin embargo, se puede usar de manera muy parecida mapplya la aplicación de una función a dos vectores de entradas. La diferencia es que mapplyaplicará la función a los dos primeros elementos y luego a los segundos dos, etc., mientras outerque aplicará la función a cada combinación de un elemento del primer vector y uno del segundo. Por ejemplo:

 A<-c(1,3,5,7,9)
 B<-c(0,3,6,9,12)

mapply(FUN=pmax, A, B)

> mapply(FUN=pmax, A, B)
[1]  1  3  6  9 12

outer(A,B, pmax)

 > outer(A,B, pmax)
      [,1] [,2] [,3] [,4] [,5]
 [1,]    1    3    6    9   12
 [2,]    3    3    6    9   12
 [3,]    5    5    6    9   12
 [4,]    7    7    7    9   12
 [5,]    9    9    9    9   12

Personalmente, he usado esto cuando tengo un vector de valores y un vector de condiciones y deseo ver qué valores cumplen con qué condiciones.

aplicar

eapplyes como, lapplyexcepto que, en lugar de aplicar una función a cada elemento de una lista, aplica una función a cada elemento en un entorno. Por ejemplo, si desea encontrar una lista de funciones definidas por el usuario en el entorno global:

A<-c(1,3,5,7,9)
B<-c(0,3,6,9,12)
C<-list(x=1, y=2)
D<-function(x){x+1}

> eapply(.GlobalEnv, is.function)
$A
[1] FALSE

$B
[1] FALSE

$C
[1] FALSE

$D
[1] TRUE 

Francamente, no uso mucho esto, pero si está creando muchos paquetes o creando muchos entornos, puede ser útil.


25

Quizás valga la pena mencionarlo ave. aveEs tapplyel primo amigable. Devuelve los resultados en un formulario que puede volver a conectar directamente a su marco de datos.

dfr <- data.frame(a=1:20, f=rep(LETTERS[1:5], each=4))
means <- tapply(dfr$a, dfr$f, mean)
##  A    B    C    D    E 
## 2.5  6.5 10.5 14.5 18.5 

## great, but putting it back in the data frame is another line:

dfr$m <- means[dfr$f]

dfr$m2 <- ave(dfr$a, dfr$f, FUN=mean) # NB argument name FUN is needed!
dfr
##   a f    m   m2
##   1 A  2.5  2.5
##   2 A  2.5  2.5
##   3 A  2.5  2.5
##   4 A  2.5  2.5
##   5 B  6.5  6.5
##   6 B  6.5  6.5
##   7 B  6.5  6.5
##   ...

No hay nada en el paquete base que funcione como avepara marcos de datos completos (como byes tapplypara marcos de datos). Pero puedes evitarlo:

dfr$foo <- ave(1:nrow(dfr), dfr$f, FUN=function(x) {
    x <- dfr[x,]
    sum(x$m*x$m2)
})
dfr
##     a f    m   m2    foo
## 1   1 A  2.5  2.5    25
## 2   2 A  2.5  2.5    25
## 3   3 A  2.5  2.5    25
## ...

12

Recientemente descubrí la sweepfunción bastante útil y la agregué aquí por completo:

barrer

La idea básica es barrer a través de una matriz en fila o columna y devolver una matriz modificada. Un ejemplo aclarará esto (fuente: campo de datos ):

Digamos que tiene una matriz y desea estandarizarla en columnas:

dataPoints <- matrix(4:15, nrow = 4)

# Find means per column with `apply()`
dataPoints_means <- apply(dataPoints, 2, mean)

# Find standard deviation with `apply()`
dataPoints_sdev <- apply(dataPoints, 2, sd)

# Center the points 
dataPoints_Trans1 <- sweep(dataPoints, 2, dataPoints_means,"-")
print(dataPoints_Trans1)
##      [,1] [,2] [,3]
## [1,] -1.5 -1.5 -1.5
## [2,] -0.5 -0.5 -0.5
## [3,]  0.5  0.5  0.5
## [4,]  1.5  1.5  1.5
# Return the result
dataPoints_Trans1
##      [,1] [,2] [,3]
## [1,] -1.5 -1.5 -1.5
## [2,] -0.5 -0.5 -0.5
## [3,]  0.5  0.5  0.5
## [4,]  1.5  1.5  1.5
# Normalize
dataPoints_Trans2 <- sweep(dataPoints_Trans1, 2, dataPoints_sdev, "/")

# Return the result
dataPoints_Trans2
##            [,1]       [,2]       [,3]
## [1,] -1.1618950 -1.1618950 -1.1618950
## [2,] -0.3872983 -0.3872983 -0.3872983
## [3,]  0.3872983  0.3872983  0.3872983
## [4,]  1.1618950  1.1618950  1.1618950

NB: para este ejemplo simple, por supuesto, se puede lograr el mismo resultado más fácilmente
apply(dataPoints, 2, scale)


1
¿Está esto relacionado con la agrupación?
Frank

2
@Frank: Bueno, para ser honesto, el título de esta publicación es bastante engañoso: cuando lees la pregunta en sí, se trata de "la familia que se postula". sweepes una función de orden superior como todas las demás mencionadas aquí, p. ej apply. sapply, por lapplylo que se podría hacer la misma pregunta sobre la respuesta aceptada con más de 1,000 votos a favor y los ejemplos dados en ella. Solo eche un vistazo al ejemplo dado applyallí.
vonjd

2
sweep tiene un nombre engañoso, valores predeterminados engañosos y un nombre de parámetro engañoso :). Es más fácil para mí entenderlo de esta manera: 1) STATS es un valor vectorial o único que se repetirá para formar una matriz del mismo tamaño que la primera entrada, 2) FUN se aplicará en la primera entrada y esta nueva matriz. Tal vez mejor ilustrado por: sweep(matrix(1:6,nrow=2),2,7:9,list). Por lo general, es más eficiente que applyporque where applybucles sweeppuede utilizar funciones vectorizadas.
Moody_Mudskipper

2

En el paquete de colapso lanzado recientemente en CRAN, he intentado comprimir la mayor parte de la funcionalidad de aplicación común en solo 2 funciones:

  1. dapply(Aplicar datos) aplica funciones a filas o columnas (por defecto) de matrices y datos. Marcos y (por defecto) devuelve un objeto del mismo tipo y con los mismos atributos (a menos que el resultado de cada cálculo sea atómico y drop = TRUE). El rendimiento es comparable al lapplyde las columnas data.frame y aproximadamente 2 veces más rápido que applypara las filas o columnas de matriz. El paralelismo está disponible a través de mclapply(solo para MAC).

Sintaxis:

dapply(X, FUN, ..., MARGIN = 2, parallel = FALSE, mc.cores = 1L, 
       return = c("same", "matrix", "data.frame"), drop = TRUE)

Ejemplos:

# Apply to columns:
dapply(mtcars, log)
dapply(mtcars, sum)
dapply(mtcars, quantile)
# Apply to rows:
dapply(mtcars, sum, MARGIN = 1)
dapply(mtcars, quantile, MARGIN = 1)
# Return as matrix:
dapply(mtcars, quantile, return = "matrix")
dapply(mtcars, quantile, MARGIN = 1, return = "matrix")
# Same for matrices ...
  1. BYes un genérico S3 para computación dividida-aplicada-combinada con vector, matriz y método data.frame. Es significativamente más rápido que tapply, byy aggregate( aunque también es más rápido que plyr, en datos grandes dplyres más rápido).

Sintaxis:

BY(X, g, FUN, ..., use.g.names = TRUE, sort = TRUE,
   expand.wide = FALSE, parallel = FALSE, mc.cores = 1L,
   return = c("same", "matrix", "data.frame", "list"))

Ejemplos:

# Vectors:
BY(iris$Sepal.Length, iris$Species, sum)
BY(iris$Sepal.Length, iris$Species, quantile)
BY(iris$Sepal.Length, iris$Species, quantile, expand.wide = TRUE) # This returns a matrix 
# Data.frames
BY(iris[-5], iris$Species, sum)
BY(iris[-5], iris$Species, quantile)
BY(iris[-5], iris$Species, quantile, expand.wide = TRUE) # This returns a wider data.frame
BY(iris[-5], iris$Species, quantile, return = "matrix") # This returns a matrix
# Same for matrices ...

También se pueden suministrar listas de variables de agrupación g.

Hablando sobre el rendimiento: un objetivo principal del colapso es fomentar la programación de alto rendimiento en R y avanzar más allá de dividir, aplicar y combinar por completo. Para ello, el paquete tiene un conjunto completo de funciones de C ++ basadas rápida genéricos: fmean, fmedian, fmode, fsum, fprod, fsd, fvar, fmin, fmax, ffirst, flast, fNobs, fNdistinct, fscale, fbetween, fwithin, fHDbetween, fHDwithin, flag, fdiffy fgrowth. Realizan cálculos agrupados en una sola pasada a través de los datos (es decir, sin división y recombinación).

Sintaxis:

fFUN(x, g = NULL, [w = NULL,] TRA = NULL, [na.rm = TRUE,] use.g.names = TRUE, drop = TRUE)

Ejemplos:

v <- iris$Sepal.Length
f <- iris$Species

# Vectors
fmean(v)             # mean
fmean(v, f)          # grouped mean
fsd(v, f)            # grouped standard deviation
fsd(v, f, TRA = "/") # grouped scaling
fscale(v, f)         # grouped standardizing (scaling and centering)
fwithin(v, f)        # grouped demeaning

w <- abs(rnorm(nrow(iris)))
fmean(v, w = w)      # Weighted mean
fmean(v, f, w)       # Weighted grouped mean
fsd(v, f, w)         # Weighted grouped standard-deviation
fsd(v, f, w, "/")    # Weighted grouped scaling
fscale(v, f, w)      # Weighted grouped standardizing
fwithin(v, f, w)     # Weighted grouped demeaning

# Same using data.frames...
fmean(iris[-5], f)                # grouped mean
fscale(iris[-5], f)               # grouped standardizing
fwithin(iris[-5], f)              # grouped demeaning

# Same with matrices ...

En las viñetas del paquete proporciono puntos de referencia. La programación con las funciones rápidas es significativamente más rápida que la programación con dplyr o data.table , especialmente en datos más pequeños, pero también en datos grandes.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.