---
title: Rank simulation positions by projected volume
type: task
status: completed
description: >-
  Simulation position ranks read leaky season-to-date gamelogs (preseason, same-season playoffs, the
  forecast week itself) among rostered players only; rank each whole NFL team by this week's
  projected targets and opportunities instead, and filter playoff rows from the projection stats
  loader.
aliases:
  - ████████████████████████-█████████-████-██████████-████████-████████
base_uri: user:task/league/rank-simulation-positions-by-projected-volume.md
created_at: '2026-09-27T04:57:23.100Z'
entity_id: e340e220-1a25-443e-bc56-9ff111f67983
finished_at: '2026-09-27T14:37:25.309Z'
observations:
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Low
public_read: true
relations:
  - blocked_by [[█████████████████████████-██████-██████████-██████████████]]
started_at: '2026-09-27T14:13:46.094Z'
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-09-27T14:37:37.000Z'
visibility_analyzed_at: '2026-11-06T00:17:58.731Z'
---

# Rank simulation positions by projected volume

## Problem

`load_position_ranks` (`libs-server/simulation/calculate-position-ranks.mjs`) labels each WR, TE and RB by slot on his NFL team (WR1/WR2/WR3, TE1, RB1/RB2). The label selects the default same-team correlation (QB-WR1 0.42 against QB-WR3 0.28), used for any pair with fewer than 12 games together and blended in at 6-11. The ranks sum season-to-date gamelogs through `Math.max(1, week - 1)`, and the query is wrong three ways:

- No `season_type` filter, so preseason counts: preseason weeks 1-4 of 2025 carry 2,921 targets against 3,970 regular-season.
- Same-season postseason weeks 1-4 also count, so a replay of week w ranks partly on playoff games played months later.
- Week 1 ranks on week 1's own games in a replay; live, every WR falls to the WR3 default.

Ranking is also only among the players passed in, so a rostered WR2 becomes "WR1" when the real WR1 is unrostered.

The projection stats loader (`load_player_projection_stats`, `load-projection-data.mjs`) has the same missing filter: `projections_index` holds POST rows under weeks 1-5 (1,065 in 2024, 644 in 2025), which it averages into regular-season player means today.

## Design

- Rank on this week's projected volume, not past gamelogs. Projected targets (WR/TE) and targets plus rushing attempts (RB) for the simulated week are knowable before kickoff, so there is no preseason, postseason or same-week leakage by construction, week 1 works, and a traded or promoted player gets his current role.
- Volume source per player: the AVERAGE row's value, else the mean of the positive ESPN and grades-supplier values. Only those two sources project targets, and ESPN runs about 10% higher on targets and 19% higher on RB carries, so an unweighted positive-only average ranks ESPN-only players high. The AVERAGE row carries targets wherever a source does in 2023-2026 and on 85-87% of those rows in 2021-2022, which the fallback covers.
- Rank against the whole NFL team: read every WR/TE/RB with a REG projection that week, resolve unrostered players' teams with `load_player_nfl_teams`, and use the caller's `player_info` team for rostered players. Return ranks only for the requested players. Zero projected volume takes the default rank (WR3, TE1, RB2).
- `load_position_ranks({ player_info, year, week })` stays the single async entry point, now taking the SIMULATED week; the ranking itself is a pure exported function the spec drives. Both gamelog queries and every caller's `Math.max(1, week - 1)` go away. A week with no projections yet ranks everyone at the defaults.
- Loader fix, its own commit first: `season_type: 'REG'` on `load_player_projection_stats`.
- Integration: the four orchestrators (`simulate-league-week`, `simulate-matchup`, `simulate-championship`, `simulate-playoff-weeks`) pass the simulated week.
- Knowledge Base Impact: this task body carries results; `docs/simulation-system.md` module row; the modeling contract only if it describes the rank source. No scratch slug (arm outputs go to `tmp/`), no orchestrator.

## Evidence

Role identification, 2021-2025 weeks 2-18, among players who played, whole-team ranking (week 1 excluded, where gamelog ranks have no data):

| Measure | Season-to-date REG gamelogs | This week's projection |
| --- | --- | --- |
| WR1 is the team's target leader | 0.563 | 0.673 |
| WR top 2 are the actual top 2 | 0.678 | 0.773 |
| RB1 is the team's opportunity leader | 0.724 | 0.818 |

The correlation defaults (`libs-shared/simulation/correlation-constants.mjs`, one commit, "816 NFL games (2022-2024)") have no fitting script, so there are no historical rank semantics to preserve.

## Plan

- [x] **Loader fix**: `season_type: 'REG'` on `load_player_projection_stats`, committed alone.
- [x] **Rank module**: `calculate-position-ranks.mjs` rewritten per the design, with `select_projected_volumes` and `rank_by_projected_volume` exported pure; `test/simulation.position-ranks.spec.mjs` rewritten (7 cases, including a POST row that must not count, verified to fail with the filter removed).
- [x] **Callers**: all four orchestrators pass the simulated week; no rank caller computes `week - 1`.
- [x] **Orchestrator coverage**: no spec drives matchup, championship or playoff-weeks end to end, so each ran read-only against production on the branch with rank logging: matchup (week 4) and championship (week 15) each ranked 18 players against 779 candidates, and the season forecast, which drives playoff-weeks, ranked 90 players per week against about 810 for weeks 3-14. Season forecast wall time was 14s on both the old and new ranks. All 197 `simulation.*` specs pass.
- [x] **Measure**: see Results.
- [x] **Docs**: `docs/simulation-system.md` module row and the same-team correlation section; the modeling contract does not describe the rank source.
- [x] **Merge**: `yarn verify` 20 checks passed on the branch; merged to league master.
- [x] **Deploy** (human-gated): shipped 2026-09-27 in the same deploy as [[██████████████████████████-█████████-█████-██████-██-███████████]]; both hosts at `6115aada9`.

## Results

Calibration over 2021-2026 (72 weeks, 10,000 draws), every arm on the loader fix, two seeds each:

| Arm | Seed | Brier | Team coverage 50/80/95% |
| --- | --- | --- | --- |
| Gamelog ranks as before | 42 | 0.2020 | 0.494/0.826/0.952 |
| Gamelog ranks as before | 43 | 0.2019 | 0.497/0.825/0.948 |
| Option 1: REG games before the week (reference) | 42 | 0.2019 | 0.494/0.826/0.952 |
| Option 1: REG games before the week (reference) | 43 | 0.2019 | 0.499/0.825/0.948 |
| Projected volume, whole team | 42 | 0.2020 | 0.495/0.827/0.952 |
| Projected volume, whole team | 43 | 0.2019 | 0.499/0.826/0.950 |

- Every arm sits inside the seed-to-seed band (Brier 0.0001, coverage up to 0.005), so the rank source does not move calibration: it only picks the default correlation for pairs with little shared history. The gamelog arm also matches the pre-change baseline at master `b8d1d7ceb` (0.2020, 0.494/0.826/0.952), so the loader fix is calibration-neutral too.
- Adopted on the adoption rule (not worse than option 1) plus the role-identification gain in Evidence and the leakage it removes by construction.
