Cómo hice las tablas de la Summer League (Parte 2)
Explico la comedura de tarro prevía a encontrar la manera de conseguir las estadisticas diarias y como di con la solución
Esta es la segunda entrega, la que corresponde a lo sucedido en Sin City, y para evitar que se cumpliera lo que se suele decir: “lo que pasa en Las Vegas se queda en Las Vegas”, me devané los sesos hasta encontrar la manera de darlo a conocer.
El caso es que, al contrario de como compartí los datos en la Salt Lake y en la California (donde bastaba con tirar de las estadísticas acumuladas de los tres días de competición), estaba claro que aquí no lo podía compartir de la misma manera; en Las Vegas cada partido es como una temporada en sí misma, y no quería que cualquier actuación que se saliera de madre, por lo bueno o por lo malo, terminara diluyéndose en un promedio general.
¿Cuál era entonces esa manera? Unir los boxscores de los partidos que se jugaban cada día; ordenarlos en un ranking y, además, aportar ese plus de “esto lo hago yo y nadie más” (del que mi ego es incapaz de abstraerse); dividirlos en dos enfoques: Rookies y General.
En la primera parte del tutorial contaba que la librería hoopR no funcionaba y tuve que buscarme la vida. Tenía la esperanza de que no funcionaba para la Salt Lake y para la California, por ser ligas menores (películas que me hacía) y que para Las Vegas sí tendría que funcionar; pero tampoco lo hizo. Lo primero que hice fue preguntarle a Claude (sí, eso hice), si habían reportado algún error de la librería. Me contestó algo, pero tampoco me detuve a leer un texto tan largo, porque la impaciencia por conseguir los datos lo antes posible hizo que mi mente buscara otra vía y pasara de página. Para resumir un drama (así lo estaba viviendo yo) que fue más largo tratando de averiguar qué era lo que estaba pasando, Claude se enrocó en explicaciones sobre la posibilidad de que tuviera la IP bloqueada y que tenía que olvidarme de stats.nba y que debía sacar la información desde cdn.nba o, tirar directamente de ESPN.
Primer día
Aún así tenía 2 días más para encontrar una solución; de momento, y aunque no fuera mi objetivo final ni mucho menos, podía tirar de las estadísticas generales porque solo se había jugado un partido. Si vas a X, te darás cuenta de ello.
También se puede notar que cambié el DRE por el VAL (valoración), sobre todo porque el DRE es muy restrictivo con las malas estadísticas y me destacaba jugadores que nadie conocía o por los que nadie tenía ningún interés. No quería darmelas de gurú o de nicho y destacar a desconocidos tampoco iba a ayudar a que nadie los conociera o despertar interes en ellos; así de claro.
Para conseguir estos datos cambié dos parámetros del link: DraftYear=2026 y LeagueID=15. Un dato: si tenemos interés en algún draft en particular, podemos poner el año que necesitemos:
url <- "https://stats.nba.com/stats/leaguedashplayerstats?College=&Conference=&Country=&DateFrom=&DateTo=&Division=&DraftPick=&DraftYear=2026&GameScope=&GameSegment=&Height=&LastNGames=0&LeagueID=15&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="
Usé el mismo código que en la California y en la Salt Lake City que publiqué en el tutorial anterior, pero solo de Rookies (como ya dije, iba buscando lo que interesaba), y si quieres repasar cómo lo hice, aquí tienes el enlace de la primera parte:
Cómo hice las tablas de la Summer League (Parte 1)
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 extracc…
Segundo día
En el día dos hice lo que se suele llamar liada máxima. Como podéis imaginar, yo lo que buscaba era sacar las estadísticas del segundo día desde el listado general (todavía no había encontrado el json de los boxscores), pero filtrarlas por los jugadores que habían jugado el segundo día. La idea de bombero que se me ocurrió fue: busco el calendario; filtro los partidos de ese día; uno mediante un join los equipos que habían jugado; hago la tabla (casi nada).
Buscando por la web de la NBA mediante el botón inspeccionar encontré este endpoint:
https://core-api.nba.com/cp/api/v1.9/feeds/gamecardfeed?gamedate=07/11/2026&platform=web"Una vez que ya tenía el link creamos la función get_gamecards, que recibe una fecha y la convierte al formato que pide la API de la NBA para construir la URL del feed de ese día. Con esa URL hacemos la petición GET con las cabeceras necesarias y convertimos la respuesta JSON en una lista de R con fromJSON.
Dentro de esa lista, raw$modules[[1]]$cards guarda las tarjetas de cada partido. Recorremos cada una con map_dfr, sacamos su cardData y montamos una fila con el id del partido, la fecha, el estado del encuentro y los códigos y puntuaciones de ambos equipos. Al terminar tenemos un tibble con un partido por fila.
La segunda parte del código carga el histórico guardado en data/summer_games.csv, pide los partidos de ayer y los une con bind_rows, pasando antes el gameId del histórico a texto para que coincida con el de los nuevos datos. Si un partido ya estaba guardado, por ejemplo porque el script corrió dos veces el mismo día, distinct(gameId, .keep_all = TRUE) se queda con la primera aparición y tira el resto. Todo vuelve a escribirse en el mismo CSV.
Así queda montado para correr cada día, sumar los partidos que faltan y no arrastrar filas repetidas:
El resultado es ¿era? este, un csv con los marcadores; pero esto en si mismo tampoco es como: wow; en fin.
Con este csv tampoco se puede hacer mucho hay que transformarlo para poder hacer un join:
calendario <- read_csv("data/summer_games.csv") %>%
filter(fecha == Sys.Date() - 1) |>
rename(
team_abbreviation = home,
versus = away,
puntos = homePts,
puntos_versus = awayPts
) %>%
rbind(read_csv("data/summer_games.csv") %>%
filter(fecha == Sys.Date() - 1) |>
rename(
team_abbreviation = away,
versus = home,
puntos = awayPts,
puntos_versus = homePts
)) %>%
mutate(
dif = puntos - puntos_versus,
fancy_scor = paste0(
"vs. ", versus, " ",
ifelse(dif > 0,
paste0("<span style='color:forestgreen'>(W +", dif, ")</span>"),
paste0("<span style='color:firebrick'>(L ", dif, ")</span>")
)
)
)Solo un friendly reminder de que todo esto que estoy explicando fue el quilombo que armé para aislar los partidos del segundo día.
Entonces dejé solo los partidos de ayer (fecha == Sys.Date() - 1). Ese filtrado lo hice dos veces, una vez leyendo el CSV con los nombres de columna tal cual (home, away, homePts, awayPts) y renombrándolos desde la perspectiva del equipo local: team_abbreviation es el equipo, versus es el rival, puntos son sus puntos y puntos_versus los del contrario.
La segunda lectura del mismo CSV hace lo mismo pero al revés, renombrando desde la perspectiva del visitante: ahora team_abbreviation es el away y versus es el home. Las dos versiones se unen con rbind, así que al final cada partido aparece dos veces en el resultado, una fila por equipo, con sus propios puntos primero y los del rival después. Es la forma habitual de pasar de un dataset “un partido por fila” a uno “un equipo por fila”, necesario si luego quieres agrupar o listar por equipo en vez de por enfrentamiento.
Con los datos ya en ese formato, el mutate calcula la diferencia de puntos (dif) y construye fancy_scor, un texto en HTML que dice contra quién jugó y el resultado, coloreado en verde con una “W” si gana o en rojo con una “L” si pierde, listo para insertarse directamente en una tabla que interprete HTML (como las que haces con gt).
Un apunte al margen, no del código en sí sino de cómo está escrito: verás que mezclé los pipe %>% y > en la misma cadena, fue algo que corregí en la última actualización; personalmente me gusta más usar el %>% por cosas que ya explicaré algún día, y que ahora no vienen al caso.
Usé el data frame calendario y eliminé los de NAS como filtro:
Más tarde, cuando todo acabó, me di cuenta de que lo único que tenía que haber hecho era cambiar del link esta parte DateFrom=07/10/2026&DateTo=07/10/2026, pero sigamos con el proceso
Resto de días
Se había acabado el tiempo, llegaba el momento del tercer día y fui a lo fácil: me puse a preguntar a todo bicho (IA) ¿viviente? cuál era el endpoint de los boxscores de la NBA, y después de aproximaciones varias, ChatGPT (sin suscripción) me dio la mejor respuesta. Y esta fue:
https://cdn.nba.com/static/json/VEGAS/liveData/boxscore/boxscore_", game_id, ".jsonA partir de aquí lo tenía facil solo hacer una función con los ids de los partidos de ese día. Además encontré, ese día estaba de suerte, un endpoint con los calendarios sin tener que hacer malavares por jsons infinitos
Como se puede ver, le puse los 3 ids de las ligas de verano porque soy un diógenes y me gusta almacenar todo tipo de datos, por ejemplo, los resultados (ya ves tú) de las summer leagues del 2026.
Anyway, el caso es que para mi infortunio, el eclipse del 12/08/2026 vino un día antes, y el enlace para los boxscores de Las Vegas, justo hoy que la NBA ha anunciado el calendario, ha dejado de funcionar, así que para qué explicar una parte que funcionaba y que desde hoy no lo hace.
Los suscriptores premium me acompañarán a ver cómo es el proceso sencillo, o por lo menos más sencillo con un bloque con el código completo además de un bloque extra para extraer las tablas: Traditional, Advanced, Four Factors, Misc, Scoring, USG y sobre todo, los Play by Play.
Entonces esta forma es: poner un día cualquiera de los correspondientes a las ligas de verano, y con esos ids extraemos los boxscores de ese día con la función summer_league, y en este bloque lo uno todo.
Por ahorrar espacio, he ido dejando los comentarios dentro del propio bloque de código, así que puedes copiarlo y pegarlo tal cual en tu script:









Bonus Tracks
Y ahora los remates finales, ¿qué haríamos con todo esto? Pues aquí está el verdadero premio: prácticamente todo lo que necesitas para calcular lo que se te ocurra. Así que, primero, el bloque extra para extraer las tablas: Traditional, Advanced, Four Factors, Misc, Scoring, USG:
Así llegan los datos en crudo antes de maquearlos; esto es una pequeña captura de todo lo que hay dentro de Four Factors.
Y así llegan los datos en crudo correspondiente a todo lo que hay dentro del data frame USG%.
Y ahora para finalizar la madre del cordero, el código que extrae el Play by Play:
Todo esto que acabas de ver, y cómo se consigue, es prácticamente todo lo necesario para construir las tablas que suelo publicar en X.
Al final, lo más complicado no fue escribir el código. Fue encontrar dónde estaban los datos y entender cómo estaban organizados. Una vez resuelto eso, las tablas eran casi la parte fácil. Espero que sobre todo este tutorial merezca el dinero que vale y haya sido entretenido. Muchas gracias por llegar hasta aquí.














