Loading...
Loading...
Loading...
**Branch:** `claude/analyze-sports-stats-isrOh`
# 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`, 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 integration** — `fast_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 app** — `nicegui_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
| 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) |
- Without a harness, you **can't compare** prompts, models, retrieval configs, or costs.
Evaluate, benchmark, and regression-test AI/LLM systems. Covers evaluation framework design, benchmark creation, human evaluation protocols, automated evaluation (LLM-as-judge), regression testing, statistical significance, and continuous evaluation pipelines.
<img width="1388" height="298" alt="full_diagram" src="https://github.com/user-attachments/assets/12a2371b-8be2-4219-9b48-90503eb43c69" />
A list of all public EEG-datasets. This list of EEG-resources is not exhaustive. If you find something new, or have explored any unfiltered link in depth, please update the repository.