Chapter 16: Key Takeaways - Spatial Analysis and Field Visualization
Quick Reference Summary
This chapter covered spatial analysis techniques for football, from basic field visualization to advanced tracking data animation.
Essential Concepts
1. Football Field Dimensions
| Measurement | Value | Notes |
|---|---|---|
| Total length | 120 yards | Including end zones |
| Playing field | 100 yards | Between goal lines |
| Width | 53.33 yards | 160 feet |
| End zone depth | 10 yards | Each end |
| College hash marks | 40 feet from sideline | ~13.33 yards |
| NFL hash marks | 18.5 feet from center | Narrower than college |
2. Coordinate Systems
Absolute Coordinates: - Origin at corner of field (0, 0) - x: 0-120 yards (length) - y: 0-53.33 yards (width)
Relative Coordinates (Preferred for analysis): - Origin at line of scrimmage, center of field - x: Yards downfield from LOS - y: Yards from center (positive = offense's right)
Offense-Oriented Transformation:
# Flip plays so offense always moves left-to-right
if play_direction == 'left':
x = 120 - x # Flip x
y = 53.33 - y # Flip y
3. Core Visualization Classes
# Field Drawing
class FootballField:
def draw(self, figsize, show_numbers, show_hashes, vertical)
def draw_half_field(self, side, figsize)
# Player Positions
class PlayerPlotter:
def plot_positions(self, ax, players, show_labels, highlight_players)
def plot_formation(self, ax, offensive_positions, defensive_positions, los)
# Routes
class RouteVisualizer:
def draw_route(self, ax, start, waypoints, route_type, show_break)
def draw_passing_concept(self, ax, routes, los, title)
# Heat Maps
class SpatialHeatMap:
def create_target_heatmap(self, ax, target_locations, cmap, levels)
def create_zone_heatmap(self, ax, zone_values, cmap)
Key Formulas
Speed Calculation
Speed (yd/s) = √[(x₂-x₁)² + (y₂-y₁)²] / Δt
To convert to mph: speed_yd_s × 2.045
Separation Distance
Separation = √[(receiver_x - defender_x)² + (receiver_y - defender_y)²]
Route Efficiency
Efficiency = Ideal Distance / Actual Distance
Where:
- Ideal Distance = straight lines to break point and target
- Actual Distance = sum of all segment lengths
Acceleration
Acceleration = Δspeed / Δt = (speed₂ - speed₁) / (t₂ - t₁)
Visualization Best Practices
1. Field Setup
def setup_field(ax, half=True):
"""Standard field setup."""
if half:
ax.set_xlim(-5, 55)
else:
ax.set_xlim(-10, 110)
ax.set_ylim(-30, 30)
ax.set_facecolor('#228B22') # Grass green
ax.set_aspect('equal')
2. Layer Order (Z-Order)
- Field surface (lowest) - zorder=0
- Yard lines and markings - zorder=1
- Zone overlays - zorder=2
- Routes and paths - zorder=3
- Player markers - zorder=4
- Labels and annotations - zorder=5 (highest)
3. Color Conventions
- Offense: Blue tones (#3498DB, #2980B9)
- Defense: Red tones (#E74C3C, #C0392B)
- Routes: Solid lines with arrows at endpoints
- Heat maps: Sequential colormaps (viridis, plasma, hot)
- Zones: Semi-transparent fills (alpha=0.3)
Common Patterns
Drawing a Formation
def draw_formation(ax, positions, los=25):
"""Draw offensive formation."""
for pos, (x, y) in positions.items():
ax.scatter(los + x, y, s=300, c='blue', marker='o',
edgecolor='white', zorder=5)
ax.text(los + x, y, pos, ha='center', va='center',
color='white', fontsize=8, fontweight='bold')
Creating a Heat Map
from scipy.stats import gaussian_kde
def create_heatmap(ax, x_points, y_points, levels=20):
"""Create KDE-based heat map."""
# Create grid
xx, yy = np.mgrid[0:50:100j, -25:25:100j]
positions = np.vstack([xx.ravel(), yy.ravel()])
# Estimate density
kernel = gaussian_kde(np.vstack([x_points, y_points]))
density = kernel(positions).reshape(xx.shape)
# Plot
ax.contourf(xx, yy, density, levels=levels, cmap='hot', alpha=0.7)
Animating Player Movement
from matplotlib.animation import FuncAnimation
def animate_play(fig, ax, tracking_data, fps=10):
"""Create play animation."""
def update(frame):
ax.clear()
draw_field(ax)
frame_data = tracking_data[tracking_data['frame'] == frame]
for _, player in frame_data.iterrows():
ax.scatter(player['x'], player['y'], s=200)
return ax,
anim = FuncAnimation(fig, update, frames=tracking_data['frame'].unique(),
interval=1000/fps, blit=True)
return anim
Zone Analysis Framework
Standard Field Zones
Depth Zones (from LOS): - Behind LOS: x < 0 - Short: 0 ≤ x < 5 - Intermediate: 5 ≤ x < 15 - Deep: 15 ≤ x < 25 - Very Deep: x ≥ 25
Width Zones (from center): - Left Sideline: y < -20 - Left Numbers: -20 ≤ y < -10 - Left Hash: -10 ≤ y < 0 - Right Hash: 0 ≤ y < 10 - Right Numbers: 10 ≤ y < 20 - Right Sideline: y ≥ 20
Zone-Based Metrics
def get_zone_stats(plays_df, zone_col='zone'):
"""Calculate stats by zone."""
return plays_df.groupby(zone_col).agg({
'epa': 'mean',
'success': 'mean',
'yards': 'mean',
'play_id': 'count'
}).rename(columns={'play_id': 'attempts'})
Quick Code Snippets
Check if Point is in Bounds
def in_bounds(x, y):
return 0 <= x <= 120 and 0 <= y <= 53.33
Convert Yard Line to Coordinate
def yard_line_to_x(yard_line, own_territory=True):
"""Convert yard line (1-50) to x coordinate."""
if own_territory:
return yard_line + 10 # Account for end zone
else:
return 110 - yard_line
Calculate Direction of Movement
def get_direction(x1, y1, x2, y2):
"""Get movement direction in degrees (0=right, 90=up)."""
return np.degrees(np.arctan2(y2-y1, x2-x1))
Smooth Tracking Data
from scipy.signal import savgol_filter
def smooth_tracking(positions, window=5):
"""Apply Savitzky-Golay filter to reduce noise."""
return savgol_filter(positions, window, polyorder=2)
Coverage Zone Reference
Cover 3 Zones
| Zone | X Range | Y Range |
|---|---|---|
| Deep Left | 15-50 | -26.67 to -8 |
| Deep Middle | 15-50 | -8 to 8 |
| Deep Right | 15-50 | 8 to 26.67 |
| Flat Left | 0-8 | Sideline to numbers |
| Hook/Curl | 5-15 | Hash marks |
| Flat Right | 0-8 | Numbers to sideline |
Shell Identification
- 2-High: Two safeties at 12+ yards depth
- 1-High: Single deep safety (Cover 1 or Cover 3)
- 0-High: No deep safety (Cover 0 blitz)
Performance Tips
- Use NumPy arrays for position calculations (vectorized operations)
- Pre-compute distances when analyzing multiple frames
- Limit animation frames for large tracking datasets
- Cache KDE results when creating multiple heat maps
- Use
blittingin animations for smoother playback
Common Mistakes to Avoid
- ❌ Forgetting to flip coordinates for play direction
- ❌ Using wrong coordinate system (absolute vs relative)
- ❌ Not accounting for end zones in x-coordinates
- ❌ Incorrect aspect ratio (field should be ~2.25:1)
- ❌ Placing labels behind other elements (z-order)
- ❌ Using linear interpolation for speed (use distance/time)
Tools and Libraries Summary
| Tool | Use Case |
|---|---|
| matplotlib | Static field visualizations, formations |
| scipy.stats.gaussian_kde | Heat maps and density plots |
| scipy.signal.savgol_filter | Smoothing tracking data |
| matplotlib.animation | Play animations |
| plotly | Interactive field visualizations |
| shapely | Geometric operations (zones, convex hull) |
Further Practice
- Draw all standard offensive formations
- Create route trees for each receiver position
- Build heat maps for rushing vs passing zones
- Animate a full play with speed coloring
- Classify coverages from defender positions