Finding the best possible combinations based on multiple conditions with R dplyr

Viewed 145

My goal


My goal is to find, based on the index, the best combinations of 10 players that have a score sum between 95.5-100.4.

The detail


There is an important detail. From the 10 players, there should be based on the role column 2C (two Centers), 4F (four Forwards), 4*G (four Guards).

Atm I am struggling with for loops, but I am sure there is something bright in the dplyr package that I am missing. Any help or guidance is highly appreciated.

set.seed(123)
players <- paste("player",rep(1:20))
score <- runif(20, min=4, max=16.7)
index <- runif(20, min=-1, max=9)
role <- rep(c("C","F","F","G","G"),4)

df <- data.frame(players, score, index,role)
df
#>      players     score      index role
#> 1   player 1  7.652235  7.8953932    C
#> 2   player 2 14.011475  5.9280341    F
#> 3   player 3  9.194007  5.4050681    F
#> 4   player 4 15.214321  8.9426978    G
#> 5   player 5 15.943935  5.5570580    G
#> 6   player 6  4.578568  6.0853047    C
#> 7   player 7 10.706940  4.4406602    F
#> 8   player 8 15.333722  4.9414202    F
#> 9   player 9 11.003225  1.8915974    G
#> 10 player 10  9.799007  0.4711365    G
#> 11 player 11 16.151783  8.6302423    C
#> 12 player 12  9.757344  8.0229905    F
#> 13 player 13 12.605147  5.9070528    F
#> 14 player 14 11.272444  6.9546742    G
#> 15 player 15  5.307143 -0.7538632    G
#> 16 player 16 15.427777  3.7779597    C
#> 17 player 17  7.125314  6.5845954    F
#> 18 player 18  4.534156  1.1640794    F
#> 19 player 19  8.164593  2.1818101    G
#> 20 player 20 16.122196  1.3162579    G

Created on 2021-10-16 by the reprex package (v2.0.1)

Thank you for your time

UPDATE:


My logic so far is:

  1. to transpose my df
df <- as.data.frame(t(df))
  1. and create all possible combinations of 10 players
combn(df, 10, simplify=FALSE)

Now I need to select the list which the right roles and sum between 95.5-100.4. Damn there should be a more clever way.

3 Answers

I think an efficient way is using Monte Carlo method (no need to build a full dataset of all possible combinations), where we stop searching after finding the desired output through random sampling

repeat {
  idx <- unlist(
    Map(
      sample,
      split(1:nrow(df), df$role),
      c(2, 4, 4)
    )
  )
  s <- sum(df$score[idx])
  if (s >= 95.5 & s <= 100.4) break
}
df[sort(idx), ]

I used data.table. choose(4, 2) * choose(8, 4) * choose(8, 4) = 29400 combinations. Basically brute force. I'm sure there's a more elegant solution.

library(data.table)
library(magrittr)

set.seed(123)
players <- paste("player",rep(1:20))
score <- runif(20, min=4, max=16.7)
index <- runif(20, min=-1, max=9)
role <- rep(c("C","F","F","G","G"),4)

dt <- data.table(players, score, index,role)
centers <- dt[role == "C"]
forwards <- dt[role == "F"]
guards <- dt[role == "G"]
c_combos <- combn(nrow(centers), 2) %>% t() %>% as.data.table()
c_combos <- lapply(c_combos, function(x) centers[x])
c_combos <- data.table(c_combos[[1]]$players, c_combos[[2]]$players, 
                       c_combos[[1]]$score + c_combos[[2]]$score,
                       c_combos[[1]]$index + c_combos[[2]]$index) %>% 
  setnames(c('C1', 'C2', 'score_c', 'index_c'))
f_combos <- combn(nrow(forwards), 4) %>% t() %>% as.data.table()
f_combos <- lapply(f_combos, function(x) forwards[x])
f_combos <- data.table(f_combos[[1]]$players, f_combos[[2]]$players, 
                       f_combos[[3]]$players, f_combos[[4]]$players,
                       f_combos[[1]]$score + f_combos[[2]]$score + 
                         f_combos[[3]]$score + f_combos[[4]]$score,
                       f_combos[[1]]$index + f_combos[[2]]$index + 
                         f_combos[[3]]$index + f_combos[[4]]$index) %>% 
  setnames(c('F1', 'F2', 'F3', 'F4', 'score_f', 'index_f'))
g_combos <- combn(nrow(guards), 4) %>% t() %>% as.data.table()
g_combos <- lapply(g_combos, function(x) guards[x])
g_combos <- data.table(g_combos[[1]]$players, g_combos[[2]]$players, 
                       g_combos[[3]]$players, g_combos[[4]]$players,
                       g_combos[[1]]$score + g_combos[[2]]$score + 
                         g_combos[[3]]$score + g_combos[[4]]$score,
                       g_combos[[1]]$index + g_combos[[2]]$index + 
                         g_combos[[3]]$index + g_combos[[4]]$index) %>% 
  setnames(c('G1', 'G2', 'G3', 'G4', 'score_g', 'index_g'))

combined <- expand.grid(1:nrow(c_combos), 1:nrow(f_combos), 1:nrow(g_combos))
ans <- rbindlist(lapply(1:nrow(combined), 
                               function(x) data.table(c_combos[combined$Var1[x]],
                                                 f_combos[combined$Var2[x]],
                                                 g_combos[combined$Var3[x]])))
ans[, score := score_c + score_f + score_g]
ans[, index := index_c + index_f + index_g]
ans[, c('score_c', 'score_f', 'score_g', 'index_c', 'index_f', 'index_g') := NULL]
ans[score %between% c(99.5, 100.4), .SD[which.max(index)]]

Gives:

         C1       C2       F1       F2        F3        F4       G1       G2
1: player 1 player 6 player 3 player 7 player 12 player 17 player 4 player 5
          G3        G4   score    index
1: player 14 player 19 99.6097 62.07025

Here's an approach using some tidyverse and base functions. Brute force, but probably fast enough (~0.2 sec) at this scale.

First I make vectors of player numbers for each position, then figure out the possible combinations within each position. Then use tidyr::crossing to get all the combinations of those combinations. By pivoting those long and joining to original data, we can more easily get the total score for each combination, and filter those to the required range.

Looks like 1,878 combinations below 95.5, 24,743 over 100.4, and 2,779 in the desired range.

library(dplyr); library(tidyr) 
df %>% split(.$role) -> df_split   
C <- df_split$C$players
F <- df_split$F$players
G <- df_split$G$players

C_comb <- combn(C, 2) %>% t %>% as_tibble()
F_comb <- combn(F, 4) %>% t %>% as_tibble()
G_comb <- combn(G, 4) %>% t %>% as_tibble()

crossing(C_comb, F_comb, G_comb, .name_repair = "unique") %>%
  mutate(sim_num = row_number()) %>%
  pivot_longer(-sim_num) %>%
  left_join(df, by = c("value" = "players")) %>%
  group_by(sim_num) %>%
  mutate(total_score = sum(score)) %>%
  ungroup() %>%
  filter(total_score >= 95.5, score <= 100.4)
Related