---
title: Exclude roster-placeholder gamelogs from hit-rate samples
type: task
status: completed
description: >-
  calculate-historical-hit-rates.mjs loads player_gamelogs with no participation predicate, so a
  roster placeholder counts as a played game of zero and pushes every affected rate down. Same
  defect class as the settlement guard, one consumer over.
base_uri: user:task/league/exclude-roster-placeholder-gamelogs-from-hit-rate-samples.md
created_at: '2026-09-25T15:17:22.470Z'
entity_id: c8f7106f-4815-4e2e-974d-3a839be1ea0f
finished_at: '2026-09-25T18:23:36.736Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
public_read: true
relations:
  - follows [[███████████████████████-██████-███████████-████████-██-█████████████]]
tags:
  - user:tag/league-xo-football.md
  - user:tag/software-task.md
updated_at: '2026-09-25T18:23:36.736Z'
visibility_analyzed_at: '2026-11-05T23:49:12.880Z'
---

## Problem

`scripts/calculate-historical-hit-rates.mjs` loads its gamelog sample with **no
participation predicate at all** — neither `is_active` nor `source`:

```js
const player_gamelogs = await db('player_gamelogs')
  .select('player_gamelogs.*', 'nfl_games.week', ...)
  .join('nfl_games', 'nfl_games.esbid', 'player_gamelogs.esbid')
  .whereIn('nfl_games.season_type', ['REG', 'POST'])
  .whereIn('player_gamelogs.pid', unique_pids)
```

So a roster-only placeholder — a row an importer wrote and no statistics
generator ever touched, every stat column at its 0 default — enters the sample
as **a played game of exactly zero production**. A hit rate is a count of how
often the player cleared the line in his last five, last ten, this season and
overall, so each placeholder in the window pushes the rate down by one game the
player did not play.

This is the same defect class that
[[███████████████████████-██████-███████████-████████-██-█████████████]] closed
in settlement, one consumer over. That task established the discriminator:
`is_active` is the roster file's opinion about whether a player dressed, and
`source` is which pipeline wrote the row. Grading needs both.

`libs-server/prop-hit-rate-sample.mjs` `load_player_gamelogs` is the second
surface and it is subtler — it carries no predicate either, but exposes
`is_active` through `apply_sample_filters` for the caller to narrow on. That is
an opt-in where the settlement case proved an opt-out is needed, and it has no
`source` filter at any layer.

## Why this was not folded into the settlement fix

The settlement repair moved `selection_result` and `metric_result_value`, and
hit rates read neither — they are derived from `player_gamelogs` and
`nfl_plays` against the market line. Measured 2026-09-25: a pid-scoped dry run
over the 122 repaired players reported **0 non-null transitions across 43,913
selections**, so nothing the grade repair touched moved a stored rate.

The blast radius here is the opposite shape and much larger: a corrected sample
moves rates for every player who has ever had a placeholder row in a window,
across the whole corpus rather than 122 players. That deserves its own sizing
rather than riding along on a settlement commit.

## Plan

- [ ] Size it before changing anything. How many gamelog rows in hit-rate
      windows are placeholders, how many players and market types are affected,
      and how far do the rates move — `--dry_run` reports a stored-versus-computed
      transition matrix, which is the right instrument.
- [ ] Decide whether the predicate belongs in the loaders or in the rate
      calculation. A placeholder is not a played game, so excluding it from the
      sample is probably right — but confirm the denominator that consumers
      expect is games PLAYED rather than games rostered.
- [ ] Reuse `gradeable_gamelog_sources` and `is_gradeable_player_gamelog` from
      `libs-server/prop-market-settlement/data-preloader.mjs` rather than
      restating the list, for the reason `gamelog_joins` imports it.
- [ ] Cover both surfaces or state why not: the script's own load and
      `libs-server/prop-hit-rate-sample.mjs`.
- [ ] Recompute and read the transition matrix before writing.

## Assumptions

- A hit rate is meant to measure games the player PLAYED. If any consumer
  deliberately wants rostered-games, that consumer has to be found first — the
  fix would be wrong for it.
