8. Resúmenes (group_by/summarise)

1 Resúmenes con group_by() + summarise() (conteos, medias, top-N, tablas)

Objetivo: agrupar datos y calcular indicadores por grupo (conteos, medias/medianas, dispersiones), aplicar funciones a múltiples columnas con across(), extraer Top-N por grupo y producir tablas anchas para reporte.

Tip

Partimos de BaseSal (sección 6–7). Detectamos automáticamente la columna de antigüedad si viene como yrs.service/yrs_service.

Code
library(tidyverse)
Warning: package 'tidyverse' was built under R version 4.2.2
Warning: package 'ggplot2' was built under R version 4.2.3
Warning: package 'tibble' was built under R version 4.2.3
Warning: package 'tidyr' was built under R version 4.2.2
Warning: package 'purrr' was built under R version 4.2.2
Warning: package 'stringr' was built under R version 4.2.3
Warning: package 'forcats' was built under R version 4.2.2
Warning: package 'lubridate' was built under R version 4.2.2
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   3.5.1     ✔ tibble    3.2.1
✔ lubridate 1.9.2     ✔ tidyr     1.3.0
✔ purrr     1.0.1     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
Code
# Asegurar BaseSal mínimo
if (!exists("BaseSal")) {
  if (!exists("Salaries")) {
    library(readxl)
    Salaries <- readxl::read_excel(ruta_salaries)  
  }
  BaseSal <- dplyr::select(Salaries, dplyr::any_of(c("rank","sex","yrs.service","salary")))
}

# Normalizamos nombres mínimos y detectamos antigüedad
names(BaseSal) <- names(BaseSal) |> tolower()
nombre_ant <- intersect(c("yrs.service","yrs_service","years_service"), names(BaseSal))
nombre_ant <- if (length(nombre_ant) == 1) nombre_ant else NA_character_

1.1 Conteos rápidos: count(), add_tally(), n(), n_distinct()

Code
# Conteo simple por una variable
count(BaseSal, rank, sort = TRUE)
Code
# Conteo por dos variables (tabla de frecuencia)
count(BaseSal, rank, sex, sort = TRUE)
Code
# Agregar total de filas como columna (tally global)
BaseSal %>% add_tally(name = "n_total") %>% head()
Code
# En summarise: tamaño del grupo con n(), categorías únicas con n_distinct()
BaseSal %>%
  group_by(rank) %>%
  summarise(n = n(),
            sex_unicos = n_distinct(sex),
            .groups = "drop")

1.2 Estadísticos por grupo con summarise()

Code
# Medidas por sexo
res_sex <- BaseSal %>%
  group_by(sex) %>%
  summarise(
    n       = n(),
    media   = mean(salary, na.rm = TRUE),
    mediana = median(salary, na.rm = TRUE),
    sd      = sd(salary, na.rm = TRUE),
    .groups = "drop"
  )
res_sex
Code
# Medidas por rank y sexo
res_rank_sex <- BaseSal %>%
  group_by(rank, sex) %>%
  summarise(
    n       = n(),
    media   = mean(salary, na.rm = TRUE),
    mediana = median(salary, na.rm = TRUE),
    q1      = quantile(salary, .25, na.rm = TRUE),
    q3      = quantile(salary, .75, na.rm = TRUE),
    .groups = "drop_last"   # conserva el 1er nivel de agrupación si quieres seguir encadenando
  )
res_rank_sex %>% arrange(rank, sex)

1.3 Resumir varias columnas a la vez con across()

Code
# Elegimos columnas numéricas de interés (si existe años de servicio la incluimos)
vars_resumir <- c("salary", nombre_ant)
vars_resumir <- vars_resumir[vars_resumir %in% names(BaseSal)]

res_multi <- BaseSal %>%
  group_by(rank, sex) %>%
  summarise(
    across(all_of(vars_resumir),
           list(mean = ~mean(.x, na.rm=TRUE),
                sd   = ~sd(.x, na.rm=TRUE),
                med  = ~median(.x, na.rm=TRUE)),
           .names = "{.col}_{.fn}"),
    .groups = "drop"
  )
res_multi %>% arrange(rank, sex)

1.4 Promedios ponderados (idea) y funciones propias

Code
# Si tuvieras un peso (ej. años de servicio como proxy sólo para ilustrar):
if (!is.na(nombre_ant)) {
  res_pond <- BaseSal %>%
    filter(!is.na(.data[[nombre_ant]])) %>%
    group_by(rank) %>%
    summarise(
      media_w = weighted.mean(salary, w = .data[[nombre_ant]], na.rm = TRUE),
      n = n(),
      .groups = "drop"
    )
  res_pond
}
Code
# Función propia: coeficiente de variación seguro
cv <- function(x) sd(x, na.rm=TRUE) / mean(x, na.rm=TRUE) * 100

res_cv <- BaseSal %>%
  group_by(rank, sex) %>%
  summarise(
    cv_salary = cv(salary),
    .groups = "drop"
  )
res_cv

1.5 Top-N por grupo: slice_max() / slice_min()

Code
# Top 3 salarios dentro de cada 'rank'
top3_por_rank <- BaseSal %>%
  group_by(rank) %>%
  slice_max(order_by = salary, n = 3, with_ties = FALSE) %>%
  arrange(rank, desc(salary))
top3_por_rank
Code
# El salario mínimo por sexo (1 fila por grupo)
min_por_sex <- BaseSal %>%
  group_by(sex) %>%
  slice_min(order_by = salary, n = 1, with_ties = FALSE)
min_por_sex

1.6 De largo a ancho para reporte: pivot_wider()

Code
# Media de salary por rank y sex en formato ancho (columnas = sex)
tabla_ancha <- BaseSal %>%
  group_by(rank, sex) %>%
  summarise(media = mean(salary, na.rm = TRUE), .groups = "drop") %>%
  tidyr::pivot_wider(names_from = sex, values_from = media)

tabla_ancha

1.7 (Opcional) Exportar a CSV para el informe

Code
# Dir de salida
if (!dir.exists("output")) dir.create("output")

readr::write_csv(res_multi, "output/resumen_multivariado.csv")
readr::write_csv(tabla_ancha, "output/tabla_ancha_rank_sex.csv")

list.files("output")
[1] "resumen_multivariado.csv" "tabla_ancha_rank_sex.csv"

1.8 Ejercicios

  1. Crea res_rank con n, media, sd, mediana de salary por rank, ordenado por media descendente.

  2. Produce una tabla ancha con la mediana de salary por rank (filas) y sex (columnas).

  3. Extrae el Top-5 salarios por sex (sin empates) y ordénalo de mayor a menor.

  4. Si tienes años de antigüedad: calcula, por sex, el promedio ponderado de salary usando como peso esa antigüedad (solo como práctica).

  5. Construye un reporte con summarise(across()) que, por rank, calcule para salary: mean, sd, q1 (0.25), q3 (0.75) y cv (usa la función de arriba), con nombres tipo salary_mean, salary_q1, etc.

Code
# 1) Resumen por rank ordenado por media desc
res_rank <- BaseSal %>%
  group_by(rank) %>%
  summarise(n = n(),
            media = mean(salary, na.rm=TRUE),
            sd = sd(salary, na.rm=TRUE),
            mediana = median(salary, na.rm=TRUE),
            .groups = "drop") %>%
  arrange(desc(media))
res_rank
Code
# 2) Tabla ancha con mediana por rank x sex
med_rank_sex_wide <- BaseSal %>%
  group_by(rank, sex) %>%
  summarise(mediana = median(salary, na.rm = TRUE), .groups = "drop") %>%
  tidyr::pivot_wider(names_from = sex, values_from = mediana)
med_rank_sex_wide
Code
# 3) Top-5 salarios por sex
top5_sex <- BaseSal %>%
  group_by(sex) %>%
  slice_max(order_by = salary, n = 5, with_ties = FALSE) %>%
  arrange(sex, desc(salary))
top5_sex
Code
# 4) Promedio ponderado por sex (si hay antigüedad)
if (!is.na(nombre_ant)) {
  media_w_sex <- BaseSal %>%
    filter(!is.na(.data[[nombre_ant]])) %>%
    group_by(sex) %>%
    summarise(media_w = weighted.mean(salary, .data[[nombre_ant]], na.rm = TRUE),
              n = n(),
              .groups = "drop")
  media_w_sex
}
Code
# 5) Reporte con across
reporte_rank <- BaseSal %>%
  group_by(rank) %>%
  summarise(
    salary_mean = mean(salary, na.rm=TRUE),
    salary_sd   = sd(salary, na.rm=TRUE),
    salary_q1   = quantile(salary, .25, na.rm=TRUE),
    salary_q3   = quantile(salary, .75, na.rm=TRUE),
    salary_cv   = sd(salary, na.rm=TRUE)/mean(salary, na.rm=TRUE)*100,
    .groups = "drop"
  )
reporte_rank