Using R to Generate Live World Cup Notifications

Here in Belgium, World Cup fever is at fever pitch, but with matches starting during work hours, how is a desk worker supposed to follow along? By leaving the R environment? Blasphemy.

Today we show how to use R to generate live desktop notifications for The Beautiful Game.

A notification system preview, free of local bias.

A notification system preview, free of local bias.

Overview

We break the process of producing a live score notification into the following steps:

  1. Get the score
  2. Check if the score has changed
  3. If yes, show a notification
  4. Repeat steps 1-3 every minute

Step 1: Getting the Score

FIFA provides an API with detailed information about matches. The API provides a list of each day’s matches in JSON. A full description of the fields is provided in the API documentation.

For the purposes of this exercise, we need the scores (AggregateHomeTeamScore, AggregateAwayTeamScore, HomeTeamPenaltyScore, AwayTeamPenaltyScore), and team names (HomeTeam.TeamName, AwayTeam.TeamName). Additionally, we subset the data to the active World Cup matches by filtering to matches with IdSeason of 254645 (the World Cup competition ID) and MatchStatus of 3 (the live match status ID).

As functions, this looks like:

readLiveMatchScore <- function() {
  # reading in the API data
  worldcupDataDF <- 
      jsonlite::fromJSON("https://api.fifa.com/api/v1/live/football/")$Results
  # which World Cup match is currently active?
  worldcupMatchIdx <- which(worldcupDataDF$IdSeason == 254645 & 
          worldcupDataDF$MatchStatus == 3)
  
  if (length(worldcupMatchIdx) != 1) { # no matches or more than 1 match
    liveScore <- NULL
  } else {
    # get the score
    liveScore <- unlist(worldcupDataDF[worldcupMatchIdx, 
            c("AggregateHomeTeamScore", "AggregateAwayTeamScore", 
              "HomeTeamPenaltyScore", "AwayTeamPenaltyScore")])
    
    homeTeam <- worldcupDataDF$HomeTeam$TeamName[[worldcupMatchIdx]]$Description
    awayTeam <- worldcupDataDF$AwayTeam$TeamName[[worldcupMatchIdx]]$Description
    names(liveScore) <- rep(c(homeTeam, awayTeam), 2)
  }
  liveScore
}

scoreAsString <- function(matchScore, penalties = FALSE) {
  out <- paste(names(matchScore)[1], " - ", names(matchScore)[2], ":", 
      matchScore[1], " - ", matchScore[2])
  if (penalties && matchScore[1] == matchScore[2])
    out <- paste0(out, " (pen. ", matchScore[3], " - ", matchScore[4], ")" )
  out
}

Step 2: Check If the Score Has Changed

To check if the score has changed, we store the previous score and check if it differs from the current score. If there is a change, we send a notification.

checkScoreAndNotify <- function(prevScore = NULL) {
  newScore <- readLiveMatchScore()
  if (is.null(newScore) && is.null(prevScore)) {
    # nothing to do here
  } else if (is.null(newScore) && !is.null(prevScore)) {
    # end of the game
    sendNotification(title = "Match ended", message = scoreAsString(prevScore, TRUE))
  
  } else if (is.null(prevScore) && !is.null(newScore)) {
    # start of the game
    sendNotification(title = "Match started", message = scoreAsString(newScore))
  
  } else if (!is.null(prevScore) && !is.null(newScore) && !identical(newScore, prevScore)) {
    # change in the score
    sendNotification(title = "GOAL!", message = scoreAsString(newScore))
  }
  return(newScore)
}

Step 3: Display Notification

To create a notification, we use the notifier R package (now archived on CRAN). It can be installed via devtools:

devtools::install_version("notifier")

or via the CRAN Archive by giving the URL:

url <- "https://cran.rstudio.com/src/contrib/Archive/notifier/notifier_1.0.0.tar.gz"
install.packages(url, type = "source", repos = NULL)

To spice up the notification, we add the World Cup logo in the notification area.

# get the logo from FIFA website
download.file("https://api.fifa.com/api/v1/picture/tournaments-sq-4/254645_w", 
    "logo.png")

sendNotification <- function(title = "", message) {
  notifier::notify(title = title, msg = message, image = normalizePath("logo.png"))
}

Step 4: Repeat Procedure Every Minute

We use the later package to query the scores API repeatedly without blocking the R session. Taking inspiration from Yihui Xie’s blog Schedule R code to Be Executed Periodically in the Current R Session, we write a recursive function to query the scores. The previous score is tracked using a global variable.

getScoreUpdates <- function() {
  prevScore <<- checkScoreAndNotify(prevScore)
  later::later(getScoreUpdates, delay = 60)
}

To run this entire process, we simply initialize the global prevScore variable and launch the recursive function getScoreUpdates:

prevScore <- NULL 
getScoreUpdates()

Wrap-Up

World Cup Notification

That’s our quick take on generating live score notifications using R. By using a different API or alternative competition codes, this approach can be generalized to generate notifications for other settings.

Oh, and if you’re looking to predict the winner of the World Cup using statistics and historical trends, the BBC has you covered. May the loudest vuvuzela win!

Complete Script

## 0. preparatory steps
if (!require("notifier", character.only = TRUE)) {
  url <- "https://cloud.r-project.org/src/contrib/Archive/notifier/notifier_1.0.0.tar.gz"
  install.packages(url, type = "source", repos = NULL)
}
if (!require("later", character.only = TRUE)) {
  install.packages("later")
}
download.file("https://api.fifa.com/api/v1/picture/tournaments-sq-4/254645_w", "logo.png")

## 1. get match score
readLiveMatchScore <- function() {
  # reading in the API data
  worldcupDataDF <- 
      jsonlite::fromJSON("https://api.fifa.com/api/v1/live/football/")$Results
  # which World Cup match is currently active?
  worldcupMatchIdx <- which(worldcupDataDF$IdSeason == 254645 & 
          worldcupDataDF$MatchStatus == 3)
  
  if (length(worldcupMatchIdx) != 1) { # no matches or more than 1 match
    liveScore <- NULL
  } else {
    # get the score
    liveScore <- unlist(worldcupDataDF[worldcupMatchIdx, 
            c("AggregateHomeTeamScore", "AggregateAwayTeamScore", 
                "HomeTeamPenaltyScore", "AwayTeamPenaltyScore")])
    
    homeTeam <- worldcupDataDF$HomeTeam$TeamName[[worldcupMatchIdx]]$Description
    awayTeam <- worldcupDataDF$AwayTeam$TeamName[[worldcupMatchIdx]]$Description
    names(liveScore) <- rep(c(homeTeam, awayTeam), 2)
  }
  liveScore
}

scoreAsString <- function(matchScore, penalties = FALSE) {
  out <- paste(names(matchScore)[1], " - ", names(matchScore)[2], ":", 
      matchScore[1], " - ", matchScore[2])
  if (penalties && matchScore[1] == matchScore[2])
    out <- paste0(out, " (pen. ", matchScore[3], " - ", matchScore[4], ")" )
  out
}

## 2. check for score changes
checkScoreAndNotify <- function(prevScore = NULL) {
  newScore <- readLiveMatchScore()
  if (is.null(newScore) && is.null(prevScore)) {
    # nothing to do here
  } else if (is.null(newScore) && !is.null(prevScore)) {
    # end of the game
    sendNotification(title = "Match ended", message = scoreAsString(prevScore, TRUE))
    
  } else if (is.null(prevScore) && !is.null(newScore)) {
    # start of the game
    sendNotification(title = "Match started", message = scoreAsString(newScore))
    
  } else if (!is.null(prevScore) && !is.null(newScore) && !identical(newScore, prevScore)) {
    # change in the score
    sendNotification(title = "GOAL!", message = scoreAsString(newScore))
  }
  return(newScore)
}

## 3. send notification
sendNotification <- function(title = "", message) {
  notifier::notify(title = title, msg = message, image = normalizePath("logo.png"))
}

## 4. check score every minute
getScoreUpdates <- function() {
  prevScore <<- checkScoreAndNotify(prevScore)
  later::later(getScoreUpdates, delay = 60)
}

## 5. launch everything
prevScore <- NULL 
getScoreUpdates()