#!/usr/bin/env python # coding: utf-8 #
Peter Norvig
Feb 2024
# # # Probabilities for Overtime in the Super Bowl # # In American football, if the game is tied at the end of 60 minutes of play, an overtime period commences, with these rules: # # - There is a coin toss; the team that guesses right has the option of possessing the ball first or second. # - Both teams get one possession to attempt to score points. # - After those two possessions, if the score is still tied, the game continues and the next score wins the game. # # A recap of (most of) the scoring rules: # # - A team scores **3** points for kicking a **field goal**. # - A team scores **6** points for a **touchdown**. # - After scoring a touchdown, if the game is not over, a team has the option of trying for extra point(s): either **one** point (easy) or **two** points (harder). # # # # The 2024 Super Bowl went into overtime, and there was some criticism of San Francisco 49ers coach Kyle Shanahan, who, after winning the coin toss, elected to possess the ball first rather than second. As it turned out the 49ers scored a field goal and then the Chiefs scored a touchdown to win. If the 49ers had taken the ball second, and they had known that the Chiefs scored a touchdown, they could have gone for their own touchdown rather than the field goal, and perhaps tied or won the game. The first question is: Is it better to possess the ball first (team **A**) or second (team **B**)? There are two main points to consider: # # - **B** has the advantage of knowing **B**'s score on their first possession. # - **A** has the advantage that if the score is tied after each team has their first possession, **A** gets the ball next (third), and next score wins. # # The second question is: if a team scores a touchdown on their first possession, what should their strategy be for the extra point(s)? It sems like **A** should go for 1, because if they go for 2 and miss, then it is too easy for **B** to make a 1-point conversion and win. On the other hand, it seems that **B** should go for 2, because if they tie the score at 7-7, then it is too easy for **A** to score next and win. # # In this notebook I do a simulation to answer these two questions. # # # ## Code to Simulate One Random Overtime Game # # Calling the function `overtime()` below runs a random simulation and returns a tuple of the two scores for team **A** and team **B**. Optionally, you can set certain probability parameters (they will be the same for both teams): # - **TD**: the probability of scoring a touchdown on a given possession, for both teams. (About 20%, by league average in recent years.) # - **FG**: the probability of scoring a field goal, for both teams. (About 25%.) # - **go**: the *additional* probability of scoring a touchdown for a team that must score a touchdown or lose–i.e., if team **A** scores a touchdown on their first possession, team **B** would never kick a field goal (or punt), in those situations they will instead go for a touchdown, which they get with probability **go**. (Set at 10%, but I'm not sure if that is a good estimate.) # - **one**: the probability of succesfully kicking a 1-point conversion attempt. (About 98%.) # - **two**: the probability of succesfully scoring on a 2-point conversion attempt. (About 48%.) # - **A_extra**: 1 or 2, denoting what **A** should try for if they score a touchdown on their first possession. # - **B_extra**: 1 or 2, denoting what **B** should try for if **A** scored 7 points on their first possession. # # In[1]: import random from collections import Counter Prob = float # The type for a probability, a number between 0 and 1. def overtime(TD=0.20, FG=0.25, go=0.10, one=0.98, two=0.48, A_extra=1, B_extra=2) -> tuple: """Given probabilities, play a random overtime and return (team_A_score, team_B_score). Both teams have same probabilities: TD: probability of scoring a touchdown on a 'normal' possession. FG: probability of scoring a field goal on a 'normal' possession. go: additional probability of scoring a touchdown, if you resolve not to kick. one: probability of making a one-point conversion. two: probability of making a two-point conversion. A_extra: what team A goes for on the extra point. B_extra: what team B goes for on the extra point (when behind by 1).""" A = B = 0 # The scores of the two teams possession = 1 # The number of possessions for each team while A == B: extra = (0 if possession > 1 else P(one, 1) if A_extra == 1 else P(two, 2)) A += score(TD, FG, extra) if possession == 1 or A == B: # B gets a chance on their first possession, or if it is still tied. extra = (0 if B + 6 > A else P(one, 1) if B + 6 == A or (B_extra == 1 and B + 7 == A) else P(two, 2)) B += (score(TD + go, 0, extra) if A - B > 3 # Must go for TD if behind by more than 3 else score(TD, FG, 0)) possession += 1 return A, B def score(TD: Prob, FG: Prob, extra: int) -> int: """Randomly simulate a score, given probabilities for TD and FG, and given the number of extra points.""" return P(TD, 6 + extra) or P(FG / (1 - TD), 3) def P(p: Prob, points: int) -> int: """Return `points` with probability `p`, else 0.""" return points if random.random() < p else 0 # Let's play a random overtime game and see the scores of the two teams: # In[2]: overtime() # # Code to Simulate a Million Games, and Draw Conclusions # # OK, but that's just one game. What if we play a million games? # In[3]: Counter(overtime() for _ in range(1000_000)).most_common() # That gives us the range of possible scores and the frequency of each one. Note: # - Field goals are more common than touchdowns, so the most common scores are 3-0 and 0-3. # - 3-0 is more common than 0-3, because 3-0 includes times when **A** scored first and also third (or later, but that's rarer). # - Why is 0-6 more common than 7-0? Because 0-6 includes times when **B** scored second, or fourth (or later). # - Note that the total of 7-0 (**A** scores first) and 6-0 (**A** scores third, or first and misses the kick) is more than the count for 0-6. # - Note that 3-6 is almost 3 times more common than 6-3. Either could result from 3 field goals, but 3-6 could also be **B** scoring a touchdown; 6-3 could never result from **A** scoring a touchdown. # - 6-9, 9-6, 12-6, and 6-12 are all rare scores, because they mean that *both* teams missed a kick. # # That's all interesting, but the question remains: # # ## Who Has the Advantage, Team A or Team B? # # We can add up the games in which **A** wins: # In[4]: def win_probability(n=100_000, **kwds) -> float: """Probability that first team (team A) wins, in n simulations of overtime(**kwds).""" scores = (overtime(**kwds) for _ in range(n)) return sum(A > B for A, B in scores) / n # In[5]: win_probability() # Using the given scoring percentages, my simulation says team **A** has about a 54% chance of winning. This supports Shanahan's decision. # # However, I don't have confidence that the probability parameter values I chose are reflective of reality, so let's explore a wider range of parameters: # In[6]: import pandas as pd from IPython.display import HTML def chart(TDs=(0.15, 0.20, 0.33), FGs=(0.20, 0.25, 0.33), gos=(0.05, 0.10, 0.20), twos=(0, 0.48, 0.60), n=100_000) -> HTML: """Create a chart of Win percentages for various parameter values.""" data = [((win_probability(n=n, TD=TD, FG=FG, go=go)), TD, FG, go, two) for TD in TDs for FG in FGs for go in gos for two in twos] df = pd.DataFrame(data, columns=('Win', 'TD', 'FG', 'go', 'two')).sort_values('Win') print(f'Team A win probability: min: {min(df.Win):.1%}, max: {max(df.Win):.1%}') return HTML(df.to_html(index=False, formatters={'Win': '{:.1%}'.format})) # In[7]: chart() # Now I feel more confident that across a wide range of parameter values, **A** always has the advantage. # # Another question still remains: # # ## What Are the Best Strategies for Extra Points? # # Earlier I claimed that **A** should go for 1 and **B** for 2. That gives us about a 54% win probability for **A**: # In[8]: win_probability(A_extra=1, B_extra=2) # We can see that if **A** goes for 2, they will do worse: # In[9]: win_probability(A_extra=2, B_extra=2) # We can also see that if **B** goes for 1, they will do worse (i.e., **A** will do better): # In[10]: win_probability(A_extra=1, B_extra=1)