domingo, 14 de febrero de 2010

Aplicar una función según el grupo

Para aplicar una función a unas columnas de datos, separadamente según las clases o grupos que queramos, en R disponemos de dos funciones: tapply() y aggregate().
La función tapply() se aplica sobre un único vector de datos, cuyos valores se agrupan en función de una o unas variables o factores. Con los datos mtcars, por ejemplo, para saber el máximo valor de mpg según el número de cilindros hacemos

> data(mtcars)
> attach(mtcars)
> tapply(mpg, cyl, max)

4 6 8
33.9 21.4 19.2


En este caso el resultado es un vector, aunque también puede ser una lista si el resultado de la función aplicada no es un escalar:

> tapply(mpg,cyl,range)
$'4'
[1] 21.4 33.9

$'6'
[1] 17.8 21.4

$'8'
[1] 10.4 19.2


Acceder a estos resultados significa utilizar las llamadas a elementos de una lista:

> rangos <- tapply(mpg, cyl, range)
> rangos[[1]]
[1] 21.4 33.9

> rangos$'4'
[1] 21.4 33.9

> rangos[['4']]
[1] 21.4 33.9


> rangos[["4"]]
[1] 21.4 33.9

Cuando se utiliza más de una variable de agrupación y el resultado de la función a aplicar no es un escalar, el valor que retorna tapply() es más difícil de gestionar.

> rangos2 <- tapply(mpg, mtcars[c("cyl","am")], range)

> rangos2
am
cyl 0 1
4 Numeric,2 Numeric,2
6 Numeric,2 Numeric,2
8 Numeric,2 Numeric,2

> rangos2["4","0"]
[[1]]
[1] 21.5 24.4


Al llamar la función tapply() sin el argumento función resulta un índice de agrupación, según los grupos determinados, que puede ser útil en algunos casos:

> idx <- tapply(mpg,mtcars[c("cyl","am")]) > idx

[1] 5 5 4 2 3 2 3 1 1 2 2 3 3 3 3 3 3 4 4 4 1 3 3 3 3 4 4 4 6 5 6 4


Por otra parte, si lo que se pretende es resumir una o más columnas de un data.frame o matriz mediante un estadístico o función escalar, entonces utilizaremos la función aggregate(), donde el segundo argumento debe ser una lista.

> aggregate(mtcars[c("mpg","hp","wt")], mtcars$cyl, mean)
Error in aggregate.data.frame(mtcars[c("mpg", "hp", "wt")], mtcars$cyl, :
'by' must be a list
Calls: aggregate -> aggregate.data.frame

> aggregate(mtcars[c("mpg","hp","wt")], mtcars["cyl"], mean)
cyl mpg hp wt
1 4 26.66364 82.63636 2.285727
2 6 19.74286 122.28571 3.117143
3 8 15.10000 209.21429 3.999214


> aggregate(mtcars[c("mpg","hp","wt")], mtcars[c("cyl","am")], mean)
cyl am mpg hp wt
1 4 0 22.90000 84.66667 2.935000
2 6 0 19.12500 115.25000 3.388750
3 8 0 15.05000 194.16667 4.104083
4 4 1 28.07500 81.87500 2.042250
5 6 1 20.56667 131.66667 2.755000
6 8 1 15.40000 299.50000 3.370000

Finalmente, cuando el problema es mas complejo y se trata de aplicar una función no escalar a más de un vector según una determinada agrupación, deberemos combinar dos procedimientos como split() para agrupar y sapply() o lapply() para aplicar.

> xx <- split(mtcars[c("mpg","hp","wt")], mtcars["cyl"])


> aggregate(xx$'4', list(cyl=rep(4, dim(xx$'4')[1])), min)


cyl mpg hp wt


1 4 21.4 52 1.513



> sapply(xx, cor)


4 6 8


[1,] 1.0000000 1.0000000 1.00000000


[2,] -0.5235034 -0.1270678 -0.28363567


[3,] -0.7131848 -0.6815498 -0.65035801


[4,] -0.5235034 -0.1270678 -0.28363567


[5,] 1.0000000 1.0000000 1.00000000


[6,] 0.1598761 -0.3062284 0.01761795


[7,] -0.7131848 -0.6815498 -0.65035801


[8,] 0.1598761 -0.3062284 0.01761795


[9,] 1.0000000 1.0000000 1.00000000


Otra opción es utilizar la función by() que generaliza la función tapply().

> by(mtcars[c("mpg","hp","wt")],mtcars["cyl"],cor)
cyl: 4
mpg hp wt
mpg 1.0000000 -0.5235034 -0.7131848
hp -0.5235034 1.0000000 0.1598761
wt -0.7131848 0.1598761 1.0000000
------------------------------------------------------------
cyl: 6
mpg hp wt
mpg 1.0000000 -0.1270678 -0.6815498
hp -0.1270678 1.0000000 -0.3062284
wt -0.6815498 -0.3062284 1.0000000
------------------------------------------------------------
cyl: 8
mpg hp wt
mpg 1.0000000 -0.28363567 -0.65035801
hp -0.2836357 1.00000000 0.01761795
wt -0.6503580 0.01761795 1.00000000