1
# Setup -------------------------------------------------------------------

# --- Load Packages
library(conflicted)
library(lpSolve)
library(tidyverse)
conflicts_prefer(dplyr::filter, dplyr::lag, dplyr::collapse, .quiet = TRUE)

# --- Assert Conflicts
if (some(conflict_scout(), \(x) length(x) > 1)) {
  print(conflict_scout())
  stop("Fix conflicts")
}

data = tribble(
  ~id,             ~name,             ~pts,        ~team, ~opp,  ~position, ~salary,
  "117232-135260", "Logan O'Hoppe"   , 10.3372093, "LAA", "MIA", "C1B"    , 5500   ,
  "117232-135260", "Logan O'Hoppe"   , 10.3372093, "LAA", "MIA", "UTIL"   , 5500   ,
  "117232-206482", "Jacob Wilson"    , 10.5083321, "ATH", "PHI", "SS"     , 5500   ,
  "117232-206482", "Jacob Wilson"    , 10.5083321, "ATH", "PHI", "UTIL"   , 5500   ,
  "117232-2644"  , "Jesus Sanchez"   , 8.4600014 , "MIA", "LAA", "UTIL"   , 3200   ,
  "117232-82644" , "Jesus Sanchez"   , 8.4600014 , "MIA", "LAA", "UTIL"   , 3200   ,
  "117232-119306", "Shea Langeliers" , 9.9044474 , "ATH", "PHI", "C1B"    , 5500   ,
  "117232-119306", "Shea Langeliers" , 9.9044474 , "ATH", "PHI", "UTIL"   , 5500   ,
  "117232-13342" , "Nick Castellanos", 8.9459998 , "PHI", "ATH", "OF"     , 5100   ,
  "117232-13342" , "Nick Castellanos", 8.9459998 , "PHI", "ATH", "P"      , 5100   ,
  "117232-61553" , "Yoan Moncada"    , 10.8590963, "LAA", "MIA", "3B"     , 3800   ,
  "117232-61553" , "Yoan Moncada"    , 10.8590963, "LAA", "MIA", "UTIL"   , 3800   ,
  "117232-189389", "Dane Myers"      , 9.1172412 , "MIA", "LAA", "OF"     , 3500   ,
  "117232-189389", "Dane Myers"      , 9.1172412 , "MIA", "LAA", "UTIL"   , 3500   ,
  "117232-23107" , "Jorge Soler"     , 8.0347824 , "MIA", "LAA", "OF"     , 3500   ,
  "117232-23107" , "Jorge Soler"     , 8.0347824 , "MIA", "LAA", "P"      , 3500   ,
  "117232-164506", "Connor Norby"    , 8.6551724 , "MIA", "LAA", "2B"     , 3400   ,
  "117232-164506", "Connor Norby"    , 8.6551724 , "MIA", "LAA", "UTIL"   , 3400   ,
  "117232-164506", "Connor Norby"    , 8.6551724 , "MIA", "LAA", "SS"     , 3400   ,
  "117232-102393", "Xavier Edwards"  , 8.8595239 , "MIA", "LAA", "UTIL"   , 3400   ,
  "117232-102393", "Xavier Edwards"  , 8.8595239 , "MIA", "LAA", "SS"     , 3400   ,
  "117232-19358" , "Nolan Schanuel"  , 8.6851066 , "LAA", "MIA", "C1B"    , 3300   ,
  "117232-19358" , "Nolan Schanuel"  , 8.6851066 , "LAA", "MIA", "UTIL"   , 3300   ,
  "117232-119363", "JJ Bleday"       , 8.4723407 , "PHI", "ATH", "OF"     , 3300   ,
  "117232-119363", "JJ Bleday"       , 8.4723407 , "PHI", "ATH", "UTIL"   , 3300   ,
  "117232-222794", "Nick Kurtz"      , 7.6653847 , "ATH", "PHI", "C1B"    , 3200   ,
  "117232-222794", "Nick Kurtz"      , 7.6653847 , "ATH", "PHI", "UTIL"   , 3200   ,
  "117232-220835", "Liam Hicks"      , 8.8103448 , "MIA", "LAA", "P"      , 3200   ,
  "117232-220835", "Liam Hicks"      , 8.8103448 , "MIA", "LAA", "C1B"    , 3200   ,
  "117232-60636" , "Derek Hill"      , 8.3349992 , "MIA", "LAA", "OF"     , 3200   ,
  "117232-60636" , "Derek Hill"      , 8.3349992 , "MIA", "LAA", "UTIL"   , 3200   ,
  "117232-79195" , "J.T. Realmuto"   , 9.3146937 , "PHI", "ATH", "C1B"    , 3200   ,
  "117232-79195" , "J.T. Realmuto"   , 9.3146937 , "PHI", "ATH", "UTIL"   , 3200   ,
  "117232-79195" , "Miguel Andujar"  , 8.6891892 , "ATH", "PHI", "C1B"    , 3200   ,
  "117232-79195" , "Miguel Andujar"  , 8.6891892 , "ATH", "PHI", "3B"     , 3200   ,
  "117232-79195" , "Miguel Andujar"  , 8.6891892 , "ATH", "PHI", "UTIL"   , 3200   ,
)


# Functions ---------------------------------------------------------------

prepare_players_df = function(data) {
  # Add binary columns of position
  players =
    data |>
    relocate(pts, salary, .after = position) |>
    arrange(position) |>
    mutate(
      position_name = paste0("pos_", position),
      position_value = 1L
    ) |>
    pivot_wider(names_from = position_name, values_from = position_value)

  # Add binary columns for each team
  players =
    players |>
    arrange(team, id) |>
    mutate(team_name = paste0("team_", team)) |>
    mutate(team_value = cur_group_id(), .by = team) |>
    pivot_wider(names_from = team_name, values_from = team_value)

  # add binary columns for each player
  players =
    players |>
    arrange(id, position) |>
    mutate(player_name = paste0("player_", str_replace(id, "-", "_"))) |>
    mutate(player_value = cur_group_id(), .by = team) |>
    pivot_wider(names_from = player_name, values_from = player_value) |>
    mutate(across(where(is.integer), \(x) replace_na(x, 0L))) |>
    rowwise() |>
    mutate(across(where(is.integer), \(x) min(x, 1L))) |>
    ungroup()

  return(players)
}

prepare_constraints_df = function(players, max_total_salary = 6e4L) {
  constraints_base = tribble(
    ~name               , ~direction, ~rhs            ,
    "max_total_pts"     , "<="      , 99999L          , #init value
    "max_total_salary"  , "<="      , max_total_salary,
    "max_pos_2B"        , "="       , 1L              ,
    "max_pos_3B"        , "="       , 1L              ,
    "max_pos_C1B"       , "="       , 1L              ,
    "max_pos_OF"        , "="       , 3L              ,
    "max_pos_P"         , "="       , 1L              ,
    "max_pos_SS"        , "="       , 1L              ,
    "max_pos_UTIL"      , "="       , 1L              ,
  )

  constraints_teams = tibble(
    name = paste0("max_", colnames(players)[str_detect(colnames(players), "^team_")]),
    direction = "<=",
    rhs = 4L
  )

  constraints_players = tibble(
    name = paste0("max_", colnames(players)[str_detect(colnames(players), "^player_")]),
    direction = "<=",
    rhs = 1L
  )

  bind_rows(constraints_base, constraints_teams, constraints_players)
}

prepare_lp_args = function(players, constraints) {
  f.con = players |> select(pts:last_col()) |> as.matrix() |> t()
  colnames(f.con) = players$name

  list(
    f.con = f.con, 
    f.dir = structure(constraints$direction, names = constraints$name), 
    f.rhs = structure(constraints$rhs, names = constraints$name)
  )
}

solve_n_times = function(players, lp_args, n = 10, decrease_max_pts_amount = 1e-4) {  
  result = vector("list", n)

  for (i in 1:n) {
    result[[i]] = lpSolve::lp(
      direction = "max",
      objective.in = players$pts,
      const.mat = lp_args$f.con,
      const.dir = lp_args$f.dir,
      const.rhs = lp_args$f.rhs,
      all.bin = TRUE
    )

    lp_args$f.rhs[[1]] = sum(players[as.logical(result[[i]]$solution), ]$pts) - decrease_max_pts_amount
  }

  return(result)
}


# Solve -------------------------------------------------------------------

players = prepare_players_df(data)
glimpse(players)

constraints = prepare_constraints_df(players)
print(constraints, n = Inf)

lp_args = prepare_lp_args(players, constraints)
lp_args

results = solve_n_times(players, lp_args)
solutions = map(results, \(result) players[as.logical(result$solution), ])

# --- Results
# Verifies that no solution has less than 9 distinct id
map_int(solutions, \(solution) n_distinct(solution$id)) # Good !

# --- Details
solution_df =
  solutions |>
  imap(\(x, i) mutate(x, solution_i = i, .before = 1)) |>
  bind_rows() |>
  select(solution_i:salary) |>
  arrange(solution_i, position)

print(solution_df, n = 25)

summarise(
  solution_df,
  total_pts = sum(pts),
  total_salary = sum(salary),
  .by = solution_i
)

lineupsMatrix = matrix(
  c(seq_along(results), solution_df$id),
  nrow = max(solution_df$solution_i)
)

lineupsMatrix

For immediate assistance, please email our customer support: [email protected]

Download RAW File