Determining Fantasy Football Optimal Rosters in R

I found it easier to tackle this with some reference tables. The FLEX slot makes things a little tricky in that we have to find the optimal selections first and then pick the best remaining eligible player.

Anyway, this is probably overkill but should scale nicely.

library(tidyverse)

#ASSUME ORIGINAL DATA IS STORED AS df

#To start, it might be helpful to have some reference tables.
#First, what positions are eligible to be placed in what slots?
possible_slots <- tibble(Position = c("QB", "RB", "RB", "WR", "WR", "TE", "TE", "D/ST", "K"),
               possible_slot = c("QB", "RB", "FLEX", "WR/TE", "FLEX", "WR/TE", "FLEX", "D/ST", "K"))

#Next, for each slot, how many players can be selected?
topn <- tibble(possible_slot = c("QB", "RB", "WR/TE", "D/ST", "K"),
               n_slots = c(1, 2, 3, 1, 1))

#Ok, now let's create a record for every potential slot that a player could conceivably take
all_possible_slots <- df %>% 
  left_join(possible_slots, by = "Position") %>% 
  left_join(topn, by = "possible_slot") %>% 
  #For each team, week and possible slot, rank the possiblity by points (descending)
  group_by(Team, Week, possible_slot) %>% 
  #Note the use of ties.method = first to ensure only one record per rank
  mutate(rank = rank(-FPTS, ties.method = "first")) %>% 
  ungroup()

#Now limit just to those records where the rank is <= the number of available slots
optimal_slots <- all_possible_slots %>% 
  filter(rank <= n_slots)

#The FLEX slot is special case, we need to select the best REMAINING player,
#i.e. one not selected in optimal_slots
flex_slot <- all_possible_slots %>% 
  #limit just to the FLEX slot and remove anyone selected in optimal_slots
  filter(possible_slot == "FLEX") %>% 
  anti_join(optimal_slots %>% select(Team, Week, PLAYER)) %>% 
  #Among those remaining, take the highest scoring
  group_by(Team, Week) %>% 
  filter(rank(-FPTS, ties.method = "first") == 1) %>% 
  ungroup() 

#Now let's bring it all together with the original data.
df %>% 
  #append the optimal slot (including flex)
  left_join({
    optimal_slots %>% 
      bind_rows(flex_slot) %>% 
      #concatenate the slot and the rank in that slot
      mutate(optimal_slot = paste0(possible_slot, 
                                   ifelse(possible_slot %in% c("RB", "WR/TE"), rank, ""))) %>% 
      select(Team, Week, PLAYER, optimal_slot)
  }, by = c("Team", "Week", "PLAYER")) %>% 
  #Anyone without an optimal slot should be benched
  mutate(optimal_slot = coalesce(optimal_slot, "BENCH"))


# A tibble: 32 x 7
Team                  Week SLOT  PLAYER           Position  FPTS optimal_slot
<chr>                <dbl> <chr> <chr>            <chr>    <dbl> <chr>       
  1 Washington Beersnake     1 QB    Justin Herbert   QB        15.4 QB          
2 Washington Beersnake     1 RB    Alvin Kamara     RB        15.1 RB1         
3 Washington Beersnake     1 RB    Saquon Barkley   RB         2.7 BENCH       
4 Washington Beersnake     1 WR/TE Terry McLaurin   WR         6.2 FLEX        
5 Washington Beersnake     1 WR/TE Keenan Allen     WR        10   WR/TE3      
6 Washington Beersnake     1 WR/TE Brandon Aiyuk    WR         0   BENCH       
7 Washington Beersnake     1 FLEX  Myles Gaskin     RB         7.6 RB2         
8 Washington Beersnake     1 D/ST  Buccaneers D/ST  D/ST       3   D/ST        
9 Washington Beersnake     1 K     Matt Gay         K         12   K           
10 Washington Beersnake     1 Bench William Fuller V WR         0   BENCH       
# ... with 22 more rows