Изучите лучшие команды английского футбола за все время и узнайте, как составить одну из тех диаграмм гонок, которые стали популярными.

В этой статье мы собираемся проанализировать все время выступления английских футбольных команд с момента создания их лиг в 1888 году и использовать эти данные для создания анимированной гистограммы, которая циклически проходит каждый год и показывает общее количество очков, набранных десятью лучшими командами. на протяжении всей истории. Мы также будем использовать строку заголовка, чтобы рассказать о том, как спорт изменился в Англии за 130 лет. Вот готовый продукт, весь код находится на Github:

Прежде чем мы начнем, давайте проясним наши цели. Вот что мы хотим сделать:

  1. Создайте анимацию, которая циклически проходит через каждый футбольный сезон с 1888 по 2017 год и показывает совокупные очки лиги за все время для каждой команды по состоянию на этот год, показывая 10 лучших команд. Мы будем учитывать только очки, полученные в высшем дивизионе английского футбола (теперь известном как Премьер-лига).
  2. Добавьте случайные фактоиды в заголовок таблицы, которые помогут рассказать историю об изменениях в футболе с течением времени.

В этой статье я не собираюсь сосредотачиваться на стиле и красоте, а только на основных функциях, которые создают анимированную графику.

Подготовка данных

В идеале нам нужен набор данных, которые рассказывают нам об очках лиг, полученных в ходе истории, но мне не удалось найти этот набор. Вместо этого я нашел что-то более подробное и удивительное, в котором записаны все игры, сыгранные с 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.