Background

This work was somewhat motivated by a post I read on another interesting data science blog; its combination of network graphs and football seemed both accessible and visualing appealing. Due to the profileration of social media and technological advances, graph/network based approaches are becoming more common. Graph theory has been employed to study disease propagation, elephant rest sites, relationships in The Simpsons and even MMA finishes, so I wanted to try it out for myself.

I watch alot of English Premier League (EPL) football, so I’m actutely aware of its reputation as the Most Competitive league in the world, (formerly, the Best League in the World). I’m not aiming to compare the quality of each league (UEFA coefficients solves that problem), but rather determine whether the leagues themselves are becoming less competitive. This decade has seen the rise of foreign owned super rich clubs across Europe (Man City, PSG) and the domination of domestic championships by a small elite (Bayern Munich in Germany, Juventus in Italy). Then again, just last year, relegation favourites Leicester City won the EPL, so maybe the EPL has become more competitive than ever.

I suppose we need to quantify the competitiveness of a league. We’ll use two approaches: one based on graph theory and another more conventional statistical approach. I’m not particularly expecting the former to beat the latter, I just wanted an excuse to build a network graph populated with football teams.

Gathering the Data

There are numerous free sources of football data (well, at least for the major European leagues- you might struggle with the Slovakian Third Division or the Irish Premier Division). There’s a good summary here. And if you’re interested in R API wrappers, there’s the footballR package. As we want to look at historical trends within leagues, we’ll choose the csv route (APIs generally go back only a few years). The data will be sourced from this site. No need to download the files, we can import the data directly into R using the appropriate URL. Let’s start with the last year of Alex Ferguson’s reign as Man United manager (2012-13 EPL season).

#loading the packages we'll need
require(RCurl) # import csv from URL
require(dplyr) # data manipulation/filtering
require(visNetwork) # producing interactive graphs
require(igraph) # to calculate graph properties
require(ggplot2) # vanilla graphs
require(purrr) # map lists to functions

options(stringsAsFactors = FALSE)
epl_1213 <- read.csv(text=getURL("http://www.football-data.co.uk/mmz4281/1213/E0.csv"), 
                     stringsAsFactors = FALSE)
head(epl_1213[,1:10])
##   Div     Date  HomeTeam   AwayTeam FTHG FTAG FTR HTHG HTAG HTR
## 1  E0 18/08/12   Arsenal Sunderland    0    0   D    0    0   D
## 2  E0 18/08/12    Fulham    Norwich    5    0   H    2    0   H
## 3  E0 18/08/12 Newcastle  Tottenham    2    1   H    0    0   D
## 4  E0 18/08/12       QPR    Swansea    0    5   A    0    1   A
## 5  E0 18/08/12   Reading      Stoke    1    1   D    0    1   A
## 6  E0 18/08/12 West Brom  Liverpool    3    0   H    1    0   H

For each match in a given season, the data frame includes the score and various other data we can ignore (mostly betting odds). First, we must think about our network. Networks are composed of nodes and edges, where an edge connecting two nodes indicates a relationship. In its simplest form, think of a network of people, where two nodes are joined by an edge if they’re friends. We can have either undirected or directed networks. The latter means that there’s a direction to the relationship (e.g. following someone on Twitter does imply that they follow you, which contrasts with Facebook friends). We’ll keep things simple, so we’ll opt for an undirected graph.

The nodes are the 20 teams of 2012-13 EPL season, but what are the edges? Using the epl_1213 data frame, we’ll say two teams are connected if each team gained at least one point in the two matches they played against each other (teams play each other both home and away in Europe’s major football leagues). Equivalently, two teams are not connected if one team won both encounters. We can imagine how our network will look. The big teams should have fewer connections as they are more likely to have beaten their opponents both home and away. Similarly, the weaker teams will be less conencted, as they will have lost regularly. In the middle, we’ll have teams that didn’t regularly defeat the poor teams, but were resilient against the bigger teams.

Our next step is to reconstruct our data frame as a set of nodes and edges.

#convert data frame to head to head record
epl_1213 <- epl_1213 %>% dplyr::select(HomeTeam, AwayTeam, FTHG, FTAG) %>% 
  dplyr::rename(team1=HomeTeam, team2= AwayTeam, team1FT = FTHG, team2FT = FTAG) %>%
  dplyr::filter(team1!="")

epl_1213 <- bind_rows(list(epl_1213 %>% 
                        dplyr::group_by(team1,team2) %>%
                        dplyr::summarize(points = sum(case_when(team1FT>team2FT~3,
                                                                team1FT==team2FT~1,
                                                                TRUE ~ 0))),
                      epl_1213 %>% dplyr::rename(team2=team1,team1=team2) %>%
                        dplyr::group_by(team1,team2) %>%
                        dplyr::summarize(points = sum(case_when(team2FT>team1FT~3,
                                                                team2FT==team1FT~1,
                                                                TRUE ~ 0))))) %>%
  dplyr::group_by(team1, team2) %>% dplyr::summarize(tot_points = sum(points)) %>% 
  dplyr::ungroup() %>% dplyr::arrange(team1,team2)

head(epl_1213)
## # A tibble: 6 × 3
##     team1       team2 tot_points
##     <chr>       <chr>      <dbl>
## 1 Arsenal Aston Villa          4
## 2 Arsenal     Chelsea          0
## 3 Arsenal     Everton          2
## 4 Arsenal      Fulham          4
## 5 Arsenal   Liverpool          4
## 6 Arsenal    Man City          1

With a bit of dplyr, we’ve completely reformatted our csv as something approaching a network. For example, Arsenal gained 4 points against Aston Villa, but lost both matches to Chelsea. Remember, we want to exclude teams who lost/won both matches, so we filter out rows with 0 or 6 points. We also remove duplications (we make no distinction between Arsenal -> Aston Villa & Aston Villa -> Arsenal). Okay, we’re ready to construct our nodes and edges. Just note that most graph packages in R require specific column names for node and edges data frames (the various network visualisation packages in R are extensively described in this great tutorial).

# construct nodes
nodes <- dplyr::group_by(epl_1213, team1) %>% 
  dplyr::summarize(value = sum(tot_points)) %>%
  dplyr::rename(id = team1) %>% 
  dplyr::inner_join(crests, by=c("id"= "team")) %>%
  dplyr::arrange(desc(value)) %>%
  dplyr::mutate(shape="image", label = "", 
                title = paste0("<p><b>",id,"</b><br>Points: ",
                               value,"<br>Position: ",row_number(),"</p>"))

head(nodes)
## # A tibble: 6 × 6
##           id value
##        <chr> <dbl>
## 1 Man United    89
## 2   Man City    78
## 3    Chelsea    75
## 4    Arsenal    73
## 5  Tottenham    72
## 6    Everton    63
## # ... with 4 more variables: image <chr>, shape <chr>, label <chr>,
## #   title <chr>
# construct edges
edge_list <- epl_1213 %>% dplyr::filter(as.character(team1)<as.character(team2)) %>% 
  dplyr::filter(!tot_points %in% c(0,6)) %>%
  dplyr::rename(from=team1,to=team2,value=tot_points) %>% dplyr::select(from, to)

head(edge_list)
## # A tibble: 6 × 2
##      from          to
##     <chr>       <chr>
## 1 Arsenal Aston Villa
## 2 Arsenal     Everton
## 3 Arsenal      Fulham
## 4 Arsenal   Liverpool
## 5 Arsenal    Man City
## 6 Arsenal  Man United

We have a set of nodes with some supplementary information (for example, the value column represents the number of points won by that team- it will determine the size of node in the graph). The edge_list data frame is relatively intuitive, each row will create a line/connection between those two teams. We can now visualise the network graph using the visNetwork package.

# plot network graph
visNetwork(nodes,edge_list,main = "EPL 2012-13 Season",width="800px") %>%
  visEdges(color = list(color="gray",opacity=0.25)) %>%
  visOptions( highlightNearest = TRUE, nodesIdSelection = TRUE) %>%
  visEvents(stabilizationIterationsDone="function () {this.setOptions( { physics: false } );}") %>%
  visLayout(randomSeed=91)