Search code examples
roptimizationshinylinear-programming

Adding a Flex position in a fantasy football lineup optimizer


I've written some R code to produce the optimal fantasy football lineup (maximize projected points scored) constrained on user inputted roster sizes and draft budget based on a data frame called "players" that consists of player, position, fantasy points, and draft value.

The idea is to use this tool prior to drafting (to have the ideal lineup in mind) and then to update it live while drafting as the optimal lineup will most likey be a fluid thing.

I'm getting hung up on how to include the "flex" position which is a roster spot that can be a RB, WR, or TE. I am currently implicitly including it with the num_players constraint - if your league requires 1 qb, 2 rb, 2 wr, 1 te, and 1 flex position, your num_players input should be 7. Currently, this code only works if the num_players = qb+rb+wr+te with no flexes (6 in this example). It breaks when I make the num_players constrains 1 more than this total in attempt to include the one flex position.

How do I update this code to remove the implicit num_players constraint and include a constraint for flexes?

The dataframe is:

players <- structure(list(Player = c("Josh Allen", "Patrick Mahomes", "Justin Herbert", 
"Lamar Jackson", "Kyler Murray", "Jalen Hurts", "Tom Brady", 
"Dak Prescott", "Joe Burrow", "Russell Wilson", "Aaron Rodgers", 
"Trey Lance", "Matthew Stafford", "Kirk Cousins", "Derek Carr", 
"Tua Tagovailoa", "Justin Fields", "Trevor Lawrence", "Ryan Tannehill", 
"Daniel Jones", "Matt Ryan", "Jameis Winston", "Carson Wentz", 
"Mac Jones", "Jared Goff", "Zach Wilson", "Davis Mills", "Baker Mayfield", 
"Marcus Mariota", "Deshaun Watson", "Mitchell Trubisky", "Geno Smith", 
"Drew Lock", "Kenny Pickett", "Jacoby Brissett", "Desmond Ridder", 
"Travis Kelce", "Mark Andrews", "Kyle Pitts", "Darren Waller", 
"George Kittle", "Dalton Schultz", "T.J. Hockenson", "Dallas Goedert", 
"Zach Ertz", "Dawson Knox", "Hunter Henry", "Mike Gesicki", "Pat Freiermuth", 
"Cole Kmet", "Irv Smith Jr.", "Noah Fant", "Tyler Higbee", "David Njoku", 
"Albert Okwuegbunam", "Gerald Everett", "Robert Tonyan", "Jonathan Taylor", 
"Christian McCaffrey", "Derrick Henry", "Austin Ekeler", "Dalvin Cook", 
"Joe Mixon", "Najee Harris", "Alvin Kamara", "D'Andre Swift", 
"Leonard Fournette", "Saquon Barkley", "Aaron Jones", "Nick Chubb", 
"James Conner", "Javonte Williams", "Ezekiel Elliott", "David Montgomery", 
"Cam Akers", "Travis Etienne Jr.", "Breece Hall", "J.K. Dobbins", 
"Josh Jacobs", "Antonio Gibson", "Elijah Mitchell", "AJ Dillon", 
"Cordarrelle Patterson", "Damien Harris", "Miles Sanders", "Clyde Edwards-Helaire", 
"Tony Pollard", "Devin Singletary", "Kareem Hunt", "Chase Edmonds", 
"Rashaad Penny", "Rhamondre Stevenson", "Kenneth Walker III", 
"Melvin Gordon III", "Darrell Henderson Jr.", "James Robinson", 
"James Cook", "Dameon Pierce", "Michael Carter", "Jamaal Williams", 
"Nyheim Hines", "J.D. McKissic", "Kenneth Gainwell", "Alexander Mattison", 
"Isaiah Spiller", "Raheem Mostert", "Mark Ingram II", "Marlon Mack", 
"Brian Robinson", "Gus Edwards", "Rex Burkhead", "Rachaad White", 
"Khalil Herbert", "Damien Williams", "Tyler Allgeier", "D'Onta Foreman", 
"Jerick McKinnon", "Cooper Kupp", "Justin Jefferson", "Ja'Marr Chase", 
"Davante Adams", "Stefon Diggs", "Deebo Samuel", "CeeDee Lamb", 
"Mike Evans", "Tyreek Hill", "Tee Higgins", "Keenan Allen", "DJ Moore", 
"A.J. Brown", "Michael Pittman Jr.", "Mike Williams", "Brandin Cooks", 
"Jaylen Waddle", "Diontae Johnson", "Terry McLaurin", "DK Metcalf", 
"Courtland Sutton", "Amon-Ra St. Brown", "Darnell Mooney", "Allen Robinson II", 
"Marquise Brown", "Amari Cooper", "Gabriel Davis", "Chris Godwin", 
"Michael Thomas", "Jerry Jeudy", "Adam Thielen", "JuJu Smith-Schuster", 
"Hunter Renfrow", "Rashod Bateman", "Elijah Moore", "Tyler Lockett", 
"Christian Kirk", "Robert Woods", "DeVonta Smith", "Drake London", 
"Allen Lazard", "Brandon Aiyuk", "Chase Claypool", "Kadarius Toney", 
"Tyler Boyd", "Garrett Wilson", "DeVante Parker", "Chris Olave", 
"Kenny Golladay", "Jakobi Meyers", "Russell Gage", "Marquez Valdes-Scantling", 
"DeAndre Hopkins", "Marvin Jones Jr.", "Treylon Burks", "Michael Gallup", 
"Robbie Anderson", "DJ Chark", "Jahan Dotson", "Mecole Hardman"
), Position = c("QB", "QB", "QB", "QB", "QB", "QB", "QB", "QB", 
"QB", "QB", "QB", "QB", "QB", "QB", "QB", "QB", "QB", "QB", "QB", 
"QB", "QB", "QB", "QB", "QB", "QB", "QB", "QB", "QB", "QB", "QB", 
"QB", "QB", "QB", "QB", "QB", "QB", "TE", "TE", "TE", "TE", "TE", 
"TE", "TE", "TE", "TE", "TE", "TE", "TE", "TE", "TE", "TE", "TE", 
"TE", "TE", "TE", "TE", "TE", "RB", "RB", "RB", "RB", "RB", "RB", 
"RB", "RB", "RB", "RB", "RB", "RB", "RB", "RB", "RB", "RB", "RB", 
"RB", "RB", "RB", "RB", "RB", "RB", "RB", "RB", "RB", "RB", "RB", 
"RB", "RB", "RB", "RB", "RB", "RB", "RB", "RB", "RB", "RB", "RB", 
"RB", "RB", "RB", "RB", "RB", "RB", "RB", "RB", "RB", "RB", "RB", 
"RB", "RB", "RB", "RB", "RB", "RB", "RB", "RB", "RB", "RB", "WR", 
"WR", "WR", "WR", "WR", "WR", "WR", "WR", "WR", "WR", "WR", "WR", 
"WR", "WR", "WR", "WR", "WR", "WR", "WR", "WR", "WR", "WR", "WR", 
"WR", "WR", "WR", "WR", "WR", "WR", "WR", "WR", "WR", "WR", "WR", 
"WR", "WR", "WR", "WR", "WR", "WR", "WR", "WR", "WR", "WR", "WR", 
"WR", "WR", "WR", "WR", "WR", "WR", "WR", "WR", "WR", "WR", "WR", 
"WR", "WR", "WR", "WR"), FantasyPoints = c(445, 410, 407, 348, 
351, 359, 354, 364, 402, 368, 353, 347, 349, 335, 366, 325, 297, 
313, 273, 283, 302, 284, 275, 296, 291, 0, 247, 286, 276, 0, 
0, 0, 0, 269, 0, 0, 252, 231, 206, 171, 185, 177, 174, 169, 169, 
171, 139, 131, 170, 170, 162, 129, 162, 119, 130, 126, 130, 340, 
285, 260, 278, 277, 271, 277, 247, 271, 225, 247, 249, 230, 196, 
268, 205, 199, 213, 231, 220, 177, 176, 159, 178, 185, 155, 181, 
157, 190, 177, 164, 156, 166, 169, 179, 158, 129, 147, 99, 158, 
176, 150, 100, 157, 128, 156, 124, 98, 95, 75, 90, 136, 80, 82, 
143, 128, 0, 147, 97, 63, 326, 337, 308, 299, 269, 267, 271, 
242, 243, 241, 239, 243, 242, 244, 209, 220, 233, 239, 221, 198, 
221, 209, 220, 209, 218, 178, 224, 183, 186, 203, 188, 164, 207, 
211, 202, 173, 188, 163, 199, 171, 181, 182, 140, 170, 175, 144, 
142, 164, 147, 131, 170, 160, 182, 136, 153, 157, 152, 148, 175, 
144), DraftValue = c(31, 23, 20, 15, 16, 14, 16, 11, 12, 10, 
10, 3, 7, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
1, 1, 1, 1, 1, 1, 37, 34, 22, 20, 17, 16, 12, 11, 9, 6, 4, 4, 
5, 5, 2, 2, 2, 1, 1, 1, 1, 56, 55, 44, 48, 38, 38, 40, 38, 36, 
34, 34, 33, 27, 30, 28, 27, 23, 21, 23, 21, 19, 18, 10, 15, 16, 
16, 12, 12, 14, 13, 10, 11, 12, 8, 9, 1, 6, 1, 1, 1, 1, 1, 1, 
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 56, 48, 41, 
40, 37, 31, 34, 29, 30, 28, 28, 26, 24, 26, 23, 23, 22, 21, 20, 
18, 19, 20, 17, 18, 17, 15, 15, 17, 17, 16, 16, 15, 15, 13, 12, 
12, 12, 11, 9, 9, 9, 7, 5, 6, 4, 2, 2, 2, 1, 3, 1, 1, 1, 1, 1, 
1, 1, 1, 1, 1)), class = c("tbl_df", "tbl", "data.frame"), row.names = c(NA, 
-177L))

and the app code is:

library(shiny)
library(lpSolve)
library(purrr)
library(rsconnect)


# Define the UI for the app
ui <- fluidPage(
  titlePanel("Fantasy Football Lineup Optimizer"),

  sidebarLayout(
    sidebarPanel(
      numericInput("num_qb", "Enter the number of QBs:", 1, min = 1, max = 5),
      numericInput("num_rb", "Enter the number of RBs:", 3, min = 1, max = 5),
      numericInput("num_wr", "Enter the number of WRs:", 3, min = 1, max = 5),
      numericInput("num_te", "Enter the number of TEs:", 2, min = 1, max = 5),
      numericInput("num_value", "Enter your draft budget:", 200),
      numericInput("num_players", "Adding in your flex spots, enter the total number of starters:", 9, min = 1, max = 15),
      selectInput("remove", "Remove a player:", choices = c("",as.character(players$Player)), multiple = FALSE),
      selectInput("draft_player", "Draft Player", choices = c("",as.character(players$Player)), multiple = FALSE),
      actionButton("update", "Update Lineup")
    ),
    mainPanel(
      tableOutput("team")
    )
  )
)

# Define the server logic
server <- function(input, output, session) {
  players <- players

  # New col to indicate if a player has been drafted
  players$Drafted = "No"

  # Create a new column indicating the player's position
  players$QB <- ifelse(players$Position == "QB", 1, 0)
  players$RB <- ifelse(players$Position == "RB", 1, 0)
  players$WR <- ifelse(players$Position == "WR", 1, 0)
  players$TE <- ifelse(players$Position == "TE", 1, 0)
  players$Total <- 1
  rv <- reactiveValues(players=players)

  # Set up reactive table for lineup output
  updateLineup = reactiveVal(NULL)

  # Define the objective function (maximize fantasy points)
  obj <- players$FantasyPoints

  # Define the constraints (position limits and draft value limit)
  con <- reactive({
    matrix(c(
        # QB constraint
        rv$players$QB,
        # RB constraint
        rv$players$RB,
        # WR constraint
        rv$players$WR,
        # TE constraint
        rv$players$TE,
        # Draft value constraint
        rv$players$DraftValue,
        #Total players constraint
        rv$players$Total
    ), ncol = nrow(rv$players), byrow = TRUE)
  })

  # Define the variables for the lp
  dir <- c("<=", rep(">=",3),"<=","<=")

  # Define initial 'const.rhs'
  init_rhs <- reactive({
    list(
      QB = input$num_qb,
      RB = input$num_rb,
      WR = input$num_wr,
      TE = input$num_te,
      n_val = input$num_value,
      n_players = input$num_players
    )
  })

  # Define reactive 'const.rhs'
  rhs = reactiveValues(const = list())

  # Run once to get the initial values and set them to reactiveValues
  # so they can be changed later
  observeEvent(init_rhs(),{
    rhs$const = init_rhs()
  }, once = TRUE)

  # Define the initial optimal lineup
  initialLineup <- reactive({
    result <- lp("max", obj, con(), dir, init_rhs(), all.bin = TRUE)
    rv$players[result$solution == 1,]
  })

  # Define the function to run when the "update" button is pressed
  observeEvent(input$update, {
    # Remove player here
    if(input$remove != "") {
      removedPlayer <- input$remove
      rv$players <- rv$players[rv$players$Player != removedPlayer,]
      obj <- rv$players$FantasyPoints
    }

    # Draft player
    if(input$draft_player != "") {
      draftedPlayer <- input$draft_player
      draftedPlayer_details <- rv$players[rv$players$Player == draftedPlayer,]
      draftedPlayer_details$Drafted = "Yes"
      rv$players <- rv$players[rv$players$Player != draftedPlayer,]
      rv$draftedPlayers <- rbind(rv$draftedPlayers, draftedPlayer_details)
      obj <- rv$players$FantasyPoints # missing object

      # Subtract constraints: position and n_players by 1 and draft budget by the players 'DraftValue'
      # Necessary so "result" outputs a table with the remaining positions left
      # otherwise it will return an entirely new lineup
      rhs$const = purrr::imap(rhs$const, function(cs, nm) {
        if(nm == draftedPlayer_details$Position) {cs = cs - 1}
        if(nm == "n_players") {cs = cs - 1}
        if(nm == "n_val") {cs = cs - draftedPlayer_details$DraftValue}
        return(cs)
      })
    }

    # Update select inputs to remove players after "Update Lineup" is clicked
    if(input$remove != "" || input$draft_player != "") {
      updateSelectInput(session, inputId = "remove", choices = c("",rv$players), selected = "")
      updateSelectInput(session, inputId = "draft_player", choices = c("",rv$players), selected = "")
    }

    # Define result with updated arguments
    result <- lp("max", obj, con(), dir, rhs$const, all.bin = TRUE)
    # Assign new table to the reactiveVal 'updateLineup'
    updateLineup(rbind(rv$draftedPlayers, rv$players[result$solution == 1,]))
  })

  output$team <- renderTable({
    if (input$update == 0) {
      initialLineup()[, c("Player", "Position", "FantasyPoints", "DraftValue", "Drafted")]
    } else {
      updateLineup()[, c("Player", "Position", "FantasyPoints", "DraftValue", "Drafted")]
    }
  })
}

# Run the app
shinyApp(ui, server)

Solution

  • Here's an option to include the flex positions. I leaned on det's response to update the constraints in the context of the app.

    As det mentioned, you need to update the constraint matrix with your flex specific positions. Which in this case are RB, WR, and TE. Then define the direction of the new constraints. The key here is to use "<=" for your flexes. That represents the max number of draft picks for a particular position given your total number of starters.

    Afterwards, you can set up the values for the rhs of the constraints. You need to add the flex related components. I define the # of flex spots by subtracting the sum of position inputs from the total number of starters.

    Lastly, in order for this to work as expected, I added a new button "Set Draft Constraints". Once clicked, this permanently sets the constraints for your draft. If you need to make changes then you'll need to reload the app. It must be clicked first before you start the draft process.

    App code

    library(shiny)
    library(lpSolve)
    library(purrr)
    library(shinyjs)
    
    # Define the UI for the app
    ui <- fluidPage(
      useShinyjs(),
      titlePanel("Fantasy Football Lineup Optimizer"),
    
      sidebarLayout(
        sidebarPanel(
          numericInput("num_qb", "Enter the number of QBs:", 1, min = 1, max = 5),
          numericInput("num_rb", "Enter the number of RBs:", 2, min = 1, max = 5),
          numericInput("num_wr", "Enter the number of WRs:", 2, min = 1, max = 5),
          numericInput("num_te", "Enter the number of TEs:", 1, min = 1, max = 5),
          numericInput("num_value", "Enter your draft budget:", 200),
          numericInput("num_players", "Adding in your flex spots, enter the total number of starters:", 9, min = 1, max = 15),
          actionButton("set_const", "Set Draft Constraints"),
          selectInput("remove", "Remove a player:", choices = c("",as.character(players$Player)), multiple = FALSE),
          selectInput("draft_player", "Draft Player", choices = c("",as.character(players$Player)), multiple = FALSE),
          actionButton("update", "Update Lineup")
        ),
        mainPanel(
          tableOutput("team")
        )
      )
    )
    
    # Define the server logic
    server <- function(input, output, session) {
      players <- players
    
      # New col to indicate if a player has been drafted
      players$Drafted = "No"
    
      # Create a new column indicating the player's position
      players$QB <- ifelse(players$Position == "QB", 1, 0)
      players$RB <- ifelse(players$Position == "RB", 1, 0)
      players$WR <- ifelse(players$Position == "WR", 1, 0)
      players$TE <- ifelse(players$Position == "TE", 1, 0)
      players$Total <- 1
      rv <- reactiveValues(players=players)
    
      # Set up reactive table for lineup output
      updateLineup = reactiveVal(NULL)
    
      # Define the objective function (maximize fantasy points)
      obj <- players$FantasyPoints
    
      # Define the constraints (position limits and draft value limit)
      con <- reactive({
        matrix(c(
            # Position constraint
            rv$players$QB,
            rv$players$RB,
            rv$players$WR,
            rv$players$TE,
            # Flex constraints
            rv$players$RB,
            rv$players$WR,
            rv$players$TE,
            #Total players constraint
            rv$players$Total,
            # Draft value constraint
            rv$players$DraftValue
        ), ncol = nrow(rv$players), byrow = TRUE)
      })
    
      # Define the variables for the lp
      dir <- c("<=",rep(">=",3),rep("<=",3),"==","<=")
    
      # Define num of flex spots
      flex_spots = reactive(input$num_players - sum(input$num_qb,input$num_rb,input$num_wr,input$num_te))
    
      # Define initial 'const.rhs'
      init_rhs <- reactive({
        list(
          QB = input$num_qb,
          RB = input$num_rb,
          WR = input$num_wr,
          TE = input$num_te,
          RB_flex = input$num_rb + flex_spots(),
          WR_flex = input$num_wr + flex_spots(),
          TE_flex = input$num_te + flex_spots(),
          n_players = input$num_players,
          n_val = input$num_value
        )
      })
    
      # Define reactive 'const.rhs'
      rhs = reactiveValues(const = list())
    
      # Define the initial optimal lineup
      initialLineup <- reactive({
        result <- lp("max", obj, con(), dir, init_rhs(), all.bin = TRUE)
        rv$players[result$solution == 1,]
      })
      
      # Set constraints and disable draft inputs
      observeEvent(input$set_const, {
        disable(selector = "input[type = 'number']")
        disable(id = "set_const")
        rhs$const = init_rhs()
      })
    
      # Define the function to run when the "update" button is pressed
      observeEvent(input$update, {
        if(input$set_const == 0) { stop("Please set draft constraints first") }
        
        # Remove player here
        if(input$remove != "") {
          removedPlayer <- input$remove
          rv$players <- rv$players[rv$players$Player != removedPlayer,]
          obj <- rv$players$FantasyPoints
        }
    
        # Draft player
        if(input$draft_player != "") {
          draftedPlayer <- input$draft_player
          draftedPlayer_details <- rv$players[rv$players$Player == draftedPlayer,]
          draftedPlayer_details$Drafted = "Yes"
          rv$players <- rv$players[rv$players$Player != draftedPlayer,]
          rv$draftedPlayers <- rbind(rv$draftedPlayers, draftedPlayer_details)
          obj <- rv$players$FantasyPoints # missing object
    
          # Subtract constraints: position and n_players by 1 and draft budget by the players 'DraftValue'
          # Necessary so "result" outputs a table with the remaining positions left
          # otherwise it will return an entirely new lineup
          rhs$const = purrr::imap(rhs$const, function(cs, nm) {
            if(nm == draftedPlayer_details$Position) {cs = cs - 1}
            if(nm == "n_players") {cs = cs - 1}
            if(nm == "n_val") {cs = cs - draftedPlayer_details$DraftValue}
            return(cs)
          })
        }
    
        # Update select inputs to remove players after "Update Lineup" is clicked
        if(input$remove != "" || input$draft_player != "") {
          updateSelectInput(session, inputId = "remove", choices = c("",rv$players), selected = "")
          updateSelectInput(session, inputId = "draft_player", choices = c("",rv$players), selected = "")
        }
    
        # Define result with updated arguments
        result <- lp("max", obj, con(), dir, rhs$const, all.bin = TRUE)
        # Assign new table to the reactiveVal 'updateLineup'
        updateLineup(rbind(rv$draftedPlayers, rv$players[result$solution == 1,]))
      })
    
      output$team <- renderTable({
        if (input$update == 0) {
          initialLineup()[, c("Player", "Position", "FantasyPoints", "DraftValue", "Drafted")]
        } else {
          updateLineup()[, c("Player", "Position", "FantasyPoints", "DraftValue", "Drafted")]
        }
      })
    }