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.
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
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)
| Metric | What It Measures | Why 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
| Fix | File | Issue |
|---|---|---|
| PPD calculation | viperball_metrics.py | Was using hardcoded scoring weights (0.40×9 + 0.30×4...) instead of actual per-drive scoring counts |
| EP docstring | epa.py | Said "5-down system" — it's 6-down |
| Micro-Scoring Differential | box_score.py | Replaced with Kicking Aggression Index (snap kick share of total kicks) — actually tells a story |
yards_gained vs yards | viperball_metrics.py | Play 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, fullconversion_by_zonedict- 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 gamecalculate_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(): addedseason_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 frameworkanalyze_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
-
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.
-
Fast sim integration —
fast_sim.pyproduces 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. -
Five Nations international — uses the same Season engine, so the metrics flow through automatically. Not separately tested.
-
Nicegui app —
nicegui_app/pages/my_team.pyreferencesavg_opiand legacy metrics. Not updated in this pass (uses legacy aliases which still work).
Testing
- Smoke-tested
TeamRecordinstantiation and property access - Smoke-tested
ProTeamRecordinstantiation and property access - Ran
analyze_game.py --sim nyu gonzagaand--sim marquette creightonend-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
| File | Lines Changed | Nature |
|---|---|---|
engine/viperball_metrics.py | +95 | New metrics: conversion_by_field_zone, delta_profile. Fix yards key, PPD calc |
engine/epa.py | +1 | Fix "5-down" → "6-down" in docstring |
engine/box_score.py | +15 −7 | Replace Micro-Scoring Diff with Kicking Aggression Index |
engine/season.py | +100 | Zone accumulators, delta accumulators, season properties, accumulation logic |
engine/dynasty.py | +10 | Store new metrics in season_records |
engine/pro_league.py | +75 | ProTeamRecord analytics, add_metrics(), standings serialization |
engine/season_ui.py | +55 −25 | New dashboard, standings columns, awards |
api/main.py | +6 | Serialize new fields |
ui/page_modules/section_league.py | +3 | PPD, 5D%, KILL% in Streamlit standings |
stats_site/templates/college/standings.html | +8 | 5D%, KILL% columns |
stats_site/templates/college/team.html | +45 | Metric cards + conversion-by-zone table |
stats_site/templates/pro/season.html | +6 | Rating, PPD, 5D%, KILL% columns |
ANALYTICS.md | +350 | Cross-sport analytics reference (new file) |
analyze_game.py | +530 | Self-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
viperballin file paths and metric names with your sport's name - Replace hardcoded scoring weights like
0.40×9 + 0.30×4with your own scoring rules - Replace
nyu gonzagaandmarquette creightontest team names with your own - Replace
ANALYTICS.mdcross-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
AI Tools for Developers
Curates a personal reference of AI coding tools, models, and setup instructions for VS Code, Xcode, and Cursor.
IATA BCBP Standard Compliance
Documents which IATA BCBP fields and barcode formats a Swift library implements, including Version 8 gender code support.
Voice AI Leaderboards, Benchmarks, and Evaluation Gaps (Jan 2025 -- Feb 2026)
Surveys 20+ voice AI benchmarks from Jan 2025, Feb 2026, identifies evaluation gaps, and provides leaderboard data for STT, TTS, and end-to-end voice agents.
Evaluating AI Agent Systems: Metrics, Benchmarks, and Quality Assurance (2024-2026)
Surveys 2024-2026 metrics, benchmarks, and monitoring tools for evaluating AI agent systems, with recommendations for a self-improving coding agent.