Add team baserunners/game trend - #38
Merged
Merged
Conversation
Adds a fourth per-game metric page (/baserunners), defined as hits + walks + hit-by-pitch — the standard OBP numerator, excluding reached-on-error and fielder's choice. Walks and hit-by-pitch arrive in the same hitting gameLog split already being fetched for hits/runs/strikeouts, so no new MLB request was needed; a migration adds two nullable columns (following the same backfill precedent as the batting-strikeouts column), and existing rows show re-import guidance until refreshed. Includes the MLB-wide comparison gated on league coverage plus known component totals, mirroring the batting strikeouts page's two-rule gate.
A from-scratch walkthrough of the ingestion/database/analytics/web layers, written as a snapshot to make it easier to pick the project back up after time away. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0178J2Rs8QQfS1wTqG98FSK9
7 tasks
Mattsface
added a commit
that referenced
this pull request
Aug 23, 2026
…ge (#42) * feat: persist team game pitching lines Adds team_game_pitching_lines, the pitching counterpart to the batting line table, plus the ingestion path that fills it. Innings are stored as outs, an integer, and never as innings pitched. MLB returns inningsPitched as a string in baseball notation where '10.2' means ten and two-thirds innings, so parsing it as a decimal would silently corrupt every derived rate. The same split carries outs=32 for that game, and ER * 27 / outs reproduces the API's own published ERA exactly. Only raw components are stored; ERA, WHIP, K/9 and BB/9 are derived on read so a stored rate cannot drift from its components. A separate table rather than more columns on the batting line: the two are different stat groups from different requests, and half of each one's columns would be meaningless on the other row. It also means a season imported before pitching existed simply has no pitching rows, so nothing needs the nullable-until-backfilled treatment the strikeout and baserunner columns got. Fetching both groups costs four MLB requests, not six -- get_team_game_lines shares the team lookup and the season schedule between the two game logs, which is 60 requests saved over a 30-club league import. Both persist in the same transaction so a team-season can never hold batting rows without pitching ones. The two logs are validated against opposite sides of the score: a hitting split's runs must equal the selected team's scheduled score, a pitching split's runs are runs allowed and must equal the opponent's. Three definitional check constraints (earned runs within runs, home runs within hits, batters faced covering outs) were verified against 648 real 2025 team-games before being encoded. The batting and pitching upserts are one generic function rather than two near-identical copies, addressing duplication flagged in the review of #38. Refs #41. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0178J2Rs8QQfS1wTqG98FSK9 * feat: add pitching analytics, chart, and route (page template pending) Adds the team and league pitching analytics, the ERA figure, the summary-card and note formatters, the /pitching route, and the nav entry. The correctness point of this layer is that ERA, WHIP, K/9 and BB/9 are rates, not counts. Every other analytics module averages per-game values because those are counts; averaging per-game rates is a different statistic. For the 2025 Mariners the season ERA is 3.870 while the mean of the 162 game ERAs is 3.965, an error that would match no published figure. Every rate here sums the numerator and denominator across the games in scope and divides once, including the rolling window, which accumulates earned runs and outs rather than smoothing game ERAs. League rates are outs-weighted rather than game-weighted for the same reason. The sign convention on this page is the opposite of every other one: a negative difference against MLB is the better direction, because a lower ERA is better. The comparison card caption and a rendered sentence both say so rather than leaving a reader to infer it. Verified against the live API: 162 games, ERA 3.87, WHIP 1.220, K/9 8.77, BB/9 2.79, matching the figures computed directly from the raw payload. The pitching.html template and the test suite for this layer are still to come, so /pitching is not yet reachable end to end. Refs #41. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0178J2Rs8QQfS1wTqG98FSK9 * feat: add pitches-per-game page Makes /pitching reachable end to end with pitches per game as the chart, plus ERA, WHIP, K/9 and BB/9 in the summary cards. Adds number_of_pitches and strikes to the pitching table. Balls are not stored: MLB leaves that field empty on the team game log even though it populates strikes, and balls are number_of_pitches - strikes, so a column would only invite the two to drift. These columns are folded into the existing create migration rather than added by a second one. That revision has not been merged or released, and the local database was still on the previous revision, so amending it avoids a nullable column that could never legitimately be null. Pitches per game is a count, not a rate, so unlike ERA and WHIP its season figure is a plain mean of the per-game values. build_pitch_count_points says so explicitly, since the surrounding module exists largely to warn against averaging rates. A team-season imported before pitching was collected has no pitching rows at all, and the page returns 409 naming the team re-import as the fix. Every pitching column is NOT NULL, so there is no partially-known state to report. Verified against the real database: 162 rows imported for the 2025 Mariners, 23,623 pitches over 1,462.2 IP, 145.8 per game, 65% strikes, ERA 3.87, WHIP 1.22. The batting rows re-imported as unchanged. Refs #41. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0178J2Rs8QQfS1wTqG98FSK9 * test: cover pitching analytics, page, and fixtures; document the page Adds 59 tests across the pitching layer and the design doc for it, taking coverage back to 94% with the two pitching analytics modules at 96% and 100%. The tests that matter most build seasons with unequal innings. Rate aggregation is only distinguishable from a naive mean of per-game rates when the denominators differ -- with a regulation nine innings in every game the two agree exactly, so a test built that way would pass against a wrong implementation. One test documents that case explicitly so the choice is not mistaken for an accident. Also covered: that the '10.2' display string is never used in a calculation (reading it as a decimal gives 7.90 rather than the correct 7.59), that a team-season without pitching rows returns 409 naming the team re-import, and that unearned runs stay distinct from earned ones in the captured fixture. Refs #41. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0178J2Rs8QQfS1wTqG98FSK9 --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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
/baserunnerspage: rolling and season Baserunners/Game trend, defined ashits + walks + hit-by-pitch(the standard OBP numerator, excluding reached-on-error and fielder's choice)2efdbec9b07eadds nullablebase_on_balls/hit_by_pitchcolumns toteam_game_batting_lines, following the same nullable-until-reimported precedent as the batting strikeouts columnapp/services/team_game_logs.pyparses both fields from thegameLoghitting split already fetched for hits/runs/strikeouts — no new MLB requestscripts/import_team_season.py/scripts/import_league_season.pybackfill the new columns on re-importCloses #37.
Test plan
poetry run pytest— 1198 tests passing (new: team/league analytics, chart contract, web page, league-comparison gating, migration up/down, schema validation)poetry run ruff check ./poetry run ruff format --check .clean🤖 Generated with Claude Code
https://claude.ai/code/session_01RiedFmBJuphps1Vbf1HCUN