Visualização do número de vitórias para equipes da NBA usando gráficos de barras animados em R

Primeiro, algumas informações básicas. Meu nome é Vladislav e meu conhecimento de R ocorreu em agosto do ano passado. Eu decidi aprender uma linguagem de programação por causa de uma natureza aplicada. Desde a infância, eu gostava de manter estatísticas esportivas. Com a idade, esse hobby se transformou em um desejo de analisar de alguma forma esses números e, com base na análise de dados, emitir, se possível, pensamentos inteligentes. O problema é que, nos últimos anos, os esportes foram varridos por uma onda de dados, dezenas de empresas competem entre si, tentando calcular, descrever e empurrar para o neurônio qualquer ação de um jogador de futebol, jogador de basquete, jogador de beisebol na quadra. E o Excel não é categoricamente adequado para análise. Então, decidi estudar R para que a análise mais simples não levasse meio dia. Já no decorrer do estudo, foi acrescentado um interesse em programar como tal, mas essa já é a letra.


Quero notar imediatamente que muito do que vou escrever no futuro já esteve nos simpsons estava em Habr no artigo Criamos histogramas animados usando R. Este artigo, por sua vez, é uma tradução do artigo Criar gráficos de barras animados de tendências usando R do Medium. Portanto, para diferir de alguma forma dos artigos acima, tentarei descrever mais detalhadamente o que estou fazendo, bem como os momentos que não estão no artigo original. Por exemplo, para preencher as colunas, usei as cores dos comandos da NBA, não a paleta padrão ggplot2 , mas nos dados que processam o pacote data.table , não o dplyr . Eu fiz todo esse trabalho como uma função; agora, basta escrever o nome da equipe e os anos pelos quais você precisa contar o número de vitórias.


Dados


Para construir o cronograma, usei dados sobre o número de vitórias de cada uma das 30 equipes da NBA nas últimas 15 temporadas. Eles foram coletados no stats.nba.com usando a extensão NBA Data Retriever , que, através do uso da API da NBA, produz arquivos csv com as estatísticas necessárias. Aqui estão os detalhes completos do meu projeto no Github .


Bibliotecas usadas


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

Para processamento de dados, eu uso data.table (simplesmente porque conheci este pacote antes). Também transfiro um conjunto de pacotes tidyverse , e não um ggplot2 separado, para não se preocupar, se durante a análise aparecer alguma idéia que exija carregamento adicional de um pacote deste conjunto. Nesse caso em particular, o ggplot2 pode ser dispensado, outros pacotes configurados não estão envolvidos. Bem, o gganimate coloca os gráficos em movimento.


Trabalhar com dados


Primeiro você precisa colocar os dados em ordem. Basicamente, para criar gráficos, precisamos de 2 das 79 colunas de uma tabela com dados brutos. Você pode selecionar as colunas necessárias primeiro, substituir alguns valores primeiro. Eu fui pelo segundo caminho.


A tabela em data.table possui o formato dt[i, j, by] , em que by é "responsável" pelo agrupamento de elementos. Vou agrupar pela coluna TeamName. E há um problema. Esta coluna exibe os nomes das equipes: Lakers, Celtics, Heat, etc. Mas durante o período em análise (a partir da temporada 2004/05), várias equipes mudaram de nome: o New Orleans Hornets se tornou o New Orleans Pelicans, o Charlotte Bobcats retornou o nome histórico Charlotte Hornets e o Seattle Supersonics se tornou o Oklahoma City Thunder. Isso pode causar confusão. As conversões a seguir ajudam a evitar isso:


 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"] 

Nesse período, as alterações são mínimas, mas se você expandi-lo, será muito difícil agrupar por TeamName e você precisará usar uma coluna mais confiável. Nestes dados, este é o TeamID.


Para começar, nos livramos das informações "extras", deixando apenas as colunas necessárias para o trabalho:


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

No data.table o. () Construct substitui a função list . Uma opção mais "clássica" para selecionar colunas é a table1 <- table1[, c("TeamName", "WINS")] . Depois disso, a tabela assume o seguinte formato:


TeamnameWINS
Sóis62
Calor59.
Esporas59.
Pistões54

Para animação para cada temporada separadamente, isso é suficiente, mas para calcular o número total de vitórias em um período selecionado, é necessário calcular o total acumulado de vitórias.


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


Usando a função cumsum obtemos os números que precisamos. Usar: = em vez de = permite adicionar uma nova coluna à tabela; não a substituo pela mesma coluna CumWins. by = "TeamName" agrupa os dados pelo nome da equipe e o valor acumulado é by = "TeamName" para cada uma das 30 equipes separadamente.


Em seguida, adiciono uma coluna com o ano em que cada estação começou. A temporada da NBA decorre de outubro a maio, por isso cai em dois anos civis. Na designação da estação, o ano de seu início, ou seja, Temporada: 2018 no gráfico é a temporada 2018/19 na realidade.


A tabela original possui esses dados. A coluna SeasonID mostra uma figura na forma de 2 (o ano em que a temporada começou), por exemplo, 22004. Você pode remover os dois primeiros usando o pacote stringr ou as funções R básicas, mas fui um pouco diferente. Aconteceu que eu primeiro usei esta coluna para indicar as estações necessárias, depois exclua e crie uma coluna com datas novamente. Ações extras.


Fiz o seguinte:


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


Eu tive “sorte” de que, durante o período selecionado, o número de equipes na NBA não mudou, então apenas repeti os números de 2004 a 2018 30 vezes. Novamente, se você entrar na história, esse método será inconveniente, pois o número de equipes em cada temporada será diferente; portanto, é preferível usar a opção para limpar a coluna SeasonID.


Em seguida, adicione a coluna cumrank.


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


Representa a classificação das equipes em cada temporada pelo número de vitórias e será usado como valores do eixo X. frank um análogo mais rápido da data.table de dados base, menos significa classificação em ordem decrescente (isso também pode ser feito usando o argumento decreasing = TRUE . qual a ordem que as equipes com o mesmo número de vitórias irão, portanto ties.method = "random" . Bem, tudo isso é agrupado em um ano.


E a última conversão de tabela está adicionando a coluna value_rel .


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


Esta coluna considera a proporção entre o número de vitórias de cada equipe e o indicador mais alto do ano. Para o melhor time, esse indicador é 1; para o restante, é menor, dependendo do sucesso da temporada.


Após todas as adições, a tabela possui o seguinte formato:


TeamnameWINSCumwinsanocumrankvalue_rel
Esporas59.59.200430.9516129
Esporas63.122200511.0000000
Esporas58.180200620.9729730
Esporas56.236200711.0000000

Apenas uma equipe é apresentada na tabela para ilustrar a cumulatividade. Todas essas ações são realizadas, como na mudança de nome, com uma cascata de colchetes


 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"] 

Altere o preenchimento das colunas do padrão para as cores das equipes.


Você pode proceder imediatamente à construção dos gráficos, mas há, como me parece, um ponto importante: a cor das colunas no gráfico. Você pode deixar a paleta ggplot2 padrão, mas esta é uma opção ruim. Em primeiro lugar, parece-me que ela é feia. E, em segundo lugar, torna difícil encontrar uma equipe no gráfico. Para os fãs da NBA, cada time está associado a uma cor específica: Boston é verde, Chicago é vermelho, Sacramento é roxo etc. Portanto, o uso da cor do comando nas colunas de preenchimento ajuda a identificá-lo mais rapidamente, apesar da abundância de azul e vermelho.


Para fazer isso, crie uma tabela table_color com o nome do comando e sua cor principal. Cores retiradas do teamcolorcodes.com .


TeamnameTEAM_color
Hawks# E03A3E
Celtics# 007A33
Redes# 000000

Com a tabela de cores, você precisa fazer mais uma manipulação. Porque Quando fatores de plotagem são usados, a ordem das equipes muda. O primeiro da lista será Philadelphia 76, como o único proprietário do nome "digital" e, em seguida, de acordo com o alfabeto. Portanto, precisamos organizar as cores na mesma ordem e extrair o vetor que as contém da tabela. Fiz o seguinte:


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

Plotagem


Realmente construímos apenas um gráfico, que contém todos os 450 (15 temporadas * 30 equipes) indicadores de vitórias e, em seguida, "dividimos" pela variável necessária (no nosso caso, por anos) usando as funções do pacote 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) 

Primeiro, criamos um gráfico usando a função ggplot . No argumento aes , especifique como as variáveis ​​da tabela serão exibidas no gráfico. Nós os agrupamos por TeamName, o fill e a color serão responsáveis ​​pela cor das colunas.


As colunas verdadeiras chamam isso não é inteiramente verdade. Usando geom_tile "dividimos" os dados no gráfico em retângulos. Aqui está um exemplo de um gráfico deste tipo:

Pode-se ver como o gráfico é "dividido" em quadrados (eles são obtidos de retângulos usando a camada coord_equal() ), três em cada coluna. Mas, graças à width do argumento menor que um, nosso bloco assume a forma de colunas.


  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) + 

Em seguida, adiciono duas assinaturas usando geom_text : o nome da equipe e o número de vitórias. coord_flip troca os eixos, scale_fill_manual e scale_color_manual alteram a cor das colunas, scale_x_reverse "expande" o eixo X. Observe que pegamos as cores do vetor cols criado anteriormente.


A camada do theme especifica opções para ajustar a exibição do gráfico. Aqui é indicado como os cabeçalhos e os rótulos dos eixos devem ser exibidos (de maneira alguma o que element_blank nos diz no lado direito da igualdade). plot.title a legenda, plano de fundo, quadro, linhas de grade ao longo do eixo Y. plot.title argumentos plot.title , plot.subtitle , plot.caption , definimos as opções de exibição para o título, legenda e assinatura do gráfico. Mais detalhadamente, o significado de todos os parâmetros pode ser visualizado no site da 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")) 

Criar animação


Não vou insistir em usar a função transition_states , esta parte é idêntica à minha publicação anterior no Habré. Quanto aos labs ele cria o título, subtítulo e assinatura do gráfico. O uso de {closest_state} permite exibir cada ano específico no gráfico, cujas colunas estamos vendo no momento.


  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") 

Função nba_cumulative_wins para criar gráficos.


As funções de escrita simplificam e aceleram o processo de obtenção do resultado se você precisar usar o código mais de uma vez. Geralmente, uma função em R tem a seguinte forma:


 _ <- function( ) { _ } 

Primeiro você precisa entender quais parâmetros você deseja alterar usando a função, seus argumentos dependerão disso. O primeiro argumento é o nome da tabela de dados que está sendo inserida. Isso permite que você o renomeie se esse desejo surgir, sem alterar nada na própria função. Também quero que qualquer número de comandos seja exibido no gráfico: de um (que não faz sentido) a 30 (simplesmente não existe mais). Também quero poder considerar quaisquer períodos de tempo dentro dos 15 anos para os quais tenho dados. Tudo isso é implementado nesta forma de função:


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

onde table é o nome da tabela com os dados de entrada,
elements - os nomes das equipes que devem ser exibidas no gráfico
first_season - a primeira temporada a ser exibida no gráfico
last_season - a última temporada a ser exibida no gráfico.


Se o argumento for frequentemente usado com um valor específico, você poderá configurá-lo por padrão. Então, se for omitido entre os argumentos da função, esse valor será substituído. Por exemplo, se você se registrar


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


os cronogramas serão construídos para a temporada 2018/19, a menos que indicado de outra forma.


Trabalhando com elements Argumentos, last_season , last_season


Usando o argumento de elements , podemos especificar o nome das equipes que queremos ver no gráfico. Isso é muito conveniente quando existem 2 ou 3 equipes, mas se queremos exibir a liga inteira, teremos que escrever os elements = c() e o nome de todas as 30 equipes entre parênteses.


Então, decidi "dividir" os valores de entrada para o argumento dos elements em vários grupos.
A função nba_cumulative_wins pode criar gráficos para equipes individuais, divisões, conferências ou para a NBA como um todo. Para isso, usei a seguinte construção:


  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_ vetores simbólicos select_ contêm os nomes de todas as 30 equipes, 6 divisões, 2 conferências e a NBA, e a função unique deixa apenas um nome exclusivo, em vez de 15 (pelo número de anos nos dados).


Em seguida, usando a if...else , verifica-se que o argumento dos elements inserido pertence a uma das classes ( %in% usado para determinar se o elemento pertence ao vetor) e a tabela de dados é modificada de acordo. Agora, se eu quiser ver os resultados das equipes que jogam na Divisão Sudoeste,


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


basta entrar


elements = "Southwest" , que é muito mais rápido e conveniente.


Devido à possibilidade de escolher as estações, o trabalho com datas também muda. No início, a linha é adicionada:


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

Portanto, deixo na tabela apenas as linhas que se enquadram no intervalo de tempo escolhido. O código para criar a coluna do year também muda. Agora fica assim:


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

Em conexão com o agrupamento de elementos, o procedimento para obter as cores desejadas é complicado. O fato é que na tabela table_color apenas os nomes dos comandos. Portanto, precisamos "implantar" nossas contrações de volta. Para fazer isso, use a construção if...else novamente.


  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 } 

Em seguida, crie uma tabela com os nomes dos comandos que precisamos, conecte esta tabela a inner_join usando a função dplyr pacote dplyr . inner_join inclui apenas casos que correspondem nas duas tabelas.


  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"] 

A função altera a ortografia do título e da legenda. Eles assumem esse 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") 

Renderização


Além disso, tudo isso é visualizado.


 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 = "_"))) 

Selecionei o número em nframes empiricamente, de modo que, dependendo do número de comandos selecionados, a velocidade aumenta / diminui.


Graph



Espero que meu post seja interessante. Código do projeto Github .


Se você estiver interessado no componente esportivo dessas visualizações, visite o meu blog em sports.ru "Nos dois lados do Atlântico"

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


All Articles