NFL Win-probability chart
Broadcast Sports
Example from the compendium of canonical charts
Python Code
"""Broadcast Sports — NFL Win-probability chart."""
import numpy as np
import pandas as pd
import plotly.graph_objects as go
import io, requests, warnings
URL = "https://github.com/nflverse/nflverse-data/releases/download/pbp/play_by_play_2023.csv.gz"
def generate():
warnings.filterwarnings("ignore")
df = None
try:
requests.packages.urllib3.disable_warnings()
r = requests.get(URL, verify=False, timeout=120, stream=True)
r.raise_for_status()
content = r.content
df_full = pd.read_csv(io.BytesIO(content), compression="gzip",
usecols=["game_id", "home_wp", "game_seconds_remaining"],
low_memory=False)
df_full = df_full.dropna(subset=["home_wp", "game_seconds_remaining"])
def crossings(grp):
wp = grp["home_wp"].values
return int(np.sum(np.diff(np.sign(wp - 0.5)) != 0))
counts = df_full.groupby("game_id").apply(crossings)
best_game = counts.idxmax()
df = df_full[df_full["game_id"] == best_game].copy()
df["elapsed"] = 3600 - df["game_seconds_remaining"]
df = df.sort_values("elapsed")
print(f"Using game {best_game}, {len(df)} plays, {crossings(df)} crossings")
except Exception as e:
print(f"Download failed ({e}), using synthetic win probability")
if df is None:
RNG = np.random.default_rng(99)
n = 180
elapsed = np.linspace(0, 3600, n)
wp = [0.5]
for i in range(1, n):
step = RNG.normal(0, 0.025)
new = wp[-1] + step
if elapsed[i] > 3000:
new = wp[-1] + step * 0.3
wp.append(float(np.clip(new, 0.02, 0.98)))
wp = np.array(wp)
df = pd.DataFrame({"elapsed": elapsed, "home_wp": wp})
fig = go.Figure()
fig.add_hrect(y0=0.5, y1=1.0, fillcolor="rgba(132,94,238,0.07)", line_width=0)
fig.add_hrect(y0=0.0, y1=0.5, fillcolor="rgba(85,182,133,0.07)", line_width=0)
fig.add_trace(go.Scatter(
x=df["elapsed"],
y=df["home_wp"],
mode="lines",
line=dict(color=VIOLET, width=2.5),
showlegend=False,
hovertemplate="Elapsed: %{x:.0f}s<br>Win prob: %{y:.1%}<extra></extra>",
))
fig.add_hline(y=0.5, line_dash="dash", line_color=MUTED, line_width=1)
for sec in [900, 1800, 2700]:
fig.add_vline(x=sec, line_dash="dot", line_color=MUTED, line_width=1)
fig.update_layout(
xaxis=dict(
title="",
tickvals=[0, 900, 1800, 2700, 3600],
ticktext=["Start", "Q2", "Half", "Q4", "End"],
range=[0, 3700],
showgrid=False,
),
yaxis=dict(
title="Home Win Probability",
range=[0, 1],
tickformat=".0%",
),
showlegend=False,
margin=dict(t=20, b=50, l=70, r=40),
)
return fig
if __name__ == "__main__":
generate()
Made with Plotly