The Clean Shot

The Clean Shot

Tutoriales R

Cómo hice el gráfico de dispersión AST% vs TOV%

Y más cosas

Avatar de Ivo Villanueva
Ivo Villanueva
nov 28, 2025
∙ De pago

Y empezamos por las más cosas:

Quiero empezar este post informando sobre las dos pestañas que hay en mi substack. Una ya lleva desde la jornada 6, Valoraciones a favor y en contra y la otra la he incorporado esta semana, Los Mejores De La Jornada ACB,. Ambas se actualizan los lunes de madrugada.

Valoraciones a favor y en contra

Esta tabla muestra los puntos o por medias (todas como local, visitante etc,.) o por jornada de puntos, canasta de 3, rebotes, asistencias y valoración:

  • Bases a favor

  • Aleros a favor

  • Pivots a favor

  • Bases en contra

  • Aleros en contra

  • Pivots en contra

Tiene 4 filtros:

  • Total (por defecto) o media

  • Team (equipo)

  • Cancha (todos, todas, todas local, todas visitante, local o visitante.

  • Jornada

Yo la encuentro super útil, espero que vosotros igual, tanto para ver el historico desgranado, estudiar al rival o para tener contexto por si queremos apostar a algo (esto útlimo si decís que lo he dicho diré que mentís)

Los Mejores De La Jornada ACB

En esta están los mejores de la jornada filtrados, no por valoración, si no por el Rapm Estimate Dre, aunque si dais al nombre de la columna se puede filtrar por el valor que quieras.

¿Por qué por DRE y no por valoración? Porque en el Dre los números que tiene un jugador se ponderan según el peso que esa estadistica tiene en el juego y me parece más justo, que se tenga en cuenta para el analisis de un jugador, si ha perdido 5 valones aunque haya metido 25 puntos .

Si te gusta lo que lees, considera un pequeño apoyo.

¿Un café?


Los mejores de la Jornada según el Daily RAPM

La jornada dejó un grupo corto de actuaciones muy eficientes que marcaron diferencias en pocos minutos. El mejor registro lo firmó Timothé Luwawu-Cabarrot. Gracias a su gran acierto en el acierto exterior. Sumó 24 puntos en 18 minutos y mantuvo un uso alto sin afectar al flujo del juego.

Dylan Ennis repitió el patrón que hemos visto este curso cuando entra en ritmo desde fuera. Convirtió ocho triples y sostuvo a UCAM en un tramo ofensivo que permitió abrir ventaja. eso si sin asistenciás y con 4 rebotes; le bastó con castigar cada recepción abierta.

Jonah Radebaugh completó la tercera gran actuación del día. Se mantuvo agresivo hacia el aro, cerró bien el rebote y evitó errores. Su línea fue más discreta en anotación, pero equilibró el partido desde varias acciones cortas. Anontando la mitad su impacto fue igual al de Ennis.

Bakary Dibba fue el interior más fiable de la jornada. Sumó desde el rebote, corrió bien el campo y aprovechó cada finalización cerca del aro. No necesitó volumen para ser útil y su presencia atrás sostuvo al equipo cuando Burgos intentó volver al partido.

Margiris Normantas cerró el top cinco con un rendimiento estable pese a la derrota. Sumó desde las asistencias sobre todo. Su acierto interior compensó el cero desde la linea de 3, los -19 explican por qué su valoración no llegó más arriba.

El club de las 3 pérdidas y el 0 de 2

En la parte baja volvimos a ver un bloque de actuaciones castigadas por el porcentaje exterior y por pérdidas en momentos acumulados. Justin Jaworski no encontró tiros cómodos y su -12 en pista reflejó los problemas del equipo para generar ventajas con él en el perímetro.

Mark Hughes tuvo una noche parecida. El bajo acierto exterior y la falta de continuidad en defensa dejaron su impacto en negativo, incluso con algunos destellos en transición.

Alfonso Plummer cerró el trío de peores registros con una línea similar: mal porcentaje, poco peso en la creación y un parcial negativo amplio que penalizó su DRE.

En conjunto, la J8 mostró una brecha clara entre quienes sostuvieron su producción con eficiencia y quienes se suicidaron de 3 sin más que aportar cuando el acierto no llegó.


Cómo hice el Plot AST% vs TOV%

Este plot que hice para el post de Hugo Benitez el Casi MVP de la Jornada 7 y si no lo leiste, por lo que sea, aquí lo tienes:

Hugo Benitez el Casi MVP de la Jornada 7

Ivo Villanueva
·
Nov 21
Hugo Benitez el Casi MVP de la Jornada 7

Esta jornada 7 la separación entre los tres jugadores con mejor valoración ha sido de un punto y la pregunta es por qué no aparece Shannon Evans cuando usamos el DRE. Al cambiar el enfoque vemos otra lectura. Fue el jugador con mejor valoración, sí, aunque en este ranking cae al puesto siete por su

Read full story

En mi humilde mi opinión quedan muy chulos, por sus fotos difuminadas, los circulos con las imagenes y los comet Plot (geom_link) que me vi obligado a usar porque Pantzar y Benitez estaban tan juntos, que tuve que rascarme la cabeza para inventaarme algo que no fuera lo de siempre y que destacara de alguna manera la situación de Benítez, que era de quien hablaba en el post. Los comet también los usé para esta gráfica:

Había visto en las últimas semanas muchas tablas de conexiones y quería darle una vuelta a este tipo de de gráficas porque me sentía como que no me diferenciaba de nadie y se me ocurrió hacer algo así.

Bueno empecemos con el Tutorial y cómo siempre con las librerías:

library(tidyverse)   # la navaja suiza de los datos
library(prismatic)   # para cambiar tonos de color
library(cropcircles) # para las fotos dentro de circulo
library(ggrepel)     # para que el texto no se solape
library(grid)        # para las flechas del plot
library(magick)      # para la funcion traansparent
library(ggimage)     # para los logos y fotos
library(ggforce)     # para el geom_link()
library(ggtext)      # para el texto html

Cargamos el data set con los logos, colores, etc. y los boxscores:

clubs <- read.csv(”https://raw.githubusercontent.com/IvoVillanueva/acb2026/refs/heads/main/logos_calendario/clubs2026.csv”) %>%
  select(abb, logo, color) %>%
  mutate(color = ifelse(abb == “RMB”, “white”, color))


datos <- read_csv(”https://raw.githubusercontent.com/IvoVillanueva/pbp-acb-2025-26/refs/heads/main/data/boxscores_2025_26.csv”,
  show_col_types = FALSE
) 

Ponemos también la función que difumina las imagenes:

transparent <- function(img) {
  magick::image_fx(img, expression = “0.65*a”, channel = “alpha”)
}

Mi caption; el que no es el de SuperManager:


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**: *@ACBCOM* | **Gráfico**: *Ivo Villanueva* • {twitter} {tweetelcheff} • {insta} {instaelcheff} • {github} {githubelcheff}<br>”)

Primero necesito calcular el FGM y las AST de los sequipos por partido para la fórmula del AST%:

fgm_equipo <- datos %>%
  group_by(id_match, abb) %>%
  summarise(
    fgm_eq = sum(x2pt_success + x3pt_success),
    asis_eq = sum(asis),
    .groups = “drop” #con esto desagrupamos y nos evitamos el ungroup()
  )

Ahora junto el codigo de fgm_equipo con los bosxscore y calculo para los jugadores el AST% y el TOV% por partido:

por_partido <- datos %>%
  left_join(fgm_equipo, join_by(id_match, abb)) %>%
  mutate(
    fgm_jugador = x2pt_success + x3pt_success,
    fga = x2pt_tried + x3pt_tried,
    fta = x1pt_tried,

    # Fórmula AST% oficial
    ast_pct_real = if_else(
      (fgm_eq - fgm_jugador) > 0,
      asis / (fgm_eq - fgm_jugador),
      NA_real_
    ),

    # Fórmula TOV% oficial
    tov_pct_real = turnovers / (fga + 0.44 * fta + turnovers)
  )

Con este útlimo código ya extraigo lo datos por temporada y les uno el data set de los logos y colores por equipo. Depués con esto ya unido creo la columna de los logos envueltos en un ciruclo, coloco los datos de AST% de mayor a menor y filtro por jugadores que su numero de asistencias esten por encima de la media y que no se llemen Trent Forest.

por_temporada <- por_partido %>%
  group_by(jugador = license_license_str15, abb) %>%
  summarise(
    asis_tot = sum(asis, na.rm = TRUE),
    tov_tot = sum(turnovers, na.rm = TRUE),
    fgm_tot = sum(fgm_jugador, na.rm = TRUE),
    fga_tot = sum(fga, na.rm = TRUE),
    fta_tot = sum(fta, na.rm = TRUE),
    fgm_eq_tot = sum(fgm_eq, na.rm = TRUE),
    .groups = “drop”
  ) %>%
  left_join(clubs, join_by(abb)) %>%
  mutate(
    ast_pct_season = asis_tot / (fgm_eq_tot - fgm_tot),
    tov_pct_season = tov_tot / (fga_tot + 0.44 * fta_tot + tov_tot),
    logo = crop_circle(logo, border_colour = clr_darken(color), bg_fill = color, border_size = 14)
  ) %>%
  arrange(desc(ast_pct_season)) %>%
  filter(asis_tot > mean(asis_tot) & jugador != “Trent Forrest”)

Ahora necesito otro bloque separado para los 6 mejores jugadores (los que van a tener foto):

temporada_foto <- por_temporada %>%
  head(6) %>%
  mutate(
    foto = case_when(
      jugador == “Hugo Benitez” ~ “https://raw.githubusercontent.com/IvoVillanueva/acb2026/refs/heads/main/fotosSms2026/Hugo%20Benitez.png”,
      jugador == “Dani Pérez” ~ “https://raw.githubusercontent.com/IvoVillanueva/acb2026/refs/heads/main/fotosSms2026/Dani%20Pe%CC%81rez.png”,
      jugador == “Shannon Evans” ~ “https://raw.githubusercontent.com/IvoVillanueva/acb2026/refs/heads/main/fotosSms2026/Shannon%20Evans.png”,
      jugador == “Marcelinho Huertas” ~ “https://raw.githubusercontent.com/IvoVillanueva/acb2026/refs/heads/main/fotosSms2026/Marcelinho%20Huertas.png”,
      jugador == “Otis Livingston” ~ “https://raw.githubusercontent.com/IvoVillanueva/acb2026/refs/heads/main/fotosSms2026/Otis%20Livingston.png”,
      jugador == “Facu Campazzo” ~ “https://raw.githubusercontent.com/IvoVillanueva/acb2026/refs/heads/main/fotosSms2026/Facu%20Campazzo.png”,
      jugador == “Melwin Pantzar” ~ “https://raw.githubusercontent.com/IvoVillanueva/acb2026/refs/heads/main/fotosSms2026/Melwin%20Pantzar.png”
    ),
    foto = crop_circle(foto, border_colour = clr_darken(color), bg_fill = color, border_size = 14)
  )

Y ya tengo todo preparado para relizar la gráfica. Lo primero para ver los datos como es debido porque: cuantas menos perdidas mejor, mas perdidas peor; coloco el eje Y en reverse y filtro los jugadores que su porcentaje este por debajo de 15% que van a tener difuminado el logo para destacar que no son importantes. Para eso la pequeña función transparent:

por_temporada %>%
  ggplot(aes(x = ast_pct_season, y = tov_pct_season)) +
  scale_y_reverse() +
  geom_image(
    data = por_temporada %>% filter(ast_pct_season <= .15), aes(image = logo),
    size = .035,
    image_fun = transparent
  ) +

Despues añado los jugadores que están por encima del 15% y por debajo del 20% digamos la clase media, para eso en el filtro uso between(ast_pct_season, .15, .20).

Gracias por leer hasta aquí. Este contenido sigue. La información que viene a continuación es el resultado de horas de scraping, limpieza y procesamiento estadístico que no encontrarás en ningún otro sitio. Considera mejorar tu plan

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