Matter of Stats

View Original

The Mathematics of Winning Combination

Recently, I’ve started watching repeats of a British game show called Winning Combination that aired in 2020 and 2021 and in which nine contestants, numbered 1 to 9, answer questions in an attempt to become a part of a “final combination”.

That combination is simply the four numbers associated with the successful contestants and becomes the prize pool to be divided evenly amongst them if they complete a final challenge.

The elements of the game relevant to the analyses we’ll be performing are that:

  • Winning contestants are determined one at a time, and each is allowed to place themselves in any of the remaining untaken positions in the combination. So, for example, if the player associated with the number 7 is first through, he or she can decide to place themselves in the thousands, hundreds, tens, or units position.

  • Because the prize pool is determined by the combination, contestants associated with larger numbers will prefer to place themselves further left in the combination, while those with smaller numbers will prefer to place themselves further right.

  • Once the combination has been set, contestants need to answer as many questions correctly as the number they are associated with. So, for example, our contestant numbered 7 would need to answer seven questions correctly to complete his or her part of the final task.

  • Questioning starts with the person in the units position and a time bank of 30 seconds. Time ticks down as the questions are being asked and the answer provided, and 5 seconds is added back to the clock for each correct answer provided (although the time bank is never allowed to go above 30 seconds). If the time bank reaches zero before the required number of correct answers have been provided, the game is over, and all four players receive nothing.

  • Questioning continues with the person in the tens, hundreds, and then thousands position.

  • If the contestant in the thousands position answers the requisite number of questions without the clock reaching zero, all four members of the combination share equally in the prize pool.

  • The five contestants who are not part of the final combination all receive nothing, regardless of the success or otherwise of the chosen four.

THE QUESTIONS

There are two interesting questions we can ask about the game:

  1. Given a combination with none or some of the positions filled, where is the best place for the next contestant to place themselves? For example, if there are no positions filled and the contestant with number 5 wins through, in which position should he or she place themselves?

  2. Once the combination has been determined, how likely is it that a team of a certain ability, defined by its ability to get questions right, will take home the prize?

OPTIMAL PLACEMENT

For this analysis we’re going to assume that:

  • All contestants, whatever their number, are equally talented, so any of them is as likely as any other to be the next one in the combination

  • The goal is to maximise the size of the final prize pool

We determine the optimal positioning by running 1m simulations of the game in which we choose 4 of the 9 numbers at random and then place them, in turn, at random in the combination, and then calculate the resulting average prize pool. The R code for this appears at the end of this blog.

This allows us to answer questions such as: if the current combination is 8x3x, is it better to put a 5 in position 2 or position 4?

In fact, it allows us to answer this question for ALL possible combination states and next number to place. These answers are is summarised in the table at right (which assumes that, for later contestants, all earlier contestants have placed themselves optimally).

The first row tells us, for example, that, in a brand new combination (ie the State is “xxxx”), if any of the contestants associated with 6, 7, 8, or 9 go through, they should place themselves in the thousands (ie position 1). Conversely, if any of the contestants associated with 1, 2, 3, 4, or 5 go through, they should place themselves in the units (ie position 4).

Broadly speaking:

  • contestants associated with numbers 7, 8, and 9 should always place themselves as far left as they can

  • contestants associated with numbers 1, 2, and 3 should always place themselves as far right as they can

  • contestants associated with the number 4 should usually place themselves as far right as they can, but, in a few cases, should fill the spot second closest to the right

  • contestants associated with the number 6 should usually place themselves as far left as they can, but, in a few cases, should fill the spot second closest to the left

  • contestants associated with the number 5 need to refer to the table to determine where to go

It’s worth noting in passing that sub-optimal play by the contestants associated with the larger numbers has the greatest effect on the size of the final prize. If, so example, the contestant associated with the number 1 places himself or herself in the tens rather than the units, the difference in the final prize pool is only $9.

CHANCES OF VICTORY

For this analysis we’re going to add a few more assumptions

  • All four contestants who form part of the final combination have the same, fixed probability of getting a question correct. This probability remains constant regardless of the time remaining in the time bank

  • When a question is answered correctly, the net effect on the time bank (relative to when the question started to be asked) is 0 seconds 50% of the time, and an addition of 1 second 50% of the time

  • When a question is answered incorrectly, the net effect on the time bank (relative to when the question started to be asked) is to subtract 4 seconds one-third of the time, 5 seconds one-third of the time, and 6 seconds one-third of the time

Very roughly speaking, this means that a final combination can get no more than six questions incorrrect between them before running out of time.

We’ll look at how the probability of victory varies as we change two things:

  • The final combination

  • The fixed probability of answering a question correctly (which we’ll vary between 35% and 75%)

The R code to perform this analysis also appears below and allows us to select a combination at random and then simulate the final question-asking process 10,000 times for that combination to calculate the proportion of the times that the contestants with that combination win or lose. It does this for 1m randomly-chosen combinations. (Since there are only 9x8x7x6 or 3,024 combinations, each will appear a number of times in the simulation).

Inspecting the outputs reveals that the order in which numbers appear in the final combination is irrelevant - so, for example, 9621 wins as often as 1269 - and what matters is the total of the numbers in the combination.

The chart below shows how the victory probability alters as we change that total and the probability of answering a question (click on it to access a larger version).

This tells us that, for example, given the assumptions we’ve made, a team with a final combination that adds to 20 (which is the average) needs to answer correctly about 72 or 73% of the time in order to be 50:50 chances of taking home the money.

One interesting aspect that this chart reveals is the high rate of payoff for improvements in question-answering ability from about 65% onwards. For example, with a combination total of 20, moving from a 65% rate or question-answering to a 70% rate lifts a team’s victory chances from 22% to 40%.

If anyone reading this blog would like me to try other assumptions, please leave a comment or send me an email using the address in the navigation bar.

CODE FOR OPTIMAL PLACEMENT

library(dplyr)
library(doParallel)
reps = 1000000
make_choice = function(State, numchosen)
{
    trial_order = sample(1:4,4)
    if (substr(State, trial_order[1], trial_order[1]) == 0)
    {
       where_placed = trial_order[1]
    }  else
       {
          if (substr(State, trial_order[2], trial_order[2]) == 0)
          {
             where_placed = trial_order[2]
          } else
            {
              if (substr(State, trial_order[3], trial_order[3]) == 0)
              {
                 where_placed = trial_order[3]
              }  else
                 {
                     where_placed = trial_order[4]
                 } 
            }
        }
    return(where_placed)
}
t0 = Sys.time()
registerDoParallel(cl <- makeCluster(10))
results_list = foreach(repnum = 1:reps, .packages = c("dplyr"), .errorhandling="pass") %dopar% {
  row_num = 0
  scp = data.frame(Rep_Num = rep(0,4), 
                 Move_Num = 1:4, 
                 Number_Chosen = rep(0, 4), 
                 Spot_Chosen = rep(0, 4), 
                 ExactStateBefore = rep("0000", 4),                 
                 ExactStateAfter = rep("0000", 4))
  chosen = sample(1:9,4)
  for (move in 1:4)
  {
    row_num = row_num + 1
    if ( move != 1 ) { scp$ExactStateBefore[row_num] = scp$ExactStateAfter[row_num-1] }
    pos_to_fill = make_choice(scp$ExactStateBefore[row_num], chosen[move])
    scp$ExactStateAfter[row_num] = scp$ExactStateBefore[row_num]
    substr(scp$ExactStateAfter[row_num], pos_to_fill, pos_to_fill) = as.character(chosen[move])
    scp$Number_Chosen[row_num] = chosen[move]
    scp$Spot_Chosen[row_num] = pos_to_fill
  }
  scp$Rep_Num = repnum
  return(scp)
}                 
stopCluster(cl)
full_results = do.call(rbind.data.frame, results_list)
print(Sys.time() - t0)
full_results = full_results %>% group_by(Rep_Num) %>% mutate(FinalValue = ExactStateAfter[Move_Num == 4])  
summary_of_results = full_results %>% group_by(ExactStateBefore, Number_Chosen, Spot_Chosen) %>% summarise(Sample_Size = length(ExactStateBefore),
MeanValue = mean(as.numeric(FinalValue))) 
best_strategy = summary_of_results %>% group_by(ExactStateBefore, Number_Chosen) %>% summarise(Min_Sample_Size = min(Sample_Size),
Sample_Size_for_Best = Sample_Size[MeanValue == max(MeanValue)],
                       Best_Spot = Spot_Chosen[MeanValue == max(MeanValue)],
                       Expected_Value = max(MeanValue))

CODE FOR VICTORY CHANCES

library(dplyr)
library(doParallel)
library(ggplot2)
library(scales)
library(stringr)
library(viridis)
person_answer = function(time_left, target, prob_right)
{
   wrong_time = sample(c(-4:-6), 1)
   right_time = sample(c(0:1), 1)
   while (time_left > 0 & target > 0)
   {
        if(runif(1,0,1) < prob_right)
        {
           time_left = pmin(time_left + right_time, 30)
           target = target - 1
        } else
          {
              time_left = time_left + wrong_time
          }
    }
    return(time_left)      
}
sim_game = function(init_time, combo, prob_right)
{
  failed = FALSE
  player_num = 0
  while (player_num < 4 & !failed)
  {
       if (time_left != 0)
       {
          player_num = player_num + 1
          time_left = person_answer(time_left, combo[player_num], prob_right)
       } else
         {
             failed = TRUE
         }
  }
  return(time_left)
}
reps = 1000000
t0 = Sys.time()
registerDoParallel(cl <- makeCluster(10))
results_list = foreach(repnum = 1:reps, .packages = c("dplyr", "mvtnorm", "emdbook", "moments", "tidyr"), 
                       .errorhandling="pass", .combine = rbind) %dopar% {
    time_left = 30
    combo = sample(1:9,4)
    prob_right = round(runif(1,0.35,0.75),2)
    reps_each = 1000
    results = lapply(seq_len(reps_each), function(x) sim_game(30, combo, prob_right))
    times = unlist(results)
    PWin = mean(times > 0)
    MSec = mean(times[times > 0])
    this_result = c(repnum, PWin, MSec,
                    prob_right,
                    combo[1], combo[2],
                    combo[3], combo[4])
    return(this_result)
}
stopCluster(cl)
game_results = data.frame(do.call(cbind, data.frame(results_list)))
names(game_results) = c("RepNum", "ProbWin", "MeanSecsLeftWhenWin", "ProbRight", "Combo1", "Combo2", "Combo3", "Combo4")
print(Sys.time() - t0)
ordered_res = game_results %>% arrange(desc(ProbWin))
write.csv(ordered_res, 'Results1m.csv', row.names = FALSE)
ordered_res$ComboTotal = ordered_res$Combo1 + ordered_res$Combo2 + ordered_res$Combo3 + ordered_res$Combo4
game_summary = ordered_res %>% group_by(ComboTotal, ProbRight) %>% summarise(MeanProbWin = mean(ProbWin))
game_summary$Label = paste(round(game_summary$MeanProbWin*100,0),"%", sep = "")
game_summary$TextCol = ifelse(game_summary$MeanProbWin < 0.6, "A", "B")
p4 = ggplot(game_summary, aes(x = ProbRight, y = ComboTotal)) +
geom_tile(aes(fill = MeanProbWin)) +
geom_text(aes(label = Label, colour = TextCol)) +
scale_fill_viridis(name = "Estimated Win Prob   ", labels = percent) +
scale_colour_manual(values = c("white", "grey25"), guide = "none") +
scale_x_continuous("Probability get question right", labels = percent, limits = c(0.34,0.76)) +
scale_y_continuous("Combination Total") +
theme_minimal() +      
labs(title = "Estimated Win Probability",
     subtitle = "Based on 10,000 reps of 1m combinations. Right answers assumed to add 0 or 1 second, wrong answers subtract 4, 5, or 6 seconds") + 
theme(axis.title.x = element_text(face="bold", colour="#990000", size=20),
           axis.title.y = element_text(face="bold", colour="#990000", size=20),
           axis.text.x  = element_text(size=12), 
           axis.text.y  = element_text(size=12),
           legend.title = element_text(face="bold", size=18),
           legend.text = element_text(size=18),
           legend.key.width=unit(2,"cm"),
           panel.background = element_rect(colour = "black"),
           plot.title = element_text(size = 30),
           plot.subtitle = element_text(size = 18),
           strip.text.x = element_text(size=12), legend.position = "bottom")
p4