In American football, if the game is tied at the end of 60 minutes of play, an overtime period commences, with these rules:
A recap of (most of) the scoring rules:
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:
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.
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):
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:
overtime()
(0, 3)
OK, but that's just one game. What if we play a million games?
Counter(overtime() for _ in range(1000_000)).most_common()
[((3, 0), 245539), ((0, 3), 196710), ((0, 6), 157459), ((7, 0), 137498), ((6, 0), 89649), ((3, 6), 63006), ((7, 6), 30297), ((7, 8), 28557), ((6, 3), 22473), ((9, 3), 17667), ((3, 9), 9921), ((6, 7), 1206), ((12, 6), 6), ((9, 6), 5), ((6, 12), 4), ((6, 9), 3)]
That gives us the range of possible scores and the frequency of each one. Note:
That's all interesting, but the question remains:
We can add up the games in which A wins:
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
win_probability()
0.5438
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:
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}))
chart()
Team A win probability: min: 51.3%, max: 55.7%
Win | TD | FG | go | two |
---|---|---|---|---|
51.3% | 0.33 | 0.20 | 0.20 | 0.48 |
51.4% | 0.33 | 0.20 | 0.20 | 0.00 |
51.6% | 0.33 | 0.25 | 0.20 | 0.60 |
51.7% | 0.33 | 0.20 | 0.20 | 0.60 |
51.7% | 0.33 | 0.25 | 0.20 | 0.00 |
51.9% | 0.33 | 0.25 | 0.20 | 0.48 |
52.2% | 0.33 | 0.33 | 0.20 | 0.60 |
52.6% | 0.33 | 0.33 | 0.20 | 0.48 |
52.7% | 0.33 | 0.33 | 0.20 | 0.00 |
53.2% | 0.33 | 0.25 | 0.10 | 0.00 |
53.2% | 0.33 | 0.20 | 0.10 | 0.60 |
53.2% | 0.20 | 0.20 | 0.20 | 0.00 |
53.2% | 0.20 | 0.25 | 0.20 | 0.48 |
53.3% | 0.20 | 0.20 | 0.20 | 0.48 |
53.3% | 0.20 | 0.20 | 0.20 | 0.60 |
53.3% | 0.20 | 0.25 | 0.20 | 0.00 |
53.4% | 0.33 | 0.25 | 0.10 | 0.60 |
53.4% | 0.33 | 0.20 | 0.10 | 0.00 |
53.4% | 0.20 | 0.25 | 0.20 | 0.60 |
53.4% | 0.15 | 0.20 | 0.20 | 0.00 |
53.5% | 0.33 | 0.25 | 0.10 | 0.48 |
53.5% | 0.33 | 0.20 | 0.10 | 0.48 |
53.5% | 0.15 | 0.25 | 0.20 | 0.00 |
53.7% | 0.15 | 0.20 | 0.20 | 0.48 |
53.7% | 0.33 | 0.33 | 0.10 | 0.00 |
53.7% | 0.15 | 0.25 | 0.20 | 0.60 |
53.7% | 0.33 | 0.20 | 0.05 | 0.00 |
53.8% | 0.15 | 0.25 | 0.20 | 0.48 |
53.8% | 0.15 | 0.20 | 0.20 | 0.60 |
53.9% | 0.33 | 0.20 | 0.05 | 0.48 |
53.9% | 0.33 | 0.33 | 0.10 | 0.60 |
54.0% | 0.33 | 0.25 | 0.05 | 0.00 |
54.0% | 0.20 | 0.25 | 0.10 | 0.60 |
54.0% | 0.20 | 0.33 | 0.20 | 0.48 |
54.0% | 0.20 | 0.33 | 0.20 | 0.60 |
54.1% | 0.20 | 0.20 | 0.10 | 0.60 |
54.1% | 0.33 | 0.33 | 0.10 | 0.48 |
54.1% | 0.15 | 0.20 | 0.10 | 0.00 |
54.1% | 0.20 | 0.20 | 0.10 | 0.48 |
54.1% | 0.33 | 0.20 | 0.05 | 0.60 |
54.1% | 0.20 | 0.33 | 0.20 | 0.00 |
54.2% | 0.20 | 0.20 | 0.10 | 0.00 |
54.2% | 0.33 | 0.25 | 0.05 | 0.48 |
54.2% | 0.15 | 0.20 | 0.10 | 0.48 |
54.2% | 0.15 | 0.20 | 0.10 | 0.60 |
54.3% | 0.15 | 0.33 | 0.20 | 0.00 |
54.3% | 0.20 | 0.20 | 0.05 | 0.00 |
54.3% | 0.20 | 0.20 | 0.05 | 0.60 |
54.3% | 0.33 | 0.25 | 0.05 | 0.60 |
54.4% | 0.20 | 0.25 | 0.10 | 0.48 |
54.4% | 0.15 | 0.33 | 0.20 | 0.48 |
54.5% | 0.20 | 0.25 | 0.10 | 0.00 |
54.5% | 0.15 | 0.25 | 0.10 | 0.48 |
54.5% | 0.20 | 0.25 | 0.05 | 0.00 |
54.5% | 0.15 | 0.20 | 0.05 | 0.60 |
54.6% | 0.15 | 0.20 | 0.05 | 0.48 |
54.6% | 0.15 | 0.20 | 0.05 | 0.00 |
54.6% | 0.20 | 0.20 | 0.05 | 0.48 |
54.7% | 0.33 | 0.33 | 0.05 | 0.48 |
54.7% | 0.15 | 0.25 | 0.10 | 0.60 |
54.7% | 0.33 | 0.33 | 0.05 | 0.60 |
54.7% | 0.15 | 0.33 | 0.20 | 0.60 |
54.8% | 0.15 | 0.25 | 0.10 | 0.00 |
54.8% | 0.15 | 0.25 | 0.05 | 0.60 |
54.8% | 0.33 | 0.33 | 0.05 | 0.00 |
54.8% | 0.20 | 0.25 | 0.05 | 0.60 |
54.9% | 0.15 | 0.25 | 0.05 | 0.00 |
55.0% | 0.15 | 0.25 | 0.05 | 0.48 |
55.0% | 0.20 | 0.33 | 0.10 | 0.48 |
55.0% | 0.20 | 0.33 | 0.10 | 0.00 |
55.1% | 0.20 | 0.25 | 0.05 | 0.48 |
55.1% | 0.20 | 0.33 | 0.10 | 0.60 |
55.2% | 0.20 | 0.33 | 0.05 | 0.00 |
55.2% | 0.15 | 0.33 | 0.10 | 0.60 |
55.3% | 0.15 | 0.33 | 0.10 | 0.00 |
55.3% | 0.20 | 0.33 | 0.05 | 0.48 |
55.3% | 0.15 | 0.33 | 0.10 | 0.48 |
55.4% | 0.15 | 0.33 | 0.05 | 0.00 |
55.4% | 0.20 | 0.33 | 0.05 | 0.60 |
55.4% | 0.15 | 0.33 | 0.05 | 0.60 |
55.7% | 0.15 | 0.33 | 0.05 | 0.48 |
Now I feel more confident that across a wide range of parameter values, A always has the advantage.
Another question still remains:
Earlier I claimed that A should go for 1 and B for 2. That gives us about a 54% win probability for A:
win_probability(A_extra=1, B_extra=2)
0.54277
We can see that if A goes for 2, they will do worse:
win_probability(A_extra=2, B_extra=2)
0.53674
We can also see that if B goes for 1, they will do worse (i.e., A will do better):
win_probability(A_extra=1, B_extra=1)
0.55175