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)

  1. Field surface (lowest) - zorder=0
  2. Yard lines and markings - zorder=1
  3. Zone overlays - zorder=2
  4. Routes and paths - zorder=3
  5. Player markers - zorder=4
  6. 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

  1. Use NumPy arrays for position calculations (vectorized operations)
  2. Pre-compute distances when analyzing multiple frames
  3. Limit animation frames for large tracking datasets
  4. Cache KDE results when creating multiple heat maps
  5. Use blitting in animations for smoother playback

Common Mistakes to Avoid

  1. ❌ Forgetting to flip coordinates for play direction
  2. ❌ Using wrong coordinate system (absolute vs relative)
  3. ❌ Not accounting for end zones in x-coordinates
  4. ❌ Incorrect aspect ratio (field should be ~2.25:1)
  5. ❌ Placing labels behind other elements (z-order)
  6. ❌ 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

  1. Draw all standard offensive formations
  2. Create route trees for each receiver position
  3. Build heat maps for rushing vs passing zones
  4. Animate a full play with speed coloring
  5. Classify coverages from defender positions