from gridiron.data import pbp, nfl
PCOLS = ["season_type", "game_id", "play_id", "posteam", "defteam", "play_type", "down", "ydstogo",
"yardline_100", "pass", "half_seconds_remaining", "score_differential", "qtr", "fixed_drive", "time",
"yrdln", "desc", "yards_gained", "posteam_score", "defteam_score", "air_yards", "yards_after_catch",
"qb_dropback", "run_location", "pass_location"]
def load_season(season):
"""Runs and dropbacks with FTN charting (alignment, motion, tempo, rushers) and participation
(personnel, box count, coverage label) attached."""
d = pbp([season], columns=PCOLS)
d = d[d.play_type.isin(["pass", "run"]) & d.down.notna()].copy()
f = nfl().load_ftn_charting([season]).to_pandas()
f = f.rename(columns={"nflverse_game_id": "game_id", "nflverse_play_id": "play_id"})
f["play_id"] = f.play_id.astype(float)
f = f[["game_id", "play_id", "starting_hash", "qb_location", "n_offense_backfield", "is_motion", "is_no_huddle",
"is_play_action", "is_screen_pass", "n_pass_rushers", "n_blitzers", "read_thrown",
"is_qb_out_of_pocket"]]
p = nfl().load_participation([season]).to_pandas()
p = p.rename(columns={"nflverse_game_id": "game_id"})
p["play_id"] = p.play_id.astype(float)
p = p[["game_id", "play_id", "offense_personnel", "defense_personnel", "defenders_in_box",
"defense_coverage_type", "defense_man_zone_type", "time_to_throw", "was_pressure", "route"]]
d["play_id"] = d.play_id.astype(float)
return d.merge(f, on=["game_id", "play_id"], how="left").merge(p, on=["game_id", "play_id"], how="left")
def count_pos(s, keys):
if not isinstance(s, str):
return np.nan
c = {k: int(v) for v, k in re.findall(r"(\d+) ([A-Z]+)", s)}
return sum(c.get(k, 0) for k in keys)
def pers_code(s):
"""FTN-era personnel string ('1 C, 2 G, 1 QB, 1 RB, 2 T, 1 TE, 3 WR') -> '11'."""
if not isinstance(s, str):
return "na"
c = {k: int(v) for v, k in re.findall(r"(\d+) ([A-Z]+)", s)}
code = f"{c.get('RB', 0) + c.get('FB', 0)}{c.get('TE', 0)}"
return code if code in ("10", "11", "12", "13", "21", "22", "01", "02") else "other"
def features(d):
d = d.copy()
dist = pd.cut(d.ydstogo, [0, 1, 2, 3, 6, 10, 99], labels=["1", "2", "3", "4-6", "7-10", "11+"]).astype(str)
d["dd"] = d.down.astype(int).astype(str) + "_" + dist
d["zone"] = pd.cut(d.yardline_100, [0, 5, 10, 20, 40, 60, 80, 100],
labels=["g5", "g10", "rz", "o40", "mid", "own40", "own20"]).astype(str)
d["score"] = pd.cut(d.score_differential, [-99, -9, -1, 0, 8, 99],
labels=["d9+", "d1-8", "tie", "u1-8", "u9+"]).astype(str)
d["late"] = np.where(d.half_seconds_remaining <= 120, "late", "no")
d["q4score"] = np.where(d.qtr >= 4, "q4", "no") + d["score"]
d["pers"] = d.offense_personnel.map(pers_code)
backs = d.n_offense_backfield.fillna(-1).clip(upper=2).astype(int).astype(str)
d["formation"] = d.qb_location.fillna("na").astype(str) + backs
d["motion"] = d.is_motion.fillna(False).astype(bool).astype(str)
d["tempo"] = d.is_no_huddle.fillna(False).astype(bool).astype(str)
d["box"] = pd.cut(d.defenders_in_box.fillna(-1), [-2, 0, 5, 6, 7, 99],
labels=["na", "5-", "6", "7", "8+"]).astype(str)
d["dbs"] = d.defense_personnel.map(lambda s: count_pos(s, ("CB", "FS", "SS", "S", "DB")))
return d
# The checklist steps that can be measured in public data, in checklist order
STEPS = [("Situation", ["dd", "zone", "score", "late", "q4score"]),
("+ Personnel", ["pers"]),
("+ Formation", ["formation"]),
("+ Motion and tempo", ["motion", "tempo"]),
("+ Box count", ["box"])]
def design(d, cats, levels=None):
if levels is None:
levels = {c: sorted(d[c].unique()) for c in cats}
cols = [np.ones(len(d))]
for c in cats:
for lv in levels[c][1:]:
cols.append((d[c].values == lv).astype(float))
return np.column_stack(cols), levels
def fit(X, y, lam=1.0, iters=30):
"""Ridge-penalised logistic regression by Newton's method (numpy only)."""
w = np.zeros(X.shape[1])
for _ in range(iters):
p = 1 / (1 + np.exp(-X @ w))
H = X.T @ (X * (p * (1 - p))[:, None]) + lam * np.eye(X.shape[1])
w -= np.linalg.solve(H, X.T @ (p - y) + lam * w)
return w
def nested(train):
models, cats = [], []
for name, cs in STEPS:
cats = cats + cs
X, lv = design(train, cats)
models.append((name, list(cats), lv, fit(X, train["pass"].values.astype(float))))
return models
def predict(model, d):
_, cats, lv, w = model
X, _ = design(d, cats, lv)
return 1 / (1 + np.exp(-X @ w))
S24 = features(load_season(2024))
S25 = features(load_season(2025))
TR = S24[S24.season_type == "REG"]
TE = S25[S25.season_type == "REG"]
# What each step is worth: learn on 2024, score on 2025
MODELS = nested(TR)
WORTH = [dict(step="Always guess pass", acc=float(TE["pass"].mean()), hi=np.nan, hi_acc=np.nan)]
for m in MODELS:
p_ = predict(m, TE)
y_ = TE["pass"].values
right = (p_ >= 0.5) == y_
hi = np.maximum(p_, 1 - p_) >= 0.8
WORTH.append(dict(step=m[0], acc=float(right.mean()), hi=float(hi.mean()), hi_acc=float(right[hi].mean())))
W = {w_["step"]: w_ for w_ in WORTH}
# Confidence bands after the first three steps (situation, personnel, formation)
pF = predict(MODELS[2], TE)
cF = np.maximum(pF, 1 - pF)
rF = (pF >= 0.5) == TE["pass"].values
BANDS = []
for lo, hi_ in [(0.5, 0.6), (0.6, 0.7), (0.7, 0.8), (0.8, 0.9), (0.9, 1.01)]:
m_ = (cF >= lo) & (cF < hi_)
BANDS.append(dict(lo=lo, hi=min(hi_, 1.0), share=float(m_.mean()), acc=float(rF[m_].mean()),
conf=float(cF[m_].mean())))
def prate(mask):
return float(TE.loc[mask, "pass"].mean())
RATE = dict(
all=prate(TE.down > 0),
first10=prate((TE.down == 1) & (TE.ydstogo == 10)),
third7=prate((TE.down == 3) & TE.ydstogo.between(7, 10)),
third46gun11=prate((TE.down == 3) & TE.ydstogo.between(4, 6) & (TE.pers == "11") & (TE.qb_location == "S")),
third3gun=prate((TE.down == 3) & (TE.ydstogo == 3) & (TE.qb_location == "S")),
third12uc=prate((TE.down == 3) & TE.ydstogo.between(1, 2) & (TE.qb_location == "U")),
third12gun=prate((TE.down == 3) & TE.ydstogo.between(1, 2) & (TE.qb_location == "S")),
second12=prate((TE.down == 2) & TE.ydstogo.between(1, 2)),
second12gun11=prate((TE.down == 2) & TE.ydstogo.between(1, 2) & (TE.pers == "11") & (TE.qb_location == "S")),
second12gun11nh=prate((TE.down == 2) & TE.ydstogo.between(1, 2) & (TE.pers == "11")
& (TE.qb_location == "S") & (TE.tempo == "True")),
first21uc=prate((TE.down == 1) & (TE.ydstogo == 10) & (TE.pers == "21") & (TE.qb_location == "U")),
first21uc8=prate((TE.down == 1) & (TE.ydstogo == 10) & (TE.pers == "21") & (TE.qb_location == "U")
& (TE["box"] == "8+")),
first12uc=prate((TE.down == 1) & (TE.ydstogo == 10) & (TE.pers == "12") & (TE.qb_location == "U")),
empty=prate(TE["formation"] == "S0"),
gun=prate(TE.qb_location == "S"), pistol=prate(TE.qb_location == "P"), uc=prate(TE.qb_location == "U"),
nohuddle=prate(TE.tempo == "True"),
)
LOOK6 = ((TE.down == 2) & TE.ydstogo.between(1, 2) & (TE.pers == "11") & (TE.qb_location == "S")
& (TE.n_offense_backfield == 1) & (TE.motion == "True") & (TE["box"] == "6"))
RATE["look6"] = prate(LOOK6)
N_LOOK6 = int(LOOK6.sum())
N_SECOND12NH = int(((TE.down == 2) & TE.ydstogo.between(1, 2) & (TE.pers == "11") & (TE.qb_location == "S")
& (TE.tempo == "True")).sum())
N_FIRST21UC8 = int(((TE.down == 1) & (TE.ydstogo == 10) & (TE.pers == "21") & (TE.qb_location == "U")
& (TE["box"] == "8+")).sum())
# Rushers and coverage on dropbacks (2025 regular season)
DBK = S25[(S25.season_type == "REG") & (S25.qb_dropback == 1) & (S25.n_pass_rushers > 0)].copy()
DBK["five"] = (DBK.n_pass_rushers >= 5).astype(float)
COV = DBK[DBK.defense_man_zone_type.isin(["MAN_COVERAGE", "ZONE_COVERAGE"])].copy()
COV["man"] = (COV.defense_man_zone_type == "MAN_COVERAGE").astype(float)
def rush5(mask):
return float(DBK.loc[mask, "five"].mean())
RUSH = dict(all=rush5(DBK.down > 0), third46=rush5((DBK.down == 3) & DBK.ydstogo.between(4, 6)),
third7=rush5((DBK.down == 3) & DBK.ydstogo.between(7, 10)))
MAN_ALL = float(COV["man"].mean())
MAN_RZ = float(COV.loc[COV.yardline_100 <= 20, "man"].mean())
MAN_OPEN = float(COV.loc[COV.yardline_100 > 20, "man"].mean())
def uc_habits(T):
"""How often a team lines up under center, throws from there, and makes those throws play-action."""
U = T[T.qb_location == "U"]
return dict(share=float((T.qb_location == "U").mean()), pass_=float(U["pass"].mean()),
pa=float(U.loc[U["pass"] == 1, "is_play_action"].mean()))
UC_LG, UC_LA = uc_habits(TE), uc_habits(TE[TE.posteam == "LA"])
M12 = (TE.down == 1) & (TE.pers == "12") & (TE.qb_location == "U")
RATE["first12uc_sea"] = prate(M12 & (TE.posteam == "SEA"))
RATE["first12uc_all"] = prate(M12)
N_12UC_SEA = int((M12 & (TE.posteam == "SEA")).sum())
# Field or boundary? FTN's hash is from the offense's view (screens go to the wide side, away from it)
HSH = TE[TE.starting_hash.isin(["L", "R"])]
TO_FIELD = (((HSH.starting_hash == "L") & (HSH.run_location.eq("right") | HSH.pass_location.eq("right")))
| ((HSH.starting_hash == "R") & (HSH.run_location.eq("left") | HSH.pass_location.eq("left"))))
WIDE = HSH.run_location.isin(["left", "right"]) | HSH.pass_location.isin(["left", "right"])
SCR = WIDE & (HSH.is_screen_pass == True) # noqa: E712
RUNW = WIDE & (HSH.play_type == "run")
FIELD = dict(screen=float(TO_FIELD[SCR].mean()), run=float(TO_FIELD[RUNW].mean()))
def brier(c, q):
"""Expected Brier score per call when you say c and the call comes true at rate q."""
return q * (1 - c) ** 2 + (1 - q) * c ** 2
def buckets(d):
out = pd.DataFrame(index=d.index)
dist = pd.cut(d.ydstogo, [0, 3, 6, 99], labels=["1-3", "4-6", "7+"]).astype(str)
out["b_dd"] = d.down.astype(int).clip(upper=3).astype(str) + "_" + dist
out["b_rz"] = np.where(d.yardline_100 <= 20, "rz", "open")
out["b_db"] = d.dbs.fillna(5).clip(4, 6).astype(int).astype(str)
return out
for t_ in (DBK, COV):
t_[["b_dd", "b_rz", "b_db"]] = buckets(t_)
def lookup(table, col, row, nmin=60):
"""Base rate for this down-distance, field zone and number of DBs, backing off to coarser cells."""
for keys in (("b_dd", "b_rz", "b_db"), ("b_dd", "b_rz"), ("b_dd",)):
m_ = np.ones(len(table), bool)
for k in keys:
m_ &= table[k].values == row[k]
if m_.sum() >= nmin:
return float(table.loc[m_, col].mean())
return float(table[col].mean())
# ------------------------------------------------------------------ the film-room drive
DRV = S25[(S25.game_id == "2025_21_LA_SEA") & (S25.posteam == "SEA") & (S25.fixed_drive == 15)]
DRV = DRV.sort_values("play_id").reset_index(drop=True)
TRACE = nested(pd.concat([TR, TE])) # trained on the 2024 and 2025 regular seasons
for i_, m in enumerate(TRACE):
DRV[f"p{i_}"] = predict(m, DRV)
DRV["p"] = DRV[f"p{len(TRACE) - 1}"]
def conf_of(p):
return int(np.clip(5 * round(100 * max(p, 1 - p) / 5), 50, 90))
DRV["call"] = np.where(DRV.p >= 0.5, "Pass", "Run")
DRV["conf"] = DRV.p.map(conf_of)
bk = buckets(DRV)
DRV["p_man"] = [lookup(COV, "man", bk.loc[i_]) for i_ in DRV.index]
DRV["p_five"] = [lookup(DBK, "five", bk.loc[i_]) for i_ in DRV.index]
DRV["cov_call"] = np.where(DRV.p_man >= 0.5, "man", "zone")
DRV["cov_conf"] = DRV.p_man.map(conf_of)
DRV["rush_call"] = np.where(DRV.p_five >= 0.5, "5+", "4")
DRV["rush_conf"] = DRV.p_five.map(conf_of)
DRV["is_pass"] = DRV["pass"] == 1
DRV["hit"] = (DRV.call == "Pass") == DRV.is_pass
DRV["cov_hit"] = np.where(DRV.is_pass, (DRV.cov_call == "man") == (DRV.defense_man_zone_type == "MAN_COVERAGE"), np.nan)
DRV["rush_hit"] = np.where(DRV.is_pass, (DRV.rush_call == "5+") == (DRV.n_pass_rushers >= 5), np.nan)
RESULT = {2550: "Holani run, +3", 2572: "Walker run, −2", 2594: "Kupp catch, +12 (1st)",
2619: "Walker run, +11 (1st)", 2641: "Holani catch, +9", 2666: "Smith-Njigba screen, +12 (1st)",
2691: "Play-action, Barner +7", 2716: "Incomplete to Bobo", 2739: "Kupp, 13-yard TD"}
DRV["result"] = DRV.play_id.map(lambda x: RESULT[int(x)])
D = DRV.set_index(DRV.index + 1) # D.loc[3] is the third snap of the drive
N_HIT = int(DRV.hit.sum())
N_L2 = int(np.nansum(DRV.cov_hit) + np.nansum(DRV.rush_hit))
N_L2_ALL = int(2 * DRV.is_pass.sum())
N_COV = int(np.nansum(DRV.cov_hit))
N_RUSH = int(np.nansum(DRV.rush_hit))
N_PASS = int(DRV.is_pass.sum())
_a8 = int(D.loc[8, "air_yards"])
AIR8 = f"{abs(_a8)} yard{'s' if abs(_a8) != 1 else ''} {'behind' if _a8 < 0 else 'past'} the line"
Q90 = BANDS[4]["acc"]
BRIER = dict(c90=brier(0.90, Q90), honest=brier(Q90, Q90), c99=brier(0.99, Q90))
def snap_conf(i):
return f"{D.loc[i, 'call'].lower()}, {D.loc[i, 'conf']}%"