Lineup optimization for daily fantasy sports
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>",
fig.path = "man/figures/README-",
out.width = "100%"
)
The goal of coach is to provide functions to optimize lineups for a variety of sites (draftkings, fanduel, fantasydraft) and sports (nba, mlb, nfl, nhl). Not every site/sport combination has been created yet. If you want something added, file an issue.
You can install the released version of coach from github with:
remotes::install_github("dfs-with-r/coach")
Load the library.
library(coach)
Load lineup data exported from a fantasy site and read it in. Check the documention of the read_*_*
functions for instructions on how to export the data. For example, for Draftkings you have to goto your contest page and select Export to csv.
data <- read_dk("mydata.csv")
print(data)
# for prettier printing
slim <- function(df) {
cols <- c("player_id", "player", "team", "position", "salary", "fpts_avg", "fpts_proj")
use_cols <- intersect(cols, colnames(df))
df[order(-df[["salary"]]), use_cols]
}
data <- read_dk("tests/testthat/data/dk-nfl.csv")
print(slim(data))
Add your custom projections into a column called fpts_proj
. This is very important! If your projections aren’t good then your optimized lineups won’t be good either. For this example we’ll just add some random noise to the player’s season average fantasy points.
set.seed(100)
n <- nrow(data)
data$fpts_proj <- rnorm(n, data$fpts_avg)
print(data)
n <- nrow(data)
data$fpts_proj <- rnorm(n, data$fpts_avg)
print(slim(data))
Build a fantasy model. This model contains all the constraints imposed by the site and sport.
model <- model_dk_nfl(data)
Generate three optimized lineups using your projections and the fantasy model
optimize_generic(data, model, L = 3)
# cleaner printing
results <- optimize_generic(data, model, L = 3)
print(lapply(results, slim))
Write these results to a file. This file can be submitted directly to the DFS site.
write_lineups(results, "mylineups.csv", site = "draftkings", sport = "nfl")
write_lineups(results, site = "draftkings", sport = "nfl")
You can now build custom models with the functions model_generic()
and add_generic_positions_constraint()
. To start, define a generic model by providing total salary allowed, roster size, and max number of players per team allowed. We will use our NFL data from above.
model <- model_generic(data, total_salary = 50000, roster_size = 9, max_from_team = 4)
Now we can provide custom position constraints. These will be in the form of a named list, with FLEX or wildcard positions named with a forward slash (/
) between positions. In the example below, we have one FLEX position that allows an RB, WR, or TE.
constraints <- list(
"QB" = 1,
"RB" = 2,
"WR" = 3,
"TE" = 1,
"RB/WR/TE" = 1,
"DST" = 1
)
model <- add_generic_positions_constraint(model, data, constraints)
These contraints are actually the same as the draftkings built-in model above, so we should get the same results when we optimize:
optimize_generic(data, model, L = 3)
# cleaner printing
results <- optimize_generic(data, model, L = 3)
print(lapply(results, slim))
Many times you want to limit the exposure of your lineups to certain players (due to effects not captured in the model). You can do this by setting a global max_exposure
for every player to a constant:
optimize_generic(data, model, L = 5, max_exposure = 3/5)
# cleaner printing
results <- optimize_generic(data, model, L = 5, max_exposure = 0.5)
print(lapply(results, slim))
You can also provide a vector of individual max_exposure
for every player. I find it is typically easiest to add a column to your data frame that defaults to a global value except for a few specific instances:
library(dplyr)
data <- data %>% mutate(
exposure = case_when(
player == "LeSean McCoy" ~ 0.4,
player == "Deshaun Watson" ~ 0.5,
player == "Alex Smith" ~ 0.2,
TRUE ~ 1
)
)
You can then reference this column in the optimizer:
optimize_generic(data, model, L = 5, max_exposure = data$exposure)
# cleaner printing
results <- optimize_generic(data, model, L = 5, max_exposure = data$exposure)
print(lapply(results, slim))
You can also now add randomness to your projections by providing a function. This function will run on fpts_proj
before each lineup is generated. For example, to generate projections that are randomly distributed around a center value with a standard deviation of 10 you could do this:
n <- nrow(data)
randomness <- function(x) rnorm(n, x, 10)
When you generate the lineups you can see how the fpts_proj
changes between samples.
optimize_generic(data, model, L = 5, randomness = randomness)
# cleaner printing
results <- optimize_generic(data, model, L = 5, randomness = randomness)
print(lapply(results, slim))