Draftstars Lineup Optimiser

| May 8, 2022

If you are serious about your DFS you’ve probably considered building your own Lineup Optimiser (or Cruncher) at some point. After learning that Excel isn’t the greatest tool for this you’ve probably spent 10 minutes or so googling how to build one and decided that it’s way too complicated and decided you’ll stick with the free to use options that are available. Resigned to the fact that personalised optimisers are only for those people with advanced coding skills.

I’m here to tell you that it’s not actually that complicated to build your own and below I give you the code (developed in R) for a basic single lineup optimiser on which you can expand and add any number of different criteria, including:

  • generate multiple lineups
  • unique number of players across lineups
  • player locks
  • player grouping
  • team stacking
  • positional stacking
  • etc etc

For the purpose of this example we’ll use the Draftstars player csv file which can be downloaded from the tournament page and optimise for the team with the highest average (FPPG).

# First we need to load the packages required
library(lpSolve); library(tidyverse)

# Read the player csv file into R and store into a data.frame called stats (I've also filtered the list for players named)
# This assumes that you have stored the csv file in the same location as the R script
stats <- read.csv("players_CBHbFjzX.csv") %>% 
  filter(Playing.Status == "NAMED IN TEAM TO PLAY")

At this stage you should have a list of players stored in memory that looks like below. This table has all the relevant information to complete the optimiser.

##   Player.ID Position            Name      Team       Opponent Salary   FPPG
## 1   8000004      MID  Patrick Cripps   Carlton Adelaide Crows  15520 109.50
## 2   8000005      FWD  Charlie Curnow   Carlton Adelaide Crows  11290  72.71
## 3   8000007      DEF    Sam Docherty   Carlton Adelaide Crows  15560 104.29
## 4   8000022      DEF Jacob Weitering   Carlton Adelaide Crows   8380  56.00
## 5   8000056       RK        Max Gawn Melbourne       St Kilda  17000 114.14
## 6   8000058      MID    James Harmes Melbourne       St Kilda  11240  77.29
##     Form        Playing.Status
## 1  94.67 NAMED IN TEAM TO PLAY
## 2  79.00 NAMED IN TEAM TO PLAY
## 3  97.33 NAMED IN TEAM TO PLAY
## 4  59.67 NAMED IN TEAM TO PLAY
## 5 133.33 NAMED IN TEAM TO PLAY
## 6  72.67 NAMED IN TEAM TO PLAY

At this point we are ready to set the criteria in our optimiser, for a simple lineup optimiser we only need to define the max salary ($100,000 for Draftstars) and position criteria (2 Defenders/Forwards, 4 Midfielders and 1 Ruck). To do this we assign these values to variables.

salary <- 100000 # set maximum allowable team salary to variable 'salary'
defender <- 2 # set number of defenders to variable 'defender'
midfielder <- 4 # set number of midfielders to variable 'midfielder'
ruck <- 1 # set number of rucks to variable 'ruck'
forward <-2 # set number of forwards to variable 'forward'

Next we set up a matrix with a row for each one of these criteria (each column represents 1 player), positional criteria are binary (1 when a player is equal to the nominated position, 0 when they are not). This matrix is stored in memory as criteria_matrix.

# define optimisation criteria
criteria_matrix <- rbind(
  as.numeric(stats$Position == "MID"), 
  as.numeric(stats$Position == "DEF"), 
  as.numeric(stats$Position == "RK"), 
  as.numeric(stats$Position == "FWD"), 
  stats$Salary)

# print first 10 columns of matrix for info
print(head(criteria_matrix[,c(1:10)], 6))
##       [,1]  [,2]  [,3] [,4]  [,5]  [,6] [,7]  [,8]  [,9] [,10]
## [1,]     1     0     0    0     0     1    0     1     1     1
## [2,]     0     0     1    1     0     0    0     0     0     0
## [3,]     0     0     0    0     1     0    0     0     0     0
## [4,]     0     1     0    0     0     0    1     0     0     0
## [5,] 15520 11290 15560 8380 17000 11240 6900 16600 12940 16030

To define the limits for each of these criteria we store the direction (e.g. more than, equal to, less than, etc) in a vector called criteria_direction and the limit in a vector called criteria_limit.

criteria_direction <- c("==", # midfielders - equal to
               "==", # defenders - equal to
               "==", # rucks - equal to
               "==", # forwards - equal to
               "<=") # salary - less than or equal to 

# note that we are using teh variables defined earlier
criteria_limit <- c(midfielder,
         defender,
         ruck,
         forward,
         salary)  

Now we have all the criteria defined we can run the optimiser:

# set object to optimise, in this case we are looking for maximum FPPG
obj <- stats$FPPG

# run optimiser and store solution in object 'sol'
sol <- lp(direction = "max", obj, criteria_matrix, criteria_direction, criteria_limit, all.bin = TRUE)

print(stats[sol$solution==1,])
##    Player.ID Position           Name           Team       Opponent Salary
## 1    8000004      MID Patrick Cripps        Carlton Adelaide Crows  15520
## 5    8000056       RK       Max Gawn      Melbourne       St Kilda  17000
## 18   8000175      FWD  Taylor Walker Adelaide Crows        Carlton  11310
## 23   8000394      MID    Jack Newnes        Carlton Adelaide Crows  10250
## 25   8000399      DEF  Jack Sinclair       St Kilda      Melbourne  13980
## 43   8000637      FWD  Jack Silvagni        Carlton Adelaide Crows  10230
## 63   8000970      DEF Harrison Petty      Melbourne       St Kilda   6000
## 86   8001720      MID      Sam Berry Adelaide Crows        Carlton   7060
## 88   8001732      MID   Jack Carroll        Carlton Adelaide Crows   8630
##      FPPG   Form        Playing.Status
## 1  109.50  94.67 NAMED IN TEAM TO PLAY
## 5  114.14 133.33 NAMED IN TEAM TO PLAY
## 18  87.50  86.00 NAMED IN TEAM TO PLAY
## 23  76.67  76.67 NAMED IN TEAM TO PLAY
## 25  96.00  87.00 NAMED IN TEAM TO PLAY
## 43  72.17  66.00 NAMED IN TEAM TO PLAY
## 63  51.00  51.00 NAMED IN TEAM TO PLAY
## 86  63.67  63.67 NAMED IN TEAM TO PLAY
## 88  87.00  87.00 NAMED IN TEAM TO PLAY
print(sol)
## Success: the objective function is 757.65

Below we’ve combined all the code into one chunk that can be cut and paste and modified for your own use. Note that this does not deal with dual-position players but this can be easily factored in by expanding criteria_matrix to have binary columns for each player with dual position eligibility…..we can’t give you all the answers, you’ll need to give that a go for yourself :-).


library(lpSolve); library(tidyverse)

stats <- read.csv("players_CBHbFjzX.csv") %>% 
  filter(Playing.Status == "NAMED IN TEAM TO PLAY")

salary <- 100000
defender <- 2
midfielder <- 4
ruck <- 1
forward <-2

criteria_matrix <- rbind(
  as.numeric(stats$Position == "MID"), 
  as.numeric(stats$Position == "DEF"), 
  as.numeric(stats$Position == "RK"), 
  as.numeric(stats$Position == "FWD"), 
  stats$Salary)

criteria_direction <- c("==",
               "==",
               "==",
               "==",
               "<=")

criteria_limit <- c(midfielder,
         defender,
         ruck,
         forward,
         salary)  

obj <- stats$FPPG
sol <- lp(direction = "max", obj, criteria_matrix, criteria_direction, criteria_limit, all.bin = TRUE)