‘Win’ Your Fantasy Football Auction Draft

with Integer Programming and R

Major Dusty Turner

Department of Statistical Science at Baylor University

In this talk you’ll learn to ‘win’ your fantasy football league using

ESPN API

Integer Programming

and

R

In this talk you’ll learn to ‘win’ your fantasy football league using

ESPN API

Integer Programming

and

R

But first…

I went for a run today…

So I went for a run today…

A little about me

Professional life

  • Engineer Officer
    • Training: Fort Leonard Wood Missouri
    • Platoon Leader: Hawaii (Iraq)
    • Company Commander: White Sands Missile Range, NM (Afghanistan)
  • Assistant Professor / Instructor
    • United States Miliary Academy, West Point, NY
  • Operations Research Systems Analyst (ORSA)
    • Center for Army Analysis: Fort Belvoir, VA

Educational life

  • United States Military Academy, 2007
    • BS Operations Research
  • University of Missouri of Science and Technology, 2012
    • MS Engineering Management
  • THE Ohio State, 2016
    • MS Integrated Systems Engineering
    • Applied Statistics Minor
  • Baylor University, 2025 (Hopefully)
    • Statistics PhD Student

Best life

  • Married Jill (2010)
    • Xichigan
    • Epic / Mayo Clinic
  • Cal (2013)
    • New Mexico
    • All Sports
  • Reese (2015)
    • Ohio
    • Dance / Sports

What I assume about you…

Football

Fantasy Football

Integer Programming

RStats

What I assume about you…

Football

What I assume about you…

Football

Fantasy Football

Integer Programming

RStats

What I assume about you…

Fantasy Football

What I assume about you…

Football

Fantasy Football

Integer Programming

RStats

What I assume about you…

Integer Programming

What I assume about you…

Football

Fantasy Football

Integer Programming

RStats

What I assume about you…

RStats

So what is this fantasy football thing…?

I’m glad you asked

  1. Fantasy football is a weekly game where team managers face off head-to-head with one other member in the league.

  2. Teams score points based off of the real life performance of players on their team.

  3. Teams that score more points than their opponent win.


Roster make up

  • 1 Quarterback
  • 2 Running backs
  • 2 Wide Receivers
  • 1 Tight End
  • 1 Flex (RB, WR, or TE)
  • 1 Defense
  • 1 Kicker
  • Several bench players

Teams generally score points for yards gained, touchdowns scored, turnovers, field goals…

How do we select players?

Auction draft

  1. $200 to bid on players
  2. Highest big wins
  3. The draft continues until all managers fill out their roster

Snake draft

Players alternate selecting players until rosters are filled out.

There are several strategies

  1. Spend big on big names
  2. Bargain hunt

Maybe there’s a more intelligent strategy?

Enter: Integer Programming

Assumptions:

  1. We know how many points a player is going to score throughout the year
  2. We know how much each player is worth

Subject to:

  1. Expected cost of each player
  2. Roster configuration

Goal: Maximize expected points





Other pertinent information:

  1. We do not care about the quality of our bench (spend $0 on bench)
  2. We are indifferent to our defense and kicker

Let’s get mathy

Maximize: \(\sum{p_{qrwt} x_{qrwt}}\)

where \(p_{qrwt} =\) expected points scored for player \(qrwt\)

\(q = 0, 1, 2, \ldots, Q\)

\(r = 0, 1, 2, \ldots, R\)

\(w = 0, 1, 2, \ldots, W\)

\(t = 0, 1, 2, \ldots, T\)

\(x_{qrwt} \in \{0,1\}\)

Not so fast

Constraints

Football position constraints:

\(x_{1rwt} + x_{2rwt} + \ldots + x_{Qrwt} = 1\) \(x_{q1wt} + x_{q2wt} + \ldots + x_{qRwt} \leq 3\) \(x_{qr1t} + x_{qr2t} + \ldots + x_{qrWt} \leq 3\) \(x_{qrw1} + x_{qrw2} + \ldots + x_{qrwT} \leq 2\) \(x_{q111} + x_{q211} + \ldots + x_{qRWT} = 6\)


Only 1 QB
At most 3 RBs
At most 3 WRs
At most 2 TEs
RB + WR + TE = 6

Financial constraints

\[\sum_{q=0}^Q\sum_{r=0}^R\sum_{w=0}^W\sum_{t=0}^T C_{qrwt} * X_{qrwt} <= 200 \hspace{1cm}\]

So how do we get player data….?

I’m glad you asked

ESPNGet <- httr::RETRY(
    verb = "GET",
    )

I’m glad you asked

ESPNGet <- httr::RETRY(
    verb = "GET",
    url = paste0("https://fantasy.espn.com/apis/v3/games/ffl/seasons/2022/
    segments/0/leaguedefaults/3?scoringPeriodId=0&view=kona_player_info")
)

I’m glad you asked

ESPNGet <- httr::RETRY(
    verb = "GET",
    url = paste0("https://fantasy.espn.com/apis/v3/games/ffl/seasons/2022/
    segments/0/leaguedefaults/3?scoringPeriodId=0&view=kona_player_info"),
    query = list(view = "kona_player_info"),
    httr::accept_json()
  )

I’m glad you asked

ESPNGet <- httr::RETRY(
    verb = "GET",
    url = paste0("https://fantasy.espn.com/apis/v3/games/ffl/seasons/2022/
    segments/0/leaguedefaults/3?scoringPeriodId=0&view=kona_player_info"),
    query = list(view = "kona_player_info"),
    httr::accept_json(),
    httr::add_headers(
      `X-Fantasy-Filter` = jsonlite::toJSON(
        x = list(players = list(limit = 500,
                                sortPercOwned = list(sortAsc = FALSE,
                                                     sortPriority = 1
                                                     ))),
            auto_unbox = TRUE))
   )

I’m glad you asked

ESPNGet <- httr::RETRY(
    verb = "GET",
    url = paste0("https://fantasy.espn.com/apis/v3/games/ffl/seasons/2022/
    segments/0/leaguedefaults/3?scoringPeriodId=0&view=kona_player_info"),
    query = list(view = "kona_player_info"),
    httr::accept_json(),
    httr::add_headers(
      `X-Fantasy-Filter` = jsonlite::toJSON(
        x = list(players = list(limit = 500,
                                sortPercOwned = list(sortAsc = FALSE,
                                                     sortPriority = 1
                                                     ))),
            auto_unbox = TRUE))
   )
   
ESPNRaw <- rawToChar(ESPNGet$content)
ESPNFromJSON <- jsonlite::fromJSON(ESPNRaw)

Make tibble of 1 player’s data

get_player_projection <- function(id){
  tibble(fpts = ESPNFromJSON$players$player$stat[[id]]$appliedTotal,
         season = ESPNFromJSON$players$player$stat[[id]]$seasonId, 
         score_per_id = ESPNFromJSON$players$player$stat[[id]]$scoringPeriodId) |>
  filter(score_per_id == 0) |> 
  mutate(player = ESPNFromJSON$players$player$fullName[id]) |> 
  mutate(position = ESPNFromJSON$players$player$defaultPositionId[id]) |> 
  filter(season == 2022) |> 
  filter(fpts == max(fpts)) |> 
mutate(value = ESPNFromJSON$players$player$draftRanksByRankType$PPR$auctionValue[id])
}

Show one example for Jared

get_player_projection(id = 1) |> gt::gt()
fpts season score_per_id jsonid player position value
258.2525 2022 0 2 Stefon Diggs 3 44

Clean API results for linear model

ESPN_results <- 
  1:length(ESPNFromJSON$players$draftAuctionValue) |> 
  map_dfr(~get_player_projection(id = .x)) 

Clean API results for linear model

ESPN_results <- 
  1:length(ESPNFromJSON$players$draftAuctionValue) |> 
  map_dfr(~get_player_projection(id = .x)) |>
  group_by(player) |> slice(1) |> ungroup() |> 
  mutate(position = case_when(position == 1 ~ "QB",
                              position == 2 ~ "RB",
                              position == 3 ~ "WR",
                              position == 4 ~ "TE",
                              position == 5 ~ "K",
                              position == 16 ~ "DEF")) |> 
  mutate(points_per_dollar = fpts / value) |> 
  filter(!is.infinite(points_per_dollar)) |> 
  filter(!is.nan(points_per_dollar)) |> 
  mutate(position_qb = ifelse(position == "QB",1,0)) |> 
  mutate(position_rb = ifelse(position == "RB",1,0)) |> 
  mutate(position_wr = ifelse(position == "WR",1,0)) |> 
  mutate(position_te = ifelse(position == "TE",1,0)) |> 
  mutate(position_flex = ifelse(position %in% c("RB","WR","TE"),1,0)) |>
  filter(position %in% c("QB","WR","RB","TE")) 

Partial API Results

Down to business

Select a solver engine

lpSolve

CVXR
GLPK
rmpk
ompr

lpSolve

library(lpSolve)

f_obj <- ESPN_results |> pull(fpts) 

f_con <- matrix(c(ESPN_results |> pull(position_qb),
                  ESPN_results |> pull(position_qb),
                  ESPN_results |> pull(position_wr),
                  ESPN_results |> pull(position_te),
                  ESPN_results |> pull(position_flex),
                  ESPN_results |> pull(value)
                  ),nrow = 6, byrow = T)

f_dir <- c("=",  # qb
           "<=", # rb
           "<=", # wr
           "=",  # te
           "=",  # flex
           "<="  # price
           )

f_rhs <- c(1,
           3,
           3,
           1,
           6,
           200)

lpSolve

f_obj
  [1] 220.8450 242.5602 309.6636 192.1663 168.7530 172.5595 201.0691 254.8248
  [9] 198.9663 208.8567 181.2622 284.7332 215.0983 184.9182 210.6620  43.0000
 [17] 146.7111 252.4529 180.3594 174.0654 198.2010 174.4467 200.0144 309.9427
 [25] 190.5999 145.7789 312.9066 167.8848 210.4194 253.6564 306.5225 150.3319
 [33] 176.6624 258.4904 183.6543 174.5547 212.1800 187.7119 253.5925 219.9178
 [41] 131.4289 146.5585 150.4110 261.4620 260.6069 264.4697 146.4369 188.7177
 [49] 192.2315 218.1936 146.0351 234.5686 212.4284 192.7721 197.4233 198.3393
 [57] 214.2013 207.2314 175.3097 169.5924 146.3820 199.0347 145.8136 214.3026
 [65] 276.1680 159.0293 173.9179 318.5135 240.0416 126.7101 151.1312 144.9516
 [73] 238.9946 218.2261 212.2043 294.4468 258.9057 316.9915 374.6255 242.4000
 [81] 150.9300 197.0992 253.0926 328.0368 287.7986 186.1823 182.5312 237.6153
 [89] 145.9000 269.1750 197.6367 313.9962 316.4061 258.7434  15.4000 235.0536
 [97] 172.0591 211.7768 163.0784 285.7049 144.7162 150.5256 156.7192 236.1641
[105] 206.5802 223.3164 135.9019 211.4404 184.6540 265.4768 216.0374 139.8340
[113] 144.6376 144.1705 352.0207  21.6000 130.4284 162.9332  41.4000 197.9605
[121] 183.9000 157.0269 180.4381  10.0000 168.6893 302.4224 227.7660 134.9712
[129] 258.2525 168.0302 237.3114 221.9403 306.8348 177.2297 205.0117 238.7000
[137] 260.7133 284.0327 163.8437 271.0333 168.3101 130.3365 197.0118 246.4522
[145] 148.1828
f_con
     [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11] [,12] [,13] [,14]
[1,]    0    0    1    0    0    0    0    0    0     0     0     0     0     0
[2,]    0    1    0    0    1    0    0    1    0     0     1     1     0     0
[3,]    1    0    0    1    0    1    1    0    1     1     0     0     1     1
[4,]    0    0    0    0    0    0    0    0    0     0     0     0     0     0
[5,]    1    1    0    1    1    1    1    1    1     1     1     1     1     1
[6,]   26   32    3    6    2    2    7   46    7    13     4    58    22     2
     [,15] [,16] [,17] [,18] [,19] [,20] [,21] [,22] [,23] [,24] [,25] [,26]
[1,]     0     0     0     0     0     0     0     0     0     0     0     0
[2,]     1     1     1     0     0     1     0     0     0     1     1     0
[3,]     0     0     0     1     1     0     1     1     1     0     0     0
[4,]     0     0     0     0     0     0     0     0     0     0     0     1
[5,]     1     1     1     1     1     1     1     1     1     1     1     1
[6,]    19    NA    28    40     2     4    13     2     7    59    10     2
     [,27] [,28] [,29] [,30] [,31] [,32] [,33] [,34] [,35] [,36] [,37] [,38]
[1,]     0     0     0     0     1     0     0     0     0     0     0     0
[2,]     0     1     0     1     0     0     0     1     1     1     0     0
[3,]     1     0     1     0     0     0     0     0     0     0     1     0
[4,]     0     0     0     0     0     1     1     0     0     0     0     1
[5,]     1     1     1     1     0     1     1     1     1     1     1     1
[6,]    59     4    15    42     3     3     5    49     4     4    12     9
     [,39] [,40] [,41] [,42] [,43] [,44] [,45] [,46] [,47] [,48] [,49] [,50]
[1,]     0     0     0     0     0     0     1     0     0     0     0     0
[2,]     0     1     0     0     0     0     0     1     0     1     0     0
[3,]     1     0     0     0     1     1     0     0     1     0     1     1
[4,]     0     0     1     1     0     0     0     0     0     0     0     0
[5,]     1     1     1     1     1     1     0     1     1     1     1     1
[6,]    45    21     1     1     2    44     1    52     1     9     5    23
     [,51] [,52] [,53] [,54] [,55] [,56] [,57] [,58] [,59] [,60] [,61] [,62]
[1,]     0     0     0     0     0     0     0     0     0     0     0     0
[2,]     0     0     0     0     1     0     1     0     0     0     0     0
[3,]     1     1     1     1     0     1     0     1     1     0     0     1
[4,]     0     0     0     0     0     0     0     0     0     1     1     0
[5,]     1     1     1     1     1     1     1     1     1     1     1     1
[6,]     2    30    17     6    10     6    20    11     2     8     1     6
     [,63] [,64] [,65] [,66] [,67] [,68] [,69] [,70] [,71] [,72] [,73] [,74]
[1,]     0     0     0     0     0     1     0     0     0     0     0     0
[2,]     0     1     0     0     0     0     1     1     1     0     1     0
[3,]     0     0     1     1     1     0     0     0     0     1     0     1
[4,]     1     0     0     0     0     0     0     0     0     0     0     0
[5,]     1     1     1     1     1     0     1     1     1     1     1     1
[6,]     1    14    54     1     2     5    33     2     2     1    35    22
     [,75] [,76] [,77] [,78] [,79] [,80] [,81] [,82] [,83] [,84] [,85] [,86]
[1,]     0     1     0     0     1     0     0     0     1     1     0     0
[2,]     0     0     1     1     0     1     0     0     0     0     0     0
[3,]     1     0     0     0     0     0     1     1     0     0     1     1
[4,]     0     0     0     0     0     0     0     0     0     0     0     0
[5,]     1     0     1     1     0     1     1     1     0     0     1     1
[6,]    15     3    43    61    18    10     1     6     1    10    55     3
     [,87] [,88] [,89] [,90] [,91] [,92] [,93] [,94] [,95] [,96] [,97] [,98]
[1,]     0     0     0     1     0     1     1     0     0     0     0     0
[2,]     1     0     1     0     0     0     0     1     1     0     0     0
[3,]     0     1     0     0     0     0     0     0     0     0     1     1
[4,]     0     0     0     0     1     0     0     0     0     1     0     0
[5,]     1     1     1     0     1     0     0     1     1     1     1     1
[6,]     4    32     2     1    21     8     8    37    NA    36     2    17
     [,99] [,100] [,101] [,102] [,103] [,104] [,105] [,106] [,107] [,108]
[1,]     0      1      0      0      0      0      0      0      0      0
[2,]     0      0      1      1      0      0      0      0      0      0
[3,]     1      0      0      0      1      1      1      1      0      1
[4,]     0      0      0      0      0      0      0      0      1      0
[5,]     1      0      1      1      1      1      1      1      1      1
[6,]     1      3      2      2      2     26      7     30      1     22
     [,109] [,110] [,111] [,112] [,113] [,114] [,115] [,116] [,117] [,118]
[1,]      0      0      0      0      0      0      1      0      0      0
[2,]      1      1      1      0      1      0      0      1      1      1
[3,]      0      0      0      0      0      0      0      0      0      0
[4,]      0      0      0      1      0      1      0      0      0      0
[5,]      1      1      1      1      1      1      0      1      1      1
[6,]      9     50     24      1      2      2     11     NA      1      2
     [,119] [,120] [,121] [,122] [,123] [,124] [,125] [,126] [,127] [,128]
[1,]      0      0      0      0      0      0      0      1      0      0
[2,]      0      0      1      0      0      1      0      0      1      0
[3,]      1      1      0      1      1      0      1      0      0      1
[4,]      0      0      0      0      0      0      0      0      0      0
[5,]      1      1      1      1      1      1      1      0      1      1
[6,]     NA      6      4      1      2     NA      2      3     29      2
     [,129] [,130] [,131] [,132] [,133] [,134] [,135] [,136] [,137] [,138]
[1,]      0      0      0      0      1      0      0      0      1      1
[2,]      0      0      0      0      0      1      1      0      0      0
[3,]      1      0      1      1      0      0      0      0      0      0
[4,]      0      1      0      0      0      0      0      1      0      0
[5,]      1      1      1      1      0      1      1      1      0      0
[6,]     44      4     31     25      3      4     19     34      1      2
     [,139] [,140] [,141] [,142] [,143] [,144] [,145]
[1,]      0      1      0      0      0      0      0
[2,]      0      0      0      0      0      0      0
[3,]      1      0      1      0      1      1      0
[4,]      0      0      0      1      0      0      1
[5,]      1      0      1      1      1      1      1
[6,]      1      1      2      1      5     38      3

Solution

solution <-
  lp(
    direction = "max",
    objective.in =  f_obj,
    const.mat =  f_con,
    const.dir =  f_dir,
    const.rhs =  f_rhs, 
    all.bin = TRUE
  )

Solution

solution <-
  lp(
    direction = "max",
    objective.in =  f_obj,
    const.mat =  f_con,
    const.dir =  f_dir,
    const.rhs =  f_rhs, 
    all.bin = TRUE
  )
  
solution$solution
  [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0
 [38] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 [75] 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0
[112] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0

My team

ESPN_results |>
  mutate(solution = solution$solution) |> 
  filter(solution == 1) |>
  select(fpts, player, position, value, points_per_dollar) |>
  mutate(dollars_per_point = fpts/value) |>
  arrange(position)
fpts player position exp_auction_value
375 Josh Allen QB 18
317 Jonathan Taylor RB 61
242 Josh Jacobs RB 10
239 Travis Kelce TE 34
313 Cooper Kupp WR 59
207 Gabe Davis WR 11
207 Michael Thomas WR 7

Final thoughts

  • What happens when things do not go as planned?
  • Other applications

Thanks!

github: dusty-turner
twitter: @dtdusty
linkedin: dusty-turner
email: dusty.s.turner@gmail.com
website: dustysturner.com
repo: dusty-turner/2022_r_gov_ff_draft