Visualización del número de victorias para los equipos de la NBA utilizando gráficos de barras animados en R

Primero, alguna información de fondo. Mi nombre es Vladislav y conocí a R en agosto del año pasado. Decidí aprender un lenguaje de programación debido a una naturaleza aplicada. Desde pequeño, me gustaba mantener estadísticas deportivas. Con la edad, este pasatiempo se transformó en un deseo de analizar de alguna manera estas cifras y, basándose en el análisis de datos, dar, si es posible, pensamientos inteligentes. El problema es que en los últimos años, los deportes han sido barridos por una ola de datos, docenas de compañías compiten entre sí, tratando de calcular, describir y empujar a la neurona cualquier acción de un jugador de fútbol, ​​jugador de baloncesto, jugador de béisbol en la cancha. Y Excel no es categóricamente adecuado para el análisis. Entonces decidí estudiar R para que el análisis más simple no tomara medio día. Ya en el curso del estudio, se agregó un interés en la programación como tal, pero esta ya es la letra.


Quiero notar de inmediato que gran parte de lo que escribiré en el futuro ya he estado en los simpsons estaba en Habr en el artículo Creamos histogramas animados usando R. Este artículo, a su vez, es una traducción del artículo Crear gráficos de barras animadas de tendencias usando R de Medium. Por lo tanto, para diferir de alguna manera de los artículos anteriores, trataré de describir más completamente lo que estoy haciendo, así como aquellos momentos que no están en el artículo original. Por ejemplo, para llenar las columnas, utilicé los colores de los comandos de la NBA, no la paleta ggplot2 estándar, sino que en el procesamiento de datos del paquete data.table , no dplyr . He hecho todo este trabajo como una función, así que ahora es suficiente simplemente escribir el nombre del equipo y los años para los que necesita contar el número de victorias.


Datos


Para elaborar el calendario, utilicé datos sobre el número de victorias para cada uno de los 30 equipos de la NBA en las últimas 15 temporadas. Se recopilaron de stats.nba.com utilizando la extensión NBA Data Retriever , que, mediante el uso de la API NBA, produce archivos csv con las estadísticas necesarias. Aquí están los detalles completos de mi proyecto en Github .


Bibliotecas utilizadas


 library(data.table) library(tidyverse) library(gganimate) 

Para el procesamiento de datos, uso data.table (simplemente porque conocí este paquete antes). También descargo un conjunto de paquetes tidyverse , y no un ggplot2 separado para no preocuparme, si durante el análisis aparece alguna idea que requiere la carga adicional de un paquete de este conjunto. En este caso particular, se puede prescindir de ggplot2 , otros paquetes de conjuntos no están involucrados. Bueno, gganimate pone en movimiento los gráficos.


Trabajar con datos


Primero debes poner los datos en orden. Básicamente, para construir gráficos, necesitamos 2 de 79 columnas de una tabla con datos sin procesar. Puede seleccionar las columnas necesarias primero, puede reemplazar algunos valores primero. Fui por el segundo camino.


La tabla en data.table tiene la forma dt[i, j, by] , donde by es "responsable" de la agrupación de elementos. Agruparé por la columna TeamName. Y hay un inconveniente. Esta columna muestra los nombres de los equipos: Lakers, Celtics, Heat, etc. Pero durante el período bajo revisión (de la temporada 2004/05) varios equipos cambiaron sus nombres: los New Orleans Hornets se convirtieron en New Orleans Pelicans, Charlotte Bobcats devolvió el nombre histórico Charlotte Hornets, y Seattle Supersonics se convirtió en Oklahoma City Thunder. Esto puede causar confusión. Las siguientes conversiones ayudan a evitar esto:


 table1 <- table[TeamCity == "New Orleans" & TeamName == "Hornets", TeamName := "Pelicans"][ TeamCity == "New Orleans/Oklahoma City" & TeamName == "Hornets", TeamName := "Pelicans"][ TeamName == "Bobcats", TeamName := "Hornets"][ TeamName == "SuperSonics", TeamName := "Thunder"] 

Para este período de tiempo, los cambios son mínimos, pero si lo expande, será muy difícil agruparlos por TeamName y deberá usar una columna más confiable. En estos datos, este es TeamID.


Para comenzar, nos deshacemos de la información "extra", dejando solo las columnas que necesitamos para trabajar:


 table1 <- table1[ , .(TeamName, WINS)] 

En data.table la construcción. () Reemplaza la función de list . Una opción más "clásica" para seleccionar columnas es table1 <- table1[, c("TeamName", "WINS")] . Después de eso, la tabla toma la siguiente forma:


Nombre del equipoGana
Soles62
Calor59
Espuelas59
Pistones54

Para la animación de cada temporada por separado, esto es suficiente, pero para calcular el número total de victorias para un período seleccionado, debe calcular el total acumulado de victorias.


table1 <- table1[, CumWins := cumsum(WINS), by = "TeamName"]


Usando la función cumsum obtenemos los números que necesitamos. Usar: = en lugar de = le permite agregar una nueva columna a la tabla; no la sobrescribo con la misma columna CumWins. by = "TeamName" agrupa los datos por el nombre del equipo y la cantidad acumulada se by = "TeamName" para cada uno de los 30 equipos por separado.


A continuación, agrego una columna con el año en que comenzó cada temporada. La temporada de la NBA se extiende de octubre a mayo, por lo que se divide en dos años calendario. En la designación de la temporada, el año de su inicio, es decir Temporada: 2018 en la tabla es la temporada 2018/19 en realidad.


La tabla original tiene estos datos. La columna SeasonID muestra una figura en forma de 2 (el año en que comenzó la temporada), por ejemplo, 22004. Puede eliminar los dos primeros utilizando el paquete stringr o las funciones básicas de R, pero fui un poco diferente. Resultó que primero uso esta columna para indicar las estaciones requeridas, luego elimino y creo una columna con fechas nuevamente. Acciones extra


Lo hice de la siguiente manera:


table1 <- table1[,year := rep(seq(2004, 2018), each = length(unique(table1$TeamName)))]


Tuve la suerte de que durante el período de tiempo seleccionado el número de equipos en la NBA no cambió, así que solo repetí los números de 2004 a 2018 30 veces. Nuevamente, si pasas a la historia, este método será inconveniente debido al hecho de que el número de equipos en cada temporada será diferente, por lo que es preferible usar la opción para borrar la columna SeasonID.


Luego agregue la columna cumrank.


table1 <- table1[, cumrank := frank(-CumWins, ties.method = "random"), by = "year"]


Representa la clasificación de los equipos en cada temporada por el número de victorias y se utilizará como los valores del eje X. frank un análogo de data.table más data.table del rank base, menos significa clasificación en orden descendente (esto también se puede hacer usando el argumento decreasing = TRUE . en qué orden irán los equipos con el mismo número de victorias, por ties.method = "random" tanto ties.method = "random" . Bueno, todo esto se agrupa en un año.


Y la última conversión de la tabla está agregando la columna value_rel .


table1 <- table1[, value_rel := CumWins/CumWins[cumrank==1], by = "year"]


Esta columna considera la relación entre el número de victorias de cada equipo y el indicador más alto del año. Para el mejor equipo, este indicador es 1, para el resto es menor, dependiendo del éxito de la temporada.


Después de todas las adiciones, la tabla tiene la siguiente forma:


Nombre del equipoGanaCumwinsañocumrankvalor_rel
Espuelas5959200430.9516129
Espuelas63122200511.0000000
Espuelas58180200620.9729730
Espuelas56236200711.0000000

Solo se presenta un equipo en la tabla para ilustrar la acumulabilidad. Todas estas acciones se realizan, como en el cambio de nombre, con una cascada de corchetes


 table1 <- table1[ ,.(TeamName, WINS)][ , CumWins := cumsum(WINS), by = "TeamName"][ ,year := rep(seq(2004, 2018), each = length(unique(table1$TeamName)))][ , cumrank := frank(-CumWins, ties.method = "random"), by = "year"][ , value_rel := CumWins/CumWins[cumrank==1], by = "year"] 

Cambie el relleno de las columnas del estándar a los colores de los equipos.


Puede proceder inmediatamente a la construcción de gráficos, pero hay, como me parece, un punto importante: el color de las columnas en el gráfico. Puede dejar la paleta ggplot2 estándar, pero esta es una mala opción. En primer lugar, me parece que es fea. Y en segundo lugar, hace que sea difícil encontrar un equipo en la lista. Para los fanáticos de la NBA, cada equipo está asociado con un color específico: Boston es verde, Chicago es rojo, Sacramento es púrpura, etc. Por lo tanto, usar el color del comando en las columnas de relleno ayuda a identificarlo más rápido, a pesar de la abundancia de azul y rojo.


Para hacer esto, cree una tabla table_color con el nombre del comando y su color principal. Colores tomados de teamcolorcodes.com .


Nombre del equipoTEAM_color
Halcones# E03A3E
Celtas# 007A33
Redes# 000000

Con la tabla de colores, debe hacer una manipulación más. Porque Cuando se utilizan factores de trazado, el orden de los equipos cambia. El primero en la lista será Philadelphia 76, como único propietario del nombre "digital", y luego de acuerdo con el alfabeto. Por lo tanto, debemos organizar los colores en el mismo orden y luego extraer el vector que los contiene de la tabla. Lo hice de la siguiente manera:


  table_color <- table_color[order(TeamName)] cols <- table_color[, "TEAM_color"] 

Trazado


Realmente construimos un solo cuadro, que contiene los 450 indicadores de victorias (15 temporadas * 30 equipos), y luego lo "dividimos" por la variable necesaria (en nuestro caso, por años) usando las funciones del paquete gganimate .


 gg <- ggplot(table1, aes(cumrank, group = TeamName, fill = as.factor(TeamName), color = as.factor(TeamName))) + geom_tile(aes(y = CumWins/2, height = CumWins, width = 0.7), color = NA, alpha = 0.8) 

Primero, creamos un gráfico usando la función ggplot . En el argumento aes , especifique cómo se mostrarán las variables de la tabla en el gráfico. Los agrupamos por TeamName, el fill y el color serán responsables del color de las columnas.


Las columnas verdaderas lo llaman no es del todo cierto. Usando geom_tile "dividimos" los datos en el gráfico en rectángulos. Aquí hay un ejemplo de un gráfico de este tipo:

Se puede ver cómo el gráfico se "divide" en cuadrados (se obtienen de rectángulos usando la capa coord_equal() ), tres en cada columna. Pero gracias al width argumento menor que uno, nuestro mosaico toma la forma de columnas.


  geom_text(aes(y = 0, label = paste(TeamName, " ")), vjust = 0.2, hjust = 1, size = 6) + geom_text(aes(y = CumWins, label = paste0(" ",round(CumWins))), hjust = 0, size = 7) + coord_flip(clip = "off", expand = FALSE) + scale_fill_manual(values = cols) + scale_color_manual(values = cols) + scale_y_continuous(labels = scales::comma) + scale_x_reverse() + guides(color = FALSE, fill = FALSE) + 

Luego, agrego dos firmas usando geom_text : el nombre del equipo y el número de victorias. coord_flip intercambia los ejes, scale_fill_manual y scale_color_manual cambian el color de las columnas, scale_x_reverse "expande" el eje X. Observe que tomamos los colores del vector cols creado previamente.


La capa de theme especifica opciones para ajustar la visualización del gráfico. Aquí se indica cómo deben mostrarse los encabezados y las etiquetas de los ejes (de ninguna manera lo que element_blank nos dice en el lado derecho de la igualdad). Eliminamos la leyenda, el fondo, el marco, las líneas de cuadrícula a lo largo del eje Y. plot.title argumentos plot.title , plot.subtitle , plot.caption , establecemos las opciones de visualización para el título, subtítulo y firma del gráfico. Para obtener más detalles sobre el significado de todos los parámetros, consulte gglot2


 theme(axis.line=element_blank(), axis.text.x=element_blank(), axis.text.y=element_blank(), axis.ticks=element_blank(), axis.title.x=element_blank(), axis.title.y=element_blank(), legend.position="none", panel.background=element_blank(), panel.border=element_blank(), panel.grid.major=element_blank(), panel.grid.minor=element_blank(), panel.grid.major.x = element_line( size=.1, color="grey" ), panel.grid.minor.x = element_line( size=.1, color="grey" ), plot.title=element_text(size=25, hjust=0.5, face="bold", colour="black", vjust=-1), plot.subtitle = element_text(size = 15), plot.caption =element_text(size=15, hjust=0.5, color="black"), plot.background=element_blank(), plot.margin = margin(2,2, 2, 4, "cm")) 

Crear animación


No me detendré en usar la función transition_states , esta parte es idéntica a mi publicación anterior en Habré. En cuanto a los labs crea el título, los subtítulos y la firma del gráfico. El uso de {closest_state} permite mostrar cada año específico en el gráfico, cuyas columnas estamos viendo actualmente.


  anim <- gg + transition_states(year, transition_length = 4, state_length = 1) + view_follow(fixed_x = TRUE) + labs(title = "Cumulative Wins by teams in seasons", subtitle = "Season: {closest_state}", caption = "Telegram: @NBAatlantic, Twitter: @vshufiskiy\n Data sourse: stats.nba.com") 

Función nba_cumulative_wins para crear gráficos.


Las funciones de escritura simplifican y aceleran el proceso de obtención del resultado si necesita usar el código más de una vez. Por lo general, una función en R tiene la siguiente forma:


 _ <- function( ) { _ } 

Primero debe comprender qué parámetros desea cambiar utilizando la función, sus argumentos dependerán de esto. El primer argumento es el nombre de la tabla de datos que se está ingresando. Esto le permite cambiarle el nombre si surge ese deseo, sin cambiar nada en la función misma. También quiero que se muestre cualquier número de comandos en el gráfico: de uno (que no tiene sentido) a 30 (simplemente ya no existe). También quiero poder considerar cualquier período de tiempo dentro de esos 15 años para los cuales tengo datos. Todo esto se implementa en esta forma de función:


 nba_cumulative_wins <- function(table, elements, first_season, last_season){ ... } 

donde table es el nombre de la tabla con los datos de entrada,
elements : los nombres de los equipos que deben mostrarse en el gráfico
first_season : la primera temporada que se mostrará en el gráfico
last_season : la última temporada que se mostrará en el gráfico.


Si el argumento se usa con mucha frecuencia con un valor específico, puede configurarlo de manera predeterminada. Entonces, si se omite entre los argumentos de la función, este valor será sustituido. Por ejemplo, si te registras


nba_cumulative_wins <- function(table, elements, first_season, last_season = 2018)


entonces los horarios se construirán hasta la temporada 2018/19, a menos que se indique lo contrario.


Trabajando con elements argumentos, first_season , last_season


Usando el argumento de elements , podemos especificar el nombre de los equipos que queremos ver en el gráfico. Esto es muy conveniente cuando hay 2 o 3 de estos equipos, pero si queremos mostrar toda la liga tendremos que escribir elements = c() y el nombre de los 30 equipos entre paréntesis.


Así que decidí "dividir" los valores de entrada para el argumento de los elements en varios grupos.
La función nba_cumulative_wins puede crear gráficos para equipos individuales, divisiones, conferencias o la NBA en su conjunto. Para esto, utilicé la siguiente construcción:


  select_teams <- unique(table1$TeamName) select_div <- unique(table1$Division) select_conf <- unique(table1$Conference) select_nba <- "NBA" table1 <- if(elements %in% select_teams){ table1[TeamName %in% elements] } else if (elements %in% select_div){ table1[Division %in% elements] } else if(elements %in% select_conf){ table1[Conference %in% elements] } else if(elements == "NBA"){ table1 } else { NULL } 

select_ vectores simbólicos select_ contienen los nombres de los 30 equipos, 6 divisiones, 2 conferencias y la NBA, y la función unique deja solo un nombre único, en lugar de 15 (por el número de años en los datos).


Luego, utilizando la if...else , se verifica que el argumento de los elements ingresados ​​pertenece a una de las clases ( %in% usa para determinar si el elemento pertenece al vector), y la tabla de datos se modifica en consecuencia. Ahora, si quiero ver los resultados de los equipos que juegan en la División Suroeste


elements = c("Mavericks", "Spurs", "Rockets", "Grillies", "Pelicans")


solo entra


elements = "Southwest" , que es mucho más rápido y más conveniente.


Debido a la posibilidad de elegir estaciones, el trabajo con fechas también cambia. Al principio, se agrega la línea:


 table1 <- table1[SeasonID >= as.numeric(paste(2, first_season, sep = "")) & SeasonID <= as.numeric(paste(2, last_season, sep = ""))] 

Así que dejo en la tabla solo aquellas filas que caen en nuestro intervalo de tiempo elegido. El código para crear la columna del year también cambia. Ahora se ve así:


 table1 <- table1[ ,year := rep(seq(first_season, last_season), each = length(unique(table1$TeamName)))] 

En relación con la agrupación de elementos, el procedimiento para obtener los colores deseados es complicado. El hecho es que en la tabla table_color solo los nombres de los comandos. Por lo tanto, necesitamos "desplegar" nuestras contracciones. Para hacer esto, use la construcción if...else vez.


  elements1 <- if (elements == "NBA"){ c("Hawks", "Celtics", "Nets", "Hornets", "Bulls", "Cavaliers", "Mavericks", "Nuggets", "Pistons", "Warriors", "Rockets", "Pacers", "Clippers", "Lakers", "Grizzlies", "Heat", "Bucks", "Timberwolves", "Pelicans", "Knicks", "Thunder", "Magic", "76ers", "Suns", "Trail Blazers","Kings", "Spurs", "Raptors", "Jazz", "Wizards") } else if (elements == "West") { c("Mavericks","Nuggets", "Warriors", "Rockets", "Clippers", "Lakers", "Grizzlies","Timberwolves", "Pelicans", "Thunder", "Suns", "Trail Blazers","Kings", "Spurs", "Jazz") } else if (elements == "East") { c("Hawks", "Celtics", "Nets", "Hornets", "Bulls", "Cavaliers","Pistons", "Pacers", "Heat", "Bucks", "Knicks", "Magic", "76ers", "Raptors", "Wizards") } else if (elements == "Pacific") { c("Warriors", "Clippers", "Lakers", "Suns", "Kings") } else if (elements == "Southeast") { c("Magic", "Hornets", "Heat", "Hawks", "Wizards") } else if (elements == "Southwest") { c("Mavericks", "Grizzlies", "Pelicans", "Rockets", "Spurs") } else if (elements == "Central") { c("Bucks", "Pacers", "Pistons", "Bulls", "Cavaliers") } else if (elements == "Atlantic") { c("Knicks", "Nets", "Celtics", "Raptors", "76ers") } else if (elements == "Northwest") { c("Nuggets", "Trail Blazers", "Jazz", "Thunder", "Suns") } else { elements } 

Luego, cree una tabla con los nombres de los comandos que necesitamos, conecte esta tabla a table_color usando la función dplyr paquete dplyr . inner_join incluye solo casos que coinciden en ambas tablas.


  table_elements1 <- data.table(TeamName = elements1) table_color <- table_color[order(TeamName)] inner_table_color <- inner_join(table_color, table_elements1) cols <- inner_table_color[, "TEAM_color"] 

La función cambia la ortografía del título y los subtítulos. Asumen este aspecto:


 anim <- gg + transition_states(year, transition_length = 4, state_length = 1) + view_follow(fixed_x = TRUE) + labs(title = paste("Cumulative Wins by teams in seasons", first_season, "-", last_season, sep = " "), subtitle = paste(if (elements %in% select_div ){ paste(elements, "Division", sep = " ") } else if (elements %in% select_conf ){ paste("Conference", elements, sep = " ") }, "Season: {closest_state}", sep = " "), caption = "Telegram: @NBAatlantic, Twitter: @vshufiskiy\nData sourse: stats.nba.com") 

Renderizado


Además, todo esto se visualiza.


 animate(anim, nframes = (last_season - first_season + 1) * (length(unique(table1$TeamName)) + 20), fps = 20, width = 1200, height = 1000, renderer = gifski_renderer(paste(elements[1], "cumwins.gif", sep = "_"))) 

Seleccioné el número en nframes empíricamente, de modo que dependiendo del número de comandos seleccionados, la velocidad aumenta / disminuye.


Graph



Espero que mi publicación sea interesante. Código del proyecto Github .


Si está interesado en el componente deportivo de estas visualizaciones, puede visitar mi blog en sports.ru "A ambos lados del Atlántico"

Source: https://habr.com/ru/post/458904/


All Articles