Etiquetas de texto que se repelen entre sí con {ggrepel}

11/7/2025

visualización de datos ggplot2 gráficos texto

En este post veremos a agregar textos que se distancian entre sí automáticamente a tus gráficos. Esto sirve, por ejemplo, para mejorar visualizaciones de datos a las que queremos agregarle texto que identifique las observaciones, aún cuando las observaciones son demasiadas como para etiquetarlas a todas.

Usaremos el paquete de R {ggrepel} para etiquetar puntos en un gráfico con textos que se repelen entre sí de forma automática.

Datos

A modo de ejemplo, usaremos datos del plebiscito de entrada de 2020 en Chile, obtenidos desde el repositorio de datos sobre los plebiscitos constitucionales. Éste repositorio, parte de mi mini sitio de datos sociales chilenos, ofrece versiones limpias y listas para usar de los resultados electorales de estos procesos.

library(arrow)

plebiscito <- read_parquet("https://github.com/bastianolea/plebiscitos_chile/raw/main/datos/plebiscito_2020_comunas.parquet")

Como los datos están alojados en el repositorio, pueden cargarse directamente en tu sesión de R tan sólo cargándolos desde el enlace anterior, sin necesidad de descargarse en tu computador.

Los datos vienen con cada fila representando una opción de voto por comuna del país, con su respectiva cantidad de votos, y se ven así:

library(dplyr)

head(plebiscito)
# A tibble: 6 × 6
  cut_region region   cut_comuna comuna        opciones        votos
       <int> <chr>         <int> <chr>         <chr>           <dbl>
1          1 Tarapacá       1101 Iquique       Apruebo         61103
2          1 Tarapacá       1101 Iquique       Rechazo         18879
3          1 Tarapacá       1101 Iquique       Votos En Blanco   114
4          1 Tarapacá       1101 Iquique       Votos Nulos       275
5          1 Tarapacá       1107 Alto Hospicio Apruebo         21589
6          1 Tarapacá       1107 Alto Hospicio Rechazo          4534

Procesamiento

Vamos a hacer un gráfico de dispersión con {ggplot2}, para el cual necesitamos dos columnas con las dos opciones de voto: apruebo y rechazo. Como en el conjunto de datos que cargamos las opciones vienen en una columna (opciones), y el conteo viene en otra (votos), transformaremos la estructura de los datos a un formato ancho usando pivot_wider():

library(tidyr)

plebiscito_ancho <- plebiscito |> 
  pivot_wider(names_from = opciones, values_from = votos) |> 
  janitor::clean_names()

Lo que hizo pivot_wider() fue a usar la variable opciones para crear nuevas columnas, y la variable votos para rellenar las nuevas columnas con valores. De este modo, nuestros datos pasaron del formato largo al formato ancho, con una columna que contiene los votos del apruebo y otra del rechazo:

head(plebiscito_ancho)
# A tibble: 6 × 8
  cut_region region   cut_comuna comuna        apruebo rechazo votos_en_blanco
       <int> <chr>         <int> <chr>           <dbl>   <dbl>           <dbl>
1          1 Tarapacá       1101 Iquique         61103   18879             114
2          1 Tarapacá       1107 Alto Hospicio   21589    4534              37
3          1 Tarapacá       1401 Pozo Almonte     3730    1076              11
4          1 Tarapacá       1402 Camiña            293     207               2
5          1 Tarapacá       1403 Colchane          131     374               3
6          1 Tarapacá       1404 Huara            1136     379               6
# ℹ 1 more variable: votos_nulos <dbl>

Ahora usaremos {dplyr} para calcular qué opción ganó en cada comuna del país, y el porcentaje de votos que obtuvo:

library(dplyr)

plebiscito_comunas <- plebiscito_ancho |> 
  group_by(cut_comuna) |> 
  # calcular resultado final
  mutate(resultado = case_when(apruebo > rechazo ~ "Apruebo",
                               rechazo >= apruebo ~ "Rechazo"),
         .before = apruebo) |> 
  # calcular porcentaje de votos
  mutate(porcentaje = apruebo/(apruebo+rechazo)) |> 
  ungroup()
head(plebiscito_comunas)
# A tibble: 6 × 10
  cut_region region  cut_comuna comuna resultado apruebo rechazo votos_en_blanco
       <int> <chr>        <int> <chr>  <chr>       <dbl>   <dbl>           <dbl>
1          1 Tarapa…       1101 Iquiq… Apruebo     61103   18879             114
2          1 Tarapa…       1107 Alto … Apruebo     21589    4534              37
3          1 Tarapa…       1401 Pozo … Apruebo      3730    1076              11
4          1 Tarapa…       1402 Camiña Apruebo       293     207               2
5          1 Tarapa…       1403 Colch… Rechazo       131     374               3
6          1 Tarapa…       1404 Huara  Apruebo      1136     379               6
# ℹ 2 more variables: votos_nulos <dbl>, porcentaje <dbl>

Gráfico

Para realizar un gráfico de densidad, y especificamos las dos variables que pondremos en los ejes x e y, y una variable que representará el color de cada observación, las cuales se visualizarán como puntos usando geom_point():

library(ggplot2)

plebiscito_comunas |> 
  ggplot() +
  aes(rechazo, apruebo, color = resultado) +
  geom_point()

Ahora mejoraremos un poco el gráfico anterior: definiremos el formato de los números de las escalas con scales::number_options(), ajustaremos el tamaño transparencia de los puntos de geom_point(), luego aplicaremos escalas numéricas más bonitas con scale_{x}_continuous(), y finalmente ajustaremos un poco el tema y textos del gráfico:

library(scales)
# formato de números
number_options(decimal.mark = ",", big.mark = ".")

grafico_resultado <- plebiscito_comunas |> 
  ggplot() +
  aes(rechazo, apruebo, color = resultado) +
  geom_point(size = 3, alpha = 0.5) +
  scale_x_continuous(labels = label_number()) +
  scale_y_continuous(labels = label_number()) +
  theme_minimal() +
  theme(legend.text = element_text(margin = margin(l = 2))) +
  labs(x = "Rechazo", y = "Apruebo", color = "Resultado")

grafico_resultado

Ahora, al gráfico anterior, le agregaremos inocentemente etiquetas de texto a cada punto, a ver qué pasa:

grafico_resultado +
  geom_text(aes(label = comuna), size = 2, color = "grey30", hjust = 0)

Absolutamente nefasto. Son tantos puntos que los textos se transforman en una masa gris debido a la concentración.

Un intento desesperado de corregir esto podría ser el argumento check_overlap, eliminará los textos que aparezcan encima de otros:

grafico_resultado +
  geom_text(aes(label = comuna), size = 2, color = "grey30", hjust = 0,
            check_overlap = TRUE)

Sin embargo, esta solución es demasiado básica, y aún hay textos que aparece encima de puntos, o fuera del margen del gráfico.

En estos casos resulta ideal geom_text_repel() como reemplazo de geom_text():

library(ggrepel)

grafico_resultado +
  geom_text_repel(aes(label = comuna), size = 2.5, color = "grey30",
                  point.padding = 2 # margen de cada punto
  )

La función geom_text_repel() calcula la posesión de las etiquetas de texto con respecto a las demás, y decide el posicionamiento óptimo para que la mayor cantidad de textos aparezcan, sin que salgan encima de otros textos, ni encima de los puntos.

Esta función tiene varios argumentos para ajustar el algoritmo que decide la ubicación de los textos. Uno de ellos es max.overlaps, cuyo defecto es 10, y determina la cantidad máxima de etiquetas que pueden estar unas encima de otras antes de qué sean descartadas por estar demasiado concentrados en un mismo lugar. Si aumentamos este argumento, la función tomará en cuenta etiquetas de texto en ubicaciones más complejas, e intentará graficarlas moviéndolas más lejos de los puntos, o moviendo otras etiquetas de texto a posiciones más lejanas del punto que lo origina. Para mejorar el resultado, podemos aumentar el valor del argumento max.time, el cual le da más tiempo al algoritmo para buscar resolver el posicionamiento de los textos.

grafico_resultado +
  geom_text_repel(aes(label = comuna), size = 2.5, color = "grey30",
                  segment.size = 0.1, # ancho de líneas (si es que salen)
                  max.overlaps = 30, max.time = 1)

En el gráfico anterior, aparecen nuevas etiquetas que no salieron antes, algunas de ellas conectadas por líneas con el punto correspondiente.

También podemos determinar ciertos parámetros para que aparezcan o no aparezcan ciertos textos en el gráfico. En el siguiente ejemplo, agregamos una condicional para que solamente aparezcan textos que superen ciertos valores de cada eje, excluyendo los demás.

grafico_resultado +
  geom_text_repel(aes(label = ifelse(rechazo > 30000 | apruebo > 100000, comuna, " ")), 
                  size = 2.5, color = "grey30",
                  segment.size = 0, max.overlaps = 30)

Es importante dejar las observaciones que queremos excluir de el etiquetado de texto con un texto vacío o un espacio (" "), y no solamente filtrar las observaciones del dataset, porque así los puntos del gráfico seguirán teniendo un texto vacío encima de ellos, lo que hará que el resto de las etiquetas de texto también se alejen de los puntos sin etiqueta.

Haremos un acercamiento al gráfico, definiendo los límites verticales y horizontales con coord_cartesian(), para que hayan puntos más dispersos:

grafico_resultado +
  coord_cartesian(xlim = c(0, 10000),
                  ylim = c(0, 50000),
                  clip = "off") +
  geom_text_repel(
    aes(label = ifelse(rechazo > 3000 & rechazo < 10000 
                       | apruebo > 20000 & apruebo < 50000, 
                       comuna, " ")), 
    size = 2.5, color = "grey30",
    segment.size = 0, max.overlaps = 30, max.time = 5)

En este caso aplicamos condicionales más estrictas para que aparezcan o desaparezcan los puntos, con el objetivo de que aparezca la mayor cantidad de textos de un sector específico del gráfico.

También podemos utilizar criterios estadísticos para incluir o excluir el etiquetado de textos. Por ejemplo, en el siguiente gráfico solamente mostramos el nombre de los puntos cuyo valor es superior al percentil 73 de cada variable.

grafico_resultado +
  geom_text_repel(aes(label = case_when(rechazo > quantile(rechazo, 0.73) ~ comuna, 
                                        apruebo > quantile(apruebo, 0.73) ~ comuna)), 
                  size = 2.5, color = "grey30", 
                  segment.size = 0, max.overlaps = 30, max.time = 5) +
  scale_x_continuous(labels = label_number(), limits = c(0, 10000)) +
  scale_y_continuous(labels = label_number(), limits = c(0, 30000)) +
  labs(x = "Rechazo (detalle)", y = "Apruebo (detalle)")

Gráfico interactivo

Finalmente, una mejor solución para estos casos sería permitir a las y los usuarios explorar la información de todas las observaciones del gráfico mediante la interactividad. El paquete {ggiraph} para visualizaciones interactivas nos puede ayudar a transformar cualquier gráfico de {ggplot2} en un gráfico interactivo, donde los usuarios pueden tocar o poner el Mouse encima de un elemento para obtener más información. Sólo es necesario agregarle _interactive a las funciones geom_ del gráfico, y especificar el texto que aparecerá cuándo se ponga el cursor encima de cada elemento en tooltip dentro de aes():

library(ggiraph)
library(glue)

grafico_interactivo <- plebiscito_comunas |> 
  ggplot() +
  aes(rechazo, apruebo, color = resultado) +
  geom_point_interactive(size = 3, alpha = 0.5,
                         aes(tooltip = glue("<b>{comuna}</b>: gana {resultado} con un {percent(porcentaje, 0.1)}"))
  ) +
  geom_text_repel(aes(label = comuna), size = 2.5, color = "grey30",
                  segment.size = 0.1, point.padding = 2) +
  scale_x_continuous(labels = label_number()) +
  scale_y_continuous(labels = label_number()) +
  theme_minimal() +
  theme(legend.text = element_text(margin = margin(l = 2))) +
  labs(x = "Rechazo", y = "Apruebo", color = "Resultado")

Para que el gráfico se vea y funcione interactivamente, debemos mostrarlo mediante la función girafe():

girafe(ggobj = grafico_interactivo, 
       # dimensiones del gráfico
       width_svg = 7, height_svg = 5,
       # opciones para los tooltips
       options = list(
         # ocultar barra de opciones
         opts_toolbar(hidden = "selection", saveaspng = FALSE),
         # color al poner el cursor encima
         opts_hover(css = "fill: white; stroke: black;"),
         # personalizar la apariencia del tooltip
         opts_tooltip(opacity = 0.7, use_fill = TRUE,
                      css = "font-family: sans-serif; font-size: 70%; color: white; padding: 4px; border-radius: 5px;"))
)

Vemos que en el gráfico anterior, podemos obtener información extra de cada observación tan sólo poniendo el cursor encima, lo cual amplía bastante las posibilidades de comunicación, por ejemplo, permitiendo saber información de los puntos de los sectores más densos, o agregando la información de otras variables del conjunto de datos, como el porcentaje.


Próximamente: ejemplos para aplicar {ggrepel} en mapas y otras visualizaciones geoespaciales.


Fecha de publicación:
July 11, 2025
Extensión:
9 minute read, 1750 words
Tags:
visualización de datos ggplot2 gráficos texto
Ver también:
Extensiones recomendadas para mejorar tus gráficos de `{ggplot2}`
Extracción de datos estructurados desde texto usando IA
Renombrar archivos desde R