Closest Eredivisie football club

2021-02-16

A couple of months back, I saw a map of England that showed the closest Premier League football club at any location. I can’t find the exact same map anymore, but this map on Reddit is similar.

I figured it would be a nice exercise in spatial visualization in R to try and create a similar map for the Netherlands. The Dutch highest football league is called the Eredivisie and consists of 18 clubs. In this post, I will create a map conveying the closest Eredivisie club given a location. I will use the tidyverse for data wrangling and sf for spatial data transformations.

library(tidyverse)
library(sf)

First, we need the club’s locations. For this, we will use the addresses of the stadiums.

Eredivisie clubs

The Eredivisie website has a page listing the 18 clubs currently in the league. Every club has its own page (for example, Ajax) which lists various statistics about the club, the players, and the stadium. Using rvest and jsonlite, we collect this list of clubs, and for every club obtain the stadium’s address. As a bonus, we collect the clubs’ logos, which will allow for nice plotting.

library(rvest)

url <- "https://eredivisie.nl/en-us/clubs"

club_names <- read_html(url) %>% 
  html_nodes(".clubs-list__club img") %>% 
  html_attr("title")


get_address <- function(club) {
  club <- URLencode(club)
  url <- "https://eredivisie.nl/en-us/API/DotControl/DCEredivisieLive/Stats/GetThemeSelection?teamname={club}&cid=0&moduleId=1261&tabId=255"

  str_glue(url) %>% 
    jsonlite::read_json() %>% 
    purrr::pluck("club", "stadium")
}

img_src <- "https://eredivisie-images.s3.amazonaws.com/Eredivisie%20images/Eredivisie%20Badges/{teamId}/150x150.png"

stadiums <- map_dfr(club_names, get_address) %>% 
  mutate(img = str_glue(img_src))

stadiums %>% 
  transmute(teamname, adres, city,
            img = cell_spec(stringr::str_trunc(img, 40, side = "left"),
                            "html", link = img)) %>% 
  head() %>% 
  knitr::kable(escape = FALSE) %>% 
  kable_styling(bootstrap_options = c("condensed", "hover"), font_size = 13) 
teamname adres city img
Ajax ArenA Boulevard 29 Amsterdam …s/Eredivisie%20Badges/215/150x150.png
PSV Frederiklaan 8 Eindhoven …s/Eredivisie%20Badges/204/150x150.png
AZ Stadionweg 1 Alkmaar …s/Eredivisie%20Badges/313/150x150.png
Vitesse Batavierenweg 25 Arnhem …s/Eredivisie%20Badges/232/150x150.png
Feyenoord Van Zandvlietplein 1 Rotterdam …s/Eredivisie%20Badges/198/150x150.png
FC Groningen Boumaboulevard 41 Groningen …s/Eredivisie%20Badges/425/150x150.png

The addresses do not yet allow for plotting on a map — for that, we need coordinates. Converting a human-readable address into coordinates (such as latitude and longitude) is called geocoding. There are various APIs that do this. Most of these are paid, but I’ve found a free one that does the job. We provide it with an address as a string, and it gives us back the latitude and longitude. We have to be as specific as possible, to prevent it from finding a location with the same address in another country for example.

geo_locate <- function(address) {
  query_string <- URLencode(address)
  url <- "https://api.opencagedata.com/geocode/v1/json?q={query_string}&key=e5580345eb0e46d9a2e17e6a7e2373f7&no_annotations=1&language=nl"

  str_glue(url) %>% 
    jsonlite::read_json() %>% 
    purrr::pluck("results", 1, "geometry")
}

stadiums <- stadiums %>% 
  mutate(latlon = map(str_glue("{adres}, {postalcode} {city}, Nederland"), geo_locate)) %>% 
  unnest_wider(latlon)

stadiums %>% 
  transmute(teamname, lat, lng,
            img = cell_spec(stringr::str_trunc(img, 40, side = "left"),
                            "html", link = img)) %>% 
  head() %>% 
  knitr::kable(escape = FALSE) %>% 
  kable_styling(bootstrap_options = c("condensed", "hover"), font_size = 13) 
teamname lat lng img
Ajax 52.31435 4.942843 …s/Eredivisie%20Badges/215/150x150.png
PSV 51.44083 5.477780 …s/Eredivisie%20Badges/204/150x150.png
AZ 52.61295 4.740751 …s/Eredivisie%20Badges/313/150x150.png
Vitesse 51.96366 5.893330 …s/Eredivisie%20Badges/232/150x150.png
Feyenoord 51.89434 4.524698 …s/Eredivisie%20Badges/198/150x150.png
FC Groningen 53.20702 6.591678 …s/Eredivisie%20Badges/425/150x150.png

Now that we have the clubs’ locations, we can do some data wrangling in order to create the map.

Map of the Netherlands

In order to plot a map of the Netherlands, we need the geometry of its boundaries. Below, we obtain municipality boundaries from the CBS (the Dutch national statistics bureau). From this, the country boundaries can be obtained with sf::st_union(). Since the CBS data is in a Dutch coordinate system called Rijksdriehoekscoördinaten, we first convert this to WGS84 via st_transform().

municipalities <- st_read("https://geodata.nationaalgeoregister.nl/cbsgebiedsindelingen/wfs?request=GetFeature&service=WFS&version=2.0.0&typeName=cbs_gemeente_2021_gegeneraliseerd&outputFormat=json", quiet = TRUE) %>% 
  st_transform(4326) # WGS84

nl <- st_union(municipalities)

ggplot(municipalities) +
  geom_sf() +
  theme_void()

Voronoi

The type of map we want to generate is called a Voronoi diagram. We can create a Voronoi diagram from a set of points using sf::st_voronoi(). Since that function operates on a multipoint and we want the result to be of type sf, we do some pre- and post-processing.

voronoi <- stadiums %>% 
  select(lng, lat) %>% 
  as.matrix() %>% 
  st_multipoint() %>%
  st_voronoi(envelope = nl) %>%
  st_collection_extract() %>% 
  st_set_crs(4326) %>% # WGS84
  st_sf()

plot(voronoi)
stadiums %>% 
  select(lng, lat) %>% 
  as.matrix() %>% 
  points()

Now that we have the Voronoi diagram, we can join it to the stadium data. We intersect it with the boundaries of the Netherlands to make sure the Voronoi lines do not extend to outside the country borders.

stadiums_voronoi <- stadiums %>%
  st_as_sf(coords = c("lng", "lat"),
           crs = 4326, remove = FALSE) %>% 
  st_join(voronoi, .) %>% # join makes sure we map each tile to the correct centroid
  st_cast("MULTILINESTRING") %>% # turn polygons into lines
  st_intersection(nl) # within NL

Plotting

Finally, we can plot the map of the Netherlands together with the club logos and the Voronoi edges.
We do some manual work to move the club logos away from the points, to make the plot look nicer. The result is a map of the closest Eredivisie football club!

offsets <- tribble(~ teamname, ~ dlng, ~dlat,
                   "Feyenoord", 0.12, -0.12,
                   "Sparta Rotterdam", -0.15, -0.13,
                   "Fortuna Sittard", -0.15, -0.1,
                   "PSV", 0.12, 0.1,
                   "Willem II", 0, -0.15,
                   "RKC Waalwijk", 0.13, 0.08,
                   "ADO Den Haag", 0.1, 0.1,
                   "Ajax", 0.05, 0.14,
                   "FC Utrecht", 0.1, 0.08,
                   "AZ", 0.08, 0.1,
                   "Vitesse", 0.05, -0.15,
                   "FC Twente", 0.12, 0.1,
                   "Heracles Almelo", -0.1, -0.15,
                   "PEC Zwolle", 0, -0.14,
                   "sc Heerenveen", -0.1, 0.15,
                   "FC Groningen", -0.15, -0.1,
                   "FC Emmen", 0.14, 0.1,
                   "VVV-Venlo", -0.1, 0.1)

stadiums_voronoi %>% 
  left_join(offsets, by = "teamname") %>% 
  mutate(lng_img = lng + dlng,
         lat_img = lat + dlat) %>% 
  ggplot() +
  geom_sf(data = municipalities, size = 0.1, fill = "#cccccc") +
  geom_sf(size = 0.6, fill = NA, color = "black", alpha = 0.8) +
  ggimage::geom_image(aes(image = img, x = lng_img, y = lat_img), size = 0.08) + 
  geom_point(aes(x = lng, y = lat)) +
  theme_void()

Bonus: interactive map

Using the leaflet library, it is easy to create an interactive map in R. Below, we show the Netherlands with OpenStreetMap, and draw the Voronoi boundaries and markers for the stadiums on top of it. Adding images to leaflet is not straightforward, so we will not add these.

library(leaflet)

stadiums %>%
  st_as_sf(coords = c("lng", "lat"),
           crs = 4326, remove = FALSE) %>% 
  st_join(voronoi, .) %>% # join makes sure we map each tile to the correct centroid
  st_cast("MULTILINESTRING") %>% # turn polygons into lines
  leaflet() %>% 
  setView(lng = 5.6, lat = 52.2, zoom = 7) %>%
  addTiles() %>% 
  addMarkers(lng = ~lng, lat = ~lat, label = ~teamname) %>% 
  addPolylines()