Skip to content

Add team baserunners/game trend - #38

Merged
Mattsface merged 2 commits into
mainfrom
issue-37-baserunners
Aug 23, 2026
Merged

Add team baserunners/game trend#38
Mattsface merged 2 commits into
mainfrom
issue-37-baserunners

Conversation

@Mattsface

Copy link
Copy Markdown
Member

Summary

  • Adds a /baserunners page: rolling and season Baserunners/Game trend, defined as hits + walks + hit-by-pitch (the standard OBP numerator, excluding reached-on-error and fielder's choice)
  • Migration 2efdbec9b07e adds nullable base_on_balls / hit_by_pitch columns to team_game_batting_lines, following the same nullable-until-reimported precedent as the batting strikeouts column
  • app/services/team_game_logs.py parses both fields from the gameLog hitting split already fetched for hits/runs/strikeouts — no new MLB request
  • No new import script: scripts/import_team_season.py / scripts/import_league_season.py backfill the new columns on re-import
  • Rows imported before this migration show the existing 409/re-import-guidance pattern rather than treating an unknown total as zero
  • MLB-wide comparison gated on league coverage + known component totals, mirroring the batting strikeouts page's two-rule gate

Closes #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
  • Verified end-to-end against real MLB fixture data (2025 Cubs) and against a live re-import of the 2026 Mariners season (128 rows backfilled)
  • Migration upgrade/downgrade/upgrade round-trip verified against a temporary SQLite database

🤖 Generated with Claude Code

https://claude.ai/code/session_01RiedFmBJuphps1Vbf1HCUN

Mattsface and others added 2 commits August 22, 2026 12:29
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
@Mattsface
Mattsface merged commit 438fc5a into main Aug 23, 2026
1 check passed
@Mattsface
Mattsface deleted the issue-37-baserunners branch August 23, 2026 16:32
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add team baserunners/game trend

1 participant