Skip to content

Add Player season hitting overview - #58

Merged
Mattsface merged 2 commits into
mainfrom
issue-57-player-hitting-overview
Sep 26, 2026
Merged

Mattsface merged 2 commits into
mainfrom
issue-57-player-hitting-overview

Conversation

@Mattsface

Copy link
Copy Markdown
Member

Summary

Adds the first real Player analytics page for issue #57.

  • adds DB-only GET /players/hitting?season=...&player_id=...
  • validates Player-season membership from persisted player_seasons
  • derives TB, AVG, OBP, SLG, OPS, K%, BB%, and HR% from stored season hitting totals
  • preserves undefined-rate semantics instead of coercing zero-denominator rates to zero
  • adds a neutral Plate Appearance Rate Profile visualization using local Plotly
  • links the Player directory to the overview only when a stored hitting line exists
  • shows the real Player-season import command when analytics data is missing
  • adds focused repository, analytics, formatting, chart, and web coverage
  • clarifies that stored season aggregates may reflect an in-progress season and do not imply completion

Data integrity

The analytics layer explicitly rejects impossible stored relationships such as:

  • hits > at-bats
  • strikeouts > at-bats
  • strikeouts > plate appearances
  • walks > plate appearances
  • home runs > plate appearances

These surface as the existing browser-facing 409 conflict state rather than escaping as schema/runtime errors.

Semantics

The page describes the Player season aggregate currently stored locally.

It does not imply:

  • the season is complete
  • historical team-stint detail is modeled
  • Player-vs-MLB context exists
  • rankings or quality scores exist
  • game-by-game or rolling trends exist

Multi-club seasons remain one combined stored aggregate.

Preserved behavior

This PR does not add:

  • new Player ingestion
  • schema migrations
  • browser MLB/API calls
  • Player pitching
  • Player comparisons
  • Player game logs
  • Team route changes
  • generic dashboard/entity abstractions

Existing Team navigation, Team analytics, /players, and /health remain intact.

Testing

Coverage includes:

  • AVG / OBP / SLG / OPS / total-bases formulas
  • K% / BB% / HR% formulas
  • zero-denominator behavior
  • season catalog membership requirements
  • missing hitting-data state
  • corrupted stored-data 409 handling
  • DB-only browser rendering
  • local Plotly rendering
  • directory → hitting overview navigation
  • Player primary navigation
  • Team and health regressions

Closes #57.

Mattsface and others added 2 commits September 26, 2026 09:20
Add GET /players/hitting?season=...&player_id=..., a DB-only season-level
hitting overview for one Player.

- Confirm Player-season membership from player_seasons via a focused
  get_player_catalog_entry repository read before loading hitting data.
- Derive TB, AVG, OBP, SLG, OPS and the K%/BB%/HR% plate-appearance rate
  profile in app/analytics/player_hitting.py; undefined rates stay None.
- Render summary cards and a single-colour PA rate profile chart with
  local Plotly; no MLB comparison, rankings, or team-stint claims.
- Link the Player directory to the overview when hitting is stored, and
  show the real import command otherwise.
- Introduce a tiny player_base.html for Player primary-nav state.

Refs #57

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ording

Validate that strikeouts, walks, and home runs do not exceed plate
appearances, and that hits and strikeouts do not exceed at-bats, before
building the plate-appearance rate profile. A corrupted stored row now
raises PlayerHittingAnalysisError and renders the route's 409 state
instead of escaping as a Pydantic ValidationError. The schema bounds
remain as a second integrity boundary.

Describe the overview as the stored season aggregate rather than a
full season, since an in-progress season can be re-imported as its
totals grow, and keep the separate multi-club caveat.

Refs #57

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Mattsface

Copy link
Copy Markdown
Member Author

@claude please review

@claude

claude Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Claude finished @Mattsface's task in 2m 55s —— View job


Review: Add Player season hitting overview

  • Gather context (AGENTS.md, diff, changed files)
  • Understand request (code review)
  • Review analytics layer (app/analytics/player_hitting.py)
  • Review repositories/schemas changes
  • Review web layer (routes, charts, formatting, templates)
  • Review tests
  • Post final review

This is a well-executed PR — I don't have any blocking findings. Highlights and verification notes below.

What I checked closely

Numeric bounds are provably safe, not just tested. app/schemas/analytics.py's new PlayerHittingOverview/PlayerPlateAppearanceRates fields carry tight bounds (on_base_percentage: le=1, slugging_percentage: le=4, on_base_plus_slugging: le=5). I traced whether a corrupted stored PlayerSeasonHitting row could push a computed rate past these bounds and surface a raw Pydantic ValidationError instead of the friendly PlayerHittingAnalysisError the route (app/web/player_routes.py:243-247) catches. It can't: _require_subset_counts in app/analytics/player_hitting.py:83-108 enforces hits ≤ at_bats, which alone is enough to prove OBP ≤ 1 (since BB/HBP cancel between numerator and denominator) and SLG ≤ 4 (worst case every hit is a HR). The five checks there are exactly the ones needed to keep every downstream rate inside its declared range — nice, deliberate design rather than incidental.

Migration-order concern I ruled out. get_player_season_hitting (app/database/repositories.py:593, pre-existing) has no OperationalError→DatabaseSchemaMissingError translation, unlike the new get_player_catalog_entry. This PR is the first to call it from a live route, so I checked whether a partially-migrated DB could hit an unhandled exception. It can't: 73d9fae8fafb creates players/player_season_hitting and is the down_revision of 8b3f31d9a5c2 (which creates player_seasons), so player_seasons can never exist without player_season_hitting also existing under a normal alembic upgrade head. Not a real gap.

Data-integrity and undefined-rate semantics match AGENTS.md's requirements precisely: zero-denominator rates are None, never 0.0; corrupted stored lines (hits > at-bats, strikeouts > PA, etc.) raise a named PlayerHittingAnalysisError and surface as a 409, never a fabricated stat or a raw traceback. The distinction between "no plate appearances" (fully undefined) vs. "a lone sacrifice fly" (a real .000 OBP) is both implemented and tested (tests/test_analytics_player_hitting.py:116-124).

Architecture follows the repo's layering cleanly: analytics has zero FastAPI/SQLAlchemy/Plotly/MLB imports, the route (app/web/player_routes.py) stays thin (fetch → analyze → format/chart → render), and chart/formatting logic lives in app/web/charts.py / app/web/formatting.py rather than the route or templates.

Tests are strong: hand-worked formula values, zero-denominator branches, corrupted-row error messages (including which bound is reported first when multiple are violated), schema-validator drift rejection, and full web-layer coverage of every browser state (schema_missing, not_found, missing_hitting, inconsistent_hitting, ok) with an autouse fixture that fails the test if any MLB/network call is attempted (tests/test_web_players.py:31-56). CI is green: Test run #36257037287 passed.

Nitpicks (non-blocking, optional)

  • None significant enough to request changes on. The PR does what it says: no new ingestion, no schema migrations, no MLB calls from the browser, scope stays to the hitting overview.

Limitation

I was not able to run poetry run ruff / poetry run pytest locally in this sandbox (command execution for those was blocked), so I relied on the passing CI run above plus manual tracing of the invariants instead of a fresh local run.

Branch: issue-57-player-hitting-overview

@Mattsface
Mattsface merged commit c0290c4 into main Sep 26, 2026
1 check passed
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 Player season hitting overview

1 participant