---
title: Fix qb_pid attribution on special-teams plays
type: task
status: completed
description: >-
  nfl_plays.qb_pid names a QB-listed coverage player on special-teams plays he did not quarterback,
  so player quarterback EPA credits him kickoff and punt EPA. Repair how qb_pid is derived rather
  than excluding special teams.
base_uri: user:task/league/fix-special-teams-qb-pid-attribution.md
created_at: '2026-10-23T16:16:28.139Z'
entity_id: fb468266-267d-4992-868d-1393a9d00731
finished_at: '2026-10-24T18:22:59.655Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - relates [[█████████████████████████-████-████-███████████-████-███████████████]]
  - relates [[user:task/league/data-views/pair-player-from-plays-rate-populations.md]]
started_at: '2026-10-24T18:06:40.772Z'
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-10-24T18:22:59.655Z'
visibility_analyzed_at: '2026-11-05T23:49:29.202Z'
---

## Overview

- `nfl_plays.qb_pid` is set on 1,127 REG special-teams rows (KOFF, PUNT, FGXP, FREE) across all seasons. 401 of them name Feleipe Franks, a QB-listed player who plays special-teams coverage, on plays where the kicker, punter or holder is someone else.
- `player_quarterback_epa_from_plays` credits that EPA to him: 156 rows and -42.9 EPA on 2025 REG. Dropbacks, sacks and pressures are shielded by their own flags; EPA is not.
- Some seasons show genuine quarterback holders, so a blanket special-teams exclusion is the wrong repair. The defect is how `qb_pid` is derived.

## Plan

- [x] Find the writer. `scripts/populate-qb-pid.mjs`, called on every play import from `scripts/process-plays.mjs`, has a snap derivation that names any QB-listed player on the field. On a kick, that player is on the coverage unit.
- [x] Decide the rule. On KOFF, PUNT, FGXP and FREE plays, `qb_pid` is the passer, else a QB-listed ball carrier on a flagged fake (`is_fake_punt` or `is_fake_field_goal`), else NULL. The fake guard keeps a QB-listed returner from being credited. Real fakes are almost all typed PASS or RUSH and never reach the rule.
- [x] Repair the writer. League `498e7a2cb` skips the snap derivation on special-teams plays and lets the writer clear a special-teams row. Spec `test/scripts.populate-qb-pid-special-teams.spec.mjs` failed first. Full suite 17,454 passing.
- [x] Backfill after the deploy: `node scripts/populate-qb-pid.mjs --all --dry-run`, then without `--dry-run`. Production imports run the old writer until then and would re-add the rows. Dry-run SELECT: 1,404 rows cleared (1,125 REG, 262 PRE, 17 POST), none rewritten. `qb_pid` is derived, so preserve-unique-data does not apply.
- [x] Flush the data-views cache after the backfill, then verify Franks on 2025 REG. Expected: 158 rows and -41.60 EPA become 2 rows and +1.27. The two kickoff-return lateral passes (2016 T. Hilton, 2017 T. Cohen) keep their `qb_pid`.

## Outcome

- Deployed in league `029a03ded`. The fixed writer ran per season for 2016-2026 on the league host and cleared 1,404 special-teams rows, matching the dry run exactly.
- The same pass also filled 700 and rewrote 54 PRESEASON scrimmage and no-play rows. That is the unchanged general rule reaching rows past runs had not, not this fix.
- Franks on 2025 REG now reads 2 rows and +1.27 quarterback EPA. Exactly 2 special-teams REG rows keep a `qb_pid`, the two kickoff-return lateral passes.
- Data-views cache flushed: 18 of 18 keys.
