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.
- How to access your ESPN public fantasy football league’s data.
- How to organize that data and create a few interesting displays.
- 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!