Case Study 2: Route Efficiency and Coverage Analysis System
Overview
This case study develops a spatial analysis system that evaluates receiver route efficiency, maps defensive coverage tendencies, and identifies exploitable matchups. The system uses tracking data to quantify route running quality and coverage effectiveness.
Background
Modern NFL and college football tracking data captures player positions at 10 frames per second, enabling detailed analysis of route running and coverage. This data reveals insights that film study alone cannot provide:
- Precise separation measurements at each point of a route
- Quantified route sharpness at break points
- Coverage shell recognition in real-time
- Zone boundary identification
Business Problem
A college football program's offensive staff needs a system to: 1. Evaluate receiver route running efficiency 2. Analyze opposing team coverage tendencies 3. Identify zone soft spots to attack 4. Create data-driven game plans 5. Provide visual feedback to players in meetings
Available Data
The system uses: - Player tracking data (x, y positions at 10 Hz) - Play-by-play outcomes - Route tags for each receiver - Coverage calls from film review - Game context (down, distance, field position)
Solution Architecture
System Overview
┌─────────────────────────────────────────────────────────────────┐
│ Route and Coverage Analysis System │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Route │ │ Coverage │ │ Matchup │ │
│ │ Analyzer │ │ Mapper │ │ Finder │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └─────────┬─────────┴─────────┬────────┘ │
│ │ │ │
│ ┌─────────▼─────────┐ ┌─────▼────────────┐ │
│ │ Spatial │ │ Report │ │
│ │ Visualizer │ │ Generator │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Implementation
Part 1: Route Analysis Engine
"""
Route Efficiency and Coverage Analysis System
Part 1: Route Analysis Engine
"""
import numpy as np
import pandas as pd
from dataclasses import dataclass, field
from typing import List, Dict, Tuple, Optional
from enum import Enum
from scipy.signal import savgol_filter
from scipy.spatial.distance import euclidean
class RouteType(Enum):
"""Standard route tree."""
HITCH = "hitch"
FLAT = "flat"
SLANT = "slant"
COMEBACK = "comeback"
CURL = "curl"
OUT = "out"
IN = "in"
CORNER = "corner"
POST = "post"
GO = "go"
WHEEL = "wheel"
SCREEN = "screen"
@dataclass
class RouteTemplate:
"""Template for ideal route execution."""
route_type: RouteType
stem_depth: float # Yards before break
break_angle: float # Degrees (0 = straight, 90 = perpendicular)
final_depth: float # Target depth
expected_separation: float # Average expected separation
# Define standard route templates
ROUTE_TEMPLATES = {
RouteType.HITCH: RouteTemplate(RouteType.HITCH, 5, 180, 5, 2.5),
RouteType.SLANT: RouteTemplate(RouteType.SLANT, 3, 45, 8, 3.0),
RouteType.OUT: RouteTemplate(RouteType.OUT, 12, 90, 12, 2.0),
RouteType.IN: RouteTemplate(RouteType.IN, 12, -90, 12, 2.5),
RouteType.CURL: RouteTemplate(RouteType.CURL, 12, 160, 10, 2.0),
RouteType.POST: RouteTemplate(RouteType.POST, 10, -45, 25, 3.5),
RouteType.CORNER: RouteTemplate(RouteType.CORNER, 10, 45, 20, 3.0),
RouteType.GO: RouteTemplate(RouteType.GO, 0, 0, 40, 2.5),
}
@dataclass
class TrackingPoint:
"""Single tracking data point."""
frame: int
timestamp: float
x: float # Yards downfield from LOS
y: float # Yards from center (positive = offense's right)
speed: Optional[float] = None
direction: Optional[float] = None # Degrees
@dataclass
class RouteExecution:
"""Complete route execution data."""
play_id: str
receiver_id: str
receiver_name: str
route_type: RouteType
tracking: List[TrackingPoint]
target: bool = False
completion: bool = False
def get_positions(self) -> Tuple[np.ndarray, np.ndarray]:
"""Extract x, y arrays."""
x = np.array([p.x for p in self.tracking])
y = np.array([p.y for p in self.tracking])
return x, y
@dataclass
class RouteMetrics:
"""Calculated route efficiency metrics."""
route_type: RouteType
total_distance: float
route_efficiency: float # Ideal distance / actual distance
stem_speed: float
break_sharpness: float # Angle change rate at break
break_depth: float
separation_at_break: float
max_separation: float
time_to_break: float
grade: str # A, B, C, D, F
class RouteAnalyzer:
"""Analyze route execution quality."""
def __init__(self, frame_rate: float = 10.0):
self.frame_rate = frame_rate
self.dt = 1.0 / frame_rate
def calculate_break_point(self, route: RouteExecution) -> Tuple[int, float, float]:
"""
Identify the break point in a route.
Returns:
--------
tuple : (frame_index, x_position, y_position)
"""
x, y = route.get_positions()
if len(x) < 5:
return 0, x[0], y[0]
# Calculate direction at each point
dx = np.diff(x)
dy = np.diff(y)
directions = np.arctan2(dy, dx)
# Find largest direction change
direction_changes = np.abs(np.diff(directions))
# Handle angle wrapping
direction_changes = np.minimum(direction_changes,
2*np.pi - direction_changes)
# Smooth to avoid noise
if len(direction_changes) > 5:
direction_changes = savgol_filter(direction_changes, 5, 2)
# Find peak direction change
break_idx = np.argmax(direction_changes) + 1
return break_idx, x[break_idx], y[break_idx]
def calculate_break_sharpness(self, route: RouteExecution,
break_idx: int) -> float:
"""
Calculate sharpness of the route break.
Higher values indicate crisper breaks.
"""
x, y = route.get_positions()
if break_idx < 2 or break_idx >= len(x) - 2:
return 0.0
# Direction before break (2 frames before)
dx_before = x[break_idx] - x[break_idx-2]
dy_before = y[break_idx] - y[break_idx-2]
dir_before = np.arctan2(dy_before, dx_before)
# Direction after break (2 frames after)
dx_after = x[break_idx+2] - x[break_idx]
dy_after = y[break_idx+2] - y[break_idx]
dir_after = np.arctan2(dy_after, dx_after)
# Angle change
angle_change = abs(dir_after - dir_before)
angle_change = min(angle_change, 2*np.pi - angle_change)
# Time to complete change (4 frames)
time_window = 4 * self.dt
# Sharpness = angle change / time (degrees per second)
sharpness = np.degrees(angle_change) / time_window
return sharpness
def calculate_stem_speed(self, route: RouteExecution,
break_idx: int) -> float:
"""
Calculate average speed during the stem (before break).
"""
x, y = route.get_positions()
timestamps = np.array([p.timestamp for p in route.tracking])
if break_idx < 2:
return 0.0
# Distance covered during stem
dx = np.diff(x[:break_idx+1])
dy = np.diff(y[:break_idx+1])
distances = np.sqrt(dx**2 + dy**2)
total_distance = np.sum(distances)
# Time during stem
stem_time = timestamps[break_idx] - timestamps[0]
if stem_time <= 0:
return 0.0
return total_distance / stem_time
def calculate_route_efficiency(self, route: RouteExecution) -> float:
"""
Calculate route efficiency as ratio of ideal to actual distance.
Perfect efficiency = 1.0, lower values indicate wasted motion.
"""
x, y = route.get_positions()
# Actual distance traveled
dx = np.diff(x)
dy = np.diff(y)
actual_distance = np.sum(np.sqrt(dx**2 + dy**2))
# Ideal distance (straight lines to break point and target)
break_idx, break_x, break_y = self.calculate_break_point(route)
# Distance to break
ideal_to_break = np.sqrt((break_x - x[0])**2 + (break_y - y[0])**2)
# Distance from break to end
ideal_from_break = np.sqrt((x[-1] - break_x)**2 + (y[-1] - break_y)**2)
ideal_distance = ideal_to_break + ideal_from_break
if actual_distance <= 0:
return 0.0
return ideal_distance / actual_distance
def analyze_route(self, route: RouteExecution,
defender_tracking: Optional[List[TrackingPoint]] = None
) -> RouteMetrics:
"""
Perform comprehensive route analysis.
Parameters:
-----------
route : RouteExecution
Route tracking data
defender_tracking : List[TrackingPoint], optional
Covering defender's tracking data
Returns:
--------
RouteMetrics : Complete route analysis
"""
x, y = route.get_positions()
timestamps = np.array([p.timestamp for p in route.tracking])
# Calculate break point
break_idx, break_x, break_y = self.calculate_break_point(route)
# Calculate metrics
break_sharpness = self.calculate_break_sharpness(route, break_idx)
stem_speed = self.calculate_stem_speed(route, break_idx)
efficiency = self.calculate_route_efficiency(route)
# Total distance
dx = np.diff(x)
dy = np.diff(y)
total_distance = np.sum(np.sqrt(dx**2 + dy**2))
# Time to break
time_to_break = timestamps[break_idx] - timestamps[0] if break_idx > 0 else 0
# Separation calculations
separation_at_break = 0.0
max_separation = 0.0
if defender_tracking:
def_x = np.array([p.x for p in defender_tracking])
def_y = np.array([p.y for p in defender_tracking])
# Ensure same length
min_len = min(len(x), len(def_x))
x = x[:min_len]
y = y[:min_len]
def_x = def_x[:min_len]
def_y = def_y[:min_len]
separations = np.sqrt((x - def_x)**2 + (y - def_y)**2)
max_separation = np.max(separations)
if break_idx < min_len:
separation_at_break = separations[break_idx]
# Grade the route
grade = self._calculate_grade(efficiency, break_sharpness, stem_speed)
return RouteMetrics(
route_type=route.route_type,
total_distance=total_distance,
route_efficiency=efficiency,
stem_speed=stem_speed,
break_sharpness=break_sharpness,
break_depth=break_x,
separation_at_break=separation_at_break,
max_separation=max_separation,
time_to_break=time_to_break,
grade=grade
)
def _calculate_grade(self, efficiency: float,
sharpness: float,
speed: float) -> str:
"""Calculate overall route grade."""
score = 0
# Efficiency (0-40 points)
if efficiency >= 0.95:
score += 40
elif efficiency >= 0.90:
score += 35
elif efficiency >= 0.85:
score += 30
elif efficiency >= 0.80:
score += 25
else:
score += 15
# Break sharpness (0-30 points, based on degrees/sec)
if sharpness >= 400:
score += 30
elif sharpness >= 300:
score += 25
elif sharpness >= 200:
score += 20
elif sharpness >= 100:
score += 15
else:
score += 10
# Stem speed (0-30 points, yards/sec)
if speed >= 8.0:
score += 30
elif speed >= 7.0:
score += 25
elif speed >= 6.0:
score += 20
elif speed >= 5.0:
score += 15
else:
score += 10
# Convert to letter grade
if score >= 90:
return 'A'
elif score >= 80:
return 'B'
elif score >= 70:
return 'C'
elif score >= 60:
return 'D'
else:
return 'F'
Part 2: Coverage Analysis Engine
"""
Route Efficiency and Coverage Analysis System
Part 2: Coverage Analysis Engine
"""
import numpy as np
import pandas as pd
from dataclasses import dataclass, field
from typing import List, Dict, Tuple, Optional
from enum import Enum
from scipy.spatial import ConvexHull
from scipy.stats import gaussian_kde
class CoverageType(Enum):
"""Common coverage schemes."""
COVER_0 = "Cover 0"
COVER_1 = "Cover 1"
COVER_2 = "Cover 2"
COVER_2_MAN = "Cover 2 Man"
COVER_3 = "Cover 3"
COVER_4 = "Cover 4"
COVER_6 = "Cover 6"
MAN_FREE = "Man Free"
QUARTERS = "Quarters"
@dataclass
class CoverageZone:
"""Definition of a coverage zone."""
name: str
x_min: float # Yards from LOS
x_max: float
y_min: float # Yards from center (negative = defense's left)
y_max: float
color: str = '#3498DB'
alpha: float = 0.3
@dataclass
class DefenderPosition:
"""Defender position and assignment."""
defender_id: str
position: str # CB, S, LB
x: float
y: float
zone: Optional[str] = None
man_assignment: Optional[str] = None
@dataclass
class CoverageSnapshot:
"""Coverage at a single moment."""
play_id: str
frame: int
coverage_type: CoverageType
defenders: List[DefenderPosition]
shell: str # "1-high", "2-high", "0-high"
# Define standard zone boundaries
COVERAGE_ZONES = {
CoverageType.COVER_3: [
CoverageZone("Deep Left", 15, 50, -26.67, -8, '#2ECC71', 0.3),
CoverageZone("Deep Middle", 15, 50, -8, 8, '#2ECC71', 0.3),
CoverageZone("Deep Right", 15, 50, 8, 26.67, '#2ECC71', 0.3),
CoverageZone("Flat Left", 0, 8, -26.67, -15, '#E74C3C', 0.3),
CoverageZone("Hook/Curl Left", 5, 15, -15, -5, '#F39C12', 0.3),
CoverageZone("Hook/Curl Right", 5, 15, 5, 15, '#F39C12', 0.3),
CoverageZone("Flat Right", 0, 8, 15, 26.67, '#E74C3C', 0.3),
],
CoverageType.COVER_2: [
CoverageZone("Deep Left", 15, 50, -26.67, 0, '#2ECC71', 0.3),
CoverageZone("Deep Right", 15, 50, 0, 26.67, '#2ECC71', 0.3),
CoverageZone("Flat Left", 0, 10, -26.67, -15, '#E74C3C', 0.3),
CoverageZone("Hook Left", 5, 15, -15, -5, '#F39C12', 0.3),
CoverageZone("Middle Read", 5, 15, -5, 5, '#9B59B6', 0.3),
CoverageZone("Hook Right", 5, 15, 5, 15, '#F39C12', 0.3),
CoverageZone("Flat Right", 0, 10, 15, 26.67, '#E74C3C', 0.3),
],
CoverageType.COVER_4: [
CoverageZone("Deep Quarter 1", 12, 50, -26.67, -13, '#2ECC71', 0.3),
CoverageZone("Deep Quarter 2", 12, 50, -13, 0, '#2ECC71', 0.3),
CoverageZone("Deep Quarter 3", 12, 50, 0, 13, '#2ECC71', 0.3),
CoverageZone("Deep Quarter 4", 12, 50, 13, 26.67, '#2ECC71', 0.3),
CoverageZone("Short Left", 0, 12, -26.67, -13, '#E74C3C', 0.3),
CoverageZone("Short Middle Left", 0, 12, -13, 0, '#F39C12', 0.3),
CoverageZone("Short Middle Right", 0, 12, 0, 13, '#F39C12', 0.3),
CoverageZone("Short Right", 0, 12, 13, 26.67, '#E74C3C', 0.3),
],
}
class CoverageAnalyzer:
"""Analyze defensive coverage patterns."""
def __init__(self):
self.zone_definitions = COVERAGE_ZONES
def classify_shell(self, defenders: List[DefenderPosition]) -> str:
"""
Classify coverage shell based on safety positions.
Returns:
--------
str : "2-high", "1-high", or "0-high"
"""
# Find safeties (players deep and relatively centered)
deep_defenders = [d for d in defenders if d.x > 10]
safeties = [d for d in deep_defenders if d.position in ['S', 'FS', 'SS']]
if len(safeties) == 0:
# Check for deep cornerbacks
deep_count = len([d for d in deep_defenders if d.x > 15])
if deep_count >= 2:
return "2-high"
elif deep_count == 1:
return "1-high"
return "0-high"
# Count high safeties (>12 yards deep)
high_safeties = [s for s in safeties if s.x > 12]
if len(high_safeties) >= 2:
return "2-high"
elif len(high_safeties) == 1:
return "1-high"
else:
return "0-high"
def classify_coverage(self, snapshot: CoverageSnapshot) -> CoverageType:
"""
Classify coverage type from defender positions.
"""
shell = self.classify_shell(snapshot.defenders)
# Count defenders in box
box_defenders = [d for d in snapshot.defenders
if d.x < 7 and abs(d.y) < 10]
# Check for man indicators
# (This would be more sophisticated with receiver positions)
if shell == "0-high":
return CoverageType.COVER_0
elif shell == "1-high":
# Likely Cover 1 or Cover 3
if len(box_defenders) >= 7:
return CoverageType.COVER_1
else:
return CoverageType.COVER_3
else: # 2-high
# Likely Cover 2 or Cover 4
deep_defenders = [d for d in snapshot.defenders if d.x > 15]
if len(deep_defenders) >= 4:
return CoverageType.COVER_4
else:
return CoverageType.COVER_2
def find_soft_spots(self, coverage_type: CoverageType,
zone_success_rates: Optional[Dict[str, float]] = None
) -> List[Tuple[float, float, float]]:
"""
Identify soft spots in a coverage scheme.
Returns:
--------
List of (x, y, vulnerability_score) tuples
"""
if coverage_type not in self.zone_definitions:
return []
zones = self.zone_definitions[coverage_type]
soft_spots = []
# Find gaps between zones
for i, zone1 in enumerate(zones):
for zone2 in zones[i+1:]:
# Check for horizontal gaps
if (zone1.x_max == zone2.x_min or
zone1.x_min == zone2.x_max):
# Vertical seam
x = zone1.x_max
y = (zone1.y_min + zone1.y_max + zone2.y_min + zone2.y_max) / 4
soft_spots.append((x, y, 0.7))
# Check for vertical gaps
if (zone1.y_max == zone2.y_min or
zone1.y_min == zone2.y_max):
# Horizontal seam
y = zone1.y_max
x = (zone1.x_min + zone1.x_max + zone2.x_min + zone2.x_max) / 4
soft_spots.append((x, y, 0.6))
# Known vulnerability points for each coverage
if coverage_type == CoverageType.COVER_3:
# Holes in Cover 3
soft_spots.append((12, -18, 0.8)) # Flat-to-curl seam
soft_spots.append((12, 18, 0.8))
soft_spots.append((15, 0, 0.6)) # Deep middle seam
elif coverage_type == CoverageType.COVER_2:
# Holes in Cover 2
soft_spots.append((18, 0, 0.9)) # Deep middle
soft_spots.append((5, -22, 0.7)) # Flat corner
soft_spots.append((5, 22, 0.7))
elif coverage_type == CoverageType.COVER_4:
# Holes in Cover 4
soft_spots.append((10, -7, 0.7)) # Seams
soft_spots.append((10, 7, 0.7))
return soft_spots
def analyze_coverage_tendency(self, snapshots: List[CoverageSnapshot]
) -> pd.DataFrame:
"""
Analyze coverage tendencies across multiple plays.
"""
results = []
for snap in snapshots:
coverage = self.classify_coverage(snap)
shell = self.classify_shell(snap.defenders)
results.append({
'play_id': snap.play_id,
'coverage_type': coverage.value,
'shell': shell,
'frame': snap.frame
})
df = pd.DataFrame(results)
# Calculate frequencies
coverage_freq = df['coverage_type'].value_counts(normalize=True)
shell_freq = df['shell'].value_counts(normalize=True)
return df, coverage_freq, shell_freq
class MatchupAnalyzer:
"""Analyze receiver vs coverage matchups."""
def __init__(self, route_analyzer: RouteAnalyzer,
coverage_analyzer: CoverageAnalyzer):
self.route_analyzer = route_analyzer
self.coverage_analyzer = coverage_analyzer
def evaluate_route_vs_coverage(self, route: RouteExecution,
coverage_type: CoverageType
) -> Dict[str, float]:
"""
Evaluate how well a route attacks a coverage.
Returns:
--------
Dict with matchup scores
"""
# Get route target location
x, y = route.get_positions()
target_x, target_y = x[-1], y[-1]
# Find soft spots in coverage
soft_spots = self.coverage_analyzer.find_soft_spots(coverage_type)
# Calculate distance to nearest soft spot
min_distance = float('inf')
nearest_spot = None
for spot_x, spot_y, vulnerability in soft_spots:
dist = np.sqrt((target_x - spot_x)**2 + (target_y - spot_y)**2)
weighted_dist = dist / vulnerability # Lower is better
if weighted_dist < min_distance:
min_distance = weighted_dist
nearest_spot = (spot_x, spot_y, vulnerability)
# Calculate matchup grade
if min_distance < 2:
matchup_grade = 'A'
score = 90 + (2 - min_distance) * 5
elif min_distance < 4:
matchup_grade = 'B'
score = 80 + (4 - min_distance) * 2.5
elif min_distance < 6:
matchup_grade = 'C'
score = 70 + (6 - min_distance) * 2.5
else:
matchup_grade = 'D'
score = 60
return {
'route_type': route.route_type.value,
'coverage_type': coverage_type.value,
'target_x': target_x,
'target_y': target_y,
'nearest_soft_spot': nearest_spot,
'distance_to_soft_spot': min_distance,
'matchup_grade': matchup_grade,
'matchup_score': score
}
def recommend_routes(self, coverage_type: CoverageType,
receiver_position: str = 'X'
) -> List[Dict]:
"""
Recommend best routes against a coverage.
"""
soft_spots = self.coverage_analyzer.find_soft_spots(coverage_type)
# Starting position based on receiver alignment
if receiver_position == 'X':
start_y = -20 # Boundary receiver
elif receiver_position == 'Z':
start_y = 20 # Field receiver
else:
start_y = 5 # Slot
recommendations = []
for route_type, template in ROUTE_TEMPLATES.items():
# Estimate where route ends up
if route_type == RouteType.POST:
target_x = template.final_depth
target_y = start_y * 0.3 # Breaks inside
elif route_type == RouteType.CORNER:
target_x = template.final_depth
target_y = start_y * 1.2 # Breaks outside
elif route_type in [RouteType.IN, RouteType.SLANT]:
target_x = template.final_depth
target_y = start_y * 0.5
elif route_type == RouteType.OUT:
target_x = template.final_depth
target_y = start_y * 1.1
else:
target_x = template.final_depth
target_y = start_y
# Find distance to soft spots
min_dist = float('inf')
for spot_x, spot_y, vuln in soft_spots:
dist = np.sqrt((target_x - spot_x)**2 + (target_y - spot_y)**2)
dist = dist / vuln
min_dist = min(min_dist, dist)
score = max(0, 100 - min_dist * 10)
recommendations.append({
'route_type': route_type.value,
'target_depth': target_x,
'target_lateral': target_y,
'coverage_beater_score': score,
'recommended': score >= 70
})
# Sort by score
recommendations.sort(key=lambda x: x['coverage_beater_score'], reverse=True)
return recommendations
Part 3: Visualization System
"""
Route Efficiency and Coverage Analysis System
Part 3: Visualization System
"""
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.collections import LineCollection
from matplotlib.patches import Polygon, FancyArrowPatch
import numpy as np
from typing import List, Dict, Optional, Tuple
class FieldVisualizer:
"""Football field visualization for route/coverage analysis."""
def __init__(self, half_field: bool = True):
self.half_field = half_field
self.field_length = 50 if half_field else 100
self.field_width = 53.33
def draw_field(self, ax: plt.Axes) -> plt.Axes:
"""Draw football field."""
# Field surface
field = patches.Rectangle(
(0, -self.field_width/2),
self.field_length,
self.field_width,
facecolor='#228B22',
edgecolor='white',
linewidth=2
)
ax.add_patch(field)
# Yard lines
for yard in range(0, self.field_length + 1, 5):
ax.axvline(x=yard, color='white', linewidth=1, alpha=0.5)
# Hash marks (college width)
hash_y = 13.33 # 40 feet from sideline
for yard in range(0, self.field_length + 1, 1):
ax.plot([yard, yard], [hash_y - 0.5, hash_y + 0.5],
'w-', linewidth=0.5)
ax.plot([yard, yard], [-hash_y - 0.5, -hash_y + 0.5],
'w-', linewidth=0.5)
# LOS indicator
ax.axvline(x=0, color='yellow', linewidth=3, alpha=0.7)
ax.set_xlim(-5, self.field_length + 5)
ax.set_ylim(-self.field_width/2 - 3, self.field_width/2 + 3)
ax.set_aspect('equal')
ax.set_xlabel('Yards from LOS')
ax.set_ylabel('Yards from Center')
return ax
class RouteVisualizer:
"""Visualize route executions."""
def __init__(self, field_viz: FieldVisualizer):
self.field_viz = field_viz
def draw_route(self, ax: plt.Axes, route: RouteExecution,
color: str = '#3498DB',
show_break: bool = True,
show_metrics: bool = False,
metrics: Optional[RouteMetrics] = None) -> plt.Axes:
"""
Draw a route on the field.
Parameters:
-----------
ax : plt.Axes
Axes to draw on
route : RouteExecution
Route data
color : str
Route line color
show_break : bool
Highlight break point
show_metrics : bool
Display route metrics
metrics : RouteMetrics, optional
Pre-calculated metrics
"""
x, y = route.get_positions()
# Draw route line
ax.plot(x, y, color=color, linewidth=2.5, solid_capstyle='round')
# Start marker
ax.scatter(x[0], y[0], s=150, c=color, marker='o',
edgecolor='white', linewidth=2, zorder=5)
# End arrow
if len(x) >= 2:
dx = x[-1] - x[-2]
dy = y[-1] - y[-2]
ax.annotate('', xy=(x[-1], y[-1]),
xytext=(x[-2], y[-2]),
arrowprops=dict(arrowstyle='->', color=color, lw=2))
# Break point
if show_break:
analyzer = RouteAnalyzer()
break_idx, break_x, break_y = analyzer.calculate_break_point(route)
ax.scatter(break_x, break_y, s=100, c='white',
marker='o', edgecolor=color, linewidth=2, zorder=6)
# Add metrics text
if show_metrics and metrics:
text = (f"{route.route_type.value.upper()}\n"
f"Grade: {metrics.grade}\n"
f"Eff: {metrics.route_efficiency:.2f}")
ax.text(x[0], y[0] - 3, text, fontsize=8, ha='center',
color=color, weight='bold')
return ax
def draw_route_comparison(self, routes: List[RouteExecution],
title: str = 'Route Comparison') -> plt.Figure:
"""
Compare multiple routes on the same field.
"""
fig, ax = plt.subplots(figsize=(12, 10))
self.field_viz.draw_field(ax)
colors = plt.cm.tab10(np.linspace(0, 1, len(routes)))
for route, color in zip(routes, colors):
self.draw_route(ax, route, color=color)
ax.plot([], [], color=color, linewidth=2,
label=route.receiver_name)
ax.legend(loc='upper right')
ax.set_title(title)
return fig
class CoverageVisualizer:
"""Visualize coverage zones and soft spots."""
def __init__(self, field_viz: FieldVisualizer):
self.field_viz = field_viz
def draw_zones(self, ax: plt.Axes, coverage_type: CoverageType) -> plt.Axes:
"""Draw coverage zones."""
if coverage_type not in COVERAGE_ZONES:
return ax
zones = COVERAGE_ZONES[coverage_type]
for zone in zones:
rect = patches.Rectangle(
(zone.x_min, zone.y_min),
zone.x_max - zone.x_min,
zone.y_max - zone.y_min,
facecolor=zone.color,
edgecolor='white',
alpha=zone.alpha,
linewidth=1
)
ax.add_patch(rect)
# Zone label
center_x = (zone.x_min + zone.x_max) / 2
center_y = (zone.y_min + zone.y_max) / 2
ax.text(center_x, center_y, zone.name, fontsize=8,
ha='center', va='center', color='white', weight='bold')
return ax
def draw_soft_spots(self, ax: plt.Axes,
coverage_type: CoverageType) -> plt.Axes:
"""Highlight soft spots in coverage."""
analyzer = CoverageAnalyzer()
soft_spots = analyzer.find_soft_spots(coverage_type)
for x, y, vulnerability in soft_spots:
# Size based on vulnerability
size = 100 + vulnerability * 200
ax.scatter(x, y, s=size, c='red', marker='X',
alpha=0.7, zorder=10, edgecolor='white')
return ax
def draw_coverage_shell(self, ax: plt.Axes,
defenders: List[DefenderPosition],
show_zones: bool = False,
coverage_type: Optional[CoverageType] = None
) -> plt.Axes:
"""
Draw defenders in their coverage positions.
"""
# Draw zones if requested
if show_zones and coverage_type:
self.draw_zones(ax, coverage_type)
# Draw defenders
for defender in defenders:
if defender.position in ['CB']:
color = '#E74C3C' # Red for corners
elif defender.position in ['S', 'FS', 'SS']:
color = '#9B59B6' # Purple for safeties
else:
color = '#F39C12' # Orange for linebackers
ax.scatter(defender.x, defender.y, s=300, c=color,
marker='s', edgecolor='white', linewidth=2, zorder=8)
ax.text(defender.x, defender.y, defender.position,
fontsize=8, ha='center', va='center', color='white',
weight='bold')
return ax
class ReportGenerator:
"""Generate visual reports combining routes and coverage."""
def __init__(self):
self.field_viz = FieldVisualizer()
self.route_viz = RouteVisualizer(self.field_viz)
self.coverage_viz = CoverageVisualizer(self.field_viz)
def create_route_report(self, route: RouteExecution,
metrics: RouteMetrics) -> plt.Figure:
"""Create detailed report for a single route."""
fig = plt.figure(figsize=(16, 10))
# Main route diagram
ax1 = fig.add_subplot(2, 2, 1)
self.field_viz.draw_field(ax1)
self.route_viz.draw_route(ax1, route, show_break=True,
show_metrics=True, metrics=metrics)
ax1.set_title(f'{route.route_type.value.upper()} Route - {route.receiver_name}')
# Speed profile
ax2 = fig.add_subplot(2, 2, 2)
x, y = route.get_positions()
timestamps = np.array([p.timestamp for p in route.tracking])
dx = np.diff(x)
dy = np.diff(y)
speeds = np.sqrt(dx**2 + dy**2) / np.diff(timestamps)
ax2.plot(timestamps[:-1], speeds, 'b-', linewidth=2)
ax2.axhline(metrics.stem_speed, color='r', linestyle='--',
label=f'Stem Speed: {metrics.stem_speed:.1f} yd/s')
ax2.set_xlabel('Time (s)')
ax2.set_ylabel('Speed (yd/s)')
ax2.set_title('Speed Profile')
ax2.legend()
ax2.grid(True, alpha=0.3)
# Metrics panel
ax3 = fig.add_subplot(2, 2, 3)
ax3.axis('off')
metrics_text = f"""
ROUTE METRICS
{'═'*40}
Route Type: {metrics.route_type.value.upper()}
Grade: {metrics.grade}
Distance: {metrics.total_distance:.1f} yards
Efficiency: {metrics.route_efficiency:.2%}
Stem Speed: {metrics.stem_speed:.1f} yd/s
Break Sharpness: {metrics.break_sharpness:.0f} °/s
Break Depth: {metrics.break_depth:.1f} yards
Time to Break: {metrics.time_to_break:.2f} s
Separation:
At Break: {metrics.separation_at_break:.1f} yards
Maximum: {metrics.max_separation:.1f} yards
"""
ax3.text(0.1, 0.9, metrics_text, fontsize=11, family='monospace',
va='top', transform=ax3.transAxes)
# Grade visualization
ax4 = fig.add_subplot(2, 2, 4)
grades = ['A', 'B', 'C', 'D', 'F']
colors = ['#27AE60', '#2ECC71', '#F1C40F', '#E67E22', '#E74C3C']
grade_idx = grades.index(metrics.grade)
bars = ax4.barh(grades, [100, 80, 60, 40, 20], color='lightgray')
bars[grade_idx].set_color(colors[grade_idx])
ax4.set_xlim(0, 100)
ax4.set_xlabel('Grade Threshold')
ax4.set_title('Route Grade')
ax4.invert_yaxis()
fig.tight_layout()
return fig
def create_coverage_beater_report(self, coverage_type: CoverageType,
recommendations: List[Dict]
) -> plt.Figure:
"""Create report showing best routes against a coverage."""
fig = plt.figure(figsize=(16, 12))
# Coverage zones with soft spots
ax1 = fig.add_subplot(2, 2, (1, 2))
self.field_viz.draw_field(ax1)
self.coverage_viz.draw_zones(ax1, coverage_type)
self.coverage_viz.draw_soft_spots(ax1, coverage_type)
ax1.set_title(f'{coverage_type.value} - Zones and Soft Spots')
# Recommendations table
ax2 = fig.add_subplot(2, 2, 3)
ax2.axis('off')
table_data = []
for rec in recommendations[:5]: # Top 5
table_data.append([
rec['route_type'].upper(),
f"{rec['target_depth']:.0f}",
f"{rec['target_lateral']:.0f}",
f"{rec['coverage_beater_score']:.0f}",
'✓' if rec['recommended'] else ''
])
table = ax2.table(
cellText=table_data,
colLabels=['Route', 'Depth', 'Lateral', 'Score', 'Recommended'],
loc='center',
cellLoc='center'
)
table.auto_set_font_size(False)
table.set_fontsize(10)
table.scale(1.2, 1.8)
# Style header
for i in range(5):
table[(0, i)].set_facecolor('#2E4053')
table[(0, i)].set_text_props(color='white', weight='bold')
ax2.set_title('Route Recommendations')
# Score distribution
ax3 = fig.add_subplot(2, 2, 4)
route_names = [r['route_type'].upper() for r in recommendations]
scores = [r['coverage_beater_score'] for r in recommendations]
colors = ['#27AE60' if r['recommended'] else '#E74C3C'
for r in recommendations]
bars = ax3.barh(route_names, scores, color=colors)
ax3.axvline(70, color='gray', linestyle='--', alpha=0.5,
label='Recommendation threshold')
ax3.set_xlabel('Coverage Beater Score')
ax3.set_title('Route Scores vs ' + coverage_type.value)
ax3.set_xlim(0, 100)
ax3.legend()
fig.suptitle(f'Coverage Beater Analysis: {coverage_type.value}',
fontsize=16, y=1.02)
fig.tight_layout()
return fig
Part 4: Integration and Demonstration
"""
Route Efficiency and Coverage Analysis System
Part 4: Integration and Demonstration
"""
import numpy as np
import pandas as pd
from typing import List
def generate_sample_route() -> RouteExecution:
"""Generate sample slant route for demonstration."""
np.random.seed(42)
# Slant route: 3 yards stem, break inside
tracking = []
x, y = 0.0, -15.0 # Start outside receiver
for frame in range(30): # 3 seconds
t = frame * 0.1
if t < 0.3:
# Pre-snap
pass
elif t < 0.8:
# Stem - run vertical
x += 0.6 + np.random.normal(0, 0.05)
else:
# Break - run slant
x += 0.5 + np.random.normal(0, 0.05)
y += 0.6 + np.random.normal(0, 0.05)
tracking.append(TrackingPoint(
frame=frame,
timestamp=t,
x=x,
y=y
))
return RouteExecution(
play_id='PLAY001',
receiver_id='WR11',
receiver_name='DeVonta Smith',
route_type=RouteType.SLANT,
tracking=tracking,
target=True,
completion=True
)
def generate_sample_defenders() -> List[DefenderPosition]:
"""Generate sample Cover 3 defense."""
return [
DefenderPosition('D1', 'CB', 1, -22, zone='Flat Left'),
DefenderPosition('D2', 'CB', 1, 22, zone='Flat Right'),
DefenderPosition('D3', 'S', 18, -15, zone='Deep Left'),
DefenderPosition('D4', 'S', 25, 0, zone='Deep Middle'),
DefenderPosition('D5', 'S', 18, 15, zone='Deep Right'),
DefenderPosition('D6', 'LB', 5, -8, zone='Hook/Curl'),
DefenderPosition('D7', 'LB', 5, 8, zone='Hook/Curl'),
]
# =============================================================================
# MAIN DEMONSTRATION
# =============================================================================
if __name__ == "__main__":
print("=" * 70)
print("ROUTE EFFICIENCY AND COVERAGE ANALYSIS SYSTEM")
print("=" * 70)
# Initialize analyzers
route_analyzer = RouteAnalyzer()
coverage_analyzer = CoverageAnalyzer()
matchup_analyzer = MatchupAnalyzer(route_analyzer, coverage_analyzer)
# Generate sample data
print("\nGenerating sample data...")
route = generate_sample_route()
defenders = generate_sample_defenders()
# Analyze route
print("\nAnalyzing route execution...")
metrics = route_analyzer.analyze_route(route)
print(f" Route Type: {metrics.route_type.value}")
print(f" Grade: {metrics.grade}")
print(f" Efficiency: {metrics.route_efficiency:.2%}")
print(f" Break Sharpness: {metrics.break_sharpness:.0f} °/s")
# Analyze coverage
print("\nAnalyzing coverage...")
snapshot = CoverageSnapshot(
play_id='PLAY001',
frame=0,
coverage_type=CoverageType.COVER_3,
defenders=defenders,
shell='1-high'
)
classified = coverage_analyzer.classify_coverage(snapshot)
print(f" Classified Coverage: {classified.value}")
# Get route recommendations
print("\nGenerating route recommendations vs Cover 3...")
recommendations = matchup_analyzer.recommend_routes(
CoverageType.COVER_3, 'X'
)
for i, rec in enumerate(recommendations[:5]):
status = "RECOMMENDED" if rec['recommended'] else ""
print(f" {i+1}. {rec['route_type']:10} Score: {rec['coverage_beater_score']:.0f} {status}")
# Generate visualizations
print("\nGenerating reports...")
field_viz = FieldVisualizer()
route_viz = RouteVisualizer(field_viz)
coverage_viz = CoverageVisualizer(field_viz)
report_gen = ReportGenerator()
# Route report
fig1 = report_gen.create_route_report(route, metrics)
fig1.savefig('route_analysis_report.png', dpi=150, bbox_inches='tight')
print(" Saved: route_analysis_report.png")
# Coverage beater report
fig2 = report_gen.create_coverage_beater_report(
CoverageType.COVER_3, recommendations
)
fig2.savefig('coverage_beater_report.png', dpi=150, bbox_inches='tight')
print(" Saved: coverage_beater_report.png")
# Coverage zone visualization
fig3, ax3 = plt.subplots(figsize=(12, 10))
field_viz.draw_field(ax3)
coverage_viz.draw_coverage_shell(ax3, defenders, show_zones=True,
coverage_type=CoverageType.COVER_3)
ax3.set_title('Cover 3 Defense - Zones and Defenders')
fig3.savefig('coverage_zones.png', dpi=150, bbox_inches='tight')
print(" Saved: coverage_zones.png")
print("\n" + "=" * 70)
print("DEMONSTRATION COMPLETE")
print("=" * 70)
Key Insights
Route Analysis Findings
-
Efficiency Metric: Route efficiency (ideal distance / actual distance) quickly identifies receivers who waste motion through rounded breaks or drift.
-
Break Sharpness: Measured as degrees per second at the break point, this metric distinguishes elite route runners who can change direction quickly.
-
Speed Maintenance: The best route runners maintain high stem speed before decelerating only at the last moment before the break.
Coverage Analysis Findings
-
Shell Recognition: Pre-snap safety positions provide reliable indicators of coverage type, enabling quick identification of soft spots.
-
Zone Vulnerability Mapping: Every zone coverage has predictable soft spots where routes can find open space between defenders.
-
Matchup Optimization: Combining route templates with zone maps enables data-driven play calling against specific coverages.
Exercises
-
Add Man Coverage Analysis: Extend the system to analyze man coverage by tracking receiver-defender distances throughout routes.
-
Build Route Comparison Tool: Create a visualization comparing a receiver's routes to the ideal template for each route type.
-
Create Game Plan Generator: Build a tool that takes an opponent's coverage tendencies and generates recommended passing concepts.
Applications
- Pre-game preparation: Identify coverage tendencies and prepare route adjustments
- Player development: Provide receivers with quantified feedback on route running
- In-game adjustments: Quick identification of coverage soft spots between series
- Recruiting evaluation: Compare prospect route running to college/NFL benchmarks
Further Reading
- Route running metrics in football analytics literature
- Zone coverage theory and deployment strategies
- Tracking data applications in professional football