For as long as there has been Daily Fantasy Sports there has been Lineup Crunchers (or Lineup Optimisers) which utilise linear programming to devise optimal lineups based on a users projections. The simplest of these tools are developed in Excel and take little more than a basic understanding of Microsoft’s popular spreadsheet tool. The problem with Excel though is that it isn’t designed to handle large datasets and generating multiple lineups can be tricky with the user having to move away from the simple Solver add-in and instead move to VBA which requires more specialist coding knowledge.

A common alternative in the DFS community for those wanting to get an edge on the competition has been to learn one of the open source statistical programming languages (ie R, Python, etc) that are specifically designed for statistical analysis and make working with large datasets easy. The majority of DFS websites that provide projections will develop these through complex algorithms using these types of software.

If you’ve ever had an interest in developing your own Lineup Cruncher but thought that it is too complicated or you just don’t have the time to commit to learning a coding language this post is for you, lets get you started.

Firstly you’ll need to download the open source (ie free) software RStudio, this can be installed here.

Next off, you’ll need to download the Moneyball csv file for the slate that you are wanting to analyse. This can simply be found on the tournament page, just look for the Download Player CSV button. You’ll want to save this csv file onto your desktop and rename it Moneyball.csv. This csv file will now be treated as the database by your Lineup Cruncher.

All that you need to do now is load up RStudio and copy and paste the code provided below into the Source Editor field and run the script. This should provide you with 5 unique lineups of 9 players (2 defenders, 3 midfielders, 1 ruckman, 2 forwards, 1 flex) optimised to maximise the total player average points. If you want to generate more lineups then just change the value assigned to the iterations variable. Note that you should be able to generate 100’s of lineups in a couple of seconds.

This code can be easily modified to add extra features such as player exposure limits, team caps, etc but we can’t give away all the secrets.

If you want to upload your own projections then simply replace the values in the Average.points column in Moneyball.csv and re-run the script.

Note that there is a once off requirement to install the lpSolve package.


After the once off installation the code below will run your cruncher.

Note that depending on your pc configuration the second line of this code which sets your working directory to desktop may require a different path, if that is the case then you can navigate to desktop in the file directory window (bottom right screen of RStudio) then click on More -> Set as Working Directory. Once you’ve done this you will see a line of script similar to setwd(“~/Desktop”) generated in your console, just substitute this into the code below.

library(lpSolve)          # load the lpSolve library

setwd("~/Desktop")           # set working directory to desktop
Opt_stats<-read.csv("Moneyball.csv")           # load Moneyball csv data into dataframe called Opt_stats

iterations <- 5     # nominate number of lineups required
salary <- 60000     # max allowable salary, $60,000 for Moneyball

DEF <- 2            # nominate number of defenders = 2
MID <- 3            # nominate number of midfielders = 3
RUC <- 1            # nominate number of ruckmen = 1
FWD <- 2            # nominate number of forwards = 2
# note that we ignore the flex player here, the cruncher will automatically select a flex player from any position

solutionList <- list()          # create blank list to save lineups in
score <- 10000          # this sets an arbitrary limit on score

for (i in 1:iterations){          # start for loop to generate lineups
  num.players <- length(Opt_stats$Player...ID)          # define number of players in slate
  obj <- Opt_stats$Average.points          # assign object that cruncher needs to maximise, ie points
  var.types <- rep("B", num.players)          # sets variable type
  matrix <- rbind(as.numeric(Opt_stats$Position == "DEF / FLEX"),                # generate a binary matrix
                  as.numeric(Opt_stats$Position == "DEF / FLEX"), 
                  as.numeric(Opt_stats$Position == "MID / FLEX"), 
                  as.numeric(Opt_stats$Position == "MID / FLEX"), 
                  as.numeric(Opt_stats$Position == "RU / FLEX"), 
                  as.numeric(Opt_stats$Position == "RU / FLEX"),  
                  as.numeric(Opt_stats$Position == "FWD / FLEX"),  
                  as.numeric(Opt_stats$Position == "FWD / FLEX"),  
                  as.numeric(Opt_stats$Position %in% c("FWD / FLEX","RU / FLEX","MID / FLEX","DEF / FLEX")),  
  direction <- c(">=","<=",">=","<=",">=","<=",">=","<=","==","<=","<")          # assign criteria symbols to align with matrix
  rhs <- c(DEF,DEF+1,MID,MID+1,RUC,RUC+1,FWD,FWD+1,DEF+MID+RUC+FWD+1,salary,score)          # assign limits
  sol<-lp(direction = "max",obj,matrix,direction,rhs,all.bin = TRUE)          # generate solution
  Lineup<-Opt_stats[sol$solution==1,]          # generate lineup as a dataframe from solution
  Lineup<-Lineup[order(Lineup$Position),]          # reorder lineup by position
  solutionList[[i]] <- Lineup          # save lineup into list of solutions
  score<-sum(Lineup$Average.points)-.001          # save optimised score - 0.001 so next lineup's score is less than previous one

sol_lines<-data.frame(Line = c(1:iterations),          # generate dataframe of all optimised lineups
                      Salary = colSums(sapply(solutionList,"[[","Salary")),
                      AvgProj = colSums(sapply(solutionList,"[[","Average.points")),

colnames(sol_lines)<-c("Line","Salary","AvgProj","Player 1","Player 2","Player 3","Player 4",
                       "Player 5","Player 6","Player 7","Player 8","Player 9")          # assign column names to dataframe of lineups

View(sol_lines)          # view all lineups