Add team run differential and Pythagorean expectation - #40
Merged
Conversation
Adds a fifth per-game metric page (/run-differential) covering run prevention, which none of the existing pages describe. No migration and no new MLB request were needed: runs allowed is already in the database, on the opponent's own row for the same game_pk, so a self-join in list_team_season_run_results recovers it. Win/loss is derived the same way — a completed MLB game cannot end tied, so outscoring the opponent is the whole definition — which gives an actual record to place beside the Pythagorean expectation without a W/L column. Two things make this page unlike the other four. It is the only signed metric, so the chart draws diverging bars split by outcome and its y axis must not anchor at zero. And it has no MLB reference line, which is not an omission: league-wide run differential is exactly zero by construction, so the zero line already on the chart is the league average. The join is a LEFT join on purpose. A single-team import has no opponent rows, and an inner join would return zero games — indistinguishable from a team that was never imported. Instead unpaired game_pks are reported and the page refuses with a 409, naming the league-season import as the fix rather than the team re-import the other missing-data pages ask for. Closes #39. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0178J2Rs8QQfS1wTqG98FSK9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
/run-differentialpage: per-game run differential trend plus the Pythagorean expected record beside the actual onerunson their row for the samegame_pk. A self-join inlist_team_season_run_resultsrecovers itCloses #39.
What makes this page different from the other four
It is the only signed metric. Every other per-game value has a floor of zero; this one does not. The chart draws diverging bars split into win and loss traces rather than a marker line, and its y axis uses
rangemode="normal"— anchoring at zero would clip every loss off the chart. Its zero line is drawn darker than the grid, because zero is the win/loss boundary here rather than an arbitrary axis end.It has no MLB reference line, which is not an omission. League-wide run differential is exactly zero by construction: every run scored by one team is a run allowed by another, so the totals cancel. The zero line already on the chart is the MLB average, and the page says so rather than leaving a reader wondering what is missing.
Missing-data handling
The join is a
LEFTjoin on purpose. A single-team import has no opponent rows at all, and an inner join would silently return zero games for it — indistinguishable from a team that was never imported. Instead the unpairedgame_pkvalues are reported, and the page refuses with the established 409 pattern.The remedy differs from the batting-strikeout and baserunner backfills: nothing is wrong with the team's own rows and re-importing the team cannot help, so the page names
scripts/import_league_season.pyinstead. A partially paired season is refused too — an average over only the paired games would understate runs allowed and produce a differential that looks plausible and is wrong.The other four pages read only the team's own rows, so they are unaffected by a single-team import and keep working.
Test plan
poetry run pytest— 1309 tests passing, 98% coverage (new: 110 tests across analytics, repository join, chart contract, schemas, formatting, and the web page)poetry run ruff check ./poetry run ruff format --check .cleangame_pk, not the date), cross-season isolation, and symmetry from both clubs' perspectives🤖 Generated with Claude Code
https://claude.ai/code/session_0178J2Rs8QQfS1wTqG98FSK9