---
title: Pair player from-plays rate numerator and denominator play populations
type: task
status: completed
description: >-
  Player EPA columns count two-point conversions through a per-column play-type set (non_nullified),
  while every rate denominator stays a fixed unit per label. The set enters both the table alias and
  the output-aggregator batch key, which also fixes a live team-side batch collision.
base_uri: user:task/league/data-views/pair-player-from-plays-rate-populations.md
created_at: '2026-09-11T16:53:17.176Z'
entity_id: 5793d66e-3f94-4f62-80a8-c4dec0250553
finished_at: '2026-10-23T16:16:47.330Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
public_read: true
relations:
  - follows [[user:task/league/data-views/fix-from-plays-rate-population-mismatch.md]]
  - relates [[█████████████████████-██████████-███████████-███-████████-█████████████]]
  - relates [[█████████████████████████-████-████-███████████-████-███████████████]]
  - relates [[user:task/league/data-views/disambiguate-opportunity-column-vocabulary.md]]
started_at: '2026-10-23T15:54:17.141Z'
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-10-23T18:47:16.352Z'
visibility_analyzed_at: '2026-11-05T23:48:56.281Z'
---

## Overview

- Player from-plays populations are ruled PER MEASURE (operator, 2026-09-11): passing and receiving stats do not count CONV, EPA may, fumbles keep special teams.
- EPA is the one measure that moves. The four player EPA columns move from `stat_countable` to `non_nullified`, which adds CONV and nothing else in practice.
- Every rate denominator stays a fixed unit per label (operator, 2026-10-23). The gap this leaves is the vendor's own shape and is surfaced, not divided away.
- The play-type set must enter BOTH batching hashes. Doing that fixes a live team-side collision as well.
- This entity is the one survivor of [[user:task/league/data-views/fix-from-plays-rate-population-mismatch.md]]. Do not execute that task's plan.

## End state

### Numerators

| column | set |
| --- | --- |
| `player_total_expected_points_added_from_plays` | `non_nullified` |
| `player_passing_expected_points_added_from_plays` | `non_nullified` |
| `player_rushing_and_receiving_expected_points_added_from_plays` | `non_nullified` |
| `player_quarterback_epa_from_plays` | `non_nullified` |
| every other player from-plays column | unchanged (`stat_countable`; fantasy points `non_nullified` already) |

- `non_nullified` rather than a new pass, rush and CONV set: over 2016-2025 REG only 5 special-teams rows credit a passer, ball carrier or target. Returners are not credited.
- The success-rate ratio columns stay on `stat_countable`. `is_successful_play` is null on every CONV row, so widening them would deflate the rate.
- The 156 special-teams `qb_pid` rows on 2025 REG are all one pid on B. Pinion kicks and holds. That is an attribution defect for [[█████████████████████████-████-████-███████████-████-███████████████]], not EPA scope. `non_nullified` keeps them, exactly as `stat_countable` did.

### Denominators

- `per_player_play` stays scrimmage snaps. Action periods in `per-player.mjs` stay `stat_countable`.
- Why fixed: nflverse `calculate_stats` books `passing_epa` WITH two-point EPA over attempts WITHOUT them. The measure contract makes the denominator a unit of the subject grain, and the team lane ruled one population per label.
- Size: CONV snaps would move anyone's EPA per play by at most 0.0007 on 2025 REG. The numerator change moves a quarterback by up to 0.005.
- The mismatch is surfaced through the client notice the operator ruled on 2026-09-11 (follow-on, reads the shared declaration below).

### Hashes

- **Table alias.** `generate_table_alias` in the player factory gains a `__<set>` suffix only for a non-default set, so default CTE names stay byte-identical.
- **Batch key.** `compute_group_key` (`output-aggregator/measure-batch.mjs`) hashes `apply_filters.toString()`. A closure reading a set variable prints the same text for every set, so differently-filtered columns batch into one rate CTE. The set must enter the key explicitly, only when non-default.
- The team factory has this collision live today: `team_play_count_from_plays` (`non_nullified`) and `team_pass_yards_from_plays` (scrimmage) share a key. Its spec co-requests only the season render.

### Known disagreements, recorded not fixed

- Team EPA stays scrimmage, so player EPA summed over a team no longer equals team EPA.
- The game sample stays PASS and RUSH (`libs-server/game-sample/load-game-sample-player-play-aggregates.mjs`).

## Notes

- Deploys are human-gated. The result cache keys on table state, not SQL, and prior-year results cache 30 days, so the deploy needs a cache flush for the EPA columns.
- `player-quarterback-dropbacks-and-epa-shared-cte.json` pins a shared CTE this change splits. Rewrite its name and description to pin the split rather than re-blessing it.
- The opportunity columns are the subject of [[user:task/league/data-views/disambiguate-opportunity-column-vocabulary.md]]; check its state before re-blessing an opportunity fixture.
- Bless goldens with `NODE_ENV=test node scripts/update-data-view-snapshots.mjs <fixture>.json`, naming each fixture.
- Run the golden runner in a detached worktree at HEAD before attributing a red to this change.

## Plan

- [x] **Settle the end state.** Research and fresh review 2026-10-23; operator approved.
- [x] **Sequence behind the CONV attribution fix.** Cleared: 2025 REG CONV rows now carry `qb_pid` 127, `passer_pid` 96, `target_pid` 90 and `ball_carrier_pid` 32 of 128.
- [x] **Failing-first specs.** `test/data-views.player-epa-play-population.spec.mjs` (season and rate co-requests, fixed denominator, filter params on the widened closure); the team spec gains a rate-path co-request, red at HEAD (team_offensive_play_count per team_play read 3, not 1).
- [x] **Batch key folds a non-default set.** `compute_group_key` reads `column_def.play_type_set`; default closures stay verbatim.
- [x] **Player factory `play_type_set`.** Threaded to the with-statement and a separate non-default rate closure, folded into the alias; four EPA columns on `non_nullified`.
- [x] **Shared declaration.** The four EPA descriptions name the two-point population. A structured client field is left to [[user:task/league/data-views/surface-rate-population-mismatch-notice.md]], its first reader.
- [x] **Goldens.** Three moved (alias hash plus the CONV member); the shared-CTE fixture is renamed `player-quarterback-dropbacks-and-epa-split-cte.json`. The multi-role and dropback-period specs now encode EPA counting CONV.
- [x] **Gate set green.** Data-view queries and specs 3,333 passing; `yarn verify` 22 checks. Mutations of the EPA opt-in, the alias suffix, the batch-key fold and the rate filter-param call each go red.
- [x] **Deploy note and export check.** No `league-data` export or `check-published-dataset` names the EPA columns; the benchmark ground truth reads coverage-charted plays, which no CONV row carries.

## Outcome

- Landed in league as merge `b47d328a3` (work `8ea8ed39f`). Not deployed: deploys are human-gated.
- After deploy, flush cached view results: `node scripts/flush-view-cache.mjs --namespace data-views`, then with `--apply`. Prior-year results otherwise serve pre-change EPA for up to 30 days.
- The `qb_pid` special-teams misattribution routes to [[user:task/league/fix-special-teams-qb-pid-attribution.md]].
