The Clean Shot

The Clean Shot

Tutoriales R

Cómo se hace: Gráficas de barras y plot de dispersión

Tres gráficas del post de Kurtinaitis + boxscore completo en CSV. A la primera invito yo.

Avatar de Ivo Villanueva
Ivo Villanueva
may 23, 2025
∙ De pago

Si eres de los que te has registrado para una suscripción premium a The Clean Shot, muchas gracias.

El código completo está al final del post

La idea es publicar un post gratuito los martes y el como lo hago los viernes, no se si llegaré pero esa es la intención.

Esta semana quiero enseñar como extraigo las tablas de estadisticas individuales de ACB, como limpio los datos y como hago la gráfica final. Los datos de los gráficas de barras vienen de Las tablas de puntos y tiros de 3 con el filtro seleccionado en totales. La ACB filtra por los 50 mejores según la categoría.

Lo primero es cargar las librerias y ojo sé que son muchas

# 📚 Cargar las librerías-------------------

library(tidyverse)     # manipulación datos  
library(rvest)         # scrapeo web  
library(janitor)       # limpiar tablas  
library(shadowtext)    # texto visible  
library(prismatic)     # ajustes color  
library(ggtext)        # texto enriquecido  
library(cropcircles)   # fotos redondas  
library(glue)          # concatenar texto  
library(ggimage)       # añadir imágenes  
library(ggrepel)       # evitar solapes  
library(ggforce)       # anotaciones extra  

Luego cargo el data set con los datos de los equipos (logos, colores, nombre corto, etc.)


clubs <- read.csv("https://raw.githubusercontent.com/IvoVillanueva/logos_cuadrados_acb/refs/heads/main/acb_df.csv") %>%
  mutate(color = ifelse(abb == "RMB", "white", color))#Aquí había puesto el color amarillo del Real Madrid pero me di cuenta que el blanco funciona muy bien también

Luego el tema personalizado de los plots


theme_ivo <- function (font_size = 9) {
  theme_minimal(base_size = font_size, base_family = "Oswald") %+replace%
    theme(
      plot.background = element_rect(fill = 'white', color = "white"),
      panel.grid.minor = element_blank(),
      plot.title = element_text(hjust = 0, size = 14, face = 'bold'),
      plot.subtitle = element_text(color = 'gray65', hjust = 0, margin=margin(2.5,0,10,0), size = 11),
    )
}

y la firma


twitter <- "<span style='color:#000000;font-family: \"Font Awesome 6 Brands\"'>&#xE61A;</span>"
tweetelcheff <- "<span style='font-weight:bold;'>*@elcheff*</span>"
insta <- "<span style='color:#E1306C;font-family: \"Font Awesome 6 Brands\"'>&#xE055;</span>"
instaelcheff <- "<span style='font-weight:bold;'>*@sport_iv0*</span>"
github <- "<span style='color:#000000;font-family: \"Font Awesome 6 Brands\"'>&#xF092;</span>"
githubelcheff <- "<span style='font-weight:bold;'>*IvoVillanueva*</span>"
caption <- glue::glue("**Datos**: *ACB.COM* | **Gráfico**: *Ivo Villanueva* • {twitter} {tweetelcheff} • {insta} {instaelcheff} • {github} {githubelcheff}")

Ahora vamos ya a extraer la tabla con la libreria rvest


url <- "https://www.acb.com/estadisticas-individuales/puntos/temporada_id/2024/edicion_id/975/fase_id/107/tipo_id/0" %>%
  read_html()


df <- url %>%
  html_element("table") %>%
  html_table() 

El resultado es super sucio. Con cosas que me dan mucho coraje cuando extraigo datos: como dobles columnas y columnas sin nombre por ejemplo

En este caso me da igual porque solo necesito dos columnas la del nombre y la de PT

df <- url %>%
  html_element("table") %>%
  html_table() %>%
  row_to_names(1) %>% # le digo que ponga de nombre de columna la primera fila
  clean_names() %>% #limpio los nombres
  select( jugador = x, puntos = na_2) %>% #le doy nombres descripitivos
  arrange(desc(puntos)) %>%
  slice(1:20)

Esto ya otra cosa; al aviso que sale, ni caso. ahora ya tenemos los datos, y con esto ya se podría hacer el gráfico, pero para hacer la tabla de una manera mas vistosa y no tener que ir haciendolo a mano, la cosa se complica bastante:

  • Primero tenemos que inventarnos una tabla para extraer los ids de los jugadores y los nombres, para poder juntarlos mas adelante

jugadores <- tibble(
 id=  url %>%
  html_elements("a.colorweb_7") %>%
  html_attr("href") %>%
  str_extract(., "[0-9]+"),#extrae solo el numero de la url
 jugador = url %>%
   html_elements("a.colorweb_7") %>%
   html_text()) %>%
  hablar::retype()
  • Luego (y solo por esto te juro que los 5€ merecen la pena, porque cuando yo empecé ojala me lo hubiera explicado alguien) una función que genere una tabla que extraiga los nombres de los equipos y el código de la foto de la página del jugador y que me una todo de una vez

#jugadores$id es igual a  [1] 30002017 30003416 20212941 etc

plot_df <- map_df(jugadores$id, function(x) {
  jugador_html <- read_html(glue("https://www.acb.com/jugador/temporada-a-temporada/id/{x}"))

  # Extraer equipo
  equipo <- jugador_html %>%
    html_element(".equipo span") %>%
    html_text()

  # Extraer URL de foto
  foto <- jugador_html %>%
    html_element(".datos img") %>%
    html_attr("src") %>%
    str_extract(., "[^/]+(?=\\.jpg)")
  # [^/]+ = todo lo que no sea "/", es decir, el nombre del archivo.
  # (?=\\.jpg) = lookahead para asegurarse de que lo que viene después sea .jpg, pero sin incluirlo.

  # Crear tibble con todo
  tibble(equipo = equipo, foto = foto, id = x) %>%
    left_join(jugadores, join_by(id)) %>%
    left_join(clubs %>% select(equipo, color), join_by(equipo)) %>%
    inner_join(df, join_by(jugador)) %>%
    hablar::retype()
})

Cuando correis la función tarda un rato porque va por cada url, o sea 20 en total, extrayendo los datos que necesitamos y este es el resultado, se puede pensar que son mucho dato pero lo usaremos todos mas adelante

Creamos la base del gráfico

p <- plot_df %>%
  ggplot(aes(x = puntos, y = fct_reorder(jugador, puntos)))

Agregamos las barras horizontales. Cada jugador tiene el color de su equipo.

p <- plot_df %>%
  ggplot(aes(x = puntos, y = fct_reorder(jugador, puntos))) +
  geom_col(aes(fill = color,
               color = after_scale(clr_darken(fill, 0.15))),
           alpha = .9,
           width = 0.6)

Al final de las barras se colocan un geom_point para reforzar visualmente el valor.

p <- plot_df %>%
  ggplot(aes(x = puntos, y = fct_reorder(jugador, puntos))) +
  geom_col(...) +
  geom_point(shape = 21, size = 7.6,
             aes(fill = color,
                 x = puntos,
                 color = after_scale(clr_darken(fill, 0.3))))

Mostramos los puntos de cada jugador dentro del geom_point, con sombra negra para que se lean bien.

p <- plot_df %>%
  ggplot(aes(x = puntos, y = fct_reorder(jugador, puntos))) +
  geom_col(...) +
  geom_point(...) +
  geom_shadowtext(aes(x = puntos, y = jugador, label = puntos, family = "Roboto"),
                  bg.r = .15, fontface = 'bold',
                  bg.colour = "black", color = 'white',
                  hjust = .5, size = 2.85)

Ajustamos los límites del eje X y añadimos título y subtítulo.

p <- p +
  scale_fill_identity() +
  scale_x_continuous(limits = c(0, 700), breaks = seq(0, 800, 100)) +
  labs(
    x = "",
    y = "",
    title = "Top 20 Anotadores 2025",
    subtitle = "Puntos totales hasta J32 | Liga Endesa"
  )

Aplicamos mi tema personalizado (prueba a modificar el mio o con el que mas te guste) y ajustamos texto, márgenes y estilos.

p <- p +
  theme_ivo() +
  theme(
    panel.grid.major.y = element_blank(),
    legend.position = 'none',
    axis.title = element_blank(),
    plot.caption = element_markdown(size = 7, hjust = 0),
    plot.title = element_text(face = "bold", size = 26),
    plot.subtitle = element_text(size = 10.5),
    plot.margin = margin(b = 25, t = 25, r = 25, l = 25),
    axis.text = element_text(size = 10)
  )

Guardamos el gráfico como imagen en alta calidad para usar en redes o posts.

ggsave("anotadores.png", p, width = 6.25, height = 7, dpi = 600)

🔒 Continúa para suscriptores de pago

Hasta aquí, explico cómo se construye la primera de las tres gráficas del post.

A partir de aquí:

  • Código completo de las otras dos (triplistas y dispersión)

  • Comentarios paso a paso

  • Exportación y CSV con todos los boxscore hasta la J32 (Rows: 7180, Columns: 72)

Gracias por apoyarme 👇

Continúa leyendo con una prueba gratuita de 7 días

Suscríbete a The Clean Shot para seguir leyendo este post y obtener 7 días de acceso gratis al archivo completo de posts.

¿Ya eres suscriptor de pago? Iniciar sesión
© 2025 The Clean Shot
Privacidad ∙ Términos ∙ Aviso de recolección
Crea tu SubstackDescargar la app
Substack es el hogar de la gran cultura