Изучите лучшие команды английского футбола за все время и узнайте, как составить одну из тех диаграмм гонок, которые стали популярными.
В этой статье мы собираемся проанализировать все время выступления английских футбольных команд с момента создания их лиг в 1888 году и использовать эти данные для создания анимированной гистограммы, которая циклически проходит каждый год и показывает общее количество очков, набранных десятью лучшими командами. на протяжении всей истории. Мы также будем использовать строку заголовка, чтобы рассказать о том, как спорт изменился в Англии за 130 лет. Вот готовый продукт, весь код находится на Github:
Прежде чем мы начнем, давайте проясним наши цели. Вот что мы хотим сделать:
- Создайте анимацию, которая циклически проходит через каждый футбольный сезон с 1888 по 2017 год и показывает совокупные очки лиги за все время для каждой команды по состоянию на этот год, показывая 10 лучших команд. Мы будем учитывать только очки, полученные в высшем дивизионе английского футбола (теперь известном как Премьер-лига).
- Добавьте случайные фактоиды в заголовок таблицы, которые помогут рассказать историю об изменениях в футболе с течением времени.
В этой статье я не собираюсь сосредотачиваться на стиле и красоте, а только на основных функциях, которые создают анимированную графику.
Подготовка данных
В идеале нам нужен набор данных, которые рассказывают нам об очках лиг, полученных в ходе истории, но мне не удалось найти этот набор. Вместо этого я нашел что-то более подробное и удивительное, в котором записаны все игры, сыгранные с 1888 года, а также их результат и счет. Я нашел его здесь в виде объекта фрейма данных R - так что мы будем делать это в ребятах R. Вот краткий снимок того, как выглядят данные.
Этот набор данных довольно большой: с 1888 года на каждом уровне сыграно почти 200 000 матчей. Сначала нам нужно преобразовать каждый матч в распределение очков для данной команды. Очки распределяются между командой хозяев и командой гостей (гостей) следующим образом:
- 2 очка победителю перед сезоном 1981 года. С сезона 1981 года победитель получил 3 очка в результате изменения правил, чтобы стимулировать более атакующую игру.
- 0 очков проигравшей команде.
- По 1 очку, если игра закончилась вничью.
Итак, давайте загрузим наши данные и загрузим наши tidyverse
пакеты для стандартной обработки данных. Мы будем использовать столбец result
для назначения домашних и выездных точек, создав два новых столбца, используя dplyr::mutate()
следующим образом:
library(tidyverse) load("data/england.rda") # assign points to results (2 pts for a win up to 1980-81 season then 3 pts for a win afterwards) england <- england %>% dplyr::mutate( homepts = dplyr::case_when( Season <= 1980 & result == "H" ~ 2, Season > 1980 & result == "H" ~ 3, result == "D" ~ 1, result == "A" ~ 0 ), awaypts = dplyr::case_when( Season <= 1980 & result == "A" ~ 2, Season > 1980 & result == "A" ~ 3, result == "D" ~ 1, result == "H" ~ 0 ) )
Теперь у нас есть очки, распределенные для каждой игры, нам нужно будет получить общее количество очков дома и в гостях для каждой команды и каждого сезона, помня на этом этапе, что нас интересуют только игры, сыгранные на верхнем уровне:
# restrict to Tier 1 and assemble into total points per season home_pts <- england %>% dplyr::filter(tier == 1) %>% dplyr::group_by(Season, home) %>% dplyr::summarize(pts = sum(homepts)) away_pts <- england %>% dplyr::filter(tier == 1) %>% dplyr::group_by(Season, visitor) %>% dplyr::summarize(pts = sum(awaypts))
Теперь мы можем связать эти два фрейма данных вместе и сложить домашние и выездные очки, чтобы получить общее количество очков для каждой команды по сезонам:
total_pts <- home_pts %>% dplyr::rename(Team = home) %>% dplyr::bind_rows( away_pts %>% dplyr::rename(Team = visitor) ) %>% dplyr::group_by(Season, Team) %>% dplyr::summarise(pts = sum(pts))
Теперь у нас есть фреймворк, который показывает очки, заработанные каждой командой в каждом сезоне. Мы хотим манипулировать этим так, чтобы каждый сезон отображал нам сумму всех очков, заработанных до этого сезона включительно. Этого можно добиться с помощью быстрого for
цикла:
# create rolling sums table <- total_pts %>% dplyr::filter(Season == 1888) %>% dplyr::select(Season, Team, Points = pts) for (i in 1889:2017) { table <- total_pts %>% dplyr::filter(Season <= i) %>% dplyr::group_by(Team) %>% dplyr::summarise(Points = sum(pts, na.rm = TRUE)) %>% dplyr::mutate(Season = i) %>% dplyr::bind_rows(table) }
Мы сделали достаточно, чтобы получить данные, необходимые для цели 1. Для цели 2 я отредактирую столбец Season
, чтобы добавить некоторые важные исторические факты об английской футбольной лиге. Я буду отображать каждый факт примерно в течение трех сезонов, чтобы он отображался в анимации достаточно долго, чтобы зрители могли его прочитать. Назовем этот новый отредактированный столбец SeasonLabel
.
# add some historic facts to seasons table <- table %>% dplyr::mutate( SeasonLabel = dplyr::case_when( Season <= 1891 ~ paste(Season, "Football League is formed with 12 teams in 1888", sep = " - "), dplyr::between(Season, 1892, 1895) ~ paste(Season, "Second Division introduced in 1892", sep = " - "), dplyr::between(Season, 1914, 1918) ~ paste(Season, "League suspended during World War I", sep = " - "), dplyr::between(Season, 1920, 1924) ~ paste(Season, "Third Division North/South introduced in 1920/21", sep = " - "), dplyr::between(Season, 1925, 1928) ~ paste(Season, "New Offside Law introduced in 1925", sep = " - "), dplyr::between(Season, 1939, 1945) ~ paste(Season, "League suspended during World War II", sep = " - "), dplyr::between(Season, 1958, 1961) ~ paste(Season, "Regional Third Divisions amalgamated in 1958 to form Nationwide Third and Fourth Divisions", sep = " - "), dplyr::between(Season, 1965, 1968) ~ paste(Season, "Substitutes first allowed in 1965", sep = " - "), dplyr::between(Season, 1974, 1977) ~ paste(Season, "First match played on a Sunday in 1974", sep = " - "), dplyr::between(Season, 1981, 1984) ~ paste(Season, "Three points for a win introduced in 1981", sep = " - "), dplyr::between(Season, 1986, 1989) ~ paste(Season, "Play-offs introduced to decide some promotions", sep = " - "), dplyr::between(Season, 1992, 1995) ~ paste(Season, "Premier League formed in 1992, reducing Football League to three divisions", sep = " - "), dplyr::between(Season, 2004, 2007) ~ paste(Season, "Football League renames divisions in 2004 to Championship, League One and League Two", sep = " - "), dplyr::between(Season, 2013, 2016) ~ paste(Season, "Goal Line Technology introduced in Premier League in 2013", sep = " - "), 1L == 1L ~ as.character(Season) ) )
Давайте теперь посмотрим на наши данные:
Теперь у нас есть все необходимое для кодирования нашей анимации. Давайте просто сохраним наш набор данных с помощью save(table, 'data/table.RData')
, чтобы мы могли открыть его в новом файле, который мы будем использовать для создания анимации.
Создание анимации
С помощью этого обсуждения StackOverflow мы будем использовать пакетggplot2
для разработки статической гистограммы для этих данных, а затем мы будем использовать замечательный пакет gganimate
для создания скользящей анимации, которая проходит через каждые SeasonLabel
и обновляет гистограмму.
Давайте загрузим данные и несколько пакетов, которые нам понадобятся. Затем, во-первых, нам нужно ранжировать каждую команду в каждом сезоне, так как ранг будет определять порядок появления полос на диаграмме «гонки». Мы также создаем относительную ценность для каждой команды по сравнению с командой, находящейся на вершине рейтинга, так как это поможет с масштабированием столбцов. Наконец, мы создаем метку для вывода значения из столбца Points
. Затем мы ограничиваемся 10 лучшими командами по рангам.
library(tidyverse) library(ggplot2) library(gganimate) library(gifski) ggplot2::theme_set(theme_classic()) load("data/table.Rdata") # generate top n ranking by year group anim_table <- table %>% dplyr::group_by(Season) %>% dplyr::mutate( rank = min_rank(-Points) * 1, Value_rel = Points / Points[rank == 1], Value_lbl = paste0(" ", Points) ) %>% dplyr::filter(rank <= 10) %>% dplyr::ungroup()
Теперь у нас есть все необходимое для построения статической гистограммы - для этого нужна только строка довольно простых ggplot2
команд. Я не буду вдаваться в подробности, но если вам нужно заново ознакомиться с ними, я рекомендую tidyverse.org в качестве отправной точки. Основные моменты здесь заключаются в том, что мы используем rank
как эстетику x, Points
как эстетику y, затем мы назначаем Team
и Points
, а затем переворачиваем диаграмму, чтобы сделать ее горизонтальной, а не вертикальной.
# create static bar chart p <- ggplot2::ggplot(anim_table, aes(rank)) + ggplot2::geom_tile(aes( y = Points / 2, height = Points, width = 0.9, fill = "blue" ), alpha = 0.8, color = NA) + ggplot2::geom_text(aes(y = 0, label = paste(Team, " ")), size = 12, vjust = 0.2, hjust = 1) + ggplot2::geom_text(aes(y = Points, label = Value_lbl, hjust = 0)) + ggplot2::coord_flip(clip = "off", expand = FALSE) + ggplot2::scale_y_continuous(labels = scales::comma) + ggplot2::scale_x_reverse() + ggplot2::guides(color = FALSE, fill = FALSE)
Теперь перейдем к основной части анимации. Мы даем метки статическому графику для заголовка и осей, но одна из этих меток - это та, которую мы будем использовать для анимации графика, то есть SeasonLabel.
Это известно как переменная перехода. Итак, мы говорим ggplot2
, что хотим, чтобы заголовок печатал текущее состояние перехода, то есть SeasonLabel
как заголовок, когда он вращается в анимации. Наконец, мы используем ease_aes()
, чтобы определить способ изменения значений при перемещении по переходам - есть много функций замедления, с которыми вы можете поэкспериментировать, просто обратитесь к файлу справки для получения подробной информации.
# set SeasonLabel as transition state and set to animate p <- ggplot2::labs(p, title = "{closest_state}", x = "", y = "Total Points", caption = "Source: Github(jalapic/engsoccerdata) | Top tier points only, does not include mandatory points deductions | Plot generated by @dr_keithmcnulty" ) + ggplot2::theme( plot.title = element_text(color = "darkblue", face = "bold", hjust = 0, size = 30), axis.ticks.y = element_blank(), axis.text.y = element_blank(), plot.margin = margin(2, 2, 1, 16, "cm") ) + gganimate::transition_states(SeasonLabel, transition_length = 4, state_length = 1) + gganimate::ease_aes("cubic-in-out")
Итак, наш анимированный сюжет теперь сохранен в окружении как объект p
. Все, что нам нужно сделать сейчас, это отрендерить его как выходной файл. Существует множество устройств, которые можно использовать для рендеринга вывода в различных форматах, но наиболее распространенными являются:
gif
output - будет создан файл анимированного изображения. Для этого требуется установленный пакетgifski
.mp4
видеовыход - на вашем компьютере должен быть установленffmpeg
.
В моем случае я собираюсь создать gif
согласно изображению, показанному ранее. Вы можете настроить скорость GIF-изображения с помощью аргумента duration
, а также можете настроить размер и разрешение изображения.
# save as preferred rendered format gganimate::animate(p, nframes = 200, fps = 5, duration = 100, width = 2000, height = 1200, renderer = gifski_renderer("anim.gif"))
В зависимости от того, как вы устанавливаете аргументы в функции animate()
, для рендеринга файла может потребоваться больше времени, но вы можете отслеживать ход выполнения в консоли.
Дальнейшее изучение
Это был краткий практический пример. Он охватывает лишь очень небольшое количество возможностей анимации в R. Я рекомендую вам прочитать gganimate
больше, а затем, возможно, попытаться использовать его для анимации некоторых из ваших собственных данных. Данные временных рядов особенно хорошо подходят для хорошей анимации. Пакет plotly
в R также имеет расширенные возможности анимации, на которые стоит обратить внимание.
Изначально я был чистым математиком, затем стал психометриком и специалистом по анализу данных. Я увлечен применением всех этих дисциплин к сложным вопросам людей. Я также помешан на кодировании и большой поклонник японских ролевых игр. Найдите меня в LinkedIn или Twitter.