with Integer Programming and R
Major Dusty Turner
Department of Statistical Science at Baylor University
ESPN API
Integer Programming
and
R
ESPN API
Integer Programming
and
R
Football
Fantasy Football
Integer Programming
RStats
Football
Football
Fantasy Football
Integer Programming
RStats
Fantasy Football
Football
Fantasy Football
Integer Programming
RStats
Integer Programming
Football
Fantasy Football
Integer Programming
RStats
RStats
Fantasy football is a weekly game where team managers face off head-to-head with one other member in the league.
Teams score points based off of the real life performance of players on their team.
Teams that score more points than their opponent win.
Teams generally score points for yards gained, touchdowns scored, turnovers, field goals…
Players alternate selecting players until rosters are filled out.
Maybe there’s a more intelligent strategy?
Assumptions:
Subject to:
Goal: Maximize expected points
Other pertinent information:
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\}\)
\(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
\[\sum_{q=0}^Q\sum_{r=0}^R\sum_{w=0}^W\sum_{t=0}^T C_{qrwt} * X_{qrwt} <= 200 \hspace{1cm}\]
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))
)
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)
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])
}
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"))
lpSolve
CVXR
GLPK
rmpk
ompr
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)
[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
[,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 <-
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
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 |
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