---
title: Fix the sportradar punt and return player gates that never fire
type: task
status: planned
description: >-
  map_punt_stats and map_return_stats write their metrics but never their player ids, because the
  nested punter and returner sub-objects they gate on do not exist in the sportradar feed. The same
  defect was already found and fixed once in the same file for tackle-for-loss. Blocked on observing
  the feed shape.
base_uri: user:task/league/fix-sportradar-punt-return-player-gates.md
created_at: '2026-09-21T17:01:08.459Z'
entity_id: 5a579df2-8904-4c4b-a380-97a382491f17
owner_identity_uri: █████████████████████████
public_read: true
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-09-21T17:01:13.814Z'
visibility_analyzed_at: '2026-11-05T23:49:29.953Z'
---

## Overview

`map_punt_stats` and `map_return_stats` in the sportradar play mapper write their METRIC fields but never their PLAYER ids. The punter and returner role columns on `nfl_plays` hold zero rows across all 1,501,369 plays while the importer is demonstrably live.

## Evidence

Measured 2026-09-21 against production:

- `punt_yards` 2,018 rows and `return_yards` 17,507 rows, so both mappers RUN and their metric writes land.
- `punter_pid`, `punter_gsis`, `punter_sportradar_player_id`, `returner_pid`, `returner_gsis`, `returner_sportradar_player_id` all read **0**.
- The importer is live overall: `penalty_sportradar_player_id` holds 3,259 rows and `kicker_sportradar_player_id` 2,929, both 2025 only.

So the branches are reached and their gates never pass. Both gate on a nested sub-object — `punt_stats.punter` and `return_stats.returner` — that the feed does not appear to carry.

## This is a known shape in this exact file

The tackle-for-loss branch carries a comment recording the same defect already found and fixed once:

> Sportradar tags TFL credit on the defense statistic (stat_type='defense', tlost=1), not on the tackle_detail. Previous gate on tackle_detail.loss never fired because that field doesn't exist in the feed -- 1197 tfl=true plays in 2025 REG had no tackle_for_loss_*_pid written.

The fix pattern is therefore already demonstrated in the same module: read the player off the `statistics` array filtered by `stat_type`, not off a nested detail object.

## Why this is blocked

The sportradar `stat_type` vocabulary is **not observable from this repo**. `'defense'` is the only value attested anywhere in the tree, there is no cached feed and no fixture pins the punt or return shape.

Guessing fails in the silent direction, which is the trap this codebase has now hit twice: a wrong spelling matches nothing, writes no player, and reads as a clean no-op rather than a suppression. Whoever holds the feed can settle it in one look; nobody without it should write the gate.

## Do NOT drop these columns

An earlier reading of this data concluded the columns were dead schema and should be retired. That was wrong, and the reasoning is worth keeping: a scan for rows found zero, but the writer is live, so absence of data is a question rather than an answer. Dropping them would delete the schema for a feature that is supposed to work and hide the bug.

## Related

`sack_player_2` and `tackle_for_loss_2` also read zero. Those are a DIFFERENT and probably benign shape — the feed never naming a second participant — and are not part of this item. Size them before assuming either way.

## Plan

- [ ] Obtain a sportradar play-by-play payload covering a punt and a kick return, and record the actual shape.
- [ ] Repoint both gates at whatever names the player, mirroring the tackle-for-loss precedent.
- [ ] Add a fixture pinning the feed shape, so the next reader does not have to obtain the feed again.
- [ ] Backfill 2025 and verify `cleared 0`.
