Its that time of year!

Its that time of year again. The time when we dust off the old ESPN fantasy football API R code and fix everything that broke in the last year.

Here’s what I hope to show over the next few posts.

  1. How to access your ESPN public fantasy football league’s data.
  2. How to organize that data and create a few interesting displays.
  3. How to create a dashboard to supplement your league’s fun.

My ultimate goal is to build this into a package on Github. I’ve got that started, but its currently a work in progress.

All this code and more is located at my github.

And a special thanks to my friend and collaborator Jim Pleuss who really enhanced so much of this project!

A little background

This ESPN API exploration is a fun annual process. I first started doing this in 2018. In 2019, after some hacking and some updates, I posted about how to access public leagues, how to access private leagues using reticulate and python, and how to access private leagues in R. I went so far last year as to build a dashboard with the data to further analyze my league.

Well, in 2020, ESPN changed their some of their end points. After a lot of exploration and the help of Chrome’s inspect tool, I present code to access ESPN’s API. This should work for any league. If you find it does not work for your league, I’d be happy to take a pull request or two to handle more nuances.

How to access the API

In the function below, you provide arguments for your leagueID and the week of the information you would like to extract.

To find your leagueID, look at the URL of your fantasy football home page on ESPN.

The output of this function will be a JSON file. I provide code to explore the JSON file as well.

library(tidyverse)
library(gt)

get_data <- function(leagueID = leagueID, per_id = per_id){
base = "http://fantasy.espn.com/apis/v3/games/ffl/seasons/"
year = "2020"
mid = "/segments/0/leagues/"
tail = str_c("?view=mDraftDetail",
             "&view=mLiveScoring",
             "&view=mMatchupScore,",
             "&view=mPendingTransactions",
             "&view=mPositionalRatings",
             "&view=mRoster",
             "&view=mSettings",
             "&view=mTeam",
             "&view=modular",
             "&view=mNav",
             "&view=mMatchupScore",
             "&scoringPeriodId="
             )

url = paste0(base,year,mid,leagueID,tail,per_id)

ESPNGet <- httr::GET(url = url)
ESPNRaw <- rawToChar(ESPNGet$content)
ESPNFromJSON <- jsonlite::fromJSON(ESPNRaw)

return(ESPNFromJSON)

}

leagueID <- 89417258
per_id <- 2

ESPNFromJSON <- get_data(leagueID = leagueID, per_id = per_id)

We’ll can explore the data using the listviewer package. You can peruse the data from this JSON below.

ESPNFromJSON %>% listviewer::jsonedit()

What can we do with this data?

I’m glad you asked. In subsequent posts, I’ll provide more examples, but here are a few quick things:

Extract one player’s information

number_of_teams <- length(ESPNFromJSON$teams$id)
team_ids <- ESPNFromJSON$teams$id

player_extract <- function(team_number = 1, player_number = 1, per_id = per_id, ESPNFromJSON = ESPNFromJSON){
  player_week <-
    tibble(
      team = str_c(ESPNFromJSON$teams$location[team_number]," ",ESPNFromJSON$teams$nickname[team_number]),
      teamId = ESPNFromJSON$teams$id[team_number],
      fullName = ESPNFromJSON$teams$roster$entries[[team_number]]$playerPoolEntry$player$fullName[player_number],
      appliedTotal = ESPNFromJSON$teams$roster$entries[[team_number]]$playerPoolEntry$player$stats[[player_number]]$appliedTotal,
      seasonId = ESPNFromJSON$teams$roster$entries[[team_number]]$playerPoolEntry$player$stats[[player_number]]$seasonId,
      scoringPeriodId = ESPNFromJSON$teams$roster$entries[[team_number]]$playerPoolEntry$player$stats[[player_number]]$scoringPeriodId,
      statsplitTypeId = ESPNFromJSON$teams$roster$entries[[team_number]]$playerPoolEntry$player$stats[[player_number]]$statSplitTypeId,
      externalId = ESPNFromJSON$teams$roster$entries[[team_number]]$playerPoolEntry$player$stats[[player_number]]$externalId,
      lineupSlot_id = ESPNFromJSON$teams$roster$entries[[team_number]]$lineupSlotId[player_number],
      eligibleSlots = list(ESPNFromJSON$teams$roster$entries[[team_number]]$playerPoolEntry$player$eligibleSlots[[player_number]])
    ) %>%
    filter(seasonId==2020) %>%
    filter(scoringPeriodId != 0) %>%
    filter(scoringPeriodId == per_id)
  return(player_week)
}

player_extract(ESPNFromJSON = ESPNFromJSON,team_number = 1,player_number = 1,per_id = per_id) %>% gt()
team teamId fullName appliedTotal seasonId scoringPeriodId statsplitTypeId externalId lineupSlot_id eligibleSlots
'R'm Chair Quarterback 1 Alvin Kamara 38.40000 2020 2 1 401220231 2 2, 3, 23, 7, 20, 21
'R'm Chair Quarterback 1 Alvin Kamara 20.44241 2020 2 1 20202 2 2, 3, 23, 7, 20, 21

Get all players from all teams

To extract the players for each team, we first need to know how many players are on each team each week. One might think this is consistent, but with an IR slot, this may change from week to week.

First we create a function to determine how many roster spots are on a given team.

get_roster_slots <- function(team_number=1){
  return(tibble(team_number = team_number, player_slot = 1:length(ESPNFromJSON$teams$roster$entries[[team_number]]$playerPoolEntry$player$stats)))
}

Then we map that over all teams.

team_player_slots <- purrr::map_dfr(1:number_of_teams,~get_roster_slots(team_number = .x))

team_player_slots
## # A tibble: 182 x 2
##    team_number player_slot
##          <int>       <int>
##  1           1           1
##  2           1           2
##  3           1           3
##  4           1           4
##  5           1           5
##  6           1           6
##  7           1           7
##  8           1           8
##  9           1           9
## 10           1          10
## # ... with 172 more rows

Now, we map the team_number and player_slots data to the player_extract() function to get every player from every team.

team_list <- 
  purrr::map2_dfr(
    team_player_slots$team_number,
    team_player_slots$player_slot, 
    ~player_extract(
      ESPNFromJSON = ESPNFromJSON,
      team_number = .x,
      player_number = .y, 
      per_id = per_id
      )
    )

team_list %>% 
  group_by(teamId) %>% 
  slice_head(n = 1) %>% 
  ungroup() %>% 
  gt() %>% 
  tab_header("First player on each team")
First player on each team
team teamId fullName appliedTotal seasonId scoringPeriodId statsplitTypeId externalId lineupSlot_id eligibleSlots
'R'm Chair Quarterback 1 Alvin Kamara 38.40000 2020 2 1 401220231 2 2, 3, 23, 7, 20, 21
Twenty Twenty 2 Miles Sanders 15.50422 2020 2 1 20202 2 2, 3, 23, 7, 20, 21
The Plainsmen 3 Austin Ekeler 18.80000 2020 2 1 401220235 2 2, 3, 23, 7, 20, 21
Mother Hen 4 Christian McCaffrey 24.80000 2020 2 1 401220329 2 2, 3, 23, 7, 20, 21
Analysis Paralysis 5 Lamar Jackson 17.56000 2020 2 1 401220181 0 0, 7, 20, 21
ForWard Progress 6 Derrick Henry 8.40000 2020 2 1 401220204 2 2, 3, 23, 7, 20, 21
Syntax Error 7 Travis Kelce 24.00000 2020 2 1 401220235 6 5, 6, 23, 7, 20, 21
Chief of Chiefs 8 Clyde Edwards-Helaire 13.00000 2020 2 1 401220235 2 25, 2, 3, 23, 7, 20, 21
Monkey King 9 Ezekiel Elliott 22.20000 2020 2 1 401220249 2 2, 3, 23, 7, 20, 21
Palindrome Tikkit 10 Dalvin Cook 17.10000 2020 2 1 401220192 2 2, 3, 23, 7, 20, 21
Enemy of the Stat #1 11 Saquon Barkley 2.80000 2020 2 1 401220281 2 2, 3, 23, 7, 20, 21
The Mandalorian 12 Michael Thomas 0.00000 2020 2 1 401220231 20 3, 4, 5, 23, 7, 20, 21

To get some really good information, we need to join this to the teams and their weekly schedule.

So lets extract the schedule….

schedule <-
  tibble(
    home = ESPNFromJSON$schedule$away$teamId,
    away = ESPNFromJSON$schedule$home$teamId,
    scoringPeriodId = ESPNFromJSON$schedule$matchupPeriodId,
    gameId = ESPNFromJSON$schedule$id
  ) %>%
  pivot_longer(cols = c(home,away), values_to = "teamId")

Then join the players and schedule.

team_list <-
team_list %>% 
  left_join(schedule) %>%
  mutate(points_type = if_else(str_length(externalId) > 6, "actual", "projected")) %>%
  relocate(team:appliedTotal, points_type)

team_list %>% 
  group_by(teamId) %>% 
  slice_head(n = 1) %>% 
  ungroup() %>% 
  gt() %>% 
  tab_header("First player on each team")
First player on each team
team teamId fullName appliedTotal points_type seasonId scoringPeriodId statsplitTypeId externalId lineupSlot_id eligibleSlots gameId name
'R'm Chair Quarterback 1 Alvin Kamara 38.40000 actual 2020 2 1 401220231 2 2, 3, 23, 7, 20, 21 7 home
Twenty Twenty 2 Miles Sanders 15.50422 projected 2020 2 1 20202 2 2, 3, 23, 7, 20, 21 11 away
The Plainsmen 3 Austin Ekeler 18.80000 actual 2020 2 1 401220235 2 2, 3, 23, 7, 20, 21 9 home
Mother Hen 4 Christian McCaffrey 24.80000 actual 2020 2 1 401220329 2 2, 3, 23, 7, 20, 21 10 home
Analysis Paralysis 5 Lamar Jackson 17.56000 actual 2020 2 1 401220181 0 0, 7, 20, 21 7 away
ForWard Progress 6 Derrick Henry 8.40000 actual 2020 2 1 401220204 2 2, 3, 23, 7, 20, 21 12 home
Syntax Error 7 Travis Kelce 24.00000 actual 2020 2 1 401220235 6 5, 6, 23, 7, 20, 21 9 away
Chief of Chiefs 8 Clyde Edwards-Helaire 13.00000 actual 2020 2 1 401220235 2 25, 2, 3, 23, 7, 20, 21 10 away
Monkey King 9 Ezekiel Elliott 22.20000 actual 2020 2 1 401220249 2 2, 3, 23, 7, 20, 21 12 away
Palindrome Tikkit 10 Dalvin Cook 17.10000 actual 2020 2 1 401220192 2 2, 3, 23, 7, 20, 21 8 home
Enemy of the Stat #1 11 Saquon Barkley 2.80000 actual 2020 2 1 401220281 2 2, 3, 23, 7, 20, 21 8 away
The Mandalorian 12 Michael Thomas 0.00000 actual 2020 2 1 401220231 20 3, 4, 5, 23, 7, 20, 21 11 home

Now, we can really start analyzing our league! But that’s for the next post!