Agrupar datos en R con la función group_by() de dplyr
La función group_by de dplyr divide un data frame en grupos según los valores de una o varias columnas, de modo que los siguientes verbos (summarise, mutate, filter, slice o arrange) se aplican a cada grupo por separado. En este tutorial aprenderás cómo funciona realmente la agrupación, qué significa el mensaje "has grouped output by", por qué tienes que desagrupar los datos con ungroup y cuándo te conviene usar el argumento .by.
Datos de ejemplo
En este tutorial utilizaremos un subconjunto del conjunto de datos starwars de dplyr: los personajes de tres especies (humanos, droides y gungans) cuyo peso es conocido. El data frame resultante contiene 26 filas y cinco columnas. El código utiliza el operador pipe %>% para encadenar los pasos.
library(dplyr)
df <- starwars %>%
select(name, species, sex, height, mass) %>%
filter(species %in% c("Human", "Droid", "Gungan"), !is.na(mass))
df
# A tibble: 26 × 5
name species sex height mass
<chr> <chr> <chr> <int> <dbl>
1 Luke Skywalker Human male 172 77
2 C-3PO Droid none 167 75
3 R2-D2 Droid none 96 32
4 Darth Vader Human male 202 136
5 Leia Organa Human female 150 49
6 Owen Lars Human male 178 120
7 Beru Whitesun Lars Human female 165 75
8 R5-D4 Droid none 97 32
9 Biggs Darklighter Human male 183 84
10 Obi-Wan Kenobi Human male 182 77
# ℹ 16 more rows
¿Qué hace group_by?
Algo que sorprende a mucha gente es que group_by no modifica los datos. Si imprimes un data frame agrupado verás exactamente las mismas filas en el mismo orden. La única diferencia es una nueva línea en la cabecera, # Groups: species [3], que indica la variable de agrupación y el número de grupos.
df_g <- df %>%
group_by(species)
df_g
# A tibble: 26 × 5
# Groups: species [3]
name species sex height mass
<chr> <chr> <chr> <int> <dbl>
1 Luke Skywalker Human male 172 77
2 C-3PO Droid none 167 75
3 R2-D2 Droid none 96 32
4 Darth Vader Human male 202 136
5 Leia Organa Human female 150 49
6 Owen Lars Human male 178 120
7 Beru Whitesun Lars Human female 165 75
8 R5-D4 Droid none 97 32
9 Biggs Darklighter Human male 183 84
10 Obi-Wan Kenobi Human male 182 77
# ℹ 16 more rows
Internamente, el data frame pasa a ser un objeto de clase grouped_df que guarda qué filas pertenecen a cada grupo. No ocurre nada hasta que un verbo utiliza esa información. Puedes consultar los grupos con n_groups, group_vars y group_keys:
class(df_g)
n_groups(df_g)
group_vars(df_g)
group_keys(df_g)
[1] "grouped_df" "tbl_df" "tbl" "data.frame"
[1] 3
[1] "species"
# A tibble: 3 × 1
species
<chr>
1 Droid
2 Gungan
3 Human
La siguiente tabla resume cómo se comporta cada verbo de dplyr cuando los datos están agrupados:
| Verbo | Sin grupos | Con group_by |
|---|---|---|
summarise() |
Una fila para todos los datos | Una fila por grupo |
mutate() |
Los cálculos usan toda la columna | Los cálculos usan las filas de cada grupo |
filter() |
Las condiciones usan toda la columna | Las condiciones se evalúan dentro de cada grupo |
slice_*() |
Selecciona filas del total de datos | Selecciona filas de cada grupo |
arrange() |
Ordena todos los datos | Ignora los grupos salvo con .by_group = TRUE |
group_by con summarise
El uso más habitual de group_by es junto con la función summarise, que resume cada grupo en una única fila. La función n devuelve el número de filas de cada grupo, así que el siguiente código cuenta las observaciones de cada especie:
df %>%
group_by(species) %>%
summarise(n = n())
# A tibble: 3 × 2
species n
<chr> <int>
1 Droid 4
2 Gungan 2
3 Human 20
Puedes calcular tantos estadísticos por grupo como quieras dentro de la misma llamada a summarise. Fíjate en que los grupos aparecen ordenados alfabéticamente en la salida.
df %>%
group_by(species) %>%
summarise(n = n(),
mean_height = mean(height),
mean_mass = mean(mass),
max_mass = max(mass))
# A tibble: 3 × 5
species n mean_height mean_mass max_mass
<chr> <int> <dbl> <dbl> <dbl>
1 Droid 4 140 69.8 140
2 Gungan 2 210 74 82
3 Human 20 180. 81.3 136
Si un grupo contiene valores faltantes, funciones como mean o sum devolverán NA para ese grupo. Por ejemplo, con el conjunto starwars completo, summarise(mean_mass = mean(mass)) devuelve NA para las tres especies. Usa mean(mass, na.rm = TRUE) para ignorarlos. Consulta el artículo sobre valores NA en R para más detalles.
También puedes agrupar por una expresión en lugar de por una columna existente. En el ejemplo siguiente los personajes se dividen en altos (más de 180 cm) y no altos:
df %>%
group_by(alto = height > 180) %>%
summarise(n = n(), mean_mass = mean(mass))
# A tibble: 2 × 3
alto n mean_mass
<lgl> <int> <dbl>
1 FALSE 12 70.8
2 TRUE 14 85.9
Agrupar por varias columnas
Para agrupar por más de una variable, pásalas todas a group_by separadas por comas. Cada combinación de valores presente en los datos se convierte en un grupo:
res <- df %>%
group_by(species, sex) %>%
summarise(n = n(), mean_mass = mean(mass))
res
`summarise()` has grouped output by 'species'. You can override using the `.groups` argument.
# A tibble: 4 × 4
# Groups: species [3]
species sex n mean_mass
<chr> <chr> <int> <dbl>
1 Droid none 4 69.8
2 Gungan male 2 74
3 Human female 3 56.3
4 Human male 17 85.7
¿Qué significa “summarise() has grouped output by”?
Este mensaje no es un error ni una advertencia. Cada llamada a summarise elimina la última variable de agrupación (en este caso sex) y mantiene las demás. El mensaje simplemente te avisa de que el resultado sigue agrupado por species, como muestra la línea # Groups: species [3].
Esto es importante porque cualquier verbo que apliques después trabajará dentro de cada especie. Por ejemplo, si calculas el porcentaje de personajes de cada fila, el denominador sum(n) es el total de cada especie, no el total de los datos:
res %>%
mutate(pct = n / sum(n) * 100)
# A tibble: 4 × 5
# Groups: species [3]
species sex n mean_mass pct
<chr> <chr> <int> <dbl> <dbl>
1 Droid none 4 69.8 100
2 Gungan male 2 74 100
3 Human female 3 56.3 15
4 Human male 17 85.7 85
Esta salida es correcta si querías el porcentaje de cada sexo dentro de cada especie (el 15% de los humanos son mujeres). Si lo que querías era el porcentaje sobre los 26 personajes, utiliza .groups = "drop" para eliminar todos los grupos después de resumir:
df %>%
group_by(species, sex) %>%
summarise(n = n(), .groups = "drop") %>%
mutate(pct = n / sum(n) * 100)
# A tibble: 4 × 4
species sex n pct
<chr> <chr> <int> <dbl>
1 Droid none 4 15.4
2 Gungan male 2 7.69
3 Human female 3 11.5
4 Human male 17 65.4
Los valores posibles de .groups son:
"drop_last": elimina la última variable de agrupación. Es el comportamiento por defecto y el que muestra el mensaje."drop": elimina todos los grupos. El resultado es un tibble sin agrupar. Es la opción más segura en la mayoría de los casos."keep": mantiene los mismos grupos que la entrada (# Groups: species, sex [4])."rowwise": cada fila pasa a ser su propio grupo.
Si indicas .groups de forma explícita, aunque sea "drop_last", el mensaje deja de aparecer.
group_by con mutate
Cuando los datos están agrupados, la función mutate conserva todas las filas, pero los cálculos utilizan solo los valores del grupo de cada fila. Esto resulta útil para comparar cada observación con su grupo. En el siguiente ejemplo mean(mass) es la media de cada especie, por lo que diff_mean es la diferencia entre el peso de cada personaje y el peso medio de su especie:
df %>%
group_by(species) %>%
mutate(mean_mass = mean(mass),
diff_mean = mass - mean_mass) %>%
select(name, species, mass, mean_mass, diff_mean)
# A tibble: 26 × 5
# Groups: species [3]
name species mass mean_mass diff_mean
<chr> <chr> <dbl> <dbl> <dbl>
1 Luke Skywalker Human 77 81.3 -4.31
2 C-3PO Droid 75 69.8 5.25
3 R2-D2 Droid 32 69.8 -37.8
4 Darth Vader Human 136 81.3 54.7
5 Leia Organa Human 49 81.3 -32.3
6 Owen Lars Human 120 81.3 38.7
7 Beru Whitesun Lars Human 75 81.3 -6.31
8 R5-D4 Droid 32 69.8 -37.8
9 Biggs Darklighter Human 84 81.3 2.69
10 Obi-Wan Kenobi Human 77 81.3 -4.31
# ℹ 16 more rows
Observa que mean_mass vale 81.3 para todos los humanos y 69.8 para todos los droides. Sin group_by, la columna contendría la media global (79.0) en todas las filas.
Del mismo modo, puedes calcular el porcentaje sobre el total del grupo o la posición dentro de cada grupo. A continuación, pct_mass es el porcentaje que representa cada personaje sobre el peso total de su especie y rank_height es la posición de cada personaje dentro de su especie ordenando de más alto a más bajo:
df %>%
group_by(species) %>%
mutate(pct_mass = mass / sum(mass) * 100,
rank_height = min_rank(desc(height))) %>%
select(name, species, height, mass, pct_mass, rank_height)
# A tibble: 26 × 6
# Groups: species [3]
name species height mass pct_mass rank_height
<chr> <chr> <int> <dbl> <dbl> <int>
1 Luke Skywalker Human 172 77 4.73 16
2 C-3PO Droid 167 75 26.9 2
3 R2-D2 Droid 96 32 11.5 4
4 Darth Vader Human 202 136 8.36 1
5 Leia Organa Human 150 49 3.01 20
6 Owen Lars Human 178 120 7.38 13
7 Beru Whitesun Lars Human 165 75 4.61 19
8 R5-D4 Droid 97 32 11.5 3
9 Biggs Darklighter Human 183 84 5.17 8
10 Obi-Wan Kenobi Human 182 77 4.73 11
# ℹ 16 more rows
Darth Vader es el humano más alto (posición 1) y C-3PO es el segundo droide más alto. A diferencia de summarise, mutate no elimina ninguna variable de agrupación, por lo que el resultado sigue agrupado por species.
group_by con filter
La función filter evalúa las condiciones dentro de cada grupo cuando los datos están agrupados. Esto permite quedarte, por ejemplo, con los personajes que pesan más que la media de su propia especie:
df %>%
group_by(species) %>%
filter(mass > mean(mass))
# A tibble: 9 × 5
# Groups: species [3]
name species sex height mass
<chr> <chr> <chr> <int> <dbl>
1 C-3PO Droid none 167 75
2 Darth Vader Human male 202 136
3 Owen Lars Human male 178 120
4 Biggs Darklighter Human male 183 84
5 Anakin Skywalker Human male 188 84
6 IG-88 Droid none 200 140
7 Qui-Gon Jinn Human male 193 89
8 Roos Tarpals Gungan male 224 82
9 Mace Windu Human male 188 84
C-3PO pesa 75 kg, menos que la media global, pero se conserva porque pesa más que el droide medio (69.8 kg).
También puedes filtrar grupos completos. Usando n() dentro de filter puedes quedarte solo con los grupos que tienen más de un número determinado de filas:
df %>%
group_by(species) %>%
filter(n() > 2) %>%
summarise(n = n())
# A tibble: 2 × 2
species n
<chr> <int>
1 Droid 4
2 Human 20
Primeras filas de cada grupo con slice
Para obtener las N primeras filas de cada grupo según una variable, la opción más sencilla es slice_max (o slice_min para los valores más bajos). El siguiente código devuelve los dos personajes más pesados de cada especie:
df %>%
group_by(species) %>%
slice_max(mass, n = 2)
# A tibble: 6 × 5
# Groups: species [3]
name species sex height mass
<chr> <chr> <chr> <int> <dbl>
1 IG-88 Droid none 200 140
2 C-3PO Droid none 167 75
3 Roos Tarpals Gungan male 224 82
4 Jar Jar Binks Gungan male 196 66
5 Darth Vader Human male 202 136
6 Owen Lars Human male 178 120
Ten en cuenta que slice_max devuelve todas las filas empatadas por defecto, así que puedes obtener más de n filas por grupo. Utiliza with_ties = FALSE si necesitas exactamente n filas. Las demás funciones slice también trabajan por grupo: slice_head(n = 1) devuelve la primera fila de cada grupo y slice_sample extrae una muestra aleatoria de cada grupo. Puedes leer más sobre seleccionar filas por posición.
df %>%
group_by(species) %>%
slice_head(n = 1)
# A tibble: 3 × 5
# Groups: species [3]
name species sex height mass
<chr> <chr> <chr> <int> <dbl>
1 C-3PO Droid none 167 75
2 Jar Jar Binks Gungan male 196 66
3 Luke Skywalker Human male 172 77
group_by con arrange
La función arrange es la excepción: ignora los grupos por defecto y ordena todo el data frame. Si quieres ordenar las filas dentro de cada grupo (y que los grupos aparezcan juntos) tendrás que indicar .by_group = TRUE:
df %>%
group_by(species) %>%
arrange(desc(height), .by_group = TRUE)
# A tibble: 26 × 5
# Groups: species [3]
name species sex height mass
<chr> <chr> <chr> <int> <dbl>
1 IG-88 Droid none 200 140
2 C-3PO Droid none 167 75
3 R5-D4 Droid none 97 32
4 R2-D2 Droid none 96 32
5 Roos Tarpals Gungan male 224 82
6 Jar Jar Binks Gungan male 196 66
7 Darth Vader Human male 202 136
8 Qui-Gon Jinn Human male 193 89
9 Dooku Human male 193 80
10 Anakin Skywalker Human male 188 84
# ℹ 16 more rows
Sin .by_group = TRUE las primeras filas serían Roos Tarpals, Darth Vader e IG-88, los tres personajes más altos sin importar su especie.
ungroup: eliminar los grupos
Los grupos se mantienen hasta que los eliminas. mutate, filter, slice y arrange devuelven datos agrupados y summarise solo elimina un nivel. Por eso olvidar desagrupar es uno de los errores más frecuentes con dplyr: guardas un data frame agrupado y, más adelante, una operación que debería usar todos los datos se aplica por grupo sin ningún aviso.
En el siguiente ejemplo se agruparon los datos para calcular mean_mass. Después quieres obtener el personaje más pesado de todos, pero como los datos siguen agrupados obtienes el más pesado de cada especie:
df_g <- df %>%
group_by(species) %>%
mutate(mean_mass = mean(mass))
df_g %>% slice_max(mass, n = 1)
# A tibble: 3 × 6
# Groups: species [3]
name species sex height mass mean_mass
<chr> <chr> <chr> <int> <dbl> <dbl>
1 IG-88 Droid none 200 140 69.8
2 Roos Tarpals Gungan male 224 82 74
3 Darth Vader Human male 202 136 81.3
Si llamas a ungroup se eliminan todos los grupos y obtienes el resultado esperado:
df_g %>%
ungroup() %>%
slice_max(mass, n = 1)
# A tibble: 1 × 6
name species sex height mass mean_mass
<chr> <chr> <chr> <int> <dbl> <dbl>
1 IG-88 Droid none 200 140 69.8
Una buena costumbre es terminar cada cadena agrupada con ungroup() (o con .groups = "drop" en summarise) antes de guardar el resultado. Si no sabes si un data frame está agrupado, imprímelo y busca la línea # Groups: o ejecuta group_vars(df).
El argumento .by (dplyr 1.1.0 o posterior)
Desde dplyr 1.1.0, summarise, mutate, filter y slice admiten el argumento .by (en slice_max, slice_head y el resto de funciones slice_* se llama by). Este argumento agrupa los datos solo para esa operación y siempre devuelve un data frame sin agrupar, así que no necesitas ungroup y no verás el mensaje de .groups.
df %>%
summarise(n = n(), mean_mass = mean(mass), .by = species)
# A tibble: 3 × 3
species n mean_mass
<chr> <int> <dbl>
1 Human 20 81.3
2 Droid 4 69.8
3 Gungan 2 74
Los resultados son los mismos que con group_by, pero fíjate en que los grupos mantienen el orden en el que aparecen por primera vez en los datos en lugar de ordenarse alfabéticamente. Para agrupar por varias columnas utiliza c():
df %>%
summarise(n = n(), mean_mass = mean(mass), .by = c(species, sex))
# A tibble: 4 × 4
species sex n mean_mass
<chr> <chr> <int> <dbl>
1 Human male 17 85.7
2 Droid none 4 69.8
3 Human female 3 56.3
4 Gungan male 2 74
No puedes combinar ambos enfoques. Si los datos ya están agrupados y pasas .by, obtendrás el siguiente error:
Error in `summarise()`:
! Can't supply `.by` when `.data` is a grouped data frame.
Entonces, ¿cuál deberías usar? Usa .by para una única operación agrupada, ya que es más corto y no puede dejar grupos olvidados. Usa group_by cuando varios verbos seguidos comparten los mismos grupos o cuando necesitas arrange(.by_group = TRUE) o funciones como group_keys.
Contar filas por grupo con count
Contar observaciones por grupo es tan habitual que dplyr ofrece un atajo. count(species) equivale a group_by(species) %>% summarise(n = n()), pero además devuelve un data frame sin agrupar:
df %>%
count(species)
# A tibble: 3 × 2
species n
<chr> <int>
1 Droid 4
2 Gungan 2
3 Human 20
Puedes contar por varias variables, ordenar la salida de mayor a menor frecuencia con sort = TRUE y cambiar el nombre de la nueva columna con el argumento name:
df %>%
count(species, sex, sort = TRUE)
# A tibble: 4 × 3
species sex n
<chr> <chr> <int>
1 Human male 17
2 Droid none 4
3 Human female 3
4 Gungan male 2
Si quieres conservar todas las filas y añadir el tamaño del grupo como una nueva columna, utiliza add_count(species).
Equivalentes en R base
Si no quieres usar dplyr, también puedes obtener resúmenes por grupo en R base. La función aggregate devuelve un data frame y la función tapply devuelve un vector con nombres:
aggregate(mass ~ species, data = df, FUN = mean)
tapply(df$mass, df$species, mean)
species mass
1 Droid 69.75
2 Gungan 74.00
3 Human 81.31
Droid Gungan Human
69.75 74.00 81.31
Para contar por grupo, la función table es la alternativa a count en R base. Sin embargo, dplyr es más cómodo cuando necesitas operaciones mutate o filter por grupo, que en R base requieren más código.
Si cargas el paquete plyr después de dplyr, plyr::summarise enmascara a dplyr::summarise y los grupos se ignoran: obtendrás una sola fila para todos los datos o un error que indica que n() solo puede usarse dentro de verbos de dplyr. Carga plyr antes que dplyr o llama a dplyr::summarise de forma explícita.