group_by() + summarise(): el patrón split-apply-combine

r
tidyverse
Agregar datos por grupo: qué hace group_by() realmente, agregaciones múltiples, el warning de .groups que casi nadie entiende y el patrón group_by + mutate.

El patrón split-apply-combine

Casi todo análisis de datos hace lo mismo en algún momento: dividir los datos en grupos, aplicar una operación a cada grupo, combinar los resultados. Es el patrón group_by() + summarise() en dplyr, y es la operación que te permite ir de “10.000 ventas” a “ventas medias por región”.

En R base, esto se hace con aggregate() o con tapply(). Funcionan, pero la sintaxis es opaca y la composición con el pipe nula. En dplyr el patrón es:

ventas |>
  group_by(region) |>
  summarise(media_ingresos = mean(ingresos, na.rm = TRUE))

Léelo: “agrupa por región, resume con la media de ingresos”. Es prosa.

group_by(): qué hace realmente

group_by() no transforma los datos. Lo que hace es marcar el tibble con metadatos: “a partir de aquí, considera estos valores de region como grupos separados”. Los verbos que vienen después actúan dentro de cada grupo.

Lo puedes ver imprimiendo el resultado:

ventas |> group_by(region)
#> # A tibble: 10,000 × 5
#> # Groups:   region [4]
#> ...

Ese Groups: region [4] te dice que la próxima operación de agregación operará 4 veces (una por región) y combinará los resultados.

summarise(): de N filas a 1 por grupo

ventas |>
  group_by(region) |>
  summarise(
    n_ventas    = n(),
    media_ingr  = mean(ingresos, na.rm = TRUE),
    total_ingr  = sum(ingresos, na.rm = TRUE),
    max_ingr    = max(ingresos, na.rm = TRUE)
  )

Cada columna de summarise() produce un valor por grupo. El resultado tiene tantas filas como grupos.

Funciones de agregación frecuentes:

  • n(): número de filas del grupo.
  • n_distinct(x): número de valores únicos de x en el grupo.
  • mean(), median(), sum(), min(), max(), los clásicos. Casi siempre con na.rm = TRUE.
  • first(), last(), nth(x, 3), útiles tras un arrange() previo.
  • quantile(x, 0.75): percentiles.

Agrupación por varias columnas

ventas |>
  group_by(region, año) |>
  summarise(media_ingr = mean(ingresos, na.rm = TRUE))

Resultado: una fila por combinación region × año. Si hay 4 regiones y 3 años, salen 12 filas (asumiendo que todas las combinaciones existen en los datos).

.groups: el warning que la gente ignora

Cuando agrupas por varias columnas y haces summarise(), dplyr muestra este warning desde la versión 1.0:

`summarise()` has grouped output by 'region'. You can override using the `.groups` argument.

Significa: “el resultado sigue agrupado por la primera columna (region). Cualquier operación siguiente actuará dentro de cada región”. Esto sorprende a casi todo el mundo y es causa de bugs sutiles.

Las cuatro opciones explícitas:

summarise(..., .groups = "drop_last")   # default: quita la última agrupación
summarise(..., .groups = "drop")        # quita TODA la agrupación
summarise(..., .groups = "keep")        # mantiene todas las agrupaciones
summarise(..., .groups = "rowwise")     # cada fila pasa a ser su propio "grupo"

Recomendación práctica: usa siempre .groups = "drop" cuando termines un pipeline, salvo que tengas razón explícita para mantener la agrupación. Así no te lleva sorpresas más adelante:

ventas |>
  group_by(region, año) |>
  summarise(media_ingr = mean(ingresos, na.rm = TRUE), .groups = "drop")

group_by() + mutate() (sin summarise())

group_by() no es solo para summarise(). Combinado con mutate(), te permite calcular columnas que dependen de los demás valores del mismo grupo:

# Porcentaje del total dentro de cada región
ventas |>
  group_by(region) |>
  mutate(pct_region = ingresos / sum(ingresos))

# Rank de cada venta dentro de su año
ventas |>
  group_by(año) |>
  mutate(rank_anual = rank(-ingresos))

Aquí la tabla mantiene su tamaño original (no agregamos), pero las columnas nuevas usan el contexto del grupo. Es una herramienta poderosísima cuando aprendes a verla.

ungroup(): cuándo y por qué

Si tu pipeline sigue después de un summarise() con .groups != "drop", o después de un mutate() agrupado, acuérdate de desagrupar:

ventas |>
  group_by(region, año) |>
  summarise(total = sum(ingresos), .groups = "drop_last") |>
  # OJO: aquí sigue agrupado por region
  mutate(pct = total / sum(total)) |>   # el sum() es POR region, no global
  ungroup()

Si esperabas un porcentaje sobre el total global y obtienes porcentajes que suman 1 dentro de cada región (cada región = 100%), te falta un ungroup() o un .groups = "drop".

Adopta el hábito: .groups = "drop" por defecto en summarise(), ungroup() por defecto después de un mutate() agrupado. Pierdes muy poco y ganas robustez.

Trampas habituales

  • El warning de .groups no es ruido, es información. Si lo ignoras, en algún momento sufres un bug donde un porcentaje “suma 100% en cada grupo” en vez de globalmente.
  • n() cuenta filas. sum(!is.na(x)) cuenta no-NAs. No es lo mismo si la columna tiene huecos. Sé explícito sobre qué quieres contar.
  • Mezclar group_by() con arrange() en orden equivocado. arrange() ANTES de group_by() ordena el conjunto entero. arrange() después ordena dentro de cada grupo (con .by_group = TRUE).
  • Olvidar na.rm = TRUE en agregaciones. Si una columna tiene un solo NA, mean(x) devuelve NA. Todos los mean, sum, etc., aceptan na.rm = TRUE. Es una decisión consciente que conviene tomar explícitamente, no por reflejo.

En la siguiente entrega

Has completado el bloque 2, los cuatro verbos núcleo de dplyr que cualquier analista usa cada día. El siguiente bloque entra en operaciones que combinan varios data frames: los joins, que es donde la gente que viene de Excel realmente entiende por qué R y SQL piensan en términos relacionales. Es lo siguiente.