Presentación: el problema de resumir por grupos.

Mientras realizamos la exploración de un conjunto de datos suele ser frecuente que queramos calcular medidas descriptivas (medias, desviaciones típicas, etc) por grupos. Por ejemplo, en nuestro estudio de los hábitos de nidificación de las tortugas de Cabo Verde, tras leer los datos:

library(readxl)
tortugas <- data.frame(read_excel("datos/tortugas.xlsx"))

podemos comprobar que nuestra base de datos contiene las siguientes variables:

kable(head(tortugas)) %>% 
  kable_styling(full_width = FALSE)
Año periodo LCC ACC peso Huevos playa distancia profNido crias_Emerg crias_Muertas hrotos cangrejos
1999 1 77.4 71.6 55.7 81 Ervatao 28.0 61.3 65 3 13 0
1999 1 82.0 76.3 54.4 99 Ervatao 35.0 59.5 30 6 63 1
1999 1 87.3 85.5 66.5 127 Ervatao 28.8 60.2 NA NA NA NA
1999 1 85.4 79.7 67.4 107 Ervatao 29.3 60.6 9 2 96 1
1999 1 79.2 74.4 56.2 101 Ervatao 41.2 69.5 0 1 100 1
1999 1 80.7 74.5 64.1 77 Ervatao 42.8 74.3 NA NA NA NA

 

Supongamos que quisiéramos calcular la profundidad media de los nidos según la playa en que se encuentren, o el total de huevos por playa. Podemos llevar a cabo este cálculo de diversas maneras. En este pequeño tutorial explicaremos como hacerlo de dos formas:

  1. A través de la función aggregate (función del paquete base de R).

  2. A través de las funciones de la librería dplyr.

 

Debemos señalar, en cualquier caso, que las funciones de dplyr resultan mucho más cómodas y eficientes para realizar estas tareas y constituyen la opción actualmente más recomendable. Quien lo desee puede saltarse la sección del aggregate e ir directamente a la sección del dplyr

 

 

Resumenes por grupos mediante la función aggregate.

 

Para usar esta función indicamos en primer lugar la variable que queremos resumir (profNido), seguida del símbolo “~”, y de la variable de agrupación (playa). Con ello indicamos que queremos resumir la primera variable para cada valor de la segunda; a continuación especificamos el nombre del data.frame que contiene los datos (en este caso, tortugas), la función a aplicar (en este caso la media mean), y las opciones que queramos añadir a esta función (utilizaremos na.rm=TRUE para evitar que los valores perdidos interfieran con el cálculo):

medias <- aggregate(profNido~playa, tortugas, mean, na.rm=TRUE)

El resultado (medias) es un data.frame cuyo contenido podemos mostrar de manera “elegante” mediante:

kable(medias) %>%
  kable_styling(full_width = FALSE)
playa profNido
Calheta 36.89082
Ervatao 58.88425
Ponta Cosme 45.11165
Porto Ferreiro 42.73293

Para calcular el total de huevos en cada playa, usamos la función sum dentro de aggregate:

totalHuevos <- aggregate(Huevos~playa, tortugas, sum, na.rm=TRUE)
kable(totalHuevos) %>%
  kable_styling(full_width = FALSE)
playa Huevos
Calheta 16162
Ervatao 35179
Ponta Cosme 41236
Porto Ferreiro 13418

La función aggregate permite agrupar por más de una variable. Si, por ejemplo, quisiéramos calcular la profundidad media de los nidos por playa según periodos de muestreo, utilizaríamos la siguiente sintaxis:

medias <- aggregate(profNido~playa + periodo, tortugas, mean, na.rm=TRUE)
kable(medias) %>%
  kable_styling(full_width = FALSE)
playa periodo profNido
Calheta 1 36.48333
Ervatao 1 60.22667
Ponta Cosme 1 45.40989
Porto Ferreiro 1 42.55000
Calheta 2 36.76250
Ervatao 2 58.35213
Ponta Cosme 2 45.41650
Porto Ferreiro 2 41.70857
Calheta 3 36.57750
Ervatao 3 58.42609
Ponta Cosme 3 45.85862
Porto Ferreiro 3 43.82750
Calheta 4 37.32381
Ervatao 4 58.39062
Ponta Cosme 4 45.44299
Porto Ferreiro 4 43.48966
Calheta 5 37.15682
Ervatao 5 59.21765
Ponta Cosme 5 43.66636
Porto Ferreiro 5 41.99412

 

Si queremos aplicar varias funciones en cada grupo de datos, debemos construir primero una función que “contenga” todas las funciones que queremos aplicar. Por ejemplo, si para cada playa queremos calcular el valor medio y la desviación típica de la profundidad de los nidos, construimos primero una función que calcule estas dos cantidades:

resumen <- function(x) c(media=mean(x,na.rm=TRUE),sd=sd(x,na.rm=TRUE))

y a continuación la utilizamos como argumento de aggregate:

profPlaya <- aggregate(profNido~playa,tortugas,resumen)
profPlaya
##            playa profNido.media profNido.sd
## 1        Calheta      36.890816    5.723314
## 2        Ervatao      58.884248    5.825956
## 3    Ponta Cosme      45.111647    7.064254
## 4 Porto Ferreiro      42.732927    6.394189

Merece la pena observar la estructura del objeto que hemos construido:

str(profPlaya)
## 'data.frame':    4 obs. of  2 variables:
##  $ playa   : chr  "Calheta" "Ervatao" "Ponta Cosme" "Porto Ferreiro"
##  $ profNido: num [1:4, 1:2] 36.89 58.88 45.11 42.73 5.72 ...
##   ..- attr(*, "dimnames")=List of 2
##   .. ..$ : NULL
##   .. ..$ : chr  "media" "sd"

Como vemos, es un data.frame que contiene 2 variables a pesar de que parece que contiene 3; en realidad los valores de media y sd forman en realidad una matriz, de nombre profNido. Esto impide que podamos utilizar kable, ya que esta función espera un data.frame “puro”. Si usamos kable directamente se produce un error:

kable(profPlaya)
## Error in dimnames(x) <- dn: la longitud de 'dimnames' [2] no es igual a la extensión del arreglo

Para evitar este comportamiento debemos convertir profPlaya en un verdadero data.fame. Ello se consigue mediante:

profPlaya=data.frame(playa=profPlaya[,1], profPlaya[,-1])

Como vemos ahora sí que tenemos un data.frame con tres columnas:

str(profPlaya)
## 'data.frame':    4 obs. of  3 variables:
##  $ playa: Factor w/ 4 levels "Calheta","Ervatao",..: 1 2 3 4
##  $ media: num  36.9 58.9 45.1 42.7
##  $ sd   : num  5.72 5.83 7.06 6.39

Ahora podemos usar kable sin problemas para presentar la tabla:

kable(profPlaya) %>%
  kable_styling(full_width = FALSE)
playa media sd
Calheta 36.89082 5.723314
Ervatao 58.88425 5.825956
Ponta Cosme 45.11165 7.064254
Porto Ferreiro 42.73293 6.394189

Obviamente todo esto parece complicadísimo ¿Por qué la salida de aggregate en unos casos se puede pasar directamente a kable y en otros casos no? ¿Por qué aggregate genera una estructura tan “extraña” con dos elementos, uno de los cuales es una columna con los nombres de las playas y otro es una matriz con dos variables? En realidad la explicación de este comportamiento procede simplemente de que R es un compendio de librerías y funciones construidas por muchos autores que en su diseño no pensaron que la salida de su función pudiera ser usada como entrada de otra función, dando lugar a esta suerte de incompatibilidad que requiere un esfuerzo adicional de ajuste por parte del usuario.

 

 

Resúmenes por grupos utilizando el paquete dplyr

 

El paquete dplyr integra numerosas funciones para resumir y describir datos, que son totalmente compatibles unas con otras y que presentan salidas más refinadas y generales que aggregate.

En primer lugar cargamos esta librería:

library(dplyr)

Para obtener resúmenes por grupo utilizaremos la función group_by(); y para especificar qué función de resumen queremos emplear utilizamos summarize(). Así, para obtener la profundidad media de los nidos por playa, la sintaxis a emplear es:

tortugas %>% 
  group_by(playa) %>% 
  summarize(media=mean(profNido,na.rm=TRUE)) %>% 
  kable %>% 
  kable_styling(full_width = FALSE)
playa media
Calheta 36.89082
Ervatao 58.88425
Ponta Cosme 45.11165
Porto Ferreiro 42.73293

Aquí el operador tubería ( %>% ) hace que la salida de cada comando sea directamente la entrada del siguiente; por ello en kable ni siquiera hemos especificado qué objeto queremos mostrar; se entiende que es el que viene directamente de la tubería anterior.

Si queremos especificar varias funciones no es necesario definir previamente una única función combinada; basta con especificar las funciones a aplicar dentro del summarize:

tortugas %>% 
  group_by(playa) %>% 
  summarize(media=mean(profNido,na.rm=TRUE), 
            sd=sd(profNido,na.rm=TRUE),
            mediana=median(profNido,na.rm=TRUE)) %>% 
  kable %>% 
  kable_styling(full_width = FALSE)
playa media sd mediana
Calheta 36.89082 5.723314 36.95
Ervatao 58.88425 5.825956 58.90
Ponta Cosme 45.11165 7.064254 45.00
Porto Ferreiro 42.73293 6.394189 42.45

Para obtener el número total y el número medio de huevos por nido en playa:

tortugas %>% 
  group_by(playa) %>% 
  summarize(total=sum(Huevos,na.rm=TRUE),
            numero_Medio=mean(Huevos,na.rm=TRUE)) %>% 
  kable %>% 
  kable_styling(full_width = FALSE)
playa total numero_Medio
Calheta 16162 82.45918
Ervatao 35179 83.95943
Ponta Cosme 41236 82.80321
Porto Ferreiro 13418 81.81707

 

 

Con dplyr es fácil también obtener resúmenes agrupando por varias variables; por ejemplo, para describir la profundidad de los nidos durante cada periodo dentro de cada playa:

tortugas %>% 
  group_by(periodo,playa) %>%
  summarize(media=mean(profNido,na.rm=TRUE), 
            sd=sd(profNido,na.rm=TRUE),
            mediana=median(profNido,na.rm=TRUE)) %>% 
  kable %>% 
  kable_styling(full_width = FALSE)
periodo playa media sd mediana
1 Calheta 36.48333 4.934962 37.10
1 Ervatao 60.22667 5.514682 60.10
1 Ponta Cosme 45.40989 7.507094 45.50
1 Porto Ferreiro 42.55000 6.119886 42.05
2 Calheta 36.76250 6.955156 36.00
2 Ervatao 58.35213 5.649717 58.25
2 Ponta Cosme 45.41650 6.970373 45.10
2 Porto Ferreiro 41.70857 7.705627 41.20
3 Calheta 36.57750 5.424894 36.80
3 Ervatao 58.42609 5.437010 58.00
3 Ponta Cosme 45.85862 5.788351 45.50
3 Porto Ferreiro 43.82750 6.190149 44.15
4 Calheta 37.32381 5.376997 37.25
4 Ervatao 58.39062 6.195076 58.90
4 Ponta Cosme 45.44299 7.622337 44.80
4 Porto Ferreiro 43.48966 5.288353 42.90
5 Calheta 37.15682 5.788778 37.00
5 Ervatao 59.21765 6.078302 58.90
5 Ponta Cosme 43.66636 7.055810 44.00
5 Porto Ferreiro 41.99412 6.303147 42.55

 

 

Otra posibilidad que ofrece dplyr es resumir simultáneamente varias variables por grupos. Así, por ejemplo, si queremos calcular los valores medios de LCC, ACC y peso por playa, utilizamos la función summarize_at():

tortugas %>% 
  group_by(playa) %>%
  summarize_at(c("LCC","ACC","peso"), mean, na.rm=TRUE) %>% 
  kable %>% 
  kable_styling(full_width = FALSE)
playa LCC ACC peso
Calheta 81.91480 77.22347 60.05561
Ervatao 81.66563 76.85513 60.03866
Ponta Cosme 82.22731 77.46747 60.86546
Porto Ferreiro 81.60061 77.13659 60.63659

 

Si para cada variable en cada grupo se desea evaluar varias funciones, éstas deben especificarse como una lista, donde resulta conveniente asignar un nombre distinto al resultado de cada función (en caso contrario, la salida puede resultar confusa):

tortugas %>% 
  group_by(playa) %>%
  summarize_at(c("LCC","ACC","peso"), list(media=mean,sd=sd), na.rm=TRUE) %>% 
  kable %>% 
  kable_styling(full_width = FALSE)
playa LCC_media ACC_media peso_media LCC_sd ACC_sd peso_sd
Calheta 81.91480 77.22347 60.05561 4.947751 3.973639 5.301690
Ervatao 81.66563 76.85513 60.03866 4.458465 3.970850 5.335256
Ponta Cosme 82.22731 77.46747 60.86546 4.698330 4.031958 5.161669
Porto Ferreiro 81.60061 77.13659 60.63659 4.294675 3.902524 5.061039

 

 

 

Gráficos de los resúmenes por grupo

Si hemos utilizado dplyr es muy fácil emplear ggplot para representar gráficamente los valores obtenidos. Si con el resumen por grupos queremos hacer tanto tablas como gráficos es conveniente guardar previamente en un objeto el resultado. Así, por ejemplo, si queremos mostrar el número total de huevos por año realizamos primero el cálculo y lo asignamos al objeto huevosAño:

huevosAño <- tortugas %>% 
  group_by(Año) %>%
  summarize(total=sum(Huevos,na.rm=TRUE))

que en forma de tabla puede presentarse como:

kable(huevosAño) %>% 
  kable_styling(full_width = FALSE)
Año total
1999 18375
2000 19462
2001 14512
2002 17326
2003 17151
2004 19169

y gráficamente: (téngase en cuenta que la variable “Año” se ha leido como carácter, por lo que la pasamos a numérica mediante as.numeric())

library(ggplot2)
ggplot(huevosAño, aes(x=as.numeric(Año), y=total)) + geom_point() + geom_line(linetype=2,color="red") + 
  xlab("Año") + ylab("Número total de huevos")

 

Si sólo queremos el gráfico sin mostrar una tabla, puede generarse directamente el gráfico mediante una tubería ( %>% ) directamente desde los datos:

tortugas %>% 
  group_by(Año) %>%
  summarize(total=sum(Huevos,na.rm=TRUE)) %>% 
  ggplot(aes(x=as.numeric(Año), y=total)) + geom_point() + geom_line(linetype=2,color="red") + 
  xlab("Año") + ylab("Número total de huevos")

 

Si realizamos la agrupación según varias variables (año y playa, por ejemplo), tampoco entraña mucha dificultad trazar el gráfico:

tortugas %>% 
  group_by(Año,playa) %>%
  summarize(total=sum(Huevos,na.rm=TRUE)) %>% 
  ggplot(aes(x=as.numeric(Año), y=total, color=playa)) + geom_point() + geom_line() +
  xlab("Año") + ylab("Número total de huevos")

 

 

 

 





© 2016 Angelo Santana, Carmen N. Hernández, Departamento de Matemáticas   ULPGC