{ "cells": [ { "cell_type": "markdown", "id": "13d72a29-e929-4b88-8d4f-6db3154dec12", "metadata": {}, "source": [ "
Peter Norvig
Feb 2024
\n", "\n", "# Probabilities for Overtime in the Super Bowl\n", "\n", "In American football, if the game is tied at the end of 60 minutes of play, an overtime period commences, with these rules:\n", "\n", "- There is a coin toss; the team that wins the flip has the option of possessing the ball first or second.\n", "- Both teams get one possession to attempt to score points.\n", "- After those two possessions, if the score is still tied, the game continues and the next score wins the game.\n", "\n", "A recap of (most of) the scoring rules:\n", "\n", "- A team scores **3** points for kicking a **field goal**. \n", "- A team scores **6** points for a **touchdown**. \n", "- After scoring a touchdown, if the game is not over, a team gets a chance to score **extra point**(s); they have the choice of going for either **one** point (easier) or **two** points (harder).\n", "\n", "\n", "\n", "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 first, they could have gone for their own touchdown rather than the field goal, and perhaps tied or won the game. There are two key question:\n", "\n", "1) Is it better to possess the ball first or second?\n", "2) After a team scores a touchdown, should they go for 1 extra point or 2?\n", "# TL;DR\n", "\n", "The short answers:\n", "1) It is (slightly) better to **take the ball first.** (Why? Despite the avantage of possessing the ball second rather than first, it is more important to possess it *third*, because then if you score you win. So it is better to be first (and myabe third) than second (and maybe fourth).)\n", "3) The first team should **go for 1** extra point. If they succeed, the second team should **go for 2** extra points. \n", "\n", "In this notebook I make some simplifying assumptions, and then do a simulation to verify these answers.\n", "\n", "\n", "## Code to Simulate One Random Overtime Game\n", "\n", "\n", "\n", "Calling the function `overtime()` below runs a random simulation of an overtime game and returns a pair of integers, (*A*, *B*), indicating the scores for the first team to possess the ball (**A**) and the second team (**B**). I could have done a detailed play-by-play simulation, but I decided to use a possession-by-possession simulation that considers just a few probabilities (and makes the inaccurate assumption that each possession is independent of the previous ones). You can set the following probability parameters (they will be the same for both teams):\n", "- **TD**: the probability of scoring a touchdown on a given possession. (By default 20%, the league average in recent years.)\n", "- **FG**: the probability of scoring a field goal on a given possession. (By default 25%.)\n", "- **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 attempt a field goal (or punt). They will instead *go* for a touchdown, which they get with probability **go**. (By default 5%, but I'm not sure if that is a good estimate.)\n", "- **one**: the probability of succesfully kicking a 1-point conversion attempt. (About 98%.)\n", "- **two**: the probability of succesfully scoring on a 2-point conversion attempt. (About 48%.)\n", "- **A_extra**: 1 or 2, denoting what **A** should try for if they score a touchdown on their first possession.\n", "- **B_extra**: 1 or 2, denoting what **B** should try for if **A** scored 7 points on their first possession.\n" ] }, { "cell_type": "code", "execution_count": 1, "id": "53085ed6-cb47-4a65-96bb-a21e2baa40e3", "metadata": {}, "outputs": [], "source": [ "import random\n", "from collections import Counter\n", "from typing import Iterable, Tuple\n", "from statistics import mean\n", "\n", "Prob = float # The type for a probability, a number between 0 and 1.\n", "Scores = Tuple[int, int] # Type for an (A, B) score.\n", "\n", "def overtime(TD=0.20, FG=0.25, go=0.05, one=0.98, two=0.48, A_extra=1, B_extra=2) -> Scores:\n", " \"\"\"Given probabilities, play a random overtime and return (team_A_score, team_B_score).\n", " Both teams have same probabilities:\n", " TD: probability of scoring a touchdown on a 'normal' possession. \n", " FG: probability of scoring a field goal on a 'normal' possession.\n", " go: additional probability of scoring a touchdown, if you must score or lose.\n", " one: probability of making a one-point conversion.\n", " two: probability of making a two-point conversion.\n", " A_extra: what team A goes for on the extra point (1 or 2).\n", " B_extra: what team B goes for on the extra point (1 or 2), when behind by 1.\"\"\"\n", " A = B = 0 # The scores of the two teams\n", " first_possession = True\n", " while A == B:\n", " extra = (0 if not first_possession else P(one, 1) if A_extra == 1 else P(two, 2))\n", " A += score(TD, FG, extra)\n", " if first_possession or A == B: # B gets a chance on their first possession, or if the game is still tied.\n", " extra = (0 if B + 6 > A else P(one, 1) if B + 7 > A or (B_extra == 1 and B + 7 == A) else P(two, 2))\n", " B += (score(TD, FG, 0) if B + 3 >= A else score(TD + go, 0, extra)) # must go for it if down by more than 3\n", " first_possession = False\n", " return A, B # The scores of the two teams\n", "\n", "def score(TD: Prob, FG: Prob, extra: int) -> int: \n", " \"\"\"Randomly simulate a score, given probabilities for TD and FG, and given the number of extra points.\"\"\"\n", " return P(TD, 6 + extra) or P(FG / (1 - TD), 3)\n", "\n", "def P(p: Prob, points: int) -> int: \n", " \"\"\"Return `points` with probability `p`, else 0.\"\"\"\n", " return points if random.random() < p else 0" ] }, { "cell_type": "markdown", "id": "cecd1023-957e-449b-8810-d34fcbe3ea33", "metadata": {}, "source": [ "Let's play a random overtime game and see the scores of the two teams:" ] }, { "cell_type": "code", "execution_count": 2, "id": "3ca41256-ef10-40f9-9d39-c4e12a4b70b9", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(0, 3)" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "overtime()" ] }, { "cell_type": "markdown", "id": "c78dd400-6ae7-410c-8494-7b74f9bb45aa", "metadata": {}, "source": [ "# Code to Simulate a Million Games, and Draw Conclusions\n", "\n", "OK, but that's just one game. What if we play, say, a million games?" ] }, { "cell_type": "code", "execution_count": 3, "id": "e881dac9-b490-46b6-95e2-137b50679f8d", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[((3, 0), 246591),\n", " ((0, 3), 198249),\n", " ((0, 6), 157443),\n", " ((7, 0), 146508),\n", " ((6, 0), 89334),\n", " ((3, 6), 62197),\n", " ((7, 6), 25421),\n", " ((7, 8), 23293),\n", " ((6, 3), 22070),\n", " ((9, 3), 18140),\n", " ((3, 9), 9734),\n", " ((6, 7), 996),\n", " ((12, 6), 9),\n", " ((9, 6), 8),\n", " ((6, 12), 4),\n", " ((6, 9), 3)]" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def overtimes(games=1_000_000, **kwds) -> Iterable[Scores]:\n", " \"\"\"Play `games` games and return an iterable of the `(A, B)` score for each game.\"\"\"\n", " return (overtime(**kwds) for _ in range(games))\n", "\n", "Counter(overtimes()).most_common()" ] }, { "cell_type": "markdown", "id": "3429fec7-2efe-4ee2-8309-acb8bed6b8a3", "metadata": {}, "source": [ "That gives us the possible scores and the frequency of each one. Note:\n", "- Field goals are more common than touchdowns, so the most common scores are 3-0 and 0-3.\n", "- 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).\n", "- Why is 0-6 more common than 7-0? Because 0-6 includes times when **B** scored second or fourth (or later), but 7-0 is only first.\n", "- 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.\n", "- 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.\n", "- 6-9, 9-6, 12-6, and 6-12 are very rare scores, because they mean that *both* teams missed a conversion kick.\n", "\n", "That's all interesting, but the two questions remain:\n", "\n", "# Question 1: Who Has the Advantage, Team A or Team B?\n", "\n", "We can acompute the probability of team **A** winning:" ] }, { "cell_type": "code", "execution_count": 4, "id": "6b73b711-0073-4b40-893b-875a9ae6c8fa", "metadata": {}, "outputs": [], "source": [ "def A_win_probability(scores) -> float:\n", " \"\"\"Probability that first team (team A) wins.\"\"\"\n", " return mean(A > B for A, B in scores)" ] }, { "cell_type": "code", "execution_count": 5, "id": "926afa2e-e60f-4641-bb18-b2f04c027f2f", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0.548365" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "A_win_probability(overtimes())" ] }, { "cell_type": "markdown", "id": "c7d4f929-04aa-484e-851d-2a83758d4948", "metadata": {}, "source": [ "My simulation says team **A** has nearly a 55% chance of winning. This supports Shanahan's decision. \n" ] }, { "cell_type": "markdown", "id": "de405c60-d215-46e9-85f4-ccd6d67a3ec7", "metadata": {}, "source": [ "# Question 2: What Are the Best Strategies for Extra Points?\n", "\n", "Earlier I claimed that **A** should go for 1 extra point and **B** for 2:" ] }, { "cell_type": "code", "execution_count": 6, "id": "95f231d1-b7d4-4387-8cbf-4ae5cb3f7cdb", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0.548867" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "A_win_probability(overtimes(A_extra=1, B_extra=2))" ] }, { "cell_type": "markdown", "id": "931acdc1-753a-4b7f-ae68-42c648eefef2", "metadata": {}, "source": [ "We can see that if **A** goes for 2, they will do worse (by about half a percent):" ] }, { "cell_type": "code", "execution_count": 7, "id": "3a374721-3e4e-4185-90f5-4e27cc0de458", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0.54451" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "A_win_probability(overtimes(A_extra=2, B_extra=2))" ] }, { "cell_type": "markdown", "id": "009a1a1d-8c58-45ab-8ee2-b597915b523a", "metadata": {}, "source": [ "We can also see that if **B** goes for 1, they will do worse (i.e., **A** will do better):" ] }, { "cell_type": "code", "execution_count": 8, "id": "8a24b181-16d3-4c30-84af-25ceb5c55929", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0.555354" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "A_win_probability(overtimes(A_extra=1, B_extra=1))" ] }, { "cell_type": "markdown", "id": "642faec7-035f-45b2-828f-afdc188510fa", "metadata": {}, "source": [ "These results support the claim that **A** should go for 1 extra point and **B** for 2.\n", "\n", "# Question 1: Varying the Parameters\n", "\n", "However, I don't have confidence that the probability parameter values I chose are reflective of reality, so for each parameter I'll try a lower and a higher value, and look at all combinations, displayed as a dataframe:" ] }, { "cell_type": "code", "execution_count": 9, "id": "be360c09-bb94-4328-b439-4bcef5ff6ef3", "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "from IPython.display import HTML\n", " \n", "def chart(TDs=(0.15, 0.20, 0.33), \n", " FGs=(0.20, 0.25, 0.33), \n", " gos=(0.02, 0.05, 0.10), \n", " twos=(0.4, 0.48, 0.60),\n", " games=100_000) -> HTML:\n", " \"\"\"Create a chart of Win percentages for various parameter values.\"\"\"\n", " data = [(TD, FG, go, two, A_win_probability(overtimes(games=games, TD=TD, FG=FG, go=go)))\n", " for TD in TDs for FG in FGs for go in gos for two in twos]\n", " df = pd.DataFrame(data, columns=('TD', 'FG', 'go', 'two', 'Win')).sort_values('Win', ascending=False)\n", " print(f'Team A win probability: min: {min(df.Win):.1%}, max: {max(df.Win):.1%}')\n", " return HTML(df.to_html(index=False, formatters={'Win': '{:.1%}'.format}))" ] }, { "cell_type": "code", "execution_count": 10, "id": "e323e49e-84d7-43f6-99f2-8b9def6a4b0d", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Team A win probability: min: 53.0%, max: 56.1%\n" ] }, { "data": { "text/html": [ "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
TDFGgotwoWin
0.150.330.020.4856.1%
0.150.330.020.4056.0%
0.200.330.020.4855.8%
0.150.330.020.6055.7%
0.150.330.050.4055.7%
0.200.330.020.4055.7%
0.150.330.050.6055.6%
0.200.330.020.6055.6%
0.200.330.050.6055.5%
0.200.330.050.4055.5%
0.200.330.050.4855.4%
0.330.330.020.4055.4%
0.150.330.100.4055.4%
0.150.330.050.4855.4%
0.330.330.020.6055.4%
0.330.330.020.4855.3%
0.150.250.020.4055.2%
0.150.330.100.6055.2%
0.200.250.020.4855.2%
0.150.330.100.4855.2%
0.200.250.020.4055.1%
0.150.250.020.4855.1%
0.200.330.100.4055.1%
0.150.250.020.6055.0%
0.150.250.050.4055.0%
0.200.250.050.6055.0%
0.150.250.050.6054.9%
0.150.200.020.4854.9%
0.200.250.020.6054.9%
0.150.250.050.4854.9%
0.200.330.100.4854.9%
0.200.200.020.6054.8%
0.330.330.050.4854.8%
0.150.250.100.6054.8%
0.330.330.050.4054.8%
0.200.330.100.6054.8%
0.150.250.100.4054.7%
0.200.250.050.4054.7%
0.150.200.050.4854.7%
0.150.200.020.6054.7%
0.330.250.020.4854.7%
0.330.330.050.6054.7%
0.200.250.050.4854.7%
0.330.250.020.4054.7%
0.200.200.020.4854.7%
0.200.200.050.4854.7%
0.150.200.020.4054.7%
0.150.200.050.4054.7%
0.200.200.020.4054.6%
0.200.200.050.4054.6%
0.150.200.050.6054.6%
0.330.250.020.6054.6%
0.330.200.020.6054.6%
0.200.250.100.4054.5%
0.150.250.100.4854.5%
0.330.200.020.4054.4%
0.200.250.100.4854.4%
0.200.250.100.6054.3%
0.150.200.100.6054.3%
0.330.200.020.4854.3%
0.330.200.050.4854.2%
0.200.200.050.6054.2%
0.330.250.050.4054.2%
0.150.200.100.4854.2%
0.330.330.100.6054.2%
0.200.200.100.4054.2%
0.150.200.100.4054.2%
0.330.200.050.4054.1%
0.330.200.050.6054.1%
0.330.250.050.4854.0%
0.330.250.050.6054.0%
0.200.200.100.6054.0%
0.330.330.100.4853.9%
0.200.200.100.4853.8%
0.330.330.100.4053.8%
0.330.250.100.6053.8%
0.330.250.100.4053.4%
0.330.200.100.4053.3%
0.330.250.100.4853.3%
0.330.200.100.4853.2%
0.330.200.100.6053.0%
" ], "text/plain": [ "" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "chart()" ] }, { "cell_type": "markdown", "id": "caf22648-ff10-45ac-9d5a-635672592778", "metadata": {}, "source": [ "The results show that over a wide range of assumptions about the probabilities, team **A** always wins a majority of the time (between 53% and 56%). So no more Shanahan shaming." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.15" } }, "nbformat": 4, "nbformat_minor": 5 }