The FootballData package and dashboard
Getting football data, then summarising a fan’s mood based on the teams performance
Table of Contents
Intro
A colleague made the following site in jest (https://coppingr.com/), as an outgoing present for the founding head of our department. Inspired this effort, I wondered whether you could make a service to predict a football supporters mood, by leveraging one of the many APIs out there that serve live or near live football data.
This post is an example of using the package I wrote that spawned from that idea.
Documentation: https://epijim.github.io/FootballData/
Setup environment
library(FootballData)
library(tidyverse)
library(glue)
library(emo) #devtools::install_github("hadley/emo")
# table helper - switch between html vs markdown
table_it <- function(x, type = "markdown"){
if(type == "markdown") return(x %>% knitr::kable(escape = FALSE))
x %>% kableExtra::kbl(escape = F) %>%
kableExtra::kable_classic("striped", full_width = TRUE)
}
Get competitions
First I will pull all the leagues that are in the free tier, from this particular API (football-data.org) and show them on a table.
# Get competitions
competitions <- football_get_competition() %>%
filter(
# filter to what's in the free plan
plan == "TIER_ONE"
&
# The data structures returned for country level competitions is
# different, so let's remove it
!country %in% c("World","Europe"))
competitions %>% select(flag,country,league) %>% table_it()
flag | country | league |
---|---|---|
NA | Brazil | Série A |
England | Championship | |
England | Premier League | |
France | Ligue 1 | |
Germany | Bundesliga | |
Italy | Serie A | |
Netherlands | Eredivisie | |
Portugal | Primeira Liga | |
Spain | Primera Division |
Dive into a league
Now I’ll pick my local premier league (Bundesliga
) and pull info on it’s current
standings.
# Get league info
league <- competitions %>%
filter(country == "Germany" & league == "Bundesliga") %>%
pull(competition_id) %>%
football_get_standing()
league %>%
mutate(team = paste(crest,team)) %>%
select(team,playedGames,form,points) %>%
table_it()
team | playedGames | form | points |
---|---|---|---|
FC Bayern München | 20 | W,W,W,W,W | 48 |
RB Leipzig | 20 | W,W,L,W,D | 41 |
VfL Wolfsburg | 20 | W,W,W,W,D | 38 |
Eintracht Frankfurt | 20 | W,W,W,D,W | 36 |
Bayer 04 Leverkusen | 20 | W,L,L,W,L | 35 |
Borussia Dortmund | 20 | L,W,L,L,D | 32 |
Borussia Mönchengladbach | 20 | L,D,W,W,D | 32 |
SC Freiburg | 20 | W,L,W,D,L | 30 |
1. FC Union Berlin | 20 | L,D,L,L,W | 29 |
VfB Stuttgart | 20 | L,W,L,L,D | 25 |
SV Werder Bremen | 19 | D,W,L,W,D | 22 |
TSG 1899 Hoffenheim | 20 | L,L,W,W,D | 22 |
FC Augsburg | 20 | L,L,W,L,L | 22 |
1. FC Köln | 20 | W,W,L,W,D | 21 |
Hertha BSC | 20 | L,L,L,L,D | 17 |
DSC Arminia Bielefeld | 19 | L,L,W,D,W | 17 |
1. FSV Mainz 05 | 20 | W,L,W,L,D | 13 |
FC Schalke 04 | 20 | L,D,L,L,L | 8 |
Get upcoming games for a team
From the API, I can also get more details on my local team.
team_of_interest <- "SC Freiburg"
league %>%
filter(team == team_of_interest) %>%
pull(team_id) %>%
football_get_upcoming() %>%
arrange(utcDate) %>%
slice(1) %>%
mutate(
text = glue(
"Today is {Sys.Date()}\n",
"{team_of_interest}'s next game is on {as.Date(utcDate)}\n",
"Home: {homeTeam$name}; Away {awayTeam$name}"
)
) %>%
pull(text)
#> Today is 2021-02-09
#> SC Freiburg's next game is on 2021-02-13
#> Home: SV Werder Bremen; Away SC Freiburg
Calculate the moods
Now that I have used the data prep functions, I can jump into the ‘fun’ bit of this package. Using the live data I can run simple rules to try and figure out what the mood of a supporter may be right now.
Below - I run the mood logic on the Bundesliga.
emoji_bad <- emo::ji("poop")
emoji_good <- emo::ji("beer")
add_bad <- function(x){
gsub("-",emoji_bad,x = x)
}
add_good <- function(x){
gsub("\\+",emoji_good,x = x)
}
league %>%
football_calculate_competition_metrics() %>%
mutate(
team = paste(crest,team),
metric_mood = as.character(metric_mood),
metric_mood = case_when(
str_detect(metric_mood,"Jubilant") ~ kableExtra::cell_spec(
metric_mood, color = "black", background = "#98FB98"
),
str_detect(metric_mood,"Optimistic") ~ kableExtra::cell_spec(
metric_mood, color = "black", background = "#e5f5f9"
),
str_detect(metric_mood,"Ambivalent") ~ kableExtra::cell_spec(
metric_mood, color = "black", background = "#f7fcb9"
),
str_detect(metric_mood,"Doldrums") ~ kableExtra::cell_spec(
metric_mood, color = "black", background = "#ffc4c4"
),
TRUE ~ metric_mood
)
) %>%
rename(mood = metric_mood,numeric_mood = metric_mood_numeric) %>%
mutate_at(vars(matches("metric_")),add_bad) %>%
mutate_all(add_good) %>%
select(team,points,starts_with("metric_"),numeric_mood,mood) %>%
table_it()
team | points | metric_season_position | metric_recent_wins | metric_back_of_net | numeric_mood | mood |
---|---|---|---|---|---|---|
FC Bayern München | 48 | 🍺🍺🍺🍺 top of the league | 🍺🍺🍺🍺 5 win streak | 🍺🍺🍺 strikers are on form | 11 | Jubilant: FC Bayern München are on a roll |
RB Leipzig | 41 | 🍺🍺 strong league position | 🍺🍺 recent wins | 🍺🍺🍺 strikers are on form | 7 | Jubilant: RB Leipzig are on a roll |
VfL Wolfsburg | 38 | 🍺🍺 strong league position | 🍺🍺 recent wins | 🍺 scored more than conceded | 5 | Optimistic: VfL Wolfsburg are doing well |
Eintracht Frankfurt | 36 | 🍺 safe in middle of league | 🍺🍺 recent wins | 🍺 scored more than conceded | 4 | Optimistic: Eintracht Frankfurt are doing well |
Bayer 04 Leverkusen | 35 | 🍺 safe in middle of league | 💩💩 more recent losses than wins | 🍺 scored more than conceded | 0 | Ambivalent: Bayer 04 Leverkusen aren’t doing well, but hey - at least they aren’t FC Schalke 04 |
Borussia Dortmund | 32 | 🍺 safe in middle of league | 💩💩 more recent losses than wins | 🍺 scored more than conceded | 0 | Ambivalent: Borussia Dortmund aren’t doing well, but hey - at least they aren’t FC Schalke 04 |
Borussia Mönchengladbach | 32 | 🍺 safe in middle of league | drew last game | 🍺 scored more than conceded | 2 | Optimistic: Borussia Mönchengladbach are doing well |
SC Freiburg | 30 | 🍺 safe in middle of league | 💩 lost last game | 🍺 scored more than conceded | 1 | Optimistic: SC Freiburg are doing well |
1. FC Union Berlin | 29 | 💩 so so season | 🍺 won last game | 🍺 scored more than conceded | 1 | Optimistic: 1. FC Union Berlin are doing well |
VfB Stuttgart | 25 | 💩 so so season | 💩💩 more recent losses than wins | 🍺 scored more than conceded | -2 | Ambivalent: VfB Stuttgart aren’t doing well, but hey - at least they aren’t FC Schalke 04 |
SV Werder Bremen | 22 | 💩 so so season | drew last game | 💩 conceded more than scored | -2 | Ambivalent: SV Werder Bremen aren’t doing well, but hey - at least they aren’t FC Schalke 04 |
TSG 1899 Hoffenheim | 22 | 💩 so so season | drew last game | 💩 conceded more than scored | -2 | Ambivalent: TSG 1899 Hoffenheim aren’t doing well, but hey - at least they aren’t FC Schalke 04 |
FC Augsburg | 22 | 💩 so so season | 💩💩 more recent losses than wins | 💩 conceded more than scored | -4 | Ambivalent: FC Augsburg aren’t doing well, but hey - at least they aren’t FC Schalke 04 |
1. FC Köln | 21 | 💩 so so season | 🍺🍺 recent wins | 💩 conceded more than scored | 0 | Ambivalent: 1. FC Köln aren’t doing well, but hey - at least they aren’t FC Schalke 04 |
Hertha BSC | 17 | 💩💩 poor season | 💩💩 no recent wins | 💩 conceded more than scored | -5 | Doldrums: Hertha BSC are doing poor. Now’s a good time to bad mouth the manager. |
DSC Arminia Bielefeld | 17 | 💩💩 poor season | 🍺 won last game | 💩💩💩 defense is a seive | -4 | Ambivalent: DSC Arminia Bielefeld aren’t doing well, but hey - at least they aren’t FC Schalke 04 |
1. FSV Mainz 05 | 13 | 💩💩💩💩 relagation? | drew last game | 💩💩💩 defense is a seive | -7 | Doldrums: 1. FSV Mainz 05 are doing poor. Now’s a good time to bad mouth the manager. |
FC Schalke 04 | 8 | 💩💩💩💩 relagation? | 💩💩 no recent wins | 💩💩💩 defense is a seive | -9 | Doldrums: FC Schalke 04 are doing poorly. Avoid supporters at all costs. |
Shiny app
Not interested in the Bundesliga? I also wrapped the package in a shiny app available here: https://epijim.shinyapps.io/football_moods/
Exactly what are the metrics that define mood?
The current logic is contained in the following Github file. As a summary, following gist highlights just the relevant code present when this post was authored.