Cómo hice las tablas de la Summer League (Parte 1)
Las aventuras y desdichas de un curioso riguroso, buscando datos para compartir información. Explico el proceso desde cuando aún no había decidido hacer la cobertura
Este post es un ejemplo de lo que ofrezco a los suscriptores de pago. Es un tutorial de por qué y cómo empiezo los análisis en este caso concreto, cubrir la Summer League, y cómo se construye la tabla de arriba. Los posts donde explico el proceso de extracción de datos y la manera de presentarlos quedan detrás del muro premium
La verdad es que no tenía pensado cubrir la Summer League, lo único que revoloteaba en mi cabeza era sacar una tabla un día, otra tabla otro, pero sin más ambición. Sobre todo porque soy un ¿cómo decirlo? fan de la Summer, pero por motivos variopintos, como por ejemplo: en lugar de analizar jugadores mi entretenimiento radica en intentar adivinar que están evaluando los equipos, o mejor dicho; que estarán entresacando de esto los fríos y calculadores scouts. Pero ¿a quién pretendo engañar a estas alturas? por lo que sea, nunca lo adivino.
El caso es que yo, para conseguir los datos de la NBA, uso, o mejor dicho… usaba, o aún más concretamente… abusaba, de la librería hoopR(), pero:
Pues eso, que da error y lejos de venirme abajo, fui directo a la página de la NBA en busca de endpoints para extraer la info: alguien tenía que hacerlo (pero qué se han creído).
La Summer se divide en tres competiciones, las dos primeras, la Salt Lake City (julio 4-7) y la California Classic (julio 3-6), que se disputan casi simultáneas, y la tercera, Las Vegas Summer League, que es la competición principal. En la primera el anfitrión es Utah, que invita a tres más; en la segunda, organizada por los Sacramento Kings y los Golden State Warriors, invita a otros seis. En Las Vegas Summer League están representados todos los equipos.
Entonces, lo primero que hice (como ya dije más arriba) fue ir a la página de la Salt Lake City. Tengo que decir que una de las razones, entre otras mil, por las que me encanta la NBA es que comparte la información de Synergy Stats de manera gratuita, información que en otras ligas tienen unos pocos privilegiados (y pagando)
Una vez allí, vamos a la tabla de Player Stats y botón derecho, inspeccionar.
Con esta tabla cargada en el navegador, en la pestaña de Red, buscamos cualquier petición que contenga "playerstats", y ahí aparece lo que buscaba, esperándonos.
Lo siguiente es ir a Encabezados y seleccionar la url de solicitud (y sí, esa cosa tan larga y fea es lo que hay que copiar). Un detalle: si te fijas, esa url es tan larga porque contiene todos los filtros de la tabla.
Debajo de esa url está la cabecera de solicitud completa, que también copiamos:
Vale, ahora que ya tenemos todo, iniciamos el script cargando las librerías, ajustando el día de la extracción, creando, si no está creada, la carpeta data (que es donde irán los csvs) y pegando los headers, porque la API de la NBA protege sus datos. Si detecta que un código automático (un robot) intenta entrar, lo bloquea. Los headers simulan que la petición la está haciendo una persona usando un navegador web (Google Chrome en un Mac).
library(dplyr)
library(janitor)
library(hablar)
library(httr)
library(jsonlite)
# Obtener la fecha de hoy
hoy <- Sys.Date()
# Asegúrar de que exista la carpeta de salida de datos
if (!dir.exists("data")) dir.create("data")
# Cabeceras
headers <- c(
"accept" = "*/*",
"accept-encoding" = "gzip, deflate, br, zstd",
"accept-language" = "es-ES,es;q=0.9,en;q=0.8",
"cache-control" = "no-cache",
"connection" = "keep-alive",
"host" = "stats.nba.com",
"origin" = "https://www.nba.com",
"pragma" = "no-cache",
"referer" = "https://www.nba.com/",
"sec-ch-ua" = "\"Google Chrome\";v=\"149\", \"Chromium\";v=\"149\", \"Not)A;Brand\";v=\"24\"",
"sec-ch-ua-mobile" = "?0",
"sec-ch-ua-platform" = "\"macOS\"",
"sec-fetch-dest" = "empty",
"sec-fetch-mode" = "cors",
"sec-fetch-site" = "same-site",
"user-agent" = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"
)
Sabiendo dónde y cómo esta la info, empezamos por la California Classic así que pegamos su url de solicitud. La función GET() conecta con el enlace oficial de las estadísticas de la NBA usando el "disfraz" (headers) anterior. La web responde con un archivo de texto codificado (JSON), y fromJSON() lo traduce a un formato que R puede entender (listas y tablas).
# California Classic ------------------------------------------------------
# Url de solicitud de la California Classic
url <- "https://stats.nba.com/stats/leaguedashplayerstats?College=&Conference=&Country=&DateFrom=&DateTo=&Division=&DraftPick=&DraftYear=&GameScope=&GameSegment=&Height=&LastNGames=0&LeagueID=13&Location=&MeasureType=Base&Month=0&OpponentTeamID=0&Outcome=&PORound=0&PaceAdjust=N&PerMode=Totals&Period=0&PlayerExperience=&PlayerPosition=&PlusMinus=N&Rank=N&Season=2026&SeasonSegment=&SeasonType=Regular%20Season&ShotClockRange=&StarterBench=&TeamID=0&TwoWay=&VsConference=&VsDivision=&Weight="
# añadimos a la petición GET los headers de solicitud
res <- GET(url = url, add_headers(.headers=headers))
# lo convertimos a json
json_resp <- fromJSON(content(res, "text"))
# unimos nombres de columna con los datos
as_tibble(json_resp$resultSets$rowSet[[1]],
.name_repair = ~json_resp$resultSets$headers[[1]]) %>%
retype() %>% # ajustamos si son letras o numeros de cada columna
clean_names() %>% # limpiamos los nombres de columna
write.csv(paste0("data/summer_california_general", gsub("-", "_", today), ".csv"), row.names = F) # guardamos csv con fecha
Lo guardamos todo en un csv por si pasa cualquier cosa (básicamente porque no es necesario ser más pirata de la cuenta, haciendo peticiones al servidor continuamente). Si todo ha salido bien, veremos una tabla así.
Si os fijáis, en la url, en la parte que pone LeagueID=13&, el 13 corresponde con el número de liga. Por resumir, ya digo yo que el 13 es la California Classic (por eso lo de empezar por la California, porque intento ser un tío ordenado), el 16 es la Salt Lake City y el 15 es Las Vegas. Entonces, para no complicarnos mucho… bueno, ni mucho ni poco, para conseguir los datos de la otra liga copiamos todo y cambiamos el 13 de LeagueId por el 16, y damos, lógicamente, otro nombre al csv.
# Salt Lake City ----------------------------------------------------------
url_salt <- "https://stats.nba.com/stats/leaguedashplayerstats?College=&Conference=&Country=&DateFrom=&DateTo=&Division=&DraftPick=&DraftYear=&GameScope=&GameSegment=&Height=&LastNGames=0&LeagueID=16&Location=&MeasureType=Base&Month=0&OpponentTeamID=0&Outcome=&PORound=0&PaceAdjust=N&PerMode=Totals&Period=0&PlayerExperience=&PlayerPosition=&PlusMinus=N&Rank=N&Season=2026&SeasonSegment=&SeasonType=Regular%20Season&ShotClockRange=&StarterBench=&TeamID=0&TwoWay=&VsConference=&VsDivision=&Weight="
res <- GET(url = url_salt, add_headers(.headers=headers))
json_resp <- fromJSON(content(res, "text"))
as_tibble(json_resp$resultSets$rowSet[[1]],
.name_repair = ~json_resp$resultSets$headers[[1]]) %>%
retype() %>%
clean_names() %>%
write.csv(paste0("data/summer_saltlake_general", gsub("-", "_", today), ".csv"), row.names = F)
Una vez que ya tenemos los datos, pasamos a modificarlos: leyendo los csvs y juntándolos con rbind, ajustando los tipos, limpiando cabeceras y unificando los equipos del California Classic (Golden State juega ahí dividido en BLUE y GOLD, así que los volvimos a agrupar bajo GSW), y seleccionando con transmute(), que es una mezcla entre select() y mutate(). Como vamos a usar los logotipos de equipo cuadrados que he creado en GitHub, definimos las rutas a los logos para usarlos en la tabla. También creamos las columnas para el DRE y la Valoración, entre otras.
El DRE (Daily RAPM Estimate) no es cosa mía: lo creó Kevin Ferrigan, que lo lanzó en Nylon Calculus hace once años, antes de actualizarlo en 2017.
#cargar librerias
library(tidyverse)
library(gt)
library(gtExtras)
library(gtUtils)
# hacemos rbind con los dos csvs
df –> read_csv("data/summer_saltlake_general2026_07_23.csv") %>%
rbind(read_csv("data/summer_california_general2026_07_23.csv")) %>%
#filter(player_name %in% summer_vegas_rookies2026_07_20) |>
retype() %>%
clean_names() %>%
transmute(
logo = case_when(team_abbreviation == "GWG" ~ "GSW",
team_abbreviation == "GWB" ~ "GSW",
.default = team_abbreviation
),
logo = paste0("https://raw.githubusercontent.com/IvoVillanueva/NBA/refs/heads/main/logos_cuadrados/", logo, ".png"),
player_name,
fancy_scor,
min = as.numeric(lubridate::duration(min)),
pts,
reb,
oreb = rebounds_offensive,
dreb = rebounds_defensive,
ast,
tov,
stl,
blk,
pf,
fgm,
fga,
fg3m,
fg3a,
ftm,
fta,
plus_minus,
dre = (.79 * pts - .72 * (fga - fg3a) - .55 * fg3a - .16 * fta + .13 * oreb + .40 * dreb + .54 * ast + 1.68 * stl + .76 * blk - 1.36 * tov - .11 * pf),
val = (pts + reb + ast + stl + blk + pfd) -
(fga - fgm) - (fta - ftm) - tov - blka - pf,
dre_percentil = round(percent_rank(dre)*100,1)
) %>%
mutate(
fgm = paste0(fgm, "/", fga),
fg3m = paste0(fg3m, "/", fg3a),
ftm = paste0(ftm, "/", fta)
) %>%
arrange(desc(val)) %>%
# take top 20
filter(row_number() <= 20) Sacamos hasta el top 20 porque, además, visualmente tiene la virtud de dejarnos la tabla cuadrada para que nos encaje bien con las publicaciones de X.
Ahora que ya tenemos toda la info podemos crear la tabla. Empezamos por lo básico e iremos añadiendo capas.
Como se ve, aún queda mucho por hacer. Aunque los datos ya se ven bien, hay que dar forma, por ejemplo, a la columna de los logos, porque ahora solo se ven los enlaces directos que hemos creado. Vamos también a añadir título y subtítulo.
df %>%
gt() %>%
tab_header(
title = md("<div style='display: flex; align-items: left; justify-content: left; height: 34px; text-align: center; font-weight: 600; font-size: 32px;'>
<img src='https://upload.wikimedia.org/wikipedia/en/2/27/NBA_Summer_League_logo.png' style='padding-right: 12px; height: 34px;'>
<span>Los Mejores del primer día de Summer League</span></div>"),
subtitle = "Los 20 mejores en DRE de la Salt Lake City Summer league y la California Classic"
) %>%
gt_img_rows(columns = logo, height = 30)Queda muy bien y seguramente a muchos (sobre todo a los de “done is better than perfect”) ya les valdría. Pero aún podemos mejorar la tabla mucho.
Como se puede observar en el título, he metido un código html para enriquecerlo y poner el logo de la summer centrado. Los logos han cambiado gracias a la función gt_img_rows(columns = logo, height = 30). A continuación, vamos a maquillar los nombres de las columnas para que queden iguales, intentando que sean de 3 letras.
df %>%
gt() %>%
tab_header(
title = md("<div style='display: flex; align-items: left; justify-content: left; height: 34px; text-align: center; font-weight: 600; font-size: 32px;'>
<img src='https://upload.wikimedia.org/wikipedia/en/2/27/NBA_Summer_League_logo.png' style='padding-right: 12px; height: 34px;'>
<span>Los Mejores del primer día de Summer League</span></div>"),
subtitle = "Los 20 mejores en DRE de la Salt Lake City Summer league y la California Classic"
) %>%
gt_img_rows(columns = logo, height = 30) %>%
cols_label(
logo = "",
player_name = "Player",
min = "MIN",
pts = "PTS",
reb = "REB",
ast = "AST",
tov = "TOV",
stl = "STL",
blk = "BLK",
fgm = "FGM/A",
fg3m = "3PM/A",
ftm = "FTM/A",
plus_minus = "+/-",
dre = "DRE"
)Mejorando.
Ahora, vamos a ponerle el signo + a los números positivos de la columna del “+/-” y dejar en una cifra los valores del DRE y de los minutos.
df %>%
gt() %>%
tab_header(
title = md("<div style='display: flex; align-items: left; justify-content: left; height: 34px; text-align: center; font-weight: 600; font-size: 32px;'>
<img src='https://upload.wikimedia.org/wikipedia/en/2/27/NBA_Summer_League_logo.png' style='padding-right: 12px; height: 34px;'>
<span>Los Mejores del primer día de Summer League</span></div>"),
subtitle = "Los 20 mejores en DRE de la Salt Lake City Summer league y la California Classic"
) %>%
gt_img_rows(columns = logo, height = 30) %>%
cols_label(
logo = "",
player_name = "Player",
min = "MIN",
pts = "PTS",
reb = "REB",
ast = "AST",
tov = "TOV",
stl = "STL",
blk = "BLK",
fgm = "FGM/A",
fg3m = "3PM/A",
ftm = "FTM/A",
plus_minus = "+/-",
dre = "DRE"
) %>%
fmt_number(plus_minus, force_sign = T, decimals = 0) %>%
fmt_number(dre, decimals = 1) Perfecto, se ve bien. Podríamos dejarla así y estaría perfectamente bien.
Pero para dejarla según mis estándares, vamos a añadir algunos toques finales. Coloreamos la columna DRE, añadimos franjas a las filas y hacemos algunos ajustes en las options de la tabla, hasta que quede clean, clean, y añadimos la función gt_save_crop() de la librería gtUtils(), con la que guardamos la tabla.
df %>%
gt() %>%
tab_header(
title = md("<div style='display: flex; align-items: left; justify-content: left; height: 34px; text-align: center; font-weight: 600; font-size: 32px;'>
<img src='https://upload.wikimedia.org/wikipedia/en/2/27/NBA_Summer_League_logo.png' style='padding-right: 12px; height: 34px;'>
<span>Los Mejores del primer día de Summer League</span></div>"),
subtitle = "Los 20 mejores en DRE de la Salt Lake City Summer league y la California Classic"
) %>%
gt_img_rows(columns = logo, height = 30) %>%
cols_label(
logo = "",
player_name = "Player",
min = "MIN",
pts = "PTS",
reb = "REB",
ast = "AST",
tov = "TOV",
stl = "STL",
blk = "BLK",
fgm = "FGM/A",
fg3m = "3PM/A",
ftm = "FTM/A",
plus_minus = "+/-",
dre = "DRE"
) %>%
fmt_number(plus_minus, force_sign = T, decimals = 0) %>%
fmt_number(columns = c(dre,min), decimals = 1) %>%
data_color(
columns = "dre",
alpha = .75,
reverse = F,
palette = "ggsci::blue_grey_material") %>%
tab_options(
data_row.padding = "0px",
table.font.names = "Bebas Neue",
table_body.hlines.color = "transparent",
column_labels.border.top.color = "black",
column_labels.border.top.width = px(1),
column_labels.border.bottom.style = "none",
column_labels.font.weight = "strong",
row_group.border.top.style = "none",
row_group.border.top.color = "black",
row_group.border.bottom.width = px(1),
row_group.border.bottom.color = "black",
row_group.border.bottom.style = "solid",
row_group.padding = px(1.5),
heading.align = "left",
heading.border.bottom.style = "none",
table_body.border.top.style = "none",
table_body.border.bottom.color = "white",
table.border.bottom.style = "none",
table.border.top.style = "none",
source_notes.border.lr.style = "none"
) %>%
# save table
gt_save_crop(paste0("png/summer_league_", Sys.Date(), ".png"), whitespace = 100, zoom = 2)
Está ¿feo? que yo lo diga (bueno, eso dicen, que no se debe ensalzar las cosas de uno, que es de vanidoso), pero creo que el resultado es espectacular. Sin embargo, aun estando muy satisfecho, quería ir más allá: publicar la información diaria de lo que quedaba, completa, no solo la media acumulada; aunque eso, con suerte, solo me reportara los tres likes de mis colegas.
La Salt Lake y la California son 3 días y empezaba la madre de todas las Summer League: la de Las Vegas, ahí tendría que esforzarme más y dar con la manera de gestionar la info. Y era un reto que, para qué nos vamos a engañar, me intimidaba mucho, sacar diariamente los mejores jugadores y mostrarlo haciendo dos gráficas: una para los Rookies y otra para todos en general.
Hasta ese momento era fácil, porque bueno, eran 3 partidos y con la tabla de estadísticas valía para ofrecer la información. Pero Las Vegas iban a ser 11 días y quería dar una info, no ya que no ofreciera nadie, porque de hecho, si lo ofrece mucha gente; sino publicarla diariamente y que más allá de lo muy reseñable, cualquiera pudiera fijarse en su jugador favorito y de paso sorprender, o para ser más exactos, levantar curiosidad o admiración, por mi trabajo.
El método que encontré para dominar ese toro que era sobrevivir a los once días de Sin City (Las Vegas) es un secreto que me guardo para la próxima entrega: un post premium, paso a paso (como este), reservado para los mecenas que hacen posible que este proyecto siga vivo. Si tú también quieres dar el paso, sumarte a ellos y descubrir cómo domé al Miura, te espero con los brazos abiertos.
Muchas gracias por llegar hasta aquí

















