NFL Win-probability chart
Broadcast Sports
Example from the compendium of canonical charts
Python Code
"""Broadcast Sports — NFL Win-probability chart."""
from pathlib import Path
# ── Palette + theme (matching the Plotly Studio gallery these charts ship in) ──
VIOLET, TEAL, GREEN, PINK, ORANGE = "#845EEE", "#52B3D0", "#55B685", "#DA5597", "#E9A23B"
PRIMARY, SECONDARY = VIOLET, TEAL
COLORWAY = [VIOLET, TEAL, GREEN, PINK, ORANGE]
BG, TEXT, GRID, MUTED = "#ffffff", "#1c2024", "#d9d9e0", "#60646c"
FONT = "Inter, -apple-system, BlinkMacSystemFont, sans-serif"
COLORSCALE = [[0, "rgba(132, 94, 238, 0.05)"], [1, "rgba(132, 94, 238, 0.9)"]]
def apply_theme(fig):
"""Light gallery theme: white background, Inter font, soft gridlines."""
fig.update_layout(
paper_bgcolor=BG, plot_bgcolor=BG, colorway=COLORWAY,
font=dict(family=FONT, color=TEXT, size=12),
legend=dict(font=dict(color=TEXT)),
hoverlabel=dict(bgcolor="#f0f0f3", font=dict(color=TEXT, family=FONT), bordercolor=GRID),
)
fig.update_xaxes(gridcolor=GRID, linecolor=GRID, zerolinecolor=GRID)
fig.update_yaxes(gridcolor=GRID, linecolor=GRID, zerolinecolor=GRID)
def fetch_csv(url, **kwargs):
import io
import pandas as pd
import requests
r = requests.get(url, timeout=60)
r.raise_for_status()
return pd.read_csv(io.StringIO(r.text), **kwargs)
def fetch_json(url):
import requests
r = requests.get(url, timeout=60)
r.raise_for_status()
return r.json()
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),
)
apply_theme(fig)
return fig
fig = generate()
fig.show()
Made with Plotly