Back to .md Directory

After-Action Report: Sports Analytics Framework

Documents a full-stack analytics overhaul that replaced opaque composite metrics with field-position-conditioned conversion rates and a delta profile.

May 2, 2026
0 downloads
0 views
ai llm claude
View source

What this file does

Documents a full-stack analytics overhaul that replaced opaque composite metrics with field-position-conditioned conversion rates and a delta profile.

When to use it

  • You are rewriting a sports analytics layer to produce interpretable metrics
  • You need to add field-position breakdowns to conversion stats
  • You are fixing a bug where a metric read the wrong dictionary key
  • You want to embed sport context into LLM-facing analysis output

Assumes this stack

PythonJinja2StreamlitFastAPI

After-Action Report: Sports Analytics Framework

Date: 2026-03-22 Branch: claude/analyze-sports-stats-isrOh Scope: Full-stack analytics overhaul — metrics layer, season/dynasty/pro accumulation, UI, stats site, writer/LLM tooling


Problem Statement

Viperball's analytics were not telling the right story. The existing metrics (OPI, Territory Rating, Chaos Factor, etc.) were opaque composites that didn't map to concepts readers or LLMs could reason about. When game data was shared with an LLM without context, it couldn't make sense of the sport or the math — the numbers looked wrong because they weren't grounded in anything recognizable.

Deeper issue: the analytics were measuring the wrong signals. Flat conversion rates, meaningless "Micro-Scoring Differential" (DK×5 − PK×3), and no way to distinguish where on the field performance happened. Two teams could have identical 5th-down conversion rates while operating in completely different field-position environments — and the analytics treated them as equal.

The sport also lacked any cross-sport context. A PPD of 4.2 means nothing to someone who doesn't already know the scale.


Key Finding

The league separates on small, consistent edges in late-down conversion conditioned by field position.

Winning teams don't have dramatically better raw stats. They convert 5th down at slightly higher rates, in slightly worse field positions, over many repetitions. The gap is 2-5% — small enough to be invisible in aggregate stats, large enough to compound over a season.

This is the analytical signal that was missing. Everything else (turnovers, explosive plays, kicking) is noise until you condition on field position.


What Changed

New Metrics (engine/viperball_metrics.py)

MetricWhat It MeasuresWhy It Matters
calculate_conversion_by_field_zone()4D/5D/6D conversion rates split by 4 field zones (own-deep, own-half, opp-half, opp-deep)The missing analytical layer — reveals where teams survive
calculate_delta_profile()Operating environment: Δ yards, Δ drives, Δ scores, KILL%Measures difficulty, not just performance

Math Fixes

FixFileIssue
PPD calculationviperball_metrics.pyWas using hardcoded scoring weights (0.40×9 + 0.30×4...) instead of actual per-drive scoring counts
EP docstringepa.pySaid "5-down system" — it's 6-down
Micro-Scoring Differentialbox_score.pyReplaced with Kicking Aggression Index (snap kick share of total kicks) — actually tells a story
yards_gained vs yardsviperball_metrics.pyPlay dicts use yards key, not yards_gained — explosive plays were always reading 0

Season Accumulation (engine/season.py)

  • 24 conversion-zone accumulators on TeamRecord (every down × every zone)
  • Delta profile accumulators (kill drives, drive counts, yardage differential)
  • Properties: season_5d_pct, season_5d_own_deep_pct, season_kill_pct, avg_delta_yds, season_conversion_by_zone
  • All accumulated in add_game_result() every game, automatically

Dynasty (engine/dynasty.py)

  • finalize_season() now stores: avg_ppd, avg_conversion_pct, avg_lateral_pct, avg_explosive, avg_to_margin, season_5d_pct, season_5d_own_deep_pct, season_kill_pct, avg_delta_yds, full conversion_by_zone dict
  • All preserved per-season in history.season_records[year] for historical analysis

Pro League (engine/pro_league.py)

  • ProTeamRecord.add_metrics() — new method to accumulate analytics per game
  • calculate_viperball_metrics() called after every pro game result
  • Properties: avg_team_rating, avg_ppd, season_5d_pct, season_kill_pct, avg_delta_yds
  • get_standings() includes all new fields in serialized output

CLI UI (engine/season_ui.py)

  • Team dashboard: replaced legacy metric names with PPD, Conversion %, Lateral %, etc.
  • Added conversion-by-zone table to team dashboard
  • Added 5D%, KILL%, Δ Yards to dashboard
  • Standings table: added PPD, 5D%, KILL% columns
  • Season awards: Best PPD, Best 5D%, Best Survival (5D own deep), Lowest KILL%, Best TO Margin

Web UI — Streamlit (ui/page_modules/section_league.py)

  • Standings table: added PPD, 5D%, KILL% columns

Stats Site (stats_site/templates/)

  • college/standings.html: added 5D% and KILL% columns to both overall and conference standings, with conditional styling (green for good, red for bad)
  • college/team.html: added 5D%, 5D% Own Deep, KILL%, Avg Δ Yards metric cards. Added full conversion-by-zone breakdown table (4D/5D/6D × 4 zones)
  • pro/season.html: added Rating, PPD, 5D%, KILL% columns to division standings

API (api/main.py)

  • _serialize_team_record(): added season_5d_pct, season_5d_own_deep_pct, season_kill_pct, avg_delta_yds, conversion_by_zone

Writer/LLM Tooling

  • ANALYTICS.md: Comprehensive analytics reference with cross-sport translation guide, metric benchmarks, scoring channel explanations, and the Delta/conversion-by-zone framework
  • analyze_game.py: Self-describing game analysis script. Output includes embedded sport context so any reader (human or LLM) can interpret the numbers without prior Viperball knowledge. Supports:
    • --sim home away — simulate and analyze a single game
    • --matchup home away --games N — multi-game matchup comparison
    • Direct analysis of existing play-by-play JSON files

Data Flow

Game Engine (simulate_game)
    ↓
calculate_viperball_metrics()
  ├── calculate_conversion_by_field_zone()  ← NEW
  ├── calculate_delta_profile()             ← NEW
  ├── calculate_ppd()                       ← FIXED
  ├── calculate_scoring_profile()
  ├── calculate_defensive_impact()
  └── ... existing metrics ...
    ↓
TeamRecord.add_game_result(metrics)
  ├── conv_zone accumulators (24 fields)    ← NEW
  ├── delta accumulators (5 fields)         ← NEW
  └── existing accumulators
    ↓
Season properties
  ├── season_5d_pct                         ← NEW
  ├── season_5d_own_deep_pct               ← NEW
  ├── season_kill_pct                       ← NEW
  ├── avg_delta_yds                         ← NEW
  ├── season_conversion_by_zone            ← NEW
  └── existing properties
    ↓
_serialize_team_record()
    ↓
  ├── Stats site templates (Jinja2)
  ├── Streamlit UI (section_league.py)
  ├── CLI (season_ui.py)
  └── Dynasty history (finalize_season)

Pro league follows the same pattern via ProTeamRecord.add_metrics().


What's Not Done

  1. Season-to-season trend analysis — the data is now stored per-season in dynasty, but there's no tool to compare a team's 5D% trajectory across years. The data is there; the visualization isn't.

  2. Fast sim integrationfast_sim.py produces statistical approximations without full play-by-play. The conversion-by-zone metrics require play-level data, so fast-simmed games will show 0% across all zones. This is acceptable for CPU-vs-CPU bulk simulation but means dynasty-mode CPU games won't populate these fields.

  3. Five Nations international — uses the same Season engine, so the metrics flow through automatically. Not separately tested.

  4. Nicegui appnicegui_app/pages/my_team.py references avg_opi and legacy metrics. Not updated in this pass (uses legacy aliases which still work).


Testing

  • Smoke-tested TeamRecord instantiation and property access
  • Smoke-tested ProTeamRecord instantiation and property access
  • Ran analyze_game.py --sim nyu gonzaga and --sim marquette creighton end-to-end
  • Verified API serializer compiles and includes new fields
  • Template changes are structural HTML additions — will render correctly when the serialized data includes the new keys (which it does)

Files Modified

FileLines ChangedNature
engine/viperball_metrics.py+95New metrics: conversion_by_field_zone, delta_profile. Fix yards key, PPD calc
engine/epa.py+1Fix "5-down" → "6-down" in docstring
engine/box_score.py+15 −7Replace Micro-Scoring Diff with Kicking Aggression Index
engine/season.py+100Zone accumulators, delta accumulators, season properties, accumulation logic
engine/dynasty.py+10Store new metrics in season_records
engine/pro_league.py+75ProTeamRecord analytics, add_metrics(), standings serialization
engine/season_ui.py+55 −25New dashboard, standings columns, awards
api/main.py+6Serialize new fields
ui/page_modules/section_league.py+3PPD, 5D%, KILL% in Streamlit standings
stats_site/templates/college/standings.html+85D%, KILL% columns
stats_site/templates/college/team.html+45Metric cards + conversion-by-zone table
stats_site/templates/pro/season.html+6Rating, PPD, 5D%, KILL% columns
ANALYTICS.md+350Cross-sport analytics reference (new file)
analyze_game.py+530Self-describing analysis tool (new file)

What's inside

Problem statement, key finding, 7 metric changes, 5 engine files, 4 UI layers, 2 new files, data flow, testing notes, file-change table.

Change this for your project

  • Replace viperball in file paths and metric names with your sport's name
  • Replace hardcoded scoring weights like 0.40×9 + 0.30×4 with your own scoring rules
  • Replace nyu gonzaga and marquette creighton test team names with your own
  • Replace ANALYTICS.md cross-sport translation guide with your sport's benchmarks

Where it goes

Keep alongside your test suite. Used to define and score model evaluations.

Worth borrowing

  • Conditioning conversion rates on field position zones to reveal small compounding edges
  • Storing per-season metric snapshots in dynasty history for future trend analysis
  • Embedding sport context directly into analysis output so any reader can interpret the numbers

Related Documents