Animating the Premier League using gganimate

  • 2
  • 3
  •  
  • 2

Ever wonder what an evolving gif of each premier league team’s goal difference vs points would look like made in R? Look no further! Most of this is going to be setting up the data (as always) instead of actually plotting the data. To get the data into shape, we’re going to be using the tidyverse and lubridate, which you can install the usual way via install.packages(). To animate the data we’ll be using the gganimate package. This is not on CRAN and so must be installed from GitHub, which you can do so via the devtools package

devtools::install_github("thomasp85/gganimate")

To get started let’s attach the relevant packages

library("tidyverse")
library("lubridate")
library("gganimate")

We’re going to use the last full season of matches in the premier league, which was the 17/18 season. The data was sourced from football-data.co.uk

prem = read_csv("http://www.football-data.co.uk/mmz4281/1718/E0.csv")
head(prem)

## # A tibble: 6 x 65
##   Div   Date  HomeTeam AwayTeam  FTHG  FTAG FTR    HTHG  HTAG HTR   Referee
##   <chr> <chr> <chr>    <chr>    <int> <int> <chr> <int> <int> <chr> <chr>  
## 1 E0    11/0… Arsenal  Leicest…     4     3 H         2     2 D     M Dean 
## 2 E0    12/0… Brighton Man City     0     2 A         0     0 D     M Oliv…
## 3 E0    12/0… Chelsea  Burnley      2     3 A         0     3 A     C Paws…
## 4 E0    12/0… Crystal… Hudders…     0     3 A         0     2 A     J Moss 
## 5 E0    12/0… Everton  Stoke        1     0 H         1     0 H     N Swar…
## 6 E0    12/0… Southam… Swansea      0     0 D         0     0 D     M Jones
## # ... with 54 more variables

We’re only interested in the date, teams, result and home/away goals. These variables come between the variables Date and FTR. We also need to convert Date to a date object

prem = prem %>% 
  select(Date:FTR) %>% 
  mutate(Date = dmy(Date)) 

Cumulative points per day per team

There’s probably a better way to do this, but here is mine. I added a column for each team onto the data then, using a for loop (I know I’m sorry) I transferred the “H”, “A” and “D” status of the full time result into points for each time in their respective column. For you non-football heads, thats 3 for a win, 1 for a draw and 0 for a loss.

prem[sort(unique(prem$HomeTeam))] = NA

for(i in 1:nrow(prem)) {
  if(prem$FTR[i] == "H") {
    prem[i, prem$HomeTeam[i]] = 3
    prem[i, prem$AwayTeam[i]] = 0
  } else if(prem$FTR[i] == "A") {
    prem[i, prem$AwayTeam[i]] = 3
    prem[i, prem$HomeTeam[i]] = 0
  } else{
    prem[i, c(prem$AwayTeam[i], prem$HomeTeam[i])] = 1
  }
}

head(prem)

## # A tibble: 6 x 26
##   Date       HomeTeam AwayTeam  FTHG  FTAG FTR   Arsenal Bournemouth
##   <date>     <chr>    <chr>    <int> <int> <chr>   <dbl>       <dbl>
## 1 2017-08-11 Arsenal  Leicest…     4     3 H           3          NA
## 2 2017-08-12 Brighton Man City     0     2 A          NA          NA
## 3 2017-08-12 Chelsea  Burnley      2     3 A          NA          NA
## 4 2017-08-12 Crystal… Hudders…     0     3 A          NA          NA
## 5 2017-08-12 Everton  Stoke        1     0 H          NA          NA
## 6 2017-08-12 Southam… Swansea      0     0 D          NA          NA
## # ... with 18 more variables

You can see where Arsenal beat Leicester 4-3, there is a 3 in the Arsenal variable. Now, it would be nice to have this data in long form, for plotting purposes later, so we’ll use gather(). I then don’t want any rows with an NA in the Points variable, as these only occur if a team hasn’t played on that day.

prem_points = prem  %>% 
  gather(Team, Points, Arsenal:`West Ham`) %>% 
  select(Date, Team, Points) %>% 
  drop_na(Points)

At the moment, we only have one row for each match on each day. Later, we’ll need to work out the position of each team on each day. To do this, we need the points for each team on each day, even if they didn’t play. So I’m going to create an empty data set of days and teams, join it then fill in the NA’s with 0’s.

empty = data.frame(Date = rep(unique(prem$Date), each = 20),
           Team = unique(prem$HomeTeam),
           stringsAsFactors = FALSE)

prem_points = left_join(empty, prem_points)

## Joining, by = c("Date", "Team")

prem_points[is.na(prem_points)] = 0

Now all we need to do is calculate the cumulative points for each team on each day over the course of the season

prem_points = prem_points %>% 
  group_by(Team) %>% 
  arrange(Date) %>% 
  mutate(Points = cumsum(Points)) %>% 
  ungroup() 

So, for example, for Arsenal, the data now looks like this

prem_points %>% 
  filter(Team == "Arsenal") %>% 
  arrange(Date)

## # A tibble: 105 x 3
##    Date       Team    Points
##    <date>     <chr>    <dbl>
##  1 2017-08-11 Arsenal      3
##  2 2017-08-12 Arsenal      3
##  3 2017-08-13 Arsenal      3
##  4 2017-08-19 Arsenal      3
##  5 2017-08-20 Arsenal      3
##  6 2017-08-21 Arsenal      3
##  7 2017-08-26 Arsenal      3
##  8 2017-08-27 Arsenal      3
##  9 2017-09-09 Arsenal      6
## 10 2017-09-10 Arsenal      6
## # ... with 95 more rows

We have a row for each day there was a premier league match, even if that team didn’t play. Here you can see Arsenal won on the first day of the season (they beat Leicester 4-3) and gather any more points til the won again on the 9th of September.

Cumulative goal difference per team per day

We’re going to take the exact same process to do this job. Do let’s start by overwriting those columns of points in prem with columns of NA’s ready for the goal difference

prem[sort(unique(prem$HomeTeam))] = NA

Now, using a for loop again (again, I’m sorry) for each home team and away team we calculate the goal difference by simply minusing the away team goals from the home team goals or vice versa.

for(i in 1:nrow(prem)){
  prem[i, prem$HomeTeam[i]] = prem$FTHG[i] - prem$FTAG[i]
  prem[i, prem$AwayTeam[i]] = prem$FTAG[i] - prem$FTHG[i]
}

head(prem)

## # A tibble: 6 x 26
##   Date       HomeTeam AwayTeam  FTHG  FTAG FTR   Arsenal Bournemouth
##   <date>     <chr>    <chr>    <int> <int> <chr>   <int>       <int>
## 1 2017-08-11 Arsenal  Leicest…     4     3 H           1          NA
## 2 2017-08-12 Brighton Man City     0     2 A          NA          NA
## 3 2017-08-12 Chelsea  Burnley      2     3 A          NA          NA
## 4 2017-08-12 Crystal… Hudders…     0     3 A          NA          NA
## 5 2017-08-12 Everton  Stoke        1     0 H          NA          NA
## 6 2017-08-12 Southam… Swansea      0     0 D          NA          NA
## # ... with 18 more variables:

You can see now for when Arsenal beat Leicester 4-3, instead of having a 3 in the Arsenal variable, we have a 1 to signify Arsenal won by 1 goal. Now we follow the same process as before in that we gather the data into long format, join with the empty data set of days, turn the NAs into 0’s and then calculate the cumulative goal difference over the season.

prem_gd = prem  %>% 
  gather(Team, GD, Arsenal:`West Ham`) %>% 
  select(Date, Team, GD) %>% 
  drop_na(GD)

prem_gd = left_join(empty, prem_gd)

## Joining, by = c("Date", "Team")

prem_gd[is.na(prem_gd)] = 0

prem_gd = prem_gd %>% 
  group_by(Team) %>% 
  arrange(Date) %>% 
  mutate(GD = cumsum(GD)) %>% 
  ungroup() 

Now we need to join the two data sets!

prem_total = left_join(prem_points, prem_gd)

## Joining, by = c("Date", "Team")

Again using Arsenal as the example team, the data now looks like this

prem_total %>% 
  filter(Team == "Arsenal") %>% 
  arrange(Date)

## # A tibble: 105 x 4
##    Date       Team    Points    GD
##    <date>     <chr>    <dbl> <dbl>
##  1 2017-08-11 Arsenal      3     1
##  2 2017-08-12 Arsenal      3     1
##  3 2017-08-13 Arsenal      3     1
##  4 2017-08-19 Arsenal      3     0
##  5 2017-08-20 Arsenal      3     0
##  6 2017-08-21 Arsenal      3     0
##  7 2017-08-26 Arsenal      3     0
##  8 2017-08-27 Arsenal      3    -4
##  9 2017-09-09 Arsenal      6    -1
## 10 2017-09-10 Arsenal      6    -1
## # ... with 95 more rows

Now we can see not only when Arsenal picked up points, but when they dropped points as well. For example, on the 27th of August, they got beat by 4 goals as their goal difference shifted from 0 to -4.

We’re not done there! For the gif, we want to be able to display the current status of the team on each day i.e. Champions League (4th or above), Europa League (5th – 7th), Top Half (8th – 10th), Bottom Half (11th – 17th) or Relgations Zone (18th or below). To do this, on each day, we first need to retrieve the order of each team based on their points and goal difference

prem_total = prem_total %>% 
  group_by(Date) %>% 
  arrange(desc(Points), desc(GD)) %>% 
  mutate(Position = row_number()) %>% 
  ungroup()

After that, we can quite easily calculate their position status using our own function, and a bit of purrr

Qual = function(x){
  if(x <= 4){
    y = "Champions League"
  } else if(x <= 7){
    y = "Europa League"
  } else if(x <= 10){
    y = "Top Half"
  } else if(x <= 17){
    y = "Bottom Half"
  } else { 
    y = "Relegation"
  }
  return(y)
}

prem_total = prem_total %>% 
  mutate(Status = map_chr(Position, Qual),
         Status = factor(Status, levels = c("Champions League",
                                            "Europa League",
                                            "Top Half",
                                            "Bottom Half",
                                            "Relegation")))  

head(prem_total)

## # A tibble: 6 x 6
##   Date       Team     Points    GD Position Status          
##   <date>     <chr>     <dbl> <dbl>    <int> <fct>           
## 1 2018-05-13 Man City    100    79        1 Champions League
## 2 2018-05-09 Man City     97    78        1 Champions League
## 3 2018-05-10 Man City     97    78        1 Champions League
## 4 2018-05-06 Man City     94    76        1 Champions League
## 5 2018-05-08 Man City     94    76        1 Champions League
## 6 2018-04-29 Man City     93    76        1 Champions League

Note that here I’m using a factor to reorganise the legend in the plot we’re about to make. We’re looking for a path of a teams points and goal difference over a season, with a colour scheme for where they are in the table at that point. This is what that looks for one team (here I’m using Newcastle United)

 prem_total %>%
    filter(Team == "Newcastle") %>% 
    arrange(Date) %>% 
    ggplot(aes(GD, Points)) + 
    geom_point(aes(colour = Status), size = 3) +
    geom_path(linetype = 2, alpha = 0.4) +  
    theme_minimal() + 
    labs(title = "NUFC Points/Goal Difference Path",
         subtitle = "Season 2017/2018") + 
    theme(legend.position="bottom") + 
    scale_colour_brewer(type = "qual",
                        palette = "Paired") 

Bear in mind we’re going to have 20 teams on the graph and so instead of just using points, we’re going to use labels with the team’s name on.

Now, adding gganimate is relatively pain-free. The package comes with lots of functions titled transition_*(). These dictate by what variable your gif will change. We want our gif to be over time i.e. the variable Date. There is a specific transition function that works with time, called transition_time(). gganimate is also lovely in the way that we can just add these functions to regular ggplots

g = prem_total %>%
  arrange(Date) %>%
  ggplot(aes(GD, Points)) +
  geom_label(aes(label = Team, fill = Status), label.padding = unit(0.1, "lines")) +
  theme_minimal() +
  labs(title = "PL Team Points vs Goal Difference 17/18",
  subtitle =  "Date: {frame_time}") +
  scale_colour_brewer(type = "qual",
  palette = "Paired") +
  theme(legend.position = "bottom") +
  transition_time(Date)

animate(g, nframes = 200, fps = 2)

We’ve only added one function here. Easy! If you are wanting to split it up by something more arbitrary (a character variable let’s say), then you would use transition_states(). Then all that is needed is the animate function! Within the animate() function, the nframes argument is the total number of frames whilst the fps argument is the total number of frames per second. If we wanted our gif to be a bit quicker, we’d go for a higher frame per second.

That’s all for now, thanks for reading!

2 thoughts on “Animating the Premier League using gganimate

  1. Thank you for this great post. The points/gd animation is mesmerising! If you’d rather not use a for loop to populate the points column, you could do the following instead:

    prem %
    mutate(homeTeam = case_when(FTR == “H” ~ 3,
    FTR == “A” ~ 0,
    TRUE ~ 1),
    awayTeam = case_when(FTR == “A” ~ 3,
    FTR == “H” ~ 0,
    TRUE ~ 1))

    # original code
    for(i in 1:nrow(prem)) {
    if(prem$FTR[i] == “H”) {
    prem[i, prem$HomeTeam[i]] = 3
    prem[i, prem$AwayTeam[i]] = 0
    } else if(prem$FTR[i] == “A”) {
    prem[i, prem$AwayTeam[i]] = 3
    prem[i, prem$HomeTeam[i]] = 0
    } else{
    prem[i, c(prem$AwayTeam[i], prem$HomeTeam[i])] = 1
    }
    }

    • Hi Joe,

      Thanks for your response. I’m glad you enjoyed the blog and the gif! Using your method, I’m sure the data would end up in the same place (and yours might even be quicker!). However, the two pieces of code are not the same. I’m wanting to populate 20 separate columns (one for each team), where as your method populates two separate columns (one for the home team and one for the away team).

      Cheers

Comments are closed.