Quarterback Play: Progressions, the Pocket, and Off-Script Football

What the quarterback does in the three seconds after the snap, what he does when the three seconds run out, and how to measure the difference between quarterbacks

Show the Python (shared drawing helpers for this chapter)
import os
import logging
import textwrap

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import animation
from matplotlib.patches import Circle, Ellipse, FancyArrowPatch, Polygon, RegularPolygon, Rectangle
from matplotlib.lines import Line2D

import gridiron.play as gplay
from gridiron import Play, Player, Field, style

logging.getLogger("matplotlib.font_manager").setLevel(logging.ERROR)
style.apply()
PDF = os.environ.get("RTG_FORMAT", "").lower() in ("pdf", "typst", "print")
# gridiron's default ball speed (18 yd/s) averages a whole flight; the firm 20-25 yard throws in this
# chapter travel a little faster, so the simulated plays use 20.
gplay.BALL_SPEED = 20.0

GREEN = "#0f7a55"          # darker step of the aqua highlight: reads, the quarterback's moves
PURPLE = style.SERIES[6]   # a ring for the defender who decides the read
RED = style.SERIES[7]      # danger: a rush that got home, an illegal throw
SPLIT = 1.6                # center-to-guard and guard-to-tackle spacing, as in the rest of the book
OL_KEYS = ["LT", "LG", "C", "RG", "RT"]
NONROUTE = {"LT", "LG", "C", "RG", "RT", "DE", "DT", "NT"}


def O(key, pos, d, w, label=None):
    return Player(key, pos, "off", d, w, label=key if label is None else label)


def D(key, pos, d, w, label):
    return Player(key, pos, "def", d, w, label=label)


def line():
    """The five offensive linemen (only the center is lettered, to keep the box readable)."""
    return [O(k, k, -0.7, (i - 2) * SPLIT, "C" if k == "C" else "") for i, k in enumerate(OL_KEYS)]


def four_rush():
    """A generic four-man rush: two ends outside the tackles, two tackles inside."""
    return [D("LE", "DE", 1.0, -5.4, "E"), D("NT", "DT", 1.0, -0.7, "T"), D("DT", "DT", 1.0, 2.35, "T"),
            D("RE", "DE", 1.0, 5.4, "E")]


def timed(p, key, way, kind="path"):
    """Script a player exactly: `way` = [(t, d, w), ...] absolute spots reached at t seconds after the
    snap. Repeating a spot means 'stand there until t'. (gridiron's route helpers run at a fixed speed;
    the quarterback's clock is the subject of this chapter, so here every move is timed by hand.)"""
    cur = np.array(p._cur(key), dtype=float)
    tc, wait = 0.0, 0.0
    if any(a.key == key for a in p.actions):          # continue from where his earlier moves end
        tc = max(0.0, float(p._compile()[0][key][-1, 0]))
    for t, d, w in way:
        nxt = np.array([d, w], dtype=float)
        dist = float(np.hypot(*(nxt - cur)))
        if dist < 1e-3:
            wait += t - tc
        else:
            p._add(kind, key, [tuple(cur), tuple(nxt)], speed=dist / (t - tc), delay=wait)
            wait = 0.0
            cur = nxt
        tc = t
    return p


def frame_field(ax, p, lateral, window):
    ylo, yhi = sorted((p.ball_y - lateral[0], p.ball_y - lateral[1]))
    return Field(ax=ax, orient="vertical", los=p.los, window=window, numbers=False, hashes=False,
                 lateral=(ylo, yhi), to_go=p.to_go)


def XY(fld, p, d, w):
    x, y = fld.xy(*p.to_abs(d, w))
    return float(x), float(y)


def dot(ax, x, y, side, label, ppy, r=0.9, ring=None, alpha=1.0, z=8):
    """One player: offense a filled blue circle, defense an outlined orange square."""
    if side == "off":
        ax.add_patch(Circle((x, y), r, fc=style.OFFENSE, ec="white", lw=0.6, zorder=z, alpha=alpha))
        tc = "white"
    else:
        ax.add_patch(RegularPolygon((x, y), 4, radius=r * 1.25, orientation=np.pi / 4, fc=style.DEFENSE_TINT,
                                    ec=style.DEFENSE, lw=1.0, zorder=z, alpha=alpha))
        tc = style.INK
    if ring:
        ax.add_patch(Circle((x, y), r * 1.65, fc="none", ec=ring, lw=1.7, zorder=z - 0.5))
    if label:
        fs = max(4.6, min(8.5, ppy * (1.15 if len(label) <= 1 else 0.85)))
        ax.text(x, y, label, ha="center", va="center", fontsize=fs, color=tc, fontweight="bold", zorder=z + 2)


def place_labels(ax, xs, ys, labels, fs=6.8, offs=None):
    """Greedy label placement for scatter charts: try offsets around each point until the label hits no
    point, no earlier label and stays inside the axes."""
    fig = ax.figure
    fig.canvas.draw()
    ren = fig.canvas.get_renderer()
    pts = ax.transData.transform(np.column_stack([xs, ys]))
    box = ax.get_window_extent(ren)
    offs = offs or [(5, 3), (5, -9), (-5, 3), (-5, -9), (6, -3), (-6, -3), (0, 6), (0, -12), (9, 8), (-9, 8),
                    (9, -13), (-9, -13), (12, 0), (-12, 0)]
    placed = [t.get_window_extent(ren) for t in ax.texts]          # never cover text already on the chart
    if ax.get_legend() is not None:
        placed.append(ax.get_legend().get_window_extent(ren))
    for x, y, lab in zip(xs, ys, labels):
        for dx, dy in offs:
            t = ax.annotate(lab, (x, y), xytext=(dx, dy), textcoords="offset points", fontsize=fs, color=style.INK,
                            ha="left" if dx > 0 else ("right" if dx < 0 else "center"), zorder=4)
            bb = t.get_window_extent(ren).expanded(1.04, 1.1)
            ok = (not any(bb.contains(px, py) for px, py in pts) and not any(bb.overlaps(o) for o in placed)
                  and box.contains(bb.x0, bb.y0) and box.contains(bb.x1, bb.y1))
            if ok:
                placed.append(bb)
                break
            t.remove()
    return placed


def football(ax, x, y):
    ax.add_patch(Ellipse((x, y), 1.0, 0.62, fc=style.BALL, ec="white", lw=0.6, zorder=11))


def arrow(fld, p, pts, color=GREEN, lw=1.8, ls="-", head=10, z=9, alpha=1.0):
    """A path through (d, w) points with an arrowhead at the end."""
    xy = [XY(fld, p, *q) for q in pts]
    xs, ys = zip(*xy)
    fld.ax.plot(xs[:-1] + (xs[-1],), ys[:-1] + (ys[-1],), color=color, lw=lw, ls=ls, zorder=z,
                solid_capstyle="round", alpha=alpha)
    fld.ax.add_patch(FancyArrowPatch(xy[-2], xy[-1], arrowstyle="-|>", mutation_scale=head, color=color, lw=0,
                                     shrinkA=0, shrinkB=0, zorder=z, alpha=alpha))


def say(fld, p, d, w, text, color=style.INK, fs=7.0, ha="center", weight="normal", box=True):
    x, y = XY(fld, p, d, w)
    fld.ax.text(x, y, text, fontsize=fs, color=color, ha=ha, va="center", zorder=12, fontweight=weight,
                linespacing=1.15,
                bbox=dict(boxstyle="round,pad=0.2", fc=style.SURFACE, ec="none", alpha=0.92) if box else None)


def badge(ax, x, y, n, state, fs=8.0):
    """A numbered read. wait: not looked at yet; now: his eyes are here; done: looked at and passed over."""
    fc, ec, tc = {"wait": (style.SURFACE, style.MUTED, style.INK_2), "now": (GREEN, GREEN, "white"),
                  "done": (style.GRID, style.BASELINE, style.MUTED)}[state]
    ax.text(x, y, str(n), ha="center", va="center", fontsize=fs, fontweight="bold", color=tc, zorder=13,
            bbox=dict(boxstyle="circle,pad=0.22", fc=fc, ec=ec, lw=1.1))


def clean(ax):
    """Static diagrams drawn with Play.draw: drop hash ticks and yard numbers, clip text to the window."""
    for ln in list(ax.lines):
        xs, ys = ln.get_data()
        if len(xs) == 2 and ln.get_linewidth() == 0.5 and np.hypot(xs[1] - xs[0], ys[1] - ys[0]) < 1.0:
            ln.remove()
    for t in list(ax.texts):
        if t.get_text().isdigit() and t.get_alpha() == 0.55:
            t.remove()
        else:
            t.set_clip_on(True)


def moment(ax, p, t, lateral, window, rings=None, plan=None, ghosts=(), trail_keys=None, r=0.9):
    """Everyone where he is at time t, faint trails of where he has been, the ball, and (optionally)
    the called routes as faint grey dashes, so a single panel shows both the plan and the present."""
    fld = frame_field(ax, p, lateral, window)
    ppy = fld.pts_per_yard()
    lo, hi = sorted(lateral)
    inside = lambda d, w: window[0] + 0.6 <= d <= window[1] - 0.6 and lo + 0.6 <= w <= hi - 0.6  # noqa: E731
    for pts in (plan or {}).values():
        xy = [XY(fld, p, *q) for q in pts]
        xs, ys = zip(*xy)
        ax.plot(xs, ys, color=style.MUTED, lw=0.9, ls=(0, (2, 2)), alpha=0.75, zorder=2)
        ax.add_patch(FancyArrowPatch(xy[-2], xy[-1], arrowstyle="-|>", mutation_scale=6, color=style.MUTED, lw=0,
                                     alpha=0.75, zorder=2))
    if t > 0:
        ts = np.linspace(0, t, 32)
        for k, pl in p.players.items():
            if (pl.pos in NONROUTE) if trail_keys is None else (k not in trail_keys):
                continue
            pts = np.array([p.position(k, tt) for tt in ts])
            if np.ptp(pts[:, 0]) + np.ptp(pts[:, 1]) < 0.3:
                continue
            xs, ys = fld.xy(*p.to_abs(pts[:, 0], pts[:, 1]))
            if pl.side == "off":
                ax.plot(xs, ys, color=style.OFFENSE, lw=1.1, alpha=0.5, zorder=3)
            else:
                ax.plot(xs, ys, color=style.DEFENSE, lw=1.1, ls=(0, (3, 2)), alpha=0.8, zorder=3)
    for d, w, text in ghosts:
        x, y = XY(fld, p, d, w)
        ax.add_patch(Circle((x, y), 0.72, fc="none", ec=style.OFFENSE, lw=1.0, ls=(0, (2, 1.5)), zorder=6))
        if text:
            ax.text(x, y - 1.45, text, ha="center", va="top", fontsize=max(5.0, ppy * 0.7), color=style.INK_2,
                    zorder=6)
    for k, pl in p.players.items():
        d, w = p.position(k, t)
        if inside(d, w):
            dot(ax, *XY(fld, p, d, w), pl.side, pl.label, ppy, r=r, ring=(rings or {}).get(k))
    thrown = [(to, te) for kind, to, te in p.ball_events if kind == "pass" and t > te]
    if thrown:
        bd, bw = p.ball_position(t)
        sd, sw = p.ball_position(thrown[0][1])
        rd, rw = p.position(thrown[0][0], t)
        caught = np.hypot(bd - rd, bw - rw) < 0.4
        ax.add_patch(FancyArrowPatch(XY(fld, p, sd, sw), XY(fld, p, bd, bw), arrowstyle="-|>", mutation_scale=8,
                                     color=style.BALL, lw=1.3, ls=(0, (2, 1.5)), zorder=9, shrinkA=0,
                                     shrinkB=9 if caught else 2))
        football(ax, *XY(fld, p, bd + (0.3 if caught else 0.0), bw - (1.25 if caught else 0.0)))
    elif t > 0.15 and "QB" in p.players:
        qd, qw = p.position("QB", t)
        football(ax, *XY(fld, p, qd + 0.3, qw - 1.2))
    return fld


def eyes(fld, p, t, target, color=GREEN):
    """The quarterback's eyes: a dashed line from him to the player he is reading."""
    (qd, qw), (rd, rw) = p.position("QB", t), p.position(target, t)
    x0, y0 = XY(fld, p, qd, qw)
    x1, y1 = XY(fld, p, rd, rw)
    fld.ax.add_patch(FancyArrowPatch((x0, y0), (x1, y1), arrowstyle="-", color=color, lw=1.4, ls=(0, (4, 2)),
                                     shrinkA=7, shrinkB=9, zorder=7))


def strip(draw, times, notes, ncols=2, width_in=6.5, aspect=1.0, note_lines=2):
    """Print stand-in for an animation: numbered panels with a caption under each."""
    nrows = int(np.ceil(len(times) / ncols))
    pw = width_in / ncols
    fig, axes = plt.subplots(nrows, ncols, figsize=(width_in, nrows * (pw * aspect + 0.2 + 0.16 * note_lines)),
                             squeeze=False)
    for ax in axes.flat[len(times):]:
        ax.axis("off")
    for i, (ax, t) in enumerate(zip(axes.flat, times)):
        draw(ax, t)
        ax.set_title(f"{i + 1}.  " + ("snap" if abs(t) < 1e-6 else f"{t:+.1f} s"), fontsize=8.2,
                     color=style.INK_2, loc="left", pad=3)
        ax.text(0.5, -0.01, notes[i], transform=ax.transAxes, ha="center", va="top", fontsize=7.1,
                color=style.INK, linespacing=1.2)
    fig.tight_layout(h_pad=1.2, w_pad=0.8)
    return fig


def video(draw, times, notes, t_end, width_in=5.6, aspect=1.0):
    """Web version of the strip: every tenth of a second, with the strip's captions as running titles."""
    import shutil
    from IPython.display import HTML, display
    fig, ax = plt.subplots(figsize=(width_in, width_in * aspect + 0.6))

    def update(t):
        ax.clear()
        draw(ax, t)
        cur = [n.replace("\n", " ") for tt, n in zip(times, notes) if tt <= t + 1e-6]
        head = "snap" if abs(t) < 0.05 else f"{t:+.1f} s"
        ax.set_title(textwrap.fill(head + ("   " + cur[-1] if cur else ""), 92), fontsize=7.6, color=style.INK,
                     loc="left")
        return []

    frames = list(np.round(np.arange(0.0, t_end + 0.05, 0.1), 2))
    anim = animation.FuncAnimation(fig, update, frames=frames, interval=100)
    html = (anim.to_html5_video(embed_limit=40).replace("<video ", '<video style="max-width:100%;height:auto" ')
            if shutil.which("ffmpeg") else anim.to_jshtml(default_mode="once"))
    plt.close(fig)
    display(HTML(html))


def play_out(draw, times, notes, t_end, ncols=2, aspect=1.0, note_lines=2, width_in=6.5):
    """Video on the website; a numbered, captioned frame strip in the PDF."""
    if PDF:
        strip(draw, times, notes, ncols=ncols, aspect=aspect, note_lines=note_lines, width_in=width_in)
        plt.show()
    else:
        video(draw, times, notes, t_end, aspect=aspect)


BOX_W = 3.9   # the tackle box: Rule 3-25 puts its edges at the OUTSIDE shoulders of the tackles, not their centers


def tackle_box(ax, fld, p, back):
    """The shaded tackle box (pocket area), from the line back to depth `back`."""
    xy = [XY(fld, p, *q) for q in [(-0.7, -BOX_W), (-0.7, BOX_W), (back, BOX_W), (back, -BOX_W)]]
    ax.add_patch(Polygon(xy, closed=True, fc=PURPLE, alpha=0.09, ec=PURPLE, lw=0.9, ls=(0, (3, 2)), zorder=1.4))


def pass_set(p, depths=(-2.6, -1.7, -1.4, -1.7, -2.6), t=0.8):
    """The five linemen set (tackles deeper and wider than the interior) and hold."""
    for k, dd in zip(OL_KEYS, depths):
        w0 = p.players[k].w
        timed(p, k, [(t, dd, w0 * 1.15)], "block")
    return p
Show the Python (load the quarterback data used in this chapter)
from gridiron.data import nfl, pbp

SEASONS = [2022, 2023, 2024, 2025]          # the FTN charting era, all completed seasons
PC = ["season", "season_type", "game_id", "play_id", "play_type", "qb_dropback", "qb_scramble", "sack",
      "pass_attempt", "passer", "passer_id", "posteam", "epa", "qb_epa", "success", "cpoe", "cp", "air_yards",
      "yards_after_catch", "receiving_yards", "complete_pass", "interception", "down", "ydstogo", "qb_spike",
      "rusher", "passing_yards", "pass_touchdown"]
raw = pbp(SEASONS, columns=PC)
raw = raw[raw.season_type == "REG"]
# every dropback: pass attempts, sacks and scrambles (spikes removed)
db = raw[(raw.qb_dropback == 1) & (raw.qb_spike != 1)].copy()

# FTN charting: which read the ball went to, out of the pocket or not, throwaways
ftn = nfl().load_ftn_charting(SEASONS).to_pandas()
ftn = ftn.rename(columns={"nflverse_game_id": "game_id", "nflverse_play_id": "play_id"})[
    ["game_id", "play_id", "read_thrown", "is_qb_out_of_pocket", "is_throw_away", "is_interception_worthy",
     "is_qb_fault_sack"]]
ftn["read_thrown"] = ftn.read_thrown.astype(str).str.strip()
part = nfl().load_participation(SEASONS).to_pandas().rename(columns={"nflverse_game_id": "game_id"})[
    ["game_id", "play_id", "time_to_throw", "was_pressure"]]
db = db.merge(ftn, on=["game_id", "play_id"], how="left").merge(part, on=["game_id", "play_id"], how="left")
db["oop"] = db.is_qb_out_of_pocket.fillna(False).astype(bool)
db["throwaway"] = db.is_throw_away.fillna(False).astype(bool)

# How every dropback ended, in mutually exclusive groups (order matters)
END_NAMES = ["Throw to the first read", "Throw to a later read", "Checkdown", "Designed throw (mostly screens)",
             "Scramble-drill throw", "QB scramble (he runs)", "Throwaway", "Sack", "Interception"]
conds = [db.sack == 1, db.qb_scramble == 1, db.interception == 1, db.throwaway,
         db.read_thrown == "1", db.read_thrown == "2", db.read_thrown == "CHK", db.read_thrown == "SD",
         db.read_thrown == "DES"]
names = ["Sack", "QB scramble (he runs)", "Interception", "Throwaway", "Throw to the first read",
         "Throw to a later read", "Checkdown", "Scramble-drill throw", "Designed throw (mostly screens)"]
db["ending"] = np.select(conds, names, default="Other")
END = db.groupby("ending").agg(n=("qb_epa", "size"), epa=("qb_epa", "mean"), succ=("success", "mean"))
END["share"] = END.n / len(db)
END = END.loc[END_NAMES]

# Checkdowns: what they gain, and what they do on third down
CHK = db[db.ending == "Checkdown"]
CHK_COMP, CHK_AY, CHK_YAC = CHK.complete_pass.mean(), CHK.air_yards.mean(), CHK.yards_after_catch.mean()
t3 = db[db.down == 3]
long3, short3 = t3[t3.ydstogo >= 7], t3[t3.ydstogo <= 3]
CHK3_LONG = long3.loc[long3.ending == "Checkdown", "success"].mean()
R1_3_LONG = long3.loc[long3.ending == "Throw to the first read", "success"].mean()
CHK3_SHORT = short3.loc[short3.ending == "Checkdown", "success"].mean()
CHK3_LONG_EPA = long3.loc[long3.ending == "Checkdown", "qb_epa"].mean()
SACK3_LONG_EPA = long3.loc[long3.ending == "Sack", "qb_epa"].mean()

# Out of the pocket
OOP_SHARE = db.oop.mean()
EPA_IN, EPA_OUT = db.loc[~db.oop, "qb_epa"].mean(), db.loc[db.oop, "qb_epa"].mean()
SACK_IN, SACK_OUT = db.loc[~db.oop, "sack"].mean(), db.loc[db.oop, "sack"].mean()
# ...and the confound: quarterbacks mostly leave BECAUSE the pressure has arrived. Compare like with like on
# pressured dropbacks (FTN's pressure flag, 2023-2025; the 2022 flag comes from a different source)
pz = db[db.season >= 2023]
PRESS_IN, PRESS_OUT = pz.loc[~pz.oop, "was_pressure"].eq(True).mean(), pz.loc[pz.oop, "was_pressure"].eq(True).mean()
pzp = pz[pz.was_pressure.eq(True)]
PSACK_IN, PSACK_OUT = pzp.loc[~pzp.oop, "sack"].mean(), pzp.loc[pzp.oop, "sack"].mean()
OOP_MIX = db[db.oop].ending.value_counts(normalize=True)
OOP_BAD = OOP_MIX.reindex(["Throwaway", "Sack", "Interception"]).sum()
OOP_RUN, OOP_SD = OOP_MIX["QB scramble (he runs)"], OOP_MIX["Scramble-drill throw"]
# whose sack? FTN marks the sacks its charters blame on the quarterback; pressure-to-sack rate by passer
QB_FAULT = db.loc[db.sack == 1, "is_qb_fault_sack"].eq(True).mean()
P2S = pzp.groupby("passer_id").agg(name=("passer", "first"), press=("sack", "size"), p2s=("sack", "mean"))
P2S = P2S[P2S.press >= 200].sort_values("p2s")
P2S_ALL = pzp.sack.mean()
# throwaways and the CPOE model: most have no completion probability, so they are left out of nflverse CPOE
TA_CP_SHARE = db.loc[db.throwaway & (db.pass_attempt == 1), "cp"].notna().mean()

oq = db.groupby("passer_id").agg(name=("passer", "first"), n=("qb_epa", "size"), oop=("oop", "mean"))
oq = oq.join(db[db.oop].groupby("passer_id").agg(n_out=("qb_epa", "size"), epa_out=("qb_epa", "mean")))
oq = oq.join(db[~db.oop].groupby("passer_id").agg(epa_in=("qb_epa", "mean")))
OQ = oq[(oq.n >= 800) & (oq.n_out >= 150)].copy()
OQ["gain"] = OQ.epa_out - OQ.epa_in
N_BETTER_OUT = int((OQ.gain > 0).sum())


def oqrow(short):
    return OQ[OQ.name == short].iloc[0]


# Throwaways, sacks, interceptions
TA_RATE = db.throwaway.mean()
TA_EPA, SACK_EPA, INT_EPA = END.loc["Throwaway", "epa"], END.loc["Sack", "epa"], END.loc["Interception", "epa"]

# The clock: time to throw by kind of throw (participation, sacks have no throw)
tt = db[db.time_to_throw.notna() & (db.sack == 0)]
CLOCK_ROWS = [("Throw to the first read", "First read"), ("Throw to a later read", "Later read"),
              ("Checkdown", "Checkdown"), ("Throwaway", "Throwaway"), ("Scramble-drill throw", "Scramble-drill throw")]
CLOCK = {lab: tt.loc[tt.ending == e, "time_to_throw"].quantile([0.25, 0.5, 0.75]).to_numpy() for e, lab in CLOCK_ROWS}
CHK_EARLY = (tt.loc[tt.ending == "Checkdown", "time_to_throw"] < 2.5).mean()

# Separation and catch rate (the real-data side of anticipation): NGS receiving, receiver-seasons with 50+
# targets, 2018-2025. Catch rate on average separation, holding average target depth fixed.
ngr = nfl().load_nextgen_stats(stat_type="receiving", seasons=list(range(2018, 2026))).to_pandas()
ngr = ngr[(ngr.week == 0) & (ngr.season_type == "REG") & (ngr.targets >= 50)]
_A = np.column_stack([np.ones(len(ngr)), ngr.avg_separation, ngr.avg_intended_air_yards])
SEP_COEF = float(np.linalg.lstsq(_A, ngr.catch_percentage, rcond=None)[0][1])
SEP_N = len(ngr)

# ---- 2025 quarterback table: style map and CPOE --------------------------------------------------------
d25 = db[db.season == 2025]
thr = d25[(d25.pass_attempt == 1) & (d25.sack == 0) & d25.air_yards.notna()]
thr_cp = thr[thr.cp.notna()]
Q = d25.groupby("passer_id").agg(short=("passer", "first"), team=("posteam", lambda s: s.mode().iat[0]),
                                 db=("qb_epa", "size"), epa=("qb_epa", "mean"))
Q = Q.join(thr.groupby("passer_id").agg(att=("air_yards", "size"), adot=("air_yards", "mean")))
Q = Q.join(thr_cp.groupby("passer_id").agg(comp=("complete_pass", "mean"), xcomp=("cp", "mean")))
Q["cpoe"] = 100 * (Q.comp - Q.xcomp)
ngs_all = nfl().load_nextgen_stats(stat_type="passing", seasons=list(range(2016, 2026))).to_pandas()
ngs_all = ngs_all[(ngs_all.week == 0) & (ngs_all.season_type == "REG")]
n25 = ngs_all[ngs_all.season == 2025].set_index("player_gsis_id")
Q = Q.join(n25[["avg_time_to_throw", "player_last_name", "player_first_name"]])
Q = Q[(Q.db >= 300) & Q.avg_time_to_throw.notna()].copy()
dup = Q.player_last_name.duplicated(keep=False)
Q["label"] = np.where(dup, Q.player_first_name.str[0] + ". " + Q.player_last_name, Q.player_last_name)
LG_TTT = np.average(Q.avg_time_to_throw, weights=Q.db)
LG_ADOT = thr.air_yards.mean()
LG_EPA = d25.qb_epa.mean()


def qb(last):
    return Q[Q.player_last_name == last].iloc[0]


BEST_CPOE, WORST_CPOE = Q.sort_values("cpoe").iloc[-1], Q.sort_values("cpoe").iloc[0]

# ---- why quarterbacks dominate value (2025) ------------------------------------------------------------
r25 = raw[(raw.season == 2025) & raw.play_type.isin(["pass", "run"]) & raw.down.notna()]
DB_SHARE = r25.qb_dropback.mean()
qv = r25[r25.qb_dropback == 1].groupby("passer_id").qb_epa.agg(["size", "mean"])
qv = qv[qv["size"] >= 300]
rv = r25[(r25.qb_dropback == 0) & (r25.play_type == "run")].groupby("rusher").epa.agg(["size", "mean"])
rv = rv[rv["size"] >= 150]
QB_SD, RB_SD = qv["mean"].std(), rv["mean"].std()
QB_N, RB_N = qv["size"].median(), rv["size"].median()
QB_PTS, RB_PTS = QB_SD * QB_N, RB_SD * RB_N
PLAY_SD_DB = r25.loc[r25.qb_dropback == 1, "qb_epa"].std()
PLAY_SD_RUN = r25.loc[(r25.qb_dropback == 0) & (r25.play_type == "run"), "epa"].std()
# points of season scoring margin per extra win: team-seasons 2012-2025, regular season
sch = nfl().load_schedules(list(range(2012, 2026))).to_pandas()
sch = sch[(sch.game_type == "REG") & sch.home_score.notna()]
_ts = pd.concat([sch[["season", f"{a}_team", f"{a}_score", f"{b}_score"]].set_axis(["season", "team", "pf", "pa"], axis=1)
                 for a, b in (("home", "away"), ("away", "home"))])
_ts["w"] = (_ts.pf > _ts.pa) + 0.5 * (_ts.pf == _ts.pa)
_ts = _ts.assign(pd_=_ts.pf - _ts.pa).groupby(["season", "team"]).agg(w=("w", "sum"), pd_=("pd_", "sum"))
PTS_PER_WIN = 1 / np.polyfit(_ts.pd_, _ts.w, 1)[0]

# ---- film-room numbers ---------------------------------------------------------------------------------
m13 = pbp([2013], columns=["season_type", "qb_dropback", "sack", "passer"])
m13 = m13[(m13.season_type == "REG") & (m13.qb_dropback == 1)]
MAN_SR = m13.loc[m13.passer == "P.Manning", "sack"].mean()
LG13_SR = m13.sack.mean()
_q13 = m13.groupby("passer").sack.agg(["size", "mean"])
MAN_SR_RANK = int(_q13[_q13["size"] >= 300]["mean"].rank()["P.Manning"])
brady = {}
for s in (2021, 2022):
    x = ngs_all[(ngs_all.season == s) & (ngs_all.attempts >= 300)]
    rk = x.avg_time_to_throw.rank()
    row = x[x.player_last_name == "Brady"].index[0]
    brady[s] = (x.loc[row, "avg_time_to_throw"], int(rk[row]), len(x), x.avg_time_to_throw.median())
b24 = raw[raw.season == 2024].groupby("passer").agg(yds=("passing_yards", "sum"), td=("pass_touchdown", "sum"))
BURROW_YDS, BURROW_TD = b24.loc["J.Burrow", "yds"], b24.loc["J.Burrow", "td"]
BURROW_YDS_RANK = int(b24.yds.rank(ascending=False)["J.Burrow"])
BURROW_TD_RANK = int(b24.td.rank(ascending=False, method="min")["J.Burrow"])
# Purdy 2023: value, completion over expected (two models), yards after the catch
d23 = db[db.season == 2023]
t23 = d23[(d23.pass_attempt == 1) & (d23.sack == 0) & d23.air_yards.notna()]
c23 = t23[t23.complete_pass == 1]
P23 = pd.DataFrame({"db": d23.groupby("passer").size(), "epa": d23.groupby("passer").qb_epa.mean(),
                    "cpoe": t23.groupby("passer").cpoe.mean(), "adot": t23.groupby("passer").air_yards.mean(),
                    "yac": c23.groupby("passer").yards_after_catch.sum() / c23.groupby("passer").receiving_yards.sum()})
P23 = P23[P23.db >= 250]
PURDY = P23.loc["B.Purdy"]
PURDY_EPA_RK = int(P23.epa.rank(ascending=False)["B.Purdy"])
PURDY_CPOE_RK = int(P23.cpoe.rank(ascending=False)["B.Purdy"])
LG23_YAC = c23.yards_after_catch.sum() / c23.receiving_yards.sum()
LG23_ADOT = t23.air_yards.mean()
n23 = ngs_all[(ngs_all.season == 2023) & (ngs_all.attempts >= 250)]
PURDY_NGS_RK = int(n23.completion_percentage_above_expectation.rank(ascending=False)[
    n23[n23.player_last_name == "Purdy"].index[0]])
N23 = len(P23)
sf25 = d25[d25.posteam == "SF"].groupby("passer").qb_epa.agg(["size", "mean"])
SF_PURDY, SF_JONES = sf25.loc["B.Purdy"], sf25.loc["M.Jones"]
MJ_PRIOR = db[(db.passer == "M.Jones") & (db.season < 2025)].qb_epa.mean()   # Mac Jones, NE 2022-23, JAX 2024

Third-and-8, fourth quarter. The quarterback takes the snap in the shotgun, drops, and looks hard to his left. Then his head snaps to the middle of the field. He bounces once on his back foot, then again. The two defensive ends have run around his tackles and are closing from behind him, so he takes two quick steps forward, into the space they left. Now a defensive tackle shoves a guard backward into his face, and the quarterback spins out to his right and runs toward the sideline. His receivers, who had been running set routes, suddenly do something else: the one who was deep turns and runs back toward him, the one who was short takes off deep, and a receiver from the far side of the field races across. Four and a half seconds after the snap, the ball floats over a cornerback who is still turning around, and the receiver catches it alone near the sideline. The analyst says, “That’s what the great ones do.” What, exactly, did he do? Why did he step up and not back? How did the receivers know where to go? And when none of it works, why is throwing the ball into the seats sometimes the best play he can make? This chapter is about the quarterback’s three to four seconds: what he is doing with his eyes, his feet and his judgment, on schedule and off it.

You’ll be able to…

NoteYou’ll need

How to read the diagrams. The conventions of the rest of Part 4, plus a few for this chapter:

The moving pictures add three more: faint lines are where a player has been (solid blue for offense, dashed orange for defense), grey dashes are the routes as they were called, and a dashed brown arrow is the ball in flight.

The quarterback’s clock

Every dropback is a race against a clock the defense controls, and the quarterback runs a fixed sequence of jobs against it. Coaches teach the sequence by tying each job to a moment in the drop, so that the quarterback’s feet tell him what his eyes should be doing.

Much of the work is done before the clock starts. Before the snap he has identified the Mike and set the protection (Pass Protection), read the defense’s alignment for which half of the field he expects to work (The Pre-Snap Phase), and checked whether the look calls for a change of play or a shot down the field (a subject of Concepts Versus Coverages). After the snap, here is the order, for an ordinary dropback pass from the shotgun.

  1. At the snap: the protection check. Before he looks at a single receiver, the quarterback answers one question: did more rushers come than the protection can block? If yes, the ball goes now to the hot receiver or a receiver who sight-adjusted his route, before any progression starts. The protection call has already told him which rusher is his: the one the line leaves unblocked by design if he comes, whom the quarterback must beat with a quick throw. So this check takes a glance, not a study.
  2. The drop: read 1. If the rush is covered, his eyes go to the first read as his feet go back, and the decision comes as the last step of the drop lands: open, and the ball comes out as his back foot plants; covered, and he moves on.
  3. Each hitch step: the next read. If read 1 is covered, he takes a short step forward, the hitch step of Passing Game Fundamentals (a step, not to be confused with the hitch route), which resets his feet so he can throw again, and his eyes move to read 2. Another hitch step, read 3. Each hitch step takes about half a second.
  4. The last read: the checkdown. The final look is to a back or tight end a few yards past the line, the safe throw that keeps the play from becoming a loss.
  5. After that: off schedule. If the ball still isn’t out, the play has left its design. The quarterback now has to manage the pocket (step up, slide, or escape), and decide among a scramble, an improvised throw, a throwaway, or taking the sack.
Show the Python
fig, ax = plt.subplots(figsize=(6.5, 3.5))
XMAX, SCHED = 6.0, 3.2
PH = [(0.0, 1.7, "Drop: eyes to read 1", style.SEQUENTIAL_BLUE[2], False),
      (1.7, SCHED, "Last step: read 1;\nhitch steps: 2, 3,\ncheckdown", style.SEQUENTIAL_BLUE[4], True),
      (SCHED, XMAX, "Off schedule: climb, slide, escape;\nscramble drill, throwaway or run", style.SERIES[1], True)]
TOP = 5.25
for a_, b_, lab, col, dark in PH:
    ax.add_patch(Rectangle((a_, TOP), b_ - a_ - 0.03, 1.0, fc=col, ec="none", alpha=0.95, zorder=3))
    ax.text((a_ + b_) / 2, TOP + 0.5, lab, ha="center", va="center", fontsize=6.4,
            color="white" if dark else style.INK, linespacing=1.15, fontweight="bold" if dark else "normal",
            zorder=4)
# counting the rush happens during the first steps of the drop: a thin band on top of the drop bar
ax.add_patch(Rectangle((0.0, TOP + 1.04), 0.62, 0.42, fc=style.SEQUENTIAL_BLUE[0], ec=style.SEQUENTIAL_BLUE[3],
                       lw=0.6, zorder=3))
ax.text(0.68, TOP + 1.25, "count the rush (during the first steps of the drop)", ha="left", va="center",
        fontsize=6.2, color=style.INK_2, zorder=4)
# vertical guides drawn by hand, only in the data area, so they never cross the labels above
for xv in np.arange(0.5, XMAX + 0.01, 0.5):
    ax.plot([xv, xv], [-0.2, 4.6], color=style.GRID, lw=0.6, zorder=0)
ax.plot([SCHED, SCHED], [-0.2, 4.95], color=style.SERIES[1], lw=1.1, ls=(0, (3, 2)), zorder=1)
ax.text(SCHED + 0.06, 4.85, "the schedule runs out", fontsize=6.4, color=style.DEFENSE_DARK, va="center",
        ha="left")
rows = list(CLOCK.items())
for i, (lab, (q1, med, q3)) in enumerate(rows):
    y = 4.0 - i * 0.98
    ax.plot([q1, q3], [y, y], color=style.SERIES[0], lw=5, alpha=0.35, solid_capstyle="round", zorder=2)
    ax.plot([med], [y], "o", color=style.SERIES[0], ms=7, mec="white", mew=1.0, zorder=3)
    ax.text(med, y + 0.3, f"{med:.1f} s", va="bottom", ha="center", fontsize=6.8, color=style.INK_2, zorder=3)
    ax.text(-0.08, y, lab, va="center", ha="right", fontsize=7.6, color=style.INK)
ax.set_xlim(0, XMAX)
ax.set_ylim(-0.2, 6.85)
ax.set_yticks([])
ax.set_xticks(np.arange(0, XMAX + 0.01, 0.5))
ax.tick_params(axis="x", labelsize=7.5)
ax.set_xlabel("seconds after the snap", fontsize=8)
ax.grid(False)
ax.spines["left"].set_visible(False)
fig.tight_layout()
plt.show()
Figure 26.1: The quarterback’s clock. Top: the jobs of an ordinary dropback, tied to the drop and the hitch steps (typical timings; they vary by play and team). He counts the rush during the first steps of the drop, not before it. Bottom: when each kind of throw actually leaves his hand in the NFL, from FTN charting (FTN Data’s staff chart every play from video, including which read the ball went to) of every regular-season pass from 2022 to 2025 that could be matched to a time to throw. The dot is the median, the bar the middle half of throws. First-read throws come out as the quarterback sets or makes his first hitch step; later reads cost about another half second; checkdowns leave at about the same time as later reads, because many are thrown early; throwaways and scramble-drill throws (throws after the quarterback has left the pocket and the receivers have improvised) live past the dashed line at about three seconds, where the play’s schedule has run out. Data: nflverse play-by-play, FTN charting and participation via nflreadpy.

The bottom half of Figure 26.1 is the part to remember. The first-read throw leaves the quarterback’s hand at a median of 2.3 seconds; a throw to a later read, about 0.6 seconds after that; and the throwaways and scramble-drill throws at the far right of the chart, after three seconds, when the rush has usually arrived. The checkdown is the odd one: it is the last look on paper, yet its median is no later than a second read’s. Two things pull it early. On many quick and half-field plays the back is read 2 or 3, not the last of four or five, so his turn comes fast; and some checkdowns are thrown before the deeper reads had a chance, the “bad checkdown” the checkdown section warns about. About 27% of checkdowns left the quarterback’s hand inside two and a half seconds. Every step down the list costs time, and, as you’ll see, value.

ImportantWhy it exists

Tying the eyes to the feet solves a problem that sounds trivial and isn’t: in the chaos of a rush, how does a quarterback know when to give up on a receiver? Counting seconds doesn’t work with three hundred pounds of defensive tackle in his face. Feet do. “Last step, read one; hitch step, read two; hitch step, checkdown” turns a judgment into a rhythm he can practice ten thousand times, and it keeps him on the same clock as his receivers, whose routes are timed to the same drop (Passing Game Fundamentals).

What it gives up: freedom. A quarterback who throws on rhythm throws to where the play says, not to where a receiver happens to be open. When the defense takes away the design, a rhythm passer can be stuck; that is where the second half of this chapter, the off-script game, comes in.

The progression, live

Passing Game Fundamentals drew a progression as numbers on a play diagram. Watched in real time it looks different: the numbers light up one at a time, on the beat of the quarterback’s feet, and at each one he is asking a yes-or-no question that one defender answers. The play is dagger, drawn as Dropback Concepts drew it, against the two-high coverage it was built to beat.

Show the Python
LOS, TOGO = 40, 10


def upto(p, key, way, t_stop, kind="drop"):
    """Script a defender along `way` [(t, d, w), ...] but only until t_stop, the moment he reacts to the ball."""
    cut, prev = [], (0.0, *p._cur(key))
    for t, dd, ww in way:
        if t <= t_stop:
            cut.append((t, dd, ww))
            prev = (t, dd, ww)
        else:
            f = (t_stop - prev[0]) / (t - prev[0])
            cut.append((t_stop, prev[1] + f * (dd - prev[1]), prev[2] + f * (ww - prev[2])))
            break
    timed(p, key, cut, kind)


def dagger(throw_t=2.4, react=0.5):
    off = line() + [O("QB", "QB", -5.0, 0.0, "Q"), O("RB", "RB", -5.0, 1.6, "R"), O("X", "WR", -0.7, -15.0),
                    O("H", "WR", -1.5, -8.5), O("Y", "TE", -1.5, 8.5), O("Z", "WR", -0.7, 15.0)]
    # Quarters from a nickel: two safeties at 12 over the slots, corners off, three underneath plus the nickel
    dfn = four_rush() + [D("MIKE", "MIKE", 5.0, 1.8, "M"), D("WILL", "WILL", 5.0, -5.0, "W"),
                         D("NB", "NB", 5.0, 7.6, "$"), D("SS", "SS", 12.0, 9.0, "SS"),
                         D("LCB", "CB", 7.0, -15.0, "C"), D("RCB", "CB", 7.0, 15.0, "C"),
                         D("FS", "FS", 12.0, -9.0, "FS")]
    p = Play(off, dfn, los=LOS, to_go=TOGO)
    pass_set(p)
    for k, (d, w) in {"LE": (-1.7, -4.6), "NT": (-0.6, -0.9), "DT": (-0.7, 2.0), "RE": (-1.7, 4.6)}.items():
        timed(p, k, [(0.8, d, w)], "rush")
    # quarterback: five-step drop from the gun to 9 yards, then a hitch step every half second
    timed(p, "QB", [(1.95, -9.0, 0.0), (2.4, -8.4, 0.0), (2.9, -8.0, 0.0)])
    # back: checks for a blitzer, then releases to sit underneath as the checkdown
    timed(p, "RB", [(0.5, -5.2, 2.4), (1.4, -5.2, 2.4), (2.4, 2.5, 3.2), (2.9, 3.2, 2.4)], "route")
    timed(p, "Y", [(3.3, 23.5, 8.2)], "route")                                          # seam
    timed(p, "Z", [(2.35, 15.6, 15.0), (2.55, 16.0, 13.6), (3.7, 16.0, 4.0), (4.4, 16.0, -1.5)], "route")  # dig
    timed(p, "X", [(2.8, 20.0, -15.0)], "route")                                        # go
    timed(p, "H", [(1.6, 10.5, -8.5), (1.9, 9.5, -9.3)], "route")                       # curl
    p.pass_to("Z", t=throw_t)
    _, catch = p.pass_target_spot()
    tr = throw_t + react
    # Quarters: the strong safety reads Y and carries the seam; the nickel sinks under it; the Mike sits at ten
    # yards with his eyes on the quarterback and only later walls off the crosser; the corner stays over Z
    upto(p, "SS", [(0.9, 12.8, 8.9), (1.9, 15.6, 8.6), (2.8, 20.5, 8.4), (3.6, 23.5, 8.3)], tr)
    upto(p, "NB", [(1.2, 8.5, 8.2), (2.2, 11.0, 8.4), (3.0, 12.5, 8.4)], tr)
    upto(p, "MIKE", [(1.3, 8.0, 2.4), (2.3, 9.6, 3.0), (2.8, 10.0, 3.0), (3.4, 12.5, 1.8)], tr)
    upto(p, "RCB", [(1.0, 8.5, 15.2), (2.3, 15.2, 15.4), (2.8, 17.6, 13.8), (3.6, 18.2, 8.5)], tr)
    timed(p, "WILL", [(1.3, 8.0, -5.0), (2.3, 9.0, -4.0)], "drop")
    timed(p, "FS", [(1.0, 12.0, -9.0), (2.2, 12.5, -8.0)], "drop")
    timed(p, "LCB", [(2.8, 18.0, -15.5)], "drop")
    # once the ball is in the air, they break on it (after a reaction time) and stop a step short
    for k, spd in (("MIKE", 4.5), ("SS", 4.0), ("NB", 4.0), ("RCB", 4.5)):
        start = np.array(p.position(k, tr))
        dist = float(np.hypot(*(np.array(catch) - start)))
        stop = start + (np.array(catch) - start) * max(0.0, (dist - 0.9) / dist)
        timed(p, k, [(tr + max(0.05, (dist - 0.9) / spd), *stop)], "drop")
    return p, catch


def catch_time(p, key="Z"):
    for kind, to, te in p.ball_events:
        if kind == "pass":
            start = np.array(p.ball_position(te))
            ta = te + 0.5
            for _ in range(4):
                ta = te + float(np.hypot(*(np.array(p.position(to, ta)) - start))) / gplay.BALL_SPEED
            return ta


PLAN = {"Y": [(-1.5, 8.5), (23.5, 8.2)], "Z": [(-0.7, 15.0), (15.6, 15.0), (16.0, 13.6), (16.0, 3.0)],
        "RB": [(-5.0, 1.6), (-5.2, 2.4), (2.5, 3.2), (3.2, 2.4)], "X": [(-0.7, -15.0), (20.0, -15.0)],
        "H": [(-1.5, -8.5), (10.5, -8.5), (9.5, -9.3)]}
prog, _ = dagger()
T_CATCH = catch_time(prog)
LAT, WIN = (-18.0, 18.0), (-11.5, 26.0)
# each numbered read rides beside its receiver: (receiver, offset in d, offset in w)
BADGES = {1: ("Y", 0.2, -2.4), 2: ("Z", 0.3, -2.4), 3: ("RB", -2.0, 1.6)}
T_SET, T_HITCH = 1.95, 2.4


def draw_prog(ax, t):
    s1 = "wait" if t < 1.0 else ("now" if t < T_SET + 0.2 else "done")
    s2 = "wait" if t < T_SET + 0.2 else "now"
    ring = ({"SS": PURPLE} if 1.0 <= t < T_SET + 0.2 else
            ({"MIKE": PURPLE} if T_SET + 0.2 <= t < T_HITCH + 0.4 else {}))
    fld = moment(ax, prog, t, LAT, WIN, rings=ring, plan=PLAN)
    ppy = fld.pts_per_yard()
    for n, st in ((1, s1), (2, s2), (3, "wait")):
        key, dd, dw = BADGES[n]
        rd, rw = prog.position(key, t)
        badge(ax, *XY(fld, prog, rd + dd, rw + dw), n, st, fs=max(6.0, ppy * 1.0))
    if 1.0 <= t < T_SET + 0.2:
        eyes(fld, prog, t, "SS")
    elif T_SET + 0.2 <= t <= T_HITCH + 0.05:
        eyes(fld, prog, t, "MIKE")


TIMES = [0.0, T_SET, T_HITCH, round(T_CATCH, 1)]
NOTES = ["Snap: four rush, four blocked.\nNo hot throw; eyes go to read 1.",
         "Last step of the drop: read 1, the seam.\nThe safety carries Y deep. No.",
         "Hitch step: read 2. The Mike sits at ten\nyards: the window is open. Ball out.",
         "Caught in the window. Read 3,\nthe checkdown, was never needed."]
play_out(draw_prog, TIMES, NOTES, t_end=T_CATCH + 0.4, aspect=(WIN[1] - WIN[0]) / (LAT[1] - LAT[0]), width_in=6.1)
Figure 26.2: A progression in real time (simulated play), first-and-10 (the yellow-orange line is the line to gain). The call is dagger to the right, the two-high beater of Dropback Concepts: Y runs up the seam to pull the deep defenders, Z runs a deep dig (an in-breaking route at about 16 yards) behind him, and R is the checkdown. The defense plays Quarters, two safeties deep, and rushes four. The quarterback takes a five-step drop from the shotgun. Panel 1: at the snap he counts the rush (four, all blocked: no hot throw). Panel 2: on the last step of his drop his eyes are on read 1, the seam, and its key, the strong safety (ringed), who carries Y deep, as Quarters tells him to: no. Panel 3: on his hitch step he moves to read 2. Its key is the Mike (ringed), who has sat down at about ten yards with his eyes on the quarterback, so the window behind him will be open; the ball comes out as Z breaks. Panel 4: the ball arrives in that window, under the safety and over the linebackers. Read 3 was never needed. The numbered reads ride beside their receivers; X’s go and H’s curl on the left (grey dashes) are not part of this progression.

Three things are happening in Figure 26.2 that you can’t see in a static play diagram.

Every read has a key defender. Passing Game Fundamentals separated two kinds of read: a progression, which looks at receivers one at a time, and a defender read, which watches one defender. Dagger is a progression, but even on a progression each look is answered by one defender, and good quarterbacks look at him. At read 1 the question is not really “is Y open?” but “what is the safety on Y’s side doing?” In Quarters that safety is taught to carry the inside receiver if he goes deep, so a seam route is open only if he doesn’t, and here he does. At read 2 the question is the Mike linebacker: has he dropped deep enough to take away the dig, or is the window behind him open? Here he sits at ten yards with his eyes on the quarterback, and the safety is gone with Y, so the space at sixteen yards between them is empty. Which defender is the key on which play depends on the coverage, and that is a subject of its own: Concepts Versus Coverages teaches it, once Part 6 has taught the coverages themselves.

The order is a ranking of risk and reward, and a timetable. Read 1 is the biggest gain the play can produce (a seam is a touchdown if the safety guesses wrong); read 2 a solid one; read 3 a small, safe one. A progression runs from the most valuable throw the defense might allow to the least dangerous one, so that the quarterback takes the big play when it is there and the safe one when nothing else is. It is also a schedule: each route is built to come open just as his feet arrive at that read. Z’s dig breaks at about 2.4 seconds because that is when the hitch step lands. Many coaches sum it up as deep to short, but the timetable is the real rule, and on quick concepts the order starts short: stick and slant-flat key one underneath defender and throw whichever of two short routes he leaves (The Quick Game). Dagger is a full-field read in the language of Passing Game Fundamentals; on a half-field read the order is shorter, and what the quarterback saw before the snap can decide which half he starts on.

The ball comes out before the receiver is open. In panel 3 the dig hasn’t broken yet; Z is still running straight up the field. The quarterback throws anyway, because he has seen what he needed: the Mike is in front of the window, not in it, and the safety is committed to the seam. That kind of throw has a name.

NoteMadden vs. real life

In Madden every receiver carries a button icon, the camera sits high behind the quarterback, and you can see the whole field at once and press the button of whoever looks open. A real quarterback sees through a forest of six-and-a-half-foot linemen with helmets bouncing in his face, from eye level, and can usefully look at one area at a time. That is why a progression exists at all: he can’t scan everything, so the play tells him what to look at, in what order, and when to give up on each look. When an analyst says a quarterback “got through his progression,” he means the quarterback looked at the right things, in the right order, on time, which is much harder than pressing the right button.

Anticipation: throwing to a window that isn’t there yet

An anticipation throw is a pass released before the receiver is open, usually before he makes his break, aimed at the spot where the window between defenders will be when the ball gets there. It is the bigger, riskier cousin of the timing route of Passing Game Fundamentals: a timing route is thrown to a fixed spot on a fixed step, while an anticipation throw is a judgment, made on any route, about where defenders will be in a second.

Why throw to a receiver who isn’t open? Because the ball takes time to get there. A firm 20-yard throw is in the air for about a second, and in a second a linebacker can close five yards. A window that is open when the quarterback sees it is often closed by the time the ball arrives. What makes it a judgment rather than a timing route is what he reads in the half second before he lets go. On dagger the timing is in the play; the decision is the Mike. If the Mike is sinking as Z stems upfield, eyes turned to the receivers, he is going to be standing in the window, and the throw dies. If he sits with his eyes on the quarterback, he will be in front of it. The quarterback has to make that call while Z is still running straight upfield.

Show the Python
def sep_at_catch(p):
    tc = catch_time(p)
    z = np.array(p.position("Z", tc))
    dd = {k: float(np.hypot(*(np.array(p.position(k, tc)) - z))) for k in ("MIKE", "SS", "NB", "WILL", "RCB")}
    k = min(dd, key=dd.get)
    return tc, k, dd[k]


early, _ = dagger(2.4)
late, _ = dagger(2.9)
SEP = {}
fig, axes = plt.subplots(1, 2, figsize=(6.5, 4.4))
LAT2, WIN2 = (-7.5, 19.5), (-10.5, 24.0)
for ax, p, ttl, key, lab_off in ((axes[0], early, "Anticipation: out at 2.4 s, as he breaks", "early", (-1.9, 3.6)),
                                 (axes[1], late, "“See it, throw it”: out at 2.9 s", "late", (-0.4, -4.4))):
    tc, k, dist = sep_at_catch(p)
    SEP[key] = (tc, dist)
    fld = moment(ax, p, tc, LAT2, WIN2, rings={k: PURPLE}, plan={"Z": PLAN["Z"]})
    z, dfd = p.position("Z", tc), p.position(k, tc)
    (x0, y0), (x1, y1) = XY(fld, p, *z), XY(fld, p, *dfd)
    ax.add_patch(Circle((x0, y0), dist, fc=PURPLE, alpha=0.08, ec=PURPLE, lw=1.0, ls=(0, (2, 1.5)), zorder=7))
    ax.plot([x0, x1], [y0, y1], color=PURPLE, lw=1.2, ls=(0, (1, 1.2)), zorder=12)
    dot(ax, x0, y0, "off", "Z", fld.pts_per_yard(), r=0.9, z=13)      # the receiver on top of everyone
    name = {"MIKE": "the Mike", "NB": "the nickel", "SS": "the safety", "RCB": "the corner"}.get(k, k)
    say(fld, p, dfd[0] + lab_off[0], dfd[1] + lab_off[1], name, color=PURPLE, fs=6.8, weight="bold")
    say(fld, p, -7.0, 12.5, f"caught at {tc:.1f} s\nnearest defender {dist:.1f} yd", color=PURPLE, fs=7.2,
        weight="bold")
    ax.set_title(ttl, loc="left", fontsize=8.4, fontweight="bold", color=style.INK)
fig.tight_layout(w_pad=0.6)
plt.show()
Figure 26.3: One play, two release times (simulated). Both panels freeze the moment the dig is caught. Left: the anticipation throw, released on the hitch step at 2.4 seconds, as Z breaks. Right: the quarterback waits one more hitch step, until Z looks open, and throws at 2.9 seconds. Everything else is the same, including when the defenders react (they break on the ball only once it is thrown), but the later ball is caught further across the field, where the Mike has had time to work back under the crosser. The ringed defender is the one nearest the receiver at the catch; the dashed circle around Z has that defender’s distance as its radius, so a bigger circle is a bigger window.

In the simulation the early throw is caught with the nearest defender 2.4 yards away; the late one with a defender 0.9 yards away, close enough to knock it down or catch it himself. The difference is half a second, one hitch step.

A simulation only shows what it was told, and this one was told that defenders break on the ball when it is thrown, not when they see the receiver break. So treat it as an illustration of the mechanism, not proof. The proof that windows matter is in tracking data. Next Gen Stats measures, for every target, how far the receiver was from the nearest defender when the ball arrived. Across 885 receiver-seasons with 50 or more targets from 2018 to 2025, each extra yard of average separation went with about 3.7 more percentage points of catch rate, at the same average depth of target.1 An anticipation throw is a way of buying that yard with the clock instead of the receiver’s speed.

The trade is just as plain. An anticipation throw is a bet on the future, made on information about the present: the quarterback is predicting that the receiver will break where and when he is supposed to, and that the defender he is reading will keep doing what he is doing. If the receiver slips, rounds his cut, or is held up, the ball arrives at an empty spot, often into a defender’s hands. And if the defender was disguising his intentions, sitting shallow to bait the throw, the window was never going to open. Anticipation is therefore the skill that most separates veteran quarterbacks from rookies: it takes years of reps with the same receivers to know exactly where they’ll be, and years of film to know what a linebacker’s first two steps mean.

Coaches often say the young quarterback “needs to see it before he throws it.” What they mean is that he is throwing a beat late, waiting for the receiver to look open, which, as Figure 26.3 shows, is too late against good defenses.

The pocket: climb, slide, escape

The progression assumes a quarterback standing calmly at his launch point. He rarely is. A pocket is a moving thing (the tackles giving ground, the interior bending), and the quarterback has to move inside it without losing his eyes or his feet, so that he can still throw on time from wherever he ends up. Coaches name three moves.

Show the Python
LAT3, WIN3 = (-9.6, 9.6), (-11.8, 2.4)


def still(spots, ax, title, box=False):
    off = [O(k, k, *spots[k], "C" if k == "C" else ("Q" if k == "QB" else "")) for k in OL_KEYS + ["QB"]]
    dfn = [D(k, "DE" if k in ("LE", "RE") else "DT", *spots[k], "E" if k in ("LE", "RE") else "T")
           for k in ("LE", "NT", "DT", "RE")]
    p = Play(off, dfn, los=LOS, to_go=None)
    fld = p.draw(ax=ax, lateral=LAT3, window=WIN3, title=False)
    clean(ax)
    if box:
        tackle_box(ax, fld, p, -11.8)
    ax.set_title(title, loc="left", fontsize=8.6, fontweight="bold", color=style.INK)
    return p, fld


fig, axes = plt.subplots(1, 3, figsize=(6.6, 3.3))
# 1. climb
S = {"LT": (-6.6, -4.7), "LG": (-2.1, -1.8), "C": (-1.9, 0.0), "RG": (-2.1, 1.8), "RT": (-6.6, 4.7),
     "QB": (-8.0, 0.0), "LE": (-8.4, -6.3), "NT": (-0.4, -0.9), "DT": (-0.4, 2.0), "RE": (-8.4, 6.3)}
p, fld = still(S, axes[0], "Climb")
for sg in (-1, 1):
    arrow(fld, p, [(1.0, 5.4 * sg), (-3.0, 7.0 * sg), (-6.4, 7.3 * sg), (-7.6, 6.7 * sg)], color=style.DEFENSE,
          lw=1.2, head=7, z=3, alpha=0.8)
arrow(fld, p, [(-6.9, 0.0), (-4.3, 0.0)], lw=2.2, head=11)
say(fld, p, -5.4, 0.9, "step up", color=GREEN, weight="bold", fs=7.2, ha="left")
say(fld, p, -10.4, 0.0, "ends pushed past him", fs=6.4, color=style.INK_2)
# 2. slide
S2 = dict(S, LT=(-5.8, -4.4), LE=(-6.9, -2.6), RE=(-8.6, 6.4), RT=(-7.0, 5.0))
p, fld = still(S2, axes[1], "Slide")
arrow(fld, p, [(1.0, -5.4), (-2.6, -6.2), (-5.9, -3.2)], color=style.DEFENSE, lw=1.2, head=7, z=3, alpha=0.8)
arrow(fld, p, [(-8.0, 1.0), (-8.0, 3.4)], lw=2.2, head=11)
say(fld, p, -10.2, 2.6, "slide away,\nreset the feet", color=GREEN, weight="bold", fs=6.8)
say(fld, p, -9.6, -4.4, "beaten\ninside", fs=6.4, color=RED)
# 3. escape
S3 = dict(S, C=(-5.6, 0.1), NT=(-4.0, -0.3), LT=(-7.3, -4.7), LE=(-8.6, -3.2), RT=(-8.2, 5.5), RE=(-9.9, 6.4))
p, fld = still(S3, axes[2], "Escape", box=True)
arrow(fld, p, [(1.0, -0.7), (-3.1, -0.4)], color=style.DEFENSE, lw=1.2, head=7, z=3, alpha=0.8)
say(fld, p, -3.6, -5.9, "center driven\nback into him", fs=6.2, color=RED)
arrow(fld, p, [(-8.0, 1.0), (-7.2, 3.4), (-6.0, 5.4), (-4.6, 7.6)], lw=2.2, head=11)
say(fld, p, -3.0, 6.2, "escape lane", color=GREEN, weight="bold", fs=7.0)
say(fld, p, -10.5, -1.2, "tackle box", color=PURPLE, fs=6.4)
fig.tight_layout(w_pad=0.4)
plt.show()
Figure 26.4: Three pocket moves, each the answer to a different rush picture (a frozen moment about two seconds after the snap; receivers not shown). Left, climb: both ends have run their arcs and the tackles have pushed them deeper than the quarterback, while the interior held, so he steps forward into the cup and the rushers finish behind him. Middle, slide: one end has beaten the left tackle inside, so the quarterback shuffles a couple of yards right, away from the pressure, resets his feet and keeps his eyes downfield. Right, escape: a defensive tackle has driven the center straight back into the quarterback’s face, so there is no cup to step up into. The ends decide which way he goes: the left end has come inside, but the right end was pushed past him, so the lane is outside the right tackle. Leaving the tackle box (the shaded area, out to the outside shoulders of the two tackles) also changes the rules for throwing the ball away.

Climb. Climbing the pocket means stepping forward into the pocket, between the interior linemen, as the edge rushers are pushed past. It is the single most important pocket skill, and it follows directly from the geometry of Pass Protection: the tackles give ground on purpose and ride the ends on a wide arc behind the launch point, while the guards and center hold firm in front. A quarterback who stays at his launch point, or worse, drifts backward, walks into the spot where the ends’ arcs finish. One who steps up two yards lets the ends run past behind him, and throws from the cleanest spot on the field. Quarterback coaches drill the idea endlessly: pressure from the outside is answered by moving up, not back.

Slide. When pressure comes from one side (an end who beat the tackle inside, a blitzer through a gap) and the middle is still firm, the quarterback slides a yard or two the other way, keeping his shoulders square to the line of scrimmage and his eyes downfield. A slide buys a half second without ending the play: he is still in the pocket and can still throw to anyone in the progression.

Escape. When the pocket is gone, usually because the interior collapsed and the quarterback has nowhere to step up into, he leaves. The usual escape lane is outside one of the tackles, on the side where an end rushed too far upfield and was pushed past; it opens exactly because the tackle did his job. Escaping changes the play into something else, which is the next section’s subject.

All three moves are made with both hands on the ball. Quarterback coaches drill it as hard as the footwork: a quarterback who carries the ball loose in one hand while he climbs or slides is the one who gets it knocked out from behind by the end he never saw, the strip-sack.

The skill under all three is doing them without looking at the rush. A quarterback who drops his eyes to watch the defensive end has stopped reading the coverage, and a quarterback who has stopped reading the coverage has stopped running the play. Great pocket passers feel the rush (from the color at the edge of their vision, from the sound, from the pressure on a lineman’s back) and move their feet while their eyes stay downfield. Coaches call this pocket presence, and it is one of the few things that clearly separates good quarterbacks from bad ones, as Figure 26.5 shows in motion.

Show the Python
def climb_play():
    p = Play(line() + [O("QB", "QB", -5.0, 0.0, "Q")], four_rush(), los=LOS, to_go=None)
    for k, way in {"LG": [(0.6, -1.8, -1.8), (2.6, -2.1, -1.8)], "C": [(0.6, -1.5, 0.0), (2.6, -1.8, 0.0)],
                   "RG": [(0.6, -1.8, 1.8), (2.6, -2.1, 1.8)]}.items():
        timed(p, k, way, "block")
    for sg, (lt, le) in ((-1, ("LT", "LE")), (1, ("RT", "RE"))):
        timed(p, lt, [(0.7, -2.6, 4.2 * sg), (1.3, -4.6, 5.0 * sg), (2.0, -7.2, 5.1 * sg), (2.6, -9.2, 4.4 * sg)],
              "block")
        timed(p, le, [(0.7, -1.4, 6.2 * sg), (1.3, -4.0, 6.5 * sg), (2.0, -7.2, 6.3 * sg), (2.6, -9.8, 5.3 * sg)],
              "rush")
    timed(p, "NT", [(0.6, -0.5, -1.0), (2.6, -0.9, -1.0)], "rush")
    timed(p, "DT", [(0.6, -0.6, 2.0), (2.6, -1.0, 2.0)], "rush")
    timed(p, "QB", [(0.95, -8.0, 0.0), (1.7, -8.0, 0.0), (2.25, -5.7, 0.2), (2.6, -5.6, 0.2)])
    return p


climb = climb_play()
LAT4, WIN4 = (-11, 11), (-12.4, 3.6)


def draw_climb(ax, t):
    fld = moment(ax, climb, t, LAT4, WIN4, ghosts=[(-8.0, 0.0, "launch\npoint")] if t >= 1.7 else [],
                 trail_keys={"LE", "RE", "LT", "RT", "QB"}, r=0.72)
    if t >= 2.3:
        arrow(fld, climb, [(-4.9, 0.0), (2.8, -0.9)], color=style.BALL, lw=1.3, ls=(0, (2, 1.5)), head=8)
        say(fld, climb, 1.6, -4.6, "ball out", color=style.BALL, weight="bold", fs=7.2)


CT = [0.0, 1.0, 1.8, 2.4]
CN = ["Snap: he will set at his launch\npoint, eight yards deep.",
      "1.0 s: set. The ends run their arcs;\nthe tackles ride them deeper.",
      "1.8 s: the ends are level with him.\nBack is where they are going. Up.",
      "2.4 s: two yards further up, in a clean\ncup, he throws. The ends finish behind."]
play_out(draw_climb, CT, CN, t_end=2.7, aspect=(WIN4[1] - WIN4[0]) / (LAT4[1] - LAT4[0]))
Figure 26.5: Climbing the pocket, in motion (simulated, the box only). Both ends rush wide around the tackles; the tackles kick back with them and push them deeper than the quarterback’s launch point (the dashed circle), while the guards and center hold their ground against the two tackles. At about 1.8 seconds the ends are level with the quarterback: a drift backward would put him between them. He steps up into the cup instead and throws two yards further upfield, and the ends finish their arcs behind him.
CautionCommon misconception

“A mobile quarterback should get out of the pocket whenever it gets crowded.” Quarterback coaches teach the opposite: don’t leave a pocket that still works. Outside it he has fewer throws (he can see only half the field and has to throw on the run), the rushers who were blocked are now chasing him unblocked, and the sideline is an extra defender. The best scramblers in the league are good at not leaving until the pocket is gone. Then they are devastating, as the next section shows.

Be careful with the obvious statistic here. In FTN’s charting from 2022 to 2025, quarterbacks were sacked on 8.3% of dropbacks once they were out of the pocket, against 6.2% in it,2 which looks like proof that leaving gets you sacked. It isn’t, because quarterbacks mostly leave because the pocket has already failed: from 2023 to 2025, 57% of out-of-pocket dropbacks were charted as pressured, against 24% of the rest. Compare like with like, pressured dropbacks only, and the ones that got outside ended in a sack less often (15% against 26%). Once the pocket has collapsed, escaping is a rescue. What the charting can’t tell you is what leaving a pocket that was still good costs, because nobody charts whether it was still good. That part of the rule rests on coaching judgment, not on this number.

Off-script football and the scramble drill

When the quarterback escapes the pocket, or holds the ball past the play’s last read, the play has gone off script. An off-script play (also called an off-schedule play) is one that has left its design: the called routes have run out, the protection has broken, and what happens next depends on improvisation.

Improvisation sounds like the opposite of coaching. It isn’t. The quarterback improvises, but his receivers follow rules, practiced every week, that tell them what to do when he starts running. Those rules are the scramble drill. Every team has its own version and its own vocabulary, but the core is nearly universal.3

Show the Python
LAT5, WIN5 = (-19, 22), (-10.5, 24.0)
off = line() + [O("QB", "QB", -6.6, 8.6, "Q"), O("RB", "RB", -4.6, 2.6, "R"), O("X", "WR", 13.0, -12.0),
                O("H", "WR", 4.0, -1.0), O("Y", "TE", 6.0, 9.0), O("Z", "WR", 18.0, 16.0)]
p = Play(off, [], los=LOS, to_go=None)
fld = p.draw(lateral=LAT5, window=WIN5, title=False)
clean(fld.ax)
called = {"X": [(-0.7, -15.0), (13.0, -15.0), (13.0, -12.0)], "H": [(-1.5, -8.5), (1.0, -7.5), (4.0, -1.0)],
          "Y": [(-1.5, 9.0), (6.5, 9.0), (6.0, 8.6)], "Z": [(-0.7, 15.0), (18.0, 16.0)]}
for k, pts in called.items():
    xy = [XY(fld, p, *q) for q in pts]
    xs, ys = zip(*xy)
    fld.ax.plot(xs, ys, color=style.MUTED, lw=1.1, ls=(0, (2, 2)), zorder=2)
    fld.ax.add_patch(Circle(xy[0], 0.55, fc="none", ec=style.MUTED, lw=0.8, zorder=2))
arrow(fld, p, [(-8.0, 0.0), (-7.6, 4.0), (-6.6, 8.6)], color=GREEN, lw=1.4, ls=(0, (3, 2)), head=9)
RULES = {"Y": ([(6.0, 9.0), (11.0, 10.0), (16.5, 11.0)], 1, (16.6, 13.3)),
         "Z": ([(18.0, 16.0), (14.0, 17.4), (10.5, 18.4)], 2, (8.4, 18.8)),
         "H": ([(4.0, -1.0), (4.8, 3.0), (5.2, 6.0)], 3, (2.8, 5.6)),
         "X": ([(13.0, -12.0), (17.0, -6.0), (20.0, 0.5)], 4, (21.6, 2.6)),
         "RB": ([(-4.6, 2.6), (-2.2, 5.4), (0.6, 7.4)], 5, (0.4, 10.0))}
for k, (pts, n, at) in RULES.items():
    arrow(fld, p, pts, color=GREEN, lw=2.0, head=11)
    badge(fld.ax, *XY(fld, p, *at), n, "now", fs=8)
say(fld, p, 11.8, -4.6, "1  short goes deep\n2  deep comes back\n3  go where he goes\n4  backside: come across deep\n"
    "5  back: leak out, short", fs=7.4, ha="left", color=style.INK)
say(fld, p, -9.0, 8.6, "he escapes right", color=GREEN, fs=7.0, weight="bold")
plt.show()
Figure 26.6: The scramble drill’s core rules, drawn at the moment the quarterback escapes to his right (offense only; grey dashes are the routes as called, green arrows what each receiver does once the quarterback leaves the pocket). (1) Short goes deep: Y, sitting on a short hitch route, takes off up the field. (2) Deep comes back: Z, who was running deep down the sideline, turns and works back toward the line, along the sideline on the quarterback’s side. (3) Go where he goes: H, on a shallow crossing route, keeps crossing toward the scramble side, underneath. (4) Backside receivers come across deep: X, on the far side, works back across the field behind the coverage. (5) The back, who stayed in to block, leaks out to the scramble side, just past the line, as the last outlet. The result is five receivers at five depths, all on or toward the quarterback’s side and in his line of sight. The linemen stay on their men. Rules differ from team to team; these are the common core.

Each rule has a reason.

  • Short goes deep, deep comes back. Receivers spread out vertically so the quarterback has a choice of depths, and so that two receivers never end up in the same spot dragging their defenders with them. The deep receiver comes back because a quarterback throwing on the run cannot make a 40-yard throw accurately; the short receiver goes deep because the defenders near him have all turned to chase the quarterback, and the space behind them is empty. “Short” and “deep” are measured against the linebackers: in front of them is short, behind them is deep.
  • Go where he goes. A quarterback running to his right can throw easily to his right and downfield, and only with difficulty back across his body to the left. (Throwing across your body on the run is one of the most dangerous throws in football, because the ball floats back toward the middle of the field where the defenders are.) So receivers work toward his side.
  • Backside receivers come across, deep. A receiver on the far side can’t stand still and wait; he runs back across the field behind the coverage, where defenders who have turned to chase the quarterback have left the deep middle.
  • The back leaks out; the linemen stay home. A back who stayed in to block releases toward the scramble side, a yard or two past the line, as the last, safest throw. The linemen keep blocking whoever they have and do not drift downfield: if the quarterback then throws, a lineman more than a yard beyond the line is ineligible downfield, a foul that wipes out the completion (RPOs).4
  • Get into his line of sight and keep moving. A receiver the quarterback can’t see is not an option. Receivers find grass on a different level from their teammates and stay in his vision, and if he crosses the line of scrimmage to run, they stop running routes and start blocking. Nobody calls out “scramble”: the cue is the clock. A receiver who reaches the end of his route without the ball turns to find the quarterback, and if he is no longer where the drop put him, the drill is on.

When two rules seem to apply, settle it by side first. A receiver on the far side from the scramble comes across: deep if he was deep, and a crosser keeps crossing at his depth, toward the quarterback. On the scramble side, short goes deep and deep comes back. Teams word this differently, but that order keeps the receivers on different levels, which is the whole point.

Where does the quarterback look? The rules hand him receivers at four or five depths; he can’t look at all of them. The throws he can make while moving come first: the receiver working back toward him along his sideline, and the crosser running with him. Both are short, firm throws toward the side he is running to. The deep receivers are the big play, and he takes that shot when he has room to stop, set his feet and throw; a quarterback running flat out usually can’t throw 30 yards accurately. Escaping to his throwing-hand side makes all of this easier. A right-handed quarterback rolling left has to turn his hips and square his shoulders back to the target before he can throw at all, so for him the come-back routes on that side matter even more.

Throw or run? Once he is out of the pocket he has a third choice, and it is the best-valued ending in the data, as Figure 26.10 will show. The rule of thumb is simple. As long as he is behind the line, his eyes stay downfield and every receiver is still an option. When the receivers are covered and there is open grass in front of him, ideally to the line to gain, he runs. Once he crosses the line of scrimmage the passing play is over, because a forward pass thrown from beyond the line is illegal.5 Running too early gives up the throws; running too late, after the defenders have rallied, gives up the grass.

Why does any of this work? Because the defense was built for the play the offense called, and that play is over. Against man coverage, where each defender follows one receiver, defenders are running with their backs to the quarterback; when a receiver stops and reverses, his defender has to stop, turn and find him, and he is a step behind. Against zone coverage, where each defender guards an area, defenders have dropped to spots that were chosen for the original routes, and they have to leave those spots to chase receivers who are now somewhere else, opening the areas they were guarding.

The defense has rules for this too, and the scramble drill works best against a defense that breaks them. Up front the rushers keep their rush lanes and the ends keep contain, not rushing past the quarterback’s depth, which is exactly the mistake that opens the escape lane in Figure 26.4. Some defenses assign a spy, a defender who mirrors the quarterback and tackles him if he runs. And in zone, once the quarterback leaves the pocket, coverage players are taught to stop guarding grass and lock onto the nearest receiver (coaches call it “plastering”). A defense that does all of this leaves the quarterback little; much of the off-script value in the next chart comes against defenses that don’t.

Show the Python
def scramble_play():
    off = line() + [O("QB", "QB", -5.0, 0.0, "Q"), O("RB", "RB", -5.0, 1.6, "R"), O("X", "WR", -0.7, -15.0),
                    O("H", "WR", -1.5, -8.5), O("Y", "TE", -1.5, 8.5), O("Z", "WR", -0.7, 15.0)]
    dfn = four_rush() + [D("MIKE", "MIKE", 6.0, 0.5, "M"), D("WILL", "WILL", 4.5, -1.5, "W"),
                         D("NB", "NB", 4.5, -8.5, "$"), D("SS", "SS", 5.0, 9.6, "SS"),
                         D("LCB", "CB", 6.0, -15.0, "C"), D("RCB", "CB", 6.0, 15.0, "C"),
                         D("FS", "FS", 13.0, 0.5, "FS")]
    p = Play(off, dfn, los=LOS, to_go=TOGO)
    pass_set(p, depths=(-2.6, -1.7, -1.4, -1.7, -2.6))
    timed(p, "LT", [(1.4, -4.2, -4.6), (2.2, -5.2, -4.4)], "block")
    timed(p, "LE", [(0.8, -1.7, -5.6), (1.5, -4.6, -5.0), (2.1, -7.4, -2.2), (2.6, -8.6, 0.6), (3.3, -7.6, 5.4),
                    (3.9, -7.0, 8.0)], "rush")
    timed(p, "NT", [(0.8, -0.6, -0.9)], "rush")
    timed(p, "DT", [(0.8, -0.7, 2.0)], "rush")
    timed(p, "RE", [(0.8, -1.7, 4.8), (1.6, -5.8, 5.6), (2.2, -9.6, 5.6), (2.8, -11.0, 4.8), (3.6, -9.6, 8.8)], "rush")
    timed(p, "RT", [(1.6, -5.0, 4.5), (2.2, -8.0, 4.4)], "block")
    timed(p, "RB", [(0.5, -5.2, 2.4), (2.2, -5.2, 2.2), (2.8, -5.2, 2.0), (3.6, -3.4, 5.6), (4.6, -2.6, 7.0)],
          "block")                                     # blocks, then leaks out to the scramble side
    timed(p, "QB", [(0.95, -8.0, 0.0), (1.9, -8.0, 0.0), (2.5, -7.5, 3.8), (3.3, -6.1, 9.4), (3.6, -5.7, 10.6),
                    (4.3, -5.2, 12.2)])
    # routes as called, then the scramble rules from about 2.4 s
    timed(p, "X", [(1.7, 12.0, -15.0), (2.2, 12.2, -12.0), (3.3, 16.0, -6.5), (4.6, 19.5, 2.0)], "route")
    timed(p, "H", [(0.6, 1.0, -7.5), (2.3, 3.6, 1.0), (3.4, 4.6, 6.0), (4.6, 5.2, 9.0)], "route")
    timed(p, "Y", [(1.1, 6.5, 8.5), (1.3, 6.0, 8.1), (2.4, 6.0, 8.1), (3.3, 11.5, 9.6), (4.6, 18.0, 10.8)], "route")
    timed(p, "Z", [(2.5, 18.0, 15.6), (2.75, 17.6, 16.2), (3.4, 14.0, 17.6), (4.6, 10.6, 19.0)], "route")
    p.pass_to("Z", t=3.6)
    # man coverage: each defender trails his receiver; the corner on Z keeps running when Z stops
    timed(p, "LCB", [(1.7, 11.0, -14.4), (2.3, 11.6, -12.6), (3.3, 14.6, -8.0), (4.6, 18.0, 0.0)], "path")
    timed(p, "NB", [(0.7, 3.0, -7.6), (2.3, 3.4, -2.6), (3.4, 3.6, 2.4), (4.6, 4.0, 5.6)], "path")
    timed(p, "SS", [(1.3, 7.2, 10.4), (2.5, 7.2, 10.9), (3.4, 10.4, 11.8), (4.6, 16.0, 12.8)], "path")
    timed(p, "RCB", [(2.5, 16.6, 14.0), (2.95, 19.6, 14.3), (3.4, 18.6, 15.6), (4.6, 13.8, 17.4)], "path")
    timed(p, "MIKE", [(1.2, 8.0, 1.0), (2.6, 8.5, 3.0), (3.6, 8.8, 5.0), (4.6, 9.5, 6.6)], "path")
    timed(p, "WILL", [(1.0, 4.0, 0.6), (2.6, 1.0, 3.6), (3.6, 0.4, 7.2), (4.6, 0.2, 9.6)], "path")   # R stays in: the Will mirrors Q
    timed(p, "FS", [(1.5, 14.0, 2.0), (2.6, 15.5, 4.0), (3.6, 17.5, 6.0), (4.6, 19.0, 6.8)], "path")
    return p


scr = scramble_play()
SCR_CATCH = catch_time(scr)
LAT6, WIN6 = (-17.0, 22.0), (-12.6, 23.0)


def draw_scramble(ax, t):
    ring = {"LE": RED} if 1.2 <= t < 2.4 else ({"Z": GREEN} if t >= 3.5 else {})
    fld = moment(ax, scr, t, LAT6, WIN6, rings=ring, r=1.1,
                 plan={"X": [(-0.7, -15.0), (12.0, -15.0), (12.2, -12.0)], "H": [(-1.5, -8.5), (1.0, -7.5), (3.6, 1.0)],
                       "Y": [(-1.5, 8.5), (6.5, 8.5), (6.0, 8.1)], "Z": [(-0.7, 15.0), (21.0, 15.7)]})
    if t >= 2.4:
        eyes(fld, scr, t, "Z" if t >= 3.3 else "Y")


ST = [1.6, 2.6, 3.6, round(SCR_CATCH, 1)]
SN = ["Called: dig, shallow cross, hitch route,\ngo. The left end beats the left tackle.",
      "He escapes right. Every receiver\nswitches to his scramble rule.",
      "Z comes back, Y goes deep, H keeps\ncrossing, X comes across. Throw.",
      "Z catches it on the sideline; his\ncorner is still turning around."]
play_out(draw_scramble, ST, SN, t_end=SCR_CATCH + 0.4, aspect=(WIN6[1] - WIN6[0]) / (LAT6[1] - LAT6[0]))
Figure 26.7: The scramble drill in motion (simulated). The call is a dig by X, a shallow cross by H, a hitch route by Y and a go by Z, against man coverage with a free safety deep and a linebacker in the middle; the defense rushes four. The back, R, stays in to block, so the Will, whose man he was, has nobody to cover and mirrors the quarterback instead. Panel 1: the left end beats the left tackle (red ring). Panel 2: the quarterback escapes to his right and the receivers switch to their scramble rules. Panel 3: Z has stopped his go route and is working back down the sideline, Y has turned up the field, H keeps crossing, X comes back across deep; the quarterback throws on the run to the come-back receiver, the easy throw toward the sideline he is running at. Panel 4: Z (green ring) catches it near the sideline. His cornerback, who was running deep with his back to the quarterback, is still turning around.
ImportantWhy it exists

What the scramble drill solves. A pass play is designed to finish in under three seconds. When it doesn’t, eleven offensive players are suddenly without instructions, and without rules each receiver would do something different: some keep running their routes into the sideline, some stop, two drift into the same spot. The scramble drill turns the broken play into a second, pre-planned play, so the quarterback’s improvisation lands on receivers who are where he expects them.

What it gives up. Nothing in the design, but plenty in the result: a play that goes off script is, on average, a worse play. It is a rescue, not a strategy, and the offense would usually rather the first play had worked. The exceptions are a handful of quarterbacks for whom the rescue is a weapon, and you can find them in the data.

Off-script value: who gains by leaving the pocket?

FTN charts whether the quarterback was outside the pocket on each dropback, whether by design (a rollout) or by escape; the flag does not say which. From 2022 to 2025 that happened on 18% of NFL dropbacks. On average those plays were worth +0.011 EPA (expected points added) per play, against +0.049 in the pocket. That average hides a split. About 48% of out-of-pocket dropbacks ended in a quarterback run or a scramble-drill throw, and those were worth a lot; another 22% ended in a throwaway, a sack or an interception, and those cost even more. Off script is a high-variance place: for the league as a whole it nets out slightly worse than the pocket, but for a few quarterbacks it is clearly better.

Show the Python
fig, ax = plt.subplots(figsize=(6.5, 4.3))
lo, hi = -0.25, 0.36
ax.plot([lo, hi], [lo, hi], color=style.INK_2, lw=1.0, ls=(0, (4, 2)), zorder=1)
ax.text(-0.22, 0.2, "better outside the pocket\n(above the diagonal)", fontsize=7.2, color=style.INK_2, ha="left")
ax.text(0.34, -0.17, "better inside the pocket\n(below the diagonal)", fontsize=7.2, color=style.INK_2, ha="right")
ax.axhline(EPA_OUT, color=style.BASELINE, lw=0.8)
ax.axvline(EPA_IN, color=style.BASELINE, lw=0.8)
up = OQ.gain > 0
ax.scatter(OQ.loc[~up, "epa_in"], OQ.loc[~up, "epa_out"], s=24, color=style.GRID, edgecolor=style.MUTED, lw=0.5,
           zorder=2, label="worse outside the pocket")
ax.scatter(OQ.loc[up, "epa_in"], OQ.loc[up, "epa_out"], s=34, color=style.SERIES[0], edgecolor=style.SURFACE,
           lw=0.8, zorder=3, label="better outside the pocket")
ax.legend(loc="upper left", fontsize=7.4)
ax.text(EPA_IN + 0.004, -0.235, "league, in pocket", fontsize=6.6, color=style.MUTED)
ax.text(-0.245, EPA_OUT + 0.008, "league, out of pocket", fontsize=6.6, color=style.MUTED)
ax.set_xlim(lo, hi)
ax.set_ylim(lo, hi)
ax.set_xlabel("EPA per dropback in the pocket", fontsize=8.5)
ax.set_ylabel("EPA per dropback out of the pocket", fontsize=8.5)
ax.tick_params(labelsize=7.5)
fig.tight_layout()
# name every quarterback above the diagonal, plus three below it that the text discusses
# (the four in the tight cluster near the origin get hand-placed labels with leader lines)
HAND = {"D.Jones": (-0.075, 0.09, "D. Jones"), "G.Smith": (-0.085, 0.045, "G. Smith"),
        "A.Rodgers": (0.0, -0.045, "Rodgers"), "K.Murray": (0.09, 0.115, "Murray")}
for nm, (tx, ty, lab) in HAND.items():
    r_ = OQ[OQ.name == nm].iloc[0]
    ax.annotate(lab, (r_.epa_in, r_.epa_out), xytext=(tx, ty), textcoords="data", fontsize=7.0, color=style.INK,
                ha="center", va="center", arrowprops=dict(arrowstyle="-", color=style.MUTED, lw=0.6, shrinkB=3))
lab_rows = OQ[(up | OQ.name.isin(["J.Hurts", "B.Purdy", "J.Goff"])) & ~OQ.name.isin(list(HAND))]
lab_rows = lab_rows.sort_values("epa_out", ascending=False)
place_labels(ax, lab_rows.epa_in.to_numpy(), lab_rows.epa_out.to_numpy(),
             [n.split(".")[-1] for n in lab_rows.name], fs=7.0)
plt.show()
Figure 26.8: In the pocket versus out of it: every quarterback with at least 800 dropbacks and 150 out-of-pocket dropbacks in the 2022-2025 regular seasons. Horizontal: EPA per dropback (sacks and scrambles included) while in the pocket; vertical: EPA per dropback once outside it, by escape or by design. Points above the dashed diagonal were better outside the pocket than in it, which few quarterbacks were; all of them are named (Rodgers and Murray sit almost exactly on the line), along with three notable quarterbacks below it. Josh Allen and Patrick Mahomes sit highest: their off-script plays were worth more than most quarterbacks’ on-script ones. Data: FTN charting and nflverse play-by-play via nflreadpy.

Only 11 of the 32 quarterbacks in Figure 26.8 were better outside the pocket than in it, and two of those were level within a hundredth of a point. At the top are the two players the sport thinks of first when it thinks of off-script football: Josh Allen (+0.31 EPA per dropback outside the pocket, +0.20 in it) and Patrick Mahomes (+0.30 and +0.17). Notice what kind of skill that is. Both can run, but the value isn’t mainly in the running: it is in throwing accurately on the move, to receivers who know the scramble drill, while the defense is scrambling too. And notice who is not above the line. Jalen Hurts, one of the most dangerous running quarterbacks of the era, sits well below it (-0.01 outside, +0.19 in). Much of his running comes on designed runs and sneaks, which are not dropbacks and so are not in this chart; running ability and off-script passing are different skills.

Read the chart with two cautions. First, “out of the pocket” includes designed rollouts and bootlegs (Play-Action, Bootlegs and Screens), which are good plays for everyone, so a quarterback whose offense calls many of them looks better out there for reasons that have nothing to do with improvisation. Second, it is four seasons of a few hundred plays per quarterback: the order in the middle of the pack is mostly noise, and only the ends of the chart are worth arguing about.

A short history: from Tarkenton to Mahomes

The scramble entered the NFL’s vocabulary with Fran Tarkenton, a preacher’s son from Athens, Georgia, who quarterbacked the expansion Minnesota Vikings from their first game in 1961. Behind a thin, new offensive line, he ran around behind it, often for long stretches, until someone came open. Coaches of his era, his own first head coach Norm Van Brocklin among them, thought scrambling undisciplined (the feud with Van Brocklin ended with Tarkenton demanding a trade in 1967); Tarkenton’s answer, preserved on his Hall of Fame page, was that “I scramble because I’m good at it, because I can twist and dodge those big pass rushers better than most guys, and we get a lot of touchdowns that way.” He retired in 1978 holding the career records for completions, passing yards and touchdown passes.6

For decades after that the pocket passer was the ideal: the quarterbacks who won the most, from Joe Montana to Peyton Manning and Tom Brady, did their damage on schedule, and the offenses built around them, timed to the drop, treated a scramble as a play that had failed. But the off-script line never died. John Elway passed for 3,000 yards and ran for 200 in seven straight seasons and built a career on late comebacks; Steve Young retired with 43 rushing touchdowns, then the second-most by any quarterback.7 Teams with quarterbacks like them practiced what to do when the play broke.

What changed in the last decade is scale. College football filled with spread offenses and athletic quarterbacks who had been improvising since high school, and NFL offenses stopped treating the scramble drill as an emergency procedure and started repping it weekly and designing around it. The quarterbacks who came out of that world (Mahomes, Allen, Lamar Jackson, Jalen Hurts) made off-script play part of the plan rather than an emergency.

Getting rid of it: the throwaway

The last of the quarterback’s options when nothing is open is the least glamorous and among the most valuable. A throwaway is a pass he deliberately throws where nobody can catch it, usually out of bounds or well over a covered receiver’s head, to avoid a sack or an interception. He gives up the down but keeps the yards. NFL quarterbacks threw the ball away on 3.5% of dropbacks from 2022 to 2025.

Why is that a skill and not a surrender? Look at what the alternatives cost. In the same seasons, the average throwaway was worth -0.78 EPA (an incompletion, a lost down), the average sack -1.77 (a lost down and six or seven yards), and the average interception -4.42. A quarterback who turns two sacks a game into throwaways gains about 2 points a game for his team, without completing a pass.

There is a catch, and it is a rule. Intentional grounding is a foul for throwing the ball away to avoid a sack when the pass isn’t in the direction and vicinity of an eligible receiver; it costs a loss of down and ten yards (or loss of down at the spot of the pass, if that is worse), and a safety if the quarterback throws it from his own end zone. The rule changes once the quarterback leaves the tackle box, the area between where the two offensive tackles lined up: outside it, any pass that lands at or beyond the line of scrimmage (including out of bounds past it) is legal, receiver or no receiver.8 Officiating, Replay, and the Rules of the Ball covers the full rule; here is what it means for the quarterback.

Show the Python
fig, axes = plt.subplots(1, 2, figsize=(6.5, 3.5))
# left: inside the box
off = line() + [O("QB", "QB", -7.6, 0.4, "Q"), O("Y", "TE", 6.0, 5.0)]
dfn = [D("LE", "DE", -6.6, -1.6, "E"), D("DT", "DT", -5.8, 1.8, "T"), D("MIKE", "MIKE", 6.4, 6.4, "M")]
p = Play(off, dfn, los=LOS, to_go=None)
fld = p.draw(ax=axes[0], lateral=(-15.5, 15.5), window=(-12.5, 14.0), title=False)
clean(axes[0])
tackle_box(axes[0], fld, p, -12.5)
arrow(fld, p, [(-7.6, 0.4), (2.0, 4.6), (10.0, 6.4)], color=GREEN, lw=1.6, ls=(0, (3, 2)), head=10)
arrow(fld, p, [(-7.6, 0.4), (0.0, -5.0), (6.0, -9.6)], color=RED, lw=1.6, ls=(0, (3, 2)), head=10)
say(fld, p, 11.6, 4.0, "legal: thrown\nnear Y", color=GREEN, fs=7.0, weight="bold")
say(fld, p, 8.4, -7.6, "grounding:\nnobody there", color=RED, fs=7.0, weight="bold")
say(fld, p, -11.0, 0.0, "tackle box", color=PURPLE, fs=6.6)
axes[0].set_title("Inside the tackle box", loc="left", fontsize=8.6, fontweight="bold", color=style.INK)
# right: outside the box, ball on the right hash
off = line() + [O("QB", "QB", -6.0, 11.0, "Q"), O("Z", "WR", 9.0, 19.0)]
dfn = [D("RE", "DE", -7.4, 8.0, "E"), D("RCB", "CB", 9.6, 20.4, "C")]
p = Play(off, dfn, los=LOS, ball_y="right", to_go=None)
fld = p.draw(ax=axes[1], lateral=(-4.5, 26.5), window=(-12.5, 14.0), title=False)
clean(axes[1])
tackle_box(axes[1], fld, p, -12.5)
arrow(fld, p, [(-6.0, 11.0), (2.0, 19.0), (5.0, 25.4)], color=GREEN, lw=1.6, ls=(0, (3, 2)), head=10)
arrow(fld, p, [(-6.0, 11.0), (-4.6, 17.0), (-3.4, 22.6)], color=RED, lw=1.6, ls=(0, (3, 2)), head=10)
say(fld, p, 6.4, 11.4, "legal: out of bounds\npast the line", color=GREEN, fs=7.0, weight="bold")
say(fld, p, -9.4, 15.6, "grounding: lands\nshort of the line", color=RED, fs=7.0, weight="bold")
say(fld, p, 2.4, 10.2, "line of scrimmage", color=style.OFFENSE_DARK, fs=6.4)
say(fld, p, -11.0, 0.0, "tackle box", color=PURPLE, fs=6.6)
xs_, ys_ = XY(fld, p, -4.0, 25.0)
axes[1].text(xs_, ys_, "out of bounds", rotation=90, ha="center", va="center", fontsize=6.0, color=style.INK_2)
axes[1].set_title("Outside the tackle box", loc="left", fontsize=8.6, fontweight="bold", color=style.INK)
fig.tight_layout(w_pad=0.6)
plt.show()
Figure 26.9: Where a throwaway is legal. Left: inside the tackle box (shaded), a quarterback under pressure must throw in the direction and vicinity of an eligible receiver; throwing it over Y’s head, near him, is a legal throwaway (green), while throwing it to an empty part of the field is intentional grounding (red). Right: once he is outside the tackle box, the ball only has to land at or beyond the line of scrimmage, so throwing it out of bounds past the line is legal (green), but a throw that lands short of the line, near nobody, is still grounding (red). The ball is on the right hash, so the right sideline is close.

The rule rewards exactly the skills this chapter has been describing. A quarterback who climbs and slides inside the pocket can always find a covered receiver to throw near; a quarterback who escapes the pocket earns the right to throw it into the stands. The quarterback who gets in trouble is the one who stays in the box with nobody near him, panics, and flings it at the turf, or who, outside the box, tries to throw it away on the run and doesn’t get it back to the line.

The most famous grounding call of recent decades shows how expensive the panic is. On the New England Patriots’ first offensive play of Super Bowl XLVI, against the New York Giants in February 2012, Tom Brady dropped back from his own 6-yard line with Justin Tuck bearing down on him, and threw deep down the middle to nobody. The flag came out; Brady had thrown it from his own end zone, so the grounding penalty was a safety, and the Giants, who had punted on their own opening drive, led 2-0. They won by four.9

The checkdown: risk management, not cowardice

The last read in most progressions is the checkdown: a short, safe throw, usually to a running back or tight end a few yards past the line, underneath all the coverage. It is the quarterback giving up on the big play to take a small, nearly certain one. Fans boo it. Coaches mostly don’t, and the data says why.

Show the Python
E = END.sort_values("epa", ascending=True)
fig, ax = plt.subplots(figsize=(6.5, 3.4))
ys = np.arange(len(E))
cols = [style.SERIES[0] if v >= 0 else style.SERIES[7] for v in E.epa]
cols = [style.MUTED if n.startswith("Designed") else c for n, c in zip(E.index, cols)]
ax.barh(ys, E.epa, color=cols, height=0.62)
for y, (n, r_) in zip(ys, E.iterrows()):
    x = r_.epa
    lab = f"{x:+.2f}   ({r_.share:.1%} of dropbacks)"
    if x < -2.5:      # long bar: label inside it
        ax.text(x + 0.1, y, lab, va="center", ha="left", fontsize=6.9, color="white", fontweight="bold")
    else:
        ax.text(x + (0.06 if x >= 0 else -0.06), y, lab, va="center", ha="left" if x >= 0 else "right",
                fontsize=6.9, color=style.INK)
ax.set_yticks(ys, E.index, fontsize=7.4)
ax.axvline(0, color=style.BASELINE, lw=1)
ax.set_xlim(-4.7, 2.1)
ax.grid(axis="y", visible=False)
ax.tick_params(axis="x", labelsize=7.2)
ax.set_xlabel("EPA per dropback", fontsize=7.8)
fig.tight_layout()
plt.show()
Figure 26.10: How dropbacks end, and what each ending is worth: every regular-season dropback from 2022 to 2025, grouped by FTN’s charting of where the ball went (best average outcome at the top, worst at the bottom; labels give the share of all dropbacks). A quarterback scramble tops the list partly by selection: quarterbacks run when the coverage has left open grass, and when it hasn’t, the play lands in the throwaway, sack or interception rows instead. That is why off-script football can look great here and still cost value on average. Among throws, the first read is worth the most and each step down the progression gives up value; the checkdown is worth about nothing on average, which is a great deal better than the three endings below it. A throwaway costs a down, a sack a down and yards, an interception the ball. Interceptions and throwaways are counted on their own, whichever read they were aimed at, so the read rows are completions and incompletions only. Data: FTN charting and nflverse play-by-play via nflreadpy.

Read Figure 26.10 from the middle out. A checkdown is worth, on average, +0.03 EPA: essentially nothing. It is caught 82% of the time, thrown about 0.6 yards past the line, and gains most of what it gains after the catch (6.3 yards on average). That looks like a wasted play only if you compare it to the first read. The real comparison is to what happens to a quarterback who doesn’t check the ball down when the first reads are covered: he holds it, and the next three bars are waiting. The checkdown is the cheapest way to end a play that has failed.

That is what risk management means at quarterback. Each read in the progression has an expected value and a risk of disaster, and the checkdown is the read with the least of both. A quarterback who takes it at the right moment gives up a little upside to cut off the tail of sacks and interceptions, and over a season that tail decides games.

The right moment depends on the situation, and the third-down numbers show how much (2022–2025, share of plays that gained the first down, and EPA):

Third down, and the ball goes… Converted EPA per play
7+ yards to go: to the first read 43% –
7+ yards to go: checked down 24% -0.20
7+ yards to go: sacked 0% -1.79
3 or fewer to go: checked down 58% –

On third-and-long, a checkdown usually means a punt. Even so it beats the sack: when the ball is going to be punted anyway, the yards the back gains after the catch still flip a little field position, while a sack can push a field goal out of range. On third-and-short the same throw is a good bet. Good quarterbacks know the down and distance on every snap and adjust: checkdowns early, when four yards on first down keeps the offense on schedule, patience on third-and-long, when only a throw past the sticks will do.

CautionCommon misconception

“Checking it down is what scared quarterbacks do.” Every great quarterback checks the ball down, often, and good offenses design their checkdowns to gain yards: a back who catches it in space with one linebacker to beat. The difference between a good checkdown and a bad one is when: a checkdown after the deeper reads were honestly covered is a good decision; a checkdown on the first beat of a play, before the deep reads had time to open, is a quarterback who has stopped trusting his protection. Watch where his eyes were before he threw it.

Measuring a quarterback: time to throw, aDOT, CPOE and EPA

You can now describe a quarterback’s process. The data can describe his style and his results, and four numbers do most of the work.

  • Time to throw (seconds from the snap to the release) says how long he holds the ball: quick and decisive, or patient and hunting.
  • aDOT (average depth of target) says how far downfield he aims: underneath and safe, or deep and aggressive.
  • CPOE says how accurate he is, after allowing for how hard his throws were.
  • EPA per dropback says what it all adds up to: the points his dropbacks added, sacks, scrambles and turnovers included.

The first two describe style, and neither is good or bad on its own; the last two describe results. All four are public: the Next Gen Stats passing leaderboard (https://nextgenstats.nfl.com/stats/passing) has time to throw, air yards and its own CPOE for every starter, and rbsdm.com (https://rbsdm.com/stats/), built on the same nflverse data as this chapter, has EPA per dropback and CPOE, so you can profile this Sunday’s quarterback before kickoff. The one that needs explaining is CPOE.

CPOE: completion percentage, corrected for difficulty

Completion percentage is the oldest quarterback number and one of the most misleading, because it rewards easy throws. A quarterback who checks the ball down all day completes 75% of his passes; one who throws deep outs and posts completes 60% and may be the better passer. CPOE, or completion percentage over expected, fixes this by comparing each throw with a model’s estimate of how often a throw like it is completed. The nflverse model behind the numbers here knows each throw’s depth, which part of the field it went to, the down and distance, and whether the quarterback was hit as he threw;10 the Next Gen Stats version also knows, from tracking data, how far the receiver was from the nearest defender.11 A deep out to the sideline might have an expected completion rate of 45%; a checkdown to the flat, 85%. Complete the deep out and you gain 55 percentage points over expected; drop the checkdown and you lose 85. Average those differences over a season and you have CPOE.

Show the Python
C = Q.sort_values("cpoe")
fig, ax = plt.subplots(figsize=(6.5, 0.17 * len(C) + 0.9))
ys = np.arange(len(C))
for y, (_, r_) in zip(ys, C.iterrows()):
    col = style.SERIES[0] if r_.cpoe >= 0 else style.SERIES[7]
    ax.plot([100 * r_.xcomp, 100 * r_.comp], [y, y], color=col, lw=2.2, alpha=0.7, solid_capstyle="round")
    ax.plot([100 * r_.xcomp], [y], "o", ms=5, mfc=style.SURFACE, mec=style.INK_2, mew=1.0)
    ax.plot([100 * r_.comp], [y], "o", ms=5, color=col, mec="white", mew=0.6)
    ax.text(1.06, y, f"{r_.cpoe:+.1f}", va="center", ha="right", fontsize=6.6, color=col,
            transform=ax.get_yaxis_transform(), clip_on=False)
ax.set_yticks(ys, [f"{r_.label} ({'LAR' if r_.team == 'LA' else r_.team})" for _, r_ in C.iterrows()], fontsize=6.6)
lo_x = 5 * np.floor(100 * min(C.comp.min(), C.xcomp.min()) / 5) - 1
hi_x = 5 * np.ceil(100 * max(C.comp.max(), C.xcomp.max()) / 5) + 1
ax.set_xlim(lo_x, hi_x)
ax.xaxis.set_major_formatter(plt.FuncFormatter(lambda v, _: f"{v:.0f}%"))
ax.set_xlabel("completion % (hollow: expected, filled: actual)", fontsize=7.8)
ax.tick_params(axis="x", labelsize=7.2)
ax.grid(axis="y", visible=False)
ax.text(1.06, len(C) - 0.1, "CPOE", ha="right", va="bottom", fontsize=7, color=style.INK_2, fontweight="bold",
        transform=ax.get_yaxis_transform(), clip_on=False)
ax.set_ylim(-0.8, len(C) + 0.4)
fig.tight_layout()
plt.show()
Figure 26.11: Completion percentage and what was expected, 2025 regular season (quarterbacks with at least 300 dropbacks). Each row is a quarterback: the hollow circle is the completion rate the nflverse model expected from his throws, the filled circle is what he actually completed, and the line between them is his CPOE, the number on the right. Rows are sorted by CPOE. Two quarterbacks with the same completion percentage can be far apart once difficulty is counted: find Brock Purdy and Jared Goff, whose filled circles sit at almost the same spot, about 72%, and whose hollow circles do not. Data: nflverse play-by-play (cp and complete_pass) via nflreadpy.

In 2025 the spread was enormous: Drake Maye completed +10.8 percentage points over expectation, and Michael Penix -3.6. Purdy and Goff show why the correction matters: Purdy completed 72.4% of his passes and Goff 72.0%, but Purdy’s throws were expected to be completed only 65% of the time and Goff’s 70%, so Purdy’s CPOE was +7.2 and Goff’s +1.8. Goff threw shorter (an aDOT of 6.5 yards to Purdy’s 7.6).

CPOE is the best single public measure of accuracy, and it is steadier from season to season than completion percentage, but read it with three cautions. It credits the quarterback for everything that is not in the model: a receiver who makes a great catch, or a scheme that gets receivers wide open, raises it. It says nothing about where the ball went, only whether it was caught: a quarterback who throws safely and accurately to covered checkdowns can post a fine CPOE and a poor offense. And check what each version does with throwaways. They count as incompletions in plain completion percentage, which punishes a smart habit, but the nflverse model leaves most of them out, because only 18% of charted throwaways get a completion probability.

CautionCommon misconception

“Completion percentage measures accuracy.” It measures accuracy and the difficulty of the throws a quarterback chooses, mixed together, and difficulty dominates. A conservative passer and an aggressive one can be equally accurate and be ten points apart in completion percentage. If you want accuracy, look at CPOE; if you want to know how aggressive he is, look at aDOT; if you want to know whether it worked, look at EPA.

Whose sack is it?

Sacks are charged to the quarterback’s line in the box score, but a good share of them belong to him. FTN’s charters, who watch every sack on video, marked 40% of the sacks from 2022 to 2025 as the quarterback’s fault: held the ball too long, drifted into the rush, missed the hot throw. A cleaner number separates the line’s share from his: pressure-to-sack rate, the share of the dropbacks on which he was pressured that ended in a sack. The line decides how often he is pressured; what happens next is mostly him. From 2023 to 2025 pressured quarterbacks were sacked 22% of the time, but among those with 200 or more pressured dropbacks the rate ran from 13% (Nix) to 30% (Levis).12 That spread is pocket presence, measured. The Pass Rush takes up why pressure is the steadier number and the sack the noisier one.

The style map

Put time to throw and aDOT on two axes and color each quarterback by his EPA per dropback, and quarterback styles separate into families.

Show the Python
from matplotlib.colors import TwoSlopeNorm, LinearSegmentedColormap

cmap = LinearSegmentedColormap.from_list("epa", [style.SERIES[7], "#f0efec", style.SERIES[0]])
norm = TwoSlopeNorm(vmin=-0.2, vcenter=0.0, vmax=0.3)
fig, ax = plt.subplots(figsize=(6.5, 4.9))
sc = ax.scatter(Q.avg_time_to_throw, Q.adot, c=Q.epa, cmap=cmap, norm=norm, s=46, edgecolor=style.INK_2, lw=0.5,
                zorder=3)
mx, my = Q.avg_time_to_throw.mean(), Q.adot.mean()
ax.axvline(mx, color=style.BASELINE, lw=1, ls=(0, (4, 2)), zorder=1)
ax.axhline(my, color=style.BASELINE, lw=1, ls=(0, (4, 2)), zorder=1)
x0, x1 = Q.avg_time_to_throw.min() - 0.08, Q.avg_time_to_throw.max() + 0.1
y0, y1 = Q.adot.min() - 0.5, Q.adot.max() + 0.6
ax.set_xlim(x0, x1)
ax.set_ylim(y0, y1)
corner = dict(fontsize=7.6, color=style.MUTED, fontweight="bold")
ax.text(x0 + 0.02, y1 - 0.12, "quick and deep:\nthe rhythm shooter", va="top", ha="left", **corner)
ax.text(x1 - 0.02, y1 - 0.12, "holds it and hunts deep:\nthe aggressive creator", va="top", ha="right", **corner)
ax.text(x0 + 0.02, y0 + 0.12, "quick and short:\nthe distributor", va="bottom", ha="left", **corner)
ax.text(x1 - 0.02, y0 + 0.12, "holds it, throws short:\nthe patient (or stuck) passer", va="bottom", ha="right",
        **corner)
# labels: greedy placement around each point, avoiding earlier labels and the points
fig.canvas.draw()
ren = fig.canvas.get_renderer()
placed = []
pts_disp = ax.transData.transform(np.column_stack([Q.avg_time_to_throw, Q.adot]))
OFFS = [(5, 3), (5, -9), (-5, 3), (-5, -9), (6, -3), (-6, -3), (0, 6), (0, -12), (9, 8), (-9, 8), (9, -13), (-9, -13)]
for i, (_, r_) in enumerate(Q.sort_values("db", ascending=False).iterrows()):
    for dx, dy in OFFS:
        t = ax.annotate(r_.label, (r_.avg_time_to_throw, r_.adot), xytext=(dx, dy), textcoords="offset points",
                        fontsize=6.6, color=style.INK, ha="left" if dx > 0 else ("right" if dx < 0 else "center"),
                        zorder=4)
        bb = t.get_window_extent(ren).expanded(1.04, 1.1)
        hits_pts = any(bb.contains(px, py) for px, py in pts_disp)
        hits_lab = any(bb.overlaps(o) for o in placed)
        inside = ax.get_window_extent(ren).contains(bb.x0, bb.y0) and ax.get_window_extent(ren).contains(bb.x1, bb.y1)
        if not hits_pts and not hits_lab and inside:
            placed.append(bb)
            break
        t.remove()
cb = fig.colorbar(sc, ax=ax, fraction=0.035, pad=0.02)
cb.set_label("EPA per dropback", fontsize=7.6)
cb.ax.tick_params(labelsize=7)
ax.set_xlabel("average time to throw (seconds)", fontsize=8.4)
ax.set_ylabel("aDOT (average air yards per throw)", fontsize=8.4)
ax.tick_params(labelsize=7.6)
fig.tight_layout()
plt.show()
Figure 26.12: Quarterback style map, 2025 regular season: every quarterback with at least 300 dropbacks. Horizontal: average time to throw (Next Gen Stats, snap to release, sacks excluded). Vertical: aDOT, the average air yards of his throws. Color: EPA per dropback (sacks, scrambles and turnovers included), blue above zero and red below. The dashed lines are the averages of the quarterbacks shown, splitting the map into four styles. Every red dot is in the bottom half, but so are some of the season’s best quarterbacks: style alone doesn’t decide success. Data: nflverse play-by-play and Next Gen Stats via nflreadpy.

The four corners of Figure 26.12 are four ways to play the position.

  • The distributor (quick and short) wins with decisions: the ball comes out on rhythm to the first open receiver, and the offense lives on YAC. He rarely takes sacks, and his ceiling depends on how many yards his receivers make after the catch.
  • The rhythm shooter (quick and deep) is the hardest style to play, because deep routes take time to develop; it takes anticipation and a scheme that gets receivers vertical fast.
  • The aggressive creator (holds it, throws deep) extends plays to give deep routes time, with a pocket he manages and an off-script game he trusts. His sack rate is higher and his big-play rate too.
  • The patient or stuck passer (holds it, throws short) is the corner to worry about: the ball comes out late and short, which usually means the first reads were not open and the quarterback ended up checking it down after a long look. Sometimes that is a scheme or a line problem rather than a quarterback problem, but it is rarely a good sign.

In 2025 the league MVP, Matthew Stafford, was a rhythm shooter: top-left corner, an aDOT of 9.3 yards at 2.80 seconds, and +0.24 EPA per dropback. Drake Maye, the runner-up, threw just as deep but held the ball longer, top-right, with the league’s best CPOE and its best EPA per dropback (+0.33). Brock Purdy sits at the far right, just below the average depth line, holding the ball longer than almost anyone, and still posted +0.22: the patient passer, not the stuck one.

In 2025 the top half of the map had no red at all; every quarterback below zero threw short. That fits the argument of Dropback Concepts that value lives downfield. But the bottom half also holds Purdy, Josh Allen and Jared Goff, three of the year’s better seasons. Style tells you how a quarterback plays; the color tells you whether it worked.

Why the quarterback dominates value

One last number explains why everything in this chapter matters so much, and why the quarterback is paid like no one else on the field.

First, he touches the ball on almost every play, and on most of them he decides where it goes: in 2025, 60% of NFL offensive plays were dropbacks.

Second, passing plays swing further than running plays. Play by play, the EPA of a 2025 dropback had a standard deviation of 1.57 points, against 1.00 for a designed run: dropbacks hold the touchdowns and the interceptions. And the player who decides which way they swing differs more from team to team. Among 2025 starters with 300 or more dropbacks, average EPA per dropback had a standard deviation of 0.110; among ball carriers with 150 or more carries, average EPA per carry had one of 0.075. Multiply by a season’s workload (536 dropbacks for the median qualifying quarterback, 232 carries for the median back) and a quarterback one standard deviation above average is worth about 59 points over a season, against about 17 for a back.13

Points become wins at a steady rate: since 2012, each extra win has taken about 35 points of season scoring margin.14 So that quarterback is worth something like 1.7 wins a year over an average starter, and the back about 0.5. Over a replacement, the backup a team could sign off the street, the quarterback’s margin is larger still, because the gap between an average starter and a backup is wider at quarterback than anywhere else (the Purdy and Mac Jones numbers in the film room are one example).

All of these numbers share credit with the blockers, the receivers and the play-caller, and the split is unknowable from the box score. But the gap is too big to be an accident. The quarterback is the player with the most chances to add or lose points, on the plays where the most points are at stake. That is the starting point of Building a Roster, where the salary cap turns this gap into the most important economic fact in the sport.

Film room

NoteFilm room: Tom Brady and Peyton Manning — the pocket as a craft

Peyton Manning, Denver Broncos, 2013. Manning set the single-season records for touchdown passes (55) and passing yards (5,477) in 2013, at 37.15 The number that explains how is a quieter one: he was sacked on 2.7% of his dropbacks, the lowest rate of any regular starter that season (the league rate was 6.4%).16 Manning had famously little interest in running. His pocket skill was making the decision before the rush arrived: an elaborate pre-snap read (The Pre-Snap Phase) that told him which side to work, then a progression run so fast that the ball was usually gone before pressure mattered.

Tom Brady, 2000-2022. Watch any Brady dropback and look at his feet, not his arm. When the edge closed he moved up into the pocket a step or two, rarely more, and kept his eyes downfield; when an interior rusher came he slid a yard and reset. He did it so economically that it looks like nothing happened, which is the point: a quarterback who climbs well never seems to be under pressure at all. He also got rid of the ball. Even at 44 and 45 his average time to throw was among the quickest in the league: 2.50 seconds in 2021 (rank 2 of 30 qualifying passers) and 2.45 in 2022 (rank 1 of 30).17

What to notice: neither man was a scrambler, and both were extraordinarily hard to sack. Pocket skill is mostly decision speed and footwork, not escapability.

NoteFilm room: Chiefs 2018 and Bills 2024 — the off-script pair

Patrick Mahomes’s left hand. Monday night, October 1, 2018, at Denver. The Chiefs trailed 23-20 with 3:14 left and faced third-and-5. Mahomes, in his first season as a starter, was flushed to his left, and Von Miller was dragging him down. On the way to the ground he switched the ball to his left hand and flipped it to Tyreek Hill for six yards and a first down. Kansas City scored the winning touchdown seven plays later. Asked years later for the most impressive play of Mahomes’s career, Andy Reid chose that one: a short throw, made with the wrong hand, with the game on the line.18

Josh Allen’s MVP season. Allen won the 2024 MVP award with a season that mixed both halves of this chapter.19 He is enormous (listed at 6-foot-5 and about 237 pounds), fast, and has one of the strongest arms in the league, which together make the scramble drill unusually dangerous: he can throw 50 yards on the run, so the deep defenders cannot come up to chase him, and he can run through a tackle, so the underneath defenders cannot drop off to cover. His early career was full of off-script plays that went wrong (forced throws across his body, sacks taken while looking for something big). The MVP version still improvises, but the throwaway and the checkdown are in his game too.

What to notice: the two quarterbacks at the top of Figure 26.8 got there in different ways. Mahomes’s off-script value is mostly made of throws like the left-handed flip: short, ugly and converting, from a body going the wrong way. Allen’s is power: a throw nobody else can make on the run, or a run nobody wants to tackle. What they share is the part that took Allen years: off-script talent becomes off-script value only with the judgment to give up on a play.

NoteFilm room: New York Giants 2007 season, Super Bowl XLII — the escape

February 3, 2008. The Giants trailed the unbeaten Patriots 14-10 and faced third-and-5 at their own 44 with 1:15 left. Eli Manning took the snap in the shotgun and the pocket collapsed: Jarvis Green and Richard Seymour got their hands on his jersey and should have had the sack. Manning spun free, backed out of the pile, and threw high down the middle to David Tyree, who had worked back into the middle of the field when his route was disrupted. Tyree caught it against his helmet with Rodney Harrison draped over him, at the Patriots’ 24. The Giants scored four plays later, on Plaxico Burress’s 13-yard touchdown catch, and won 17-14.20

What to notice: escape and scramble drill together. Manning was no runner; he simply refused to go down and kept his eyes downfield while he got loose. Tyree did what the drill tells a receiver to do when the play breaks: get to open grass in the quarterback’s line of sight, on a different level from everyone else.

NoteFilm room: Cincinnati Bengals 2024 — Joe Burrow’s timing

Joe Burrow led the NFL in passing yards (4,918) and touchdown passes (43) in 2024.21 Burrow can extend plays (he is one of the few quarterbacks above the diagonal in Figure 26.8), but that season was built in the pocket, on the gifts of the first half of this chapter: anticipation (throwing outs and digs before the receiver breaks, especially to Ja’Marr Chase, whose timing he has known since they were college teammates at LSU) and subtle movement in the pocket (a step up, a half-slide) that buys the extra beat for the throw.

What to notice: on replays of Bengals completions, freeze the frame at the release and find the receiver. Very often he hasn’t turned around yet.

NoteFilm room: San Francisco 49ers 2023 — Brock Purdy and the system-quarterback debate

Brock Purdy was the last player chosen in the 2022 draft, number 262, the pick nicknamed “Mr. Irrelevant.”22 In 2023, his first full season as the starter, he led the 33 quarterbacks with 250 or more dropbacks in EPA per dropback (+0.32, rank 1) and in the nflverse model’s CPOE (rank 1). The argument about him since has been whether those numbers are his or Kyle Shanahan’s: a scheme famous for motion, play-action and receivers who run after the catch, with Christian McCaffrey, Deebo Samuel, Brandon Aiyuk and George Kittle to throw to. Is he a “system quarterback”?

The data cuts both ways, which is why the argument never ends. Against the “YAC merchant” version of the charge: in 2023, 48% of his receiving yards came after the catch, about the league rate (48%), and his aDOT (8.2) was above the league average (7.8). He was throwing downfield and completing it. For the charge: the two CPOE models disagreed. The nflverse model, which doesn’t know how open the receiver was, ranked him first; Next Gen Stats’ model, which does, ranked him 5th. That is still very good, but the gap between the two rankings is the point: part of what the first model credited to Purdy’s accuracy was receivers being open, which the scheme creates. And in 2025, when a turf-toe injury cost Purdy eight games,23 his backup Mac Jones ran the same offense at +0.15 EPA per dropback, against Purdy’s +0.22: worse, but far better than Jones’s -0.06 across 2022–2024 with New England and Jacksonville.

What to notice: the question “is it the quarterback or the system?” is the right one to ask, and CPOE models that know about separation help answer it. But a quarterback who throws on time to the right read is part of what makes a system work. Watch Purdy’s anticipation on intermediate throws and judge for yourself.

Watch for it on Sunday

TipWatch for it

Watch the eyes. On replays, especially the end-zone camera that looks straight at the quarterback, count how many reads he got through before he threw.

  • Find his first look. Where do his eyes (and his helmet’s facemask) point as his drop ends? That is usually read 1. Did the ball go there? More than half the time it does.
  • Count the head turns and hitch steps. Each snap of the head to a new area, usually with a hitch step, is a new read. One look and a throw: a rhythm throw. Two or three: a progression. A long stare at one side: he was waiting for something to open, he was confused, or he was holding a defender with his eyes on purpose, a look-off (Concepts Versus Coverages). His eyes can lie; the ball can’t.
  • Note the time. Count “one-Mississippi” from the snap. Ball out by about two and a half: quick game or first read. Out after three: off schedule, or a deep shot with max protection.
  • Watch his feet when the edge closes. Step up (climb)? Slide? Or drift back into the rush? The good ones move forward.
  • When he leaves the pocket, watch the receivers. Did the deep ones come back and the short ones go deep? Is everyone moving toward his side? That is the scramble drill. Who got open?
  • Judge the ending. A checkdown on third-and-12 is a punt with a few extra yards; on first-and-10 it is a fine play. A throwaway when nothing is there is a small win. A sack after a long hold in a clean pocket is on the quarterback; a sack that arrives before he could have thrown is on the protection.

For your prediction log: for one quarter, write down for each dropback how many reads the quarterback got through (1, 2, 3, or “off script”), and whether the ball came out on time. By the end you will know which kind of quarterback you are watching.

Predict the play

TipPredict the play 1: the pocket at two seconds
Show the Python
S = {"LT": (-7.0, -4.9), "LG": (-2.0, -1.8), "C": (-1.9, 0.0), "RG": (-3.9, 1.6), "RT": (-7.0, 4.9),
     "QB": (-8.0, 0.0), "LE": (-9.0, -6.2), "NT": (-0.4, -0.9), "DT": (-2.3, 2.1), "RE": (-9.0, 6.2)}
off = [O(k, k, *S[k], "C" if k == "C" else ("Q" if k == "QB" else "")) for k in OL_KEYS + ["QB"]]
dfn = [D(k, "DE" if k in ("LE", "RE") else "DT", *S[k], "E" if k in ("LE", "RE") else "T")
       for k in ("LE", "NT", "DT", "RE")]
p = Play(off, dfn, los=LOS, to_go=10)
p.highlight("QB")
fld = p.draw(lateral=(-12, 12), window=(-12.2, 3.6), title=False, width_in=5.8)
clean(fld.ax)
for sg in (-1, 1):
    arrow(fld, p, [(1.0, 5.4 * sg), (-3.0, 7.2 * sg), (-7.0, 7.4 * sg), (-9.0, 6.2 * sg)], color=style.DEFENSE,
          lw=1.2, head=7, z=3, alpha=0.8)
arrow(fld, p, [(1.0, 2.35), (-1.5, 2.15)], color=style.DEFENSE, lw=1.2, head=7, z=3, alpha=0.8)
plt.show()
Figure 26.13: Freeze frame, about two seconds after the snap. Both ends have run their arcs and the tackles have pushed them deep. On the right, the defensive tackle has driven the right guard back about two yards. The left side of the interior is holding. The quarterback (ringed) is at his launch point, eight yards deep.

Where should the quarterback go: back, up, left, right, or out of the pocket?

Up, and a little to his left: climb into the space behind the left guard and the center. Read the rush picture piece by piece. The ends have been pushed past him, so drifting back walks him into them, and escaping outside means running around a rusher who is already at his depth. The right side of the interior is caving: the tackle has the right guard two yards deep, so stepping straight up, or up and right, takes him into that collision. The left side of the interior is holding firm, and that is where the cup still has room.

So he climbs at an angle, a step or two up and half a step left, which is a climb and a slide at once. He doesn’t have to leave the pocket, his eyes stay downfield, and he is still inside the tackle box, so if nothing is open he can still throw it near a covered receiver or check it down. Escaping would only be right if the left side collapsed too.

TipPredict the play 2: third-and-9 at the 31
Show the Python
off = line() + [O("QB", "QB", -6.5, 10.5, "Q"), O("Y", "TE", 5.0, 8.0), O("Z", "WR", 12.0, 19.5)]
dfn = [D("RE", "DE", -8.0, 7.2, "E"), D("MIKE", "MIKE", 5.6, 9.2, "M"), D("RCB", "CB", 13.4, 19.0, "C"),
       D("WILL", "WILL", 1.8, 12.6, "W"), D("FS", "FS", 17.5, 9.0, "FS")]
p = Play(off, dfn, los=79, ball_y="right", to_go=9)
p.highlight("QB")
fld = p.draw(lateral=(-10, 27), window=(-12.0, 21.0), title=False, width_in=5.8)
clean(fld.ax)
tackle_box(fld.ax, fld, p, -12.0)
arrow(fld, p, [(-8.0, 0.0), (-7.6, 5.0), (-6.5, 10.5)], color=GREEN, lw=1.4, ls=(0, (3, 2)), head=9)
arrow(fld, p, [(-8.4, 4.0), (-8.0, 7.2)], color=style.DEFENSE, lw=1.2, head=7, alpha=0.8)
say(fld, p, -10.6, 0.0, "tackle box", color=PURPLE, fs=6.6)
say(fld, p, 9.0, 2.0, "line to gain", color=style.INK_2, fs=6.6)
say(fld, p, 1.7, -3.4, "ball at the opponent's 31", color=style.OFFENSE_DARK, fs=6.4)
say(fld, p, 4.0, 25.4, "out of\nbounds", color=style.INK_2, fs=6.2, box=False)
plt.show()
Figure 26.14: Third-and-9 at the opponent’s 31-yard line, so a field goal would be about a 48-yard try. The quarterback has escaped to his right, outside the tackle box (shaded), with the right end chasing him. Z, his only receiver on that side, is running down the sideline with the cornerback on top of him; Y is covered underneath by the Mike. There is no running room to the first-down line: the Will has come up to spy him (to mirror the quarterback and tackle him if he runs).

What should the quarterback do with the ball, and is it legal?

Throw it away, out of bounds past the line of scrimmage, and kick the field goal. It is legal because he is outside the tackle box. Walk through his options. Z is covered with the cornerback on top, so a throw to him is a contested catch at best and an interception at worst, and an interception here throws away a likely three points. Y is covered by the Mike underneath. Running gets nowhere near the line to gain with the Will spying him (shadowing him, ready to tackle). The sack is the disaster that’s easiest to miss: an eight-yard sack pushes a 48-yard field goal back to about 56 yards, where even good kickers miss often.

So the ball goes out of bounds beyond the line of scrimmage, ideally over Z’s head toward the sideline. From inside the tackle box that throw would have to land near an eligible receiver; from where he is, outside the box, it only has to reach the line. A throwaway costs a down, which on third down he was about to lose anyway, and keeps every yard. That is exactly the bargain in Figure 26.10: a throwaway at about -0.8 EPA against a sack at about -1.8.

TipPredict the play 3: he breaks left
Show the Python
off = line() + [O("QB", "QB", -6.8, -8.6, "Q"), O("X", "WR", 19.0, -15.5), O("H", "WR", 5.0, -8.0),
                O("Y", "TE", 12.0, 2.0), O("Z", "WR", 12.0, 14.5)]
p = Play(off, [], los=LOS, to_go=None)
p.highlight("QB")
fld = p.draw(lateral=(-21, 19), window=(-11.0, 24.5), title=False, width_in=5.8)
clean(fld.ax)
called = {"X": [(-0.7, -15.0), (19.0, -15.5)], "H": [(-1.5, -8.5), (5.6, -8.5), (5.0, -8.0)],
          "Y": [(-1.5, 8.5), (12.0, 8.5), (12.0, 2.0)], "Z": [(-0.7, 15.0), (13.0, 15.0), (12.0, 14.5)]}
for k, pts in called.items():
    xy = [XY(fld, p, *q) for q in pts]
    xs, ys = zip(*xy)
    fld.ax.plot(xs, ys, color=style.MUTED, lw=1.1, ls=(0, (2, 2)), zorder=2)
arrow(fld, p, [(-8.0, 0.0), (-7.8, -4.0), (-6.8, -8.6)], color=GREEN, lw=1.4, ls=(0, (3, 2)), head=9)
say(fld, p, -9.3, -3.0, "he breaks left", color=GREEN, fs=7.0, weight="bold")
plt.show()
Figure 26.15: The called play has broken down and the quarterback (ringed) has escaped to his LEFT. The receivers are shown where they are at that moment, with their called routes as grey dashes: X (left outside) was running a go and is 19 yards deep; H (left slot) was on a 5-yard hitch route; Y was running a 12-yard dig from the right that has reached the middle; Z (right outside) was running a 12-yard curl on the far side. The quarterback is right-handed. The defense is not shown.

Using the scramble-drill rules, where should each of X, H, Y and Z go, and where will the quarterback look first?

X comes back down the left sideline; H takes off deep; Y keeps crossing to the left at his depth; Z works back across the field, deep. The quarterback looks first at the throws he can make on the move, X coming back and Y crossing toward him, and takes the deep shot to H or Z only if he can set his feet.

Settle each receiver by side first, then depth.

  • X is on the scramble side and deep, so he comes back: he stops his go route and works back toward the line of scrimmage along the left sideline, settling at about 8 to 10 yards, into the space his cornerback (running deep with him) leaves behind.
  • H is on the scramble side and short, so he goes deep: he turns up the field from his hitch route, behind the defenders who have turned to chase the quarterback.
  • Y is a crosser, already moving toward the scramble side, so he keeps crossing toward the quarterback, at about 12 yards: a level between X coming back and H going deep.
  • Z is on the backside. Standing on a curl 30 yards from the quarterback is useless, so he runs back across the field, deep, into the middle the defense has emptied, and into the quarterback’s line of sight.

That gives four receivers at four depths: H deep, Z deep and crossing, Y at about 12, X at 8 to 10, all moving toward the quarterback’s side. Now the look order. He is right-handed and rolling left, so before he can throw at all he has to turn his hips and square his shoulders back toward the target. The short, firm throws toward the sideline he is running at, X coming back and Y crossing, are the ones he can make like that. H and Z are the big play, worth it only if the rush gives him time to stop and set. If all four are covered he has two good options left: run, if there is grass to the line to gain, or throw it out of bounds past the line; he is outside the tackle box. What he must not do is throw back across his body to the right.

Takeaways

  • The quarterback’s eyes run on his feet’s clock. Protection check at the snap, read 1 on the last step of the drop, a hitch step for each later read, then the checkdown. Each later read costs about half a second, and value.
  • Each read is answered by a defender, and the ball leaves before the receiver is open. The quarterback looks at the key defender for each read. An anticipation throw, released before the break, beats the defenders’ reaction; a “see it, throw it” ball arrives a hitch step late into a closing window.
  • Pocket movement is a response to the rush picture. Edges pushed past: climb. Pressure from one side: slide. Interior collapsed: escape, usually outside the tackle on the side the end was run past. Eyes stay downfield throughout.
  • Off-script football is coached. The scramble drill (short goes deep, deep comes back, go where he goes, backside comes across deep, the back leaks out) turns a broken play into a second play. Off script is high variance: for most quarterbacks it nets out below the pocket; for a few (Allen, Mahomes) it adds value.
  • Bad endings have a price list. A checkdown is worth about nothing, a throwaway a down, a sack a down and yards, an interception the ball. Throwaways must avoid intentional grounding: near a receiver inside the tackle box, past the line of scrimmage outside it.
  • Four numbers profile a passer. Time to throw and aDOT describe style; CPOE (completions over expected) and EPA per dropback describe results. Because he decides most plays, and passing varies more than running, the quarterback swings more points than anyone else on the field.

Connections

Builds on: Passing Game Fundamentals (the drop as a clock, progressions, aDOT); Pass Protection (the pocket as a cup, the launch point, time to throw and pressure); The Quick Game (rhythm throws and the pre-snap read); and Dropback Concepts (the numbered reads of each concept).

Sets up: Offensive Systems and Play-Calling Languages, where the progression becomes part of a system’s language; Concepts Versus Coverages, which teaches how the quarterback reads coverage and moves safeties with a look-off (his eyes pointing one way to move a defender before he throws the other); Disguise, Rotation, and Split-Field Coverage, on defenses built to make the pre-snap read wrong and the post-snap read late; Officiating, Replay, and the Rules of the Ball, for intentional grounding and the tackle box in full; The Pass Rush, on rush lanes and why pressure is a steadier number than sacks; and Building a Roster, on what the quarterback’s value means under a salary cap.

Case study: Andy Reid’s Chiefs, an offense built to make the most of a quarterback who is as dangerous off script as on it.

NoteGo deeper
  • Ron Jaworski with Greg Cosell and David Plaut, The Games That Changed the Game (2010): seven landmark game plans broken down from the coaches’ film, by a former NFL quarterback and two NFL Films producers who spent careers studying all-22 tape.
  • Chris B. Brown, The Art of Smart Football (2015) and the Smart Football archive: short essays on progressions, reads and how quarterbacks are taught to see the field.
  • The all-22 “coaches film” on NFL+ Premium, which shows every player on every snap from above: the only way to check a quarterback’s progression against what was actually open.
  • Ben Baldwin’s open-source work on the nflverse CPOE and EPA models (Open Source Football), and the Next Gen Stats passing leaderboards for time to throw, aggressiveness and completion probability.

  1. Author’s calculation from Next Gen Stats receiving data via nflverse (load_nextgen_stats, regular seasons 2018–2025, receivers and tight ends with at least 50 targets): a least-squares fit of catch percentage on average separation and average intended air yards. NGS defines separation as “the distance (in yards) measured between a WR/TE and the nearest defender at the time of catch or incompletion”: https://nflreadr.nflverse.com/articles/dictionary_nextgen_stats.html. This is a season-average relationship across receivers, not a play-by-play estimate.↩︎

  2. FTN charting via nflverse (is_qb_out_of_pocket), merged with nflverse play-by-play; regular-season dropbacks 2022–2025, spikes removed. (A spike is the quarterback throwing the ball straight into the ground just after the snap to stop a running clock; the grounding rule allows it, so it is not a real dropback. See The Clock, the Two-Minute Drill, and Fourth Down; Daniel Tomaro, “What Is Intentional Grounding in the NFL?,” Pro Football Network, February 8, 2026: https://www.profootballnetwork.com/nfl-intentional-grounding-penalty-details/.) “Out of the pocket” includes designed rollouts as well as escapes. The pressure comparison uses participation data’s was_pressure flag, 2023–2025 only, when FTN supplies it; every sack in those seasons is flagged as pressured. Author’s calculation via nflreadpy. FTN data dictionary: https://nflreadr.nflverse.com/articles/dictionary_ftn_charting.html; participation: https://nflreadr.nflverse.com/articles/dictionary_participation.html.↩︎

  3. The rules here are the common core described in coaching and scouting glossaries, for example Inside the Pylon / Scouting Academy, “ITP Glossary: Scramble Drill” (https://scoutingacademy.com/itp-glossary-scramble-drill/), and Win With The Pass, “The Scramble Drill” (https://winwiththepass.com/?p=3138): “If you are shallow go deep, If you are deep come back,” get into the quarterback’s field of vision. Each team’s version differs in its details.↩︎

  4. NFL, 2026 Official Playing Rules, Rule 8, Section 3, Article 1 (“Ineligible Player Downfield”: on a play with a legal forward pass, a foul if the entire body of an ineligible offensive player is more than one yard beyond the line of scrimmage before the pass is thrown, unless he is still blocking a defender he engaged at the line; five yards from the previous spot): https://operations.nfl.com/rules-officiating/2026-nfl-rulebook. RPOs covers the rule and its exception.↩︎

  5. NFL Rule 8, Section 1: a legal forward pass must be thrown from in or behind the neutral zone; a forward pass thrown from beyond the line is illegal (five yards from the spot of the pass and loss of down). NFL rulebook: https://operations.nfl.com/the-rules/nfl-rulebook/.↩︎

  6. Pro Football Hall of Fame, “Fran Tarkenton” (career 1961–1978 with the Vikings and Giants; four touchdown passes and a touchdown run off the bench in the Vikings’ first game, a 37–13 win over Chicago; records at retirement; the quoted line on scrambling; his style “often frustrated Vikings coach Norm Van Brocklin, who favored a more traditional approach”): https://www.profootballhof.com/players/fran-tarkenton/. His father, Dallas Tarkenton, was a Pentecostal minister, and he grew up in Athens, Georgia; the Van Brocklin feud ended with Tarkenton’s trade demand and Van Brocklin’s resignation in February 1967: “Fran Tarkenton,” Wikipedia, https://en.wikipedia.org/wiki/Fran_Tarkenton.↩︎

  7. Pro Football Hall of Fame, “John Elway” (“the only player in National Football League history to pass for more than 3,000 yards and rush for more than 200 yards in the same season seven consecutive times”; “Elway’s record 47 fourth quarter game-winning or game-tying drives”): https://www.profootballhof.com/players/john-elway/; and “Steve Young” (“4,239 rushing yards and 43 rushing touchdowns, both the second-most by a quarterback in pro football history at the time of his retirement”): https://www.profootballhof.com/players/steve-young/.↩︎

  8. NFL Rulebook, Rule 8, Section 2 (intentional grounding): a foul if a passer facing an imminent loss of yardage throws a pass not in the direction and vicinity of an originally eligible receiver; not called when a passer who is or has been outside the pocket area throws a pass that lands at or beyond the line of scrimmage extended, including out of bounds. Since the 2024 rulebook a passer counts as outside the pocket area if any part of his body or the ball is outside it (Rule 3, Section 25 defines the pocket area as between the outside edges of the normal tackle positions). Penalty: loss of down and 10 yards from the previous spot, or loss of down at the spot of the pass if that is worse; a safety if the passer’s entire body and the ball are in his own end zone. 2026 Official Playing Rules, Rule 8, Section 2, Article 1 (Item 1 and Penalty): https://operations.nfl.com/the-rules/nfl-rulebook/. On the 2024 wording change: Mike Florio, “New rule book subtly makes it easier for quarterbacks to throw ball away,” ProFootballTalk, July 31, 2024: https://www.nbcsports.com/nfl/profootballtalk/rumor-mill/news/new-rule-book-subtly-makes-it-easier-for-quarterbacks-to-throw-ball-away.↩︎

  9. “Super Bowl XLVI,” Wikipedia (the Giants’ opening drive ended in a punt that pinned the Patriots at their own 6; Brady flagged for intentional grounding in the end zone under pressure from Justin Tuck, a safety; Giants won 21–17): https://en.wikipedia.org/wiki/Super_Bowl_XLVI.↩︎

  10. Ben Baldwin, “nflfastR EP, WP, and CP models,” Open Source Football (Sept 2020), describing the completion-probability model behind the cp and cpoe columns: https://www.opensourcefootball.com/posts/2020-09-28-nflfastr-ep-wp-and-cp-models/. The model’s features: air yards (and whether they are zero), distance to the sticks, yard line, down, yards to go, pass location (middle or not), whether the quarterback was hit, home team, roof type and era.↩︎

  11. NFL.com, “Next Gen Stats: Introduction to Completion Probability” (more than ten tracking-based factors, including the distance between the receiver and the nearest defender, the distance to the nearest pass rusher and the air distance of the pass): https://www.nfl.com/news/next-gen-stats-introduction-to-completion-probability-0ap3000000964655.↩︎

  12. Author’s calculation via nflreadpy. QB-fault sacks: FTN charting is_qb_fault_sack, regular seasons 2022–2025. Pressure-to-sack rate: sacks divided by dropbacks with participation was_pressure true, regular seasons 2023–2025, quarterbacks with at least 200 pressured dropbacks; spikes removed.↩︎

  13. Author’s calculation, nflverse play-by-play, 2025 regular season: EPA per dropback (sacks and scrambles included) for passers with at least 300 dropbacks; EPA per carry on non-dropback runs for ball carriers with at least 150 carries. A rough comparison of spreads, not a measure of individual credit; see Expected Points, EPA, and Success Rate.↩︎

  14. Author’s calculation from nflverse schedules (load_schedules), regular seasons 2012–2025: a least-squares fit of team wins (ties as half) on season point differential across all team-seasons.↩︎

  15. “2013 Denver Broncos season,” Wikipedia (Manning’s 55 touchdown passes and 5,477 passing yards, both single-season records): https://en.wikipedia.org/wiki/2013_Denver_Broncos_season. Both still stood after the 2025 season (author’s check of nflverse player stats: the closest since are Mahomes’s 50 touchdowns in 2018 and Brady’s 5,316 yards in 2021; Brees’s 5,476 in 2011 fell one yard short).↩︎

  16. Author’s calculation, nflverse play-by-play, 2013 regular season: sacks per dropback, passers with at least 300 dropbacks.↩︎

  17. Next Gen Stats average time to throw (snap to release, sacks excluded), regular season, passers with at least 300 attempts, via nflverse/nflreadpy (load_nextgen_stats).↩︎

  18. CBS Sports, “Patrick Mahomes’ greatest play was this short throw, according to Chiefs coach Andy Reid” (third-and-5, 3:14 left, trailing 23–20; six-yard completion to Tyreek Hill while being dragged down by Von Miller; winning touchdown seven plays later): https://www.cbssports.com/nfl/news/patrick-mahomes-greatest-play-was-this-short-throw-according-to-chiefs-coach-andy-reid. Final score 27–23: ESPN, https://africa.espn.com/nfl/story/_/id/24866002/patrick-mahomes-sparks-chiefs-comeback-win-broncos.↩︎

  19. NFL.com, “Bills QB Josh Allen wins 2024 AP NFL Most Valuable Player award”: https://www.nfl.com/news/bills-qb-josh-allen-wins-2024-ap-nfl-most-valuable-player-award. Height and weight (6-foot-5, 237 pounds) as listed on the 2025 roster in nflverse (load_rosters).↩︎

  20. “Helmet Catch,” Wikipedia (third-and-5 at the Giants’ 44 with 1:15 left; Seymour and Green nearly sacked Manning; Tyree thrown off his route and drifting to the middle; a 32-yard completion; Tyree pinned the ball to his helmet with Rodney Harrison defending; Patriots leading 14–10): https://en.wikipedia.org/wiki/Helmet_Catch. That article puts the ball at the Patriots’ 25, but a 32-yard gain from the Giants’ 44 ends at the 24, which is where “David Tyree,” Wikipedia, puts it (“moved the Giants to the Patriots’ 24-yard line with 59 seconds left … Four plays and 24 seconds later, Manning threw a 13-yard touchdown pass to Plaxico Burress”): https://en.wikipedia.org/wiki/David_Tyree.↩︎

  21. Passing yards and touchdown passes from nflverse play-by-play, 2024 regular season (author’s calculation via nflreadpy; Burrow ranked first in both, with 4,918 yards and 43 touchdowns). Burrow and Chase were teammates at LSU in 2018 and 2019.↩︎

  22. NFL.com, “Mr. Irrelevant: 49ers select Iowa St. QB Brock Purdy to end 2022 NFL Draft”: https://www.nfl.com/news/mr-irrelevant-49ers-select-iowa-st-qb-brock-purdy-to-end-2022-nfl-draft. The 2023 and 2025 numbers are the author’s calculations from nflverse play-by-play and Next Gen Stats.↩︎

  23. Purdy played in 9 of the 49ers’ 17 games in 2025 (nflverse player stats). On the turf-toe injury, suffered in Week 1 and aggravated in his Week 4 return: NBC Bay Area, “John Lynch states Brock Purdy, 49ers ‘encouraged’ by QB’s toe injury progress,” October 23, 2025: https://www.nbcbayarea.com/news/sports/nfl/san-francisco-49ers/brock-purdy-john-lynch-2/3969197/.↩︎