---
title: >-
  Fix careerlog *_per_game always NULL: generate-league-format-player-careerlogs reads nonexistent
  s.games
type: task
status: completed
description: >-
  generate-league-format-player-careerlogs.mjs computes games from a nonexistent seasonlog column,
  so points_added_earned_per_game and points_added_net_per_game are NULL for every careerlog row.
base_uri: user:task/league/fix-careerlog-per-game-null-from-missing-seasonlog-games.md
created_at: '2026-05-17T15:22:46.382Z'
entity_id: 3a085b80-53c1-4f5b-9a26-695c8766f227
finished_at: '2026-05-17T15:46:11.729Z'
observations:
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - relates [[████████████████████████████-██████-█████-███████████]]
  - relates [[user:repository/active/league]]
started_at: '2026-05-17T15:43:06.608Z'
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-06-08T17:42:51.234Z'
visibility_analyzed_at: '2026-11-05T23:49:19.890Z'
---

## Problem

`scripts/generate-league-format-player-careerlogs.mjs` ~L78:

```js
const games = sum(seasons.map((s) => s.games))
```

reads `s.games` from `league_format_player_seasonlogs` rows, but that table has no `games` column -- only `startable_games`. Every season yields `undefined`, so `games` is `NaN` (or 0), and the per-game divisions evaluate to null:

```js
points_added_earned_per_game = games ? earned / games : null
points_added_net_per_game = games ? net / games : null
```

Effect: every careerlog row in `league_format_player_careerlogs` has NULL `*_per_game` for both `_earned` and `_net`. Latent pre-existing bug; surfaced during the 2026-05-17 `█████████████████████████████████` cutover.

## Evidence

Post-cutover, Christian McCaffrey careerlog:

- `points_added_earned` = 644.6
- `points_added_earned_per_game` = NULL
- `points_added_net_per_game` = NULL

## Resolution

Implemented option 1 in league@851bb083: the careerlog generator queries `league_format_player_gamelogs` JOIN `nfl_games` (REG only) and aggregates `COUNT(*) AS games` per pid for the given `league_format_hash`. Each gamelog row corresponds to a game where the player had a `player_gamelogs` entry (i.e. was active and produced stats), which is the correct active-games divisor for career per-game averages.

Backfill: regenerated careerlogs for all 15 real `league_format_hash` values on 2026-05-17. Spot-check: McCaffrey now reads `points_added_earned = 1413.1`, `points_added_earned_per_game = 11.7`, `points_added_net = 1396.2`, `points_added_net_per_game = 11.5`, `best_season_points_added_earned_per_game = 18.3`. Zero NULL `*_per_game` rows remain across 52,710 careerlog rows.

## Options considered

1. **Chosen.** Compute `games` on-the-fly inside the careerlog generator from a COUNT of `league_format_player_gamelogs` rows per pid (active-games definition).
2. Add a `games` column to `league_format_player_seasonlogs` populated by the seasonlog generator -- more schema surface for no real gain since the gamelog source is authoritative.
3. Repurpose `startable_games` -- semantically wrong (startable is "weeks the player would have started given the lineup constraints", not "games the player participated in").
4. Accept the NULL behavior -- discards a useful career metric.

## Working Directory

`/Users/trashman/user-base/repository/active/league` (main branch)

## Key locations

- Bug site: `scripts/generate-league-format-player-careerlogs.mjs` ~L78
- Seasonlog schema: `db/schema.postgres.sql` (search `league_format_player_seasonlogs`)
