---
title: Diagnose the away-side matchup projection bias
type: task
status: completed
description: >-
  Away-side matchup projections over-project by up to 10 points more than home-side ones in five of
  six seasons. Determine whether that is a scheduling property, a real home-field scoring effect, or
  a lineup-to-side join defect in calculate-matchup-projection.mjs.
base_uri: user:task/league/diagnose-away-side-projection-bias.md
created_at: '2026-09-10T19:50:19.285Z'
entity_id: 509e2f04-bc13-4a7e-87d4-c1ae74c3475f
finished_at: '2026-09-29T01:15:34.418Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-09-29T01:15:34.418Z'
visibility_analyzed_at: '2026-11-05T23:49:09.403Z'
---

## Objective

Find out why `matchups.away_projection` over-projects far more than `home_projection` does, and fix it or explain it.

## The measurement

Taken 2026-09-10 on league 1, immediately after the matchup-projection lane finished rebuilding every season from one estimator. Bias is projection minus actual, so positive is over-projection.

| Season | home bias | away bias | home corr | away corr |
| --- | --- | --- | --- | --- |
| 2020 | +0.85 | -0.64 | 0.334 | 0.451 |
| 2021 | +7.16 | +17.34 | 0.553 | 0.653 |
| 2022 | +8.08 | +11.58 | 0.623 | 0.655 |
| 2023 | +2.93 | +5.28 | 0.330 | 0.383 |
| 2024 | -0.03 | +3.22 | 0.489 | 0.377 |
| 2025 | +5.29 | +8.60 | 0.486 | 0.551 |

Away runs above home in five of six seasons, by up to 10 points. 2020 is the lone inversion, and 2020 is also the season whose source projections are known thin.

## Why this is not the estimator's home/away asymmetry by construction

`scripts/calculate-matchup-projection.mjs` reads `home_lineup.baseline_total` and `away_lineup.baseline_total` out of the same `███████████████████` rows, computed by the same `optimizeLineup` call. Nothing in the projection path distinguishes home from away — the side is decided by the `matchups` row, not by how the number was produced. So a systematic 10-point gap has to come from one of:

- **The pairing.** Whether `home_team_id`/`away_team_id` are assigned in a way that correlates with team strength (a scheduler artifact), which would make this a real property of the schedule rather than a defect.
- **A join or ordering defect** in `calculate-matchup-projection.mjs` that mismatches a team's lineup to the wrong side. This is the one that would be a live bug, and the tell would be that away projections track home lineups or vice versa.
- **Actual scoring.** If `away_points` are genuinely lower than `home_points` on average, the estimator is fine and the league has a home-field effect the projection does not model.

Start by testing the third against the first: compare `avg(home_points)` to `avg(away_points)` per season. If actual points are symmetric while projections are not, the defect is in the projection path and the second bullet is where to look.

## Scope

In scope: diagnosing the asymmetry and, if it is a defect, fixing it and re-running the affected seasons. Out of scope: re-litigating the estimator choice or the baseline reconstruction, both settled 2026-09-10.

## Plan

- [x] Compare mean actual points by side, per season, to separate a scoring effect from a projection defect. Verify: projections are symmetric by side (2021 137.92 home against 138.05 away) while actual away points run 3-10 lower in five of six seasons, and `matchups` points reconcile with starter-slot gamelogs (at most 23 of 84 rows off by more than 0.5, all 2021).
- [x] If projections are at fault, audit the lineup-to-side join in `scripts/calculate-matchup-projection.mjs`. Not needed: the projections are not asymmetric.
- [x] Decide whether the finding is a defect to fix or a schedule property to document. Schedule property: from 2023 side is confounded with team (2023 team 11 home in all 14 games; teams 3, 8 and 10 never home). Net of each team's own residual, the home-minus-away gap is -1.8, 9.7, 3.6, 2.5, 3.0 and -0.3 for 2020-2025, carried by 2021 and inside noise elsewhere.
- [x] If a fix lands, re-run `project-lineups` then `calculate-matchup-projection` for the affected seasons and re-measure both sides. No fix: nothing to re-run.

## Notable context

- `user:task/league/conform-matchup-projection-column.md` — the completed lane that surfaced this, carrying the full before/after tables and the baseline reconstruction
- `scripts/calculate-matchup-projection.mjs`, `scripts/project-lineups.mjs` — the two-step rebuild path
- `scratch/league/matchup-projection-baseline-rerun/` — before/after projection snapshots and the reconstructed starter baselines
