Pressing Analytics

Beginner 10 min read 1 views Nov 27, 2025
# Pressing Analytics ## Overview Pressing analytics quantify defensive pressure applied to opponents. These metrics measure the intensity, effectiveness, and spatial distribution of pressing actions to disrupt opponent possession and regain the ball. ## Key Metrics ### PPDA (Passes Per Defensive Action) - **Formula**: Opponent Passes / Defensive Actions - **Lower values** = More intense pressing - **Typical Range**: 6-15 for high-pressing teams ### Pressing Intensity Frequency and location of defensive actions in opponent's half. ### Counter-Pressing Defensive actions within 5 seconds of losing possession. ### Press Success Rate Percentage of pressing sequences leading to ball recovery. ## Python Implementation ```python import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from scipy.spatial.distance import cdist from matplotlib.patches import Rectangle, Circle import matplotlib.patches as mpatches class PressingAnalyzer: """Analyze team pressing and defensive pressure metrics.""" def __init__(self, event_data, tracking_data=None): """ Initialize pressing analyzer. Parameters: ----------- event_data : pd.DataFrame Event data with columns: team, event_type, x, y, timestamp, outcome tracking_data : pd.DataFrame, optional Tracking data for spatial analysis """ self.event_data = event_data.copy() self.tracking_data = tracking_data self.pressing_metrics = None def calculate_ppda(self, team, opponent_half=True): """ Calculate PPDA (Passes Per Defensive Action). Parameters: ----------- team : str Team performing pressing opponent_half : bool Calculate only in opponent's half """ # Get opponent opponent = self.event_data[self.event_data['team'] != team]['team'].iloc[0] # Filter by field location if specified if opponent_half: # Opponent half means x > 50 (assuming team attacks right) opponent_events = self.event_data[ (self.event_data['team'] == opponent) & (self.event_data['x'] > 50) ] team_events = self.event_data[ (self.event_data['team'] == team) & (self.event_data['x'] > 50) ] else: opponent_events = self.event_data[self.event_data['team'] == opponent] team_events = self.event_data[self.event_data['team'] == team] # Count opponent passes opponent_passes = len(opponent_events[opponent_events['event_type'] == 'Pass']) # Count defensive actions (tackles, interceptions, pressures) defensive_actions = len(team_events[ team_events['event_type'].isin(['Tackle', 'Interception', 'Pressure']) ]) # Calculate PPDA if defensive_actions > 0: ppda = opponent_passes / defensive_actions else: ppda = float('inf') return { 'team': team, 'ppda': round(ppda, 2), 'opponent_passes': opponent_passes, 'defensive_actions': defensive_actions, 'location': 'opponent_half' if opponent_half else 'full_pitch' } def analyze_pressing_intensity(self, team): """ Analyze pressing intensity by pitch zone. Parameters: ----------- team : str Team performing pressing """ # Filter defensive actions defensive_events = self.event_data[ (self.event_data['team'] == team) & (self.event_data['event_type'].isin(['Tackle', 'Interception', 'Pressure'])) ].copy() # Define zones defensive_events['zone_x'] = pd.cut( defensive_events['x'], bins=[0, 33, 66, 100], labels=['Defensive', 'Middle', 'Attacking'] ) defensive_events['zone_y'] = pd.cut( defensive_events['y'], bins=[0, 33, 66, 100], labels=['Left', 'Center', 'Right'] ) # Count by zone zone_intensity = defensive_events.groupby(['zone_x', 'zone_y']).size().unstack(fill_value=0) return zone_intensity def detect_counter_pressing(self, team, time_window=5): """ Detect counter-pressing actions. Parameters: ----------- team : str Team to analyze time_window : float Seconds after possession loss """ # Sort by timestamp events = self.event_data.sort_values('timestamp').copy() counter_presses = [] for i in range(len(events) - 1): event = events.iloc[i] # Check if team lost possession if event['team'] == team and event['outcome'] == 'Incomplete': loss_time = event['timestamp'] loss_x = event['x'] loss_y = event['y'] # Look for defensive action in time window future_events = events[ (events['timestamp'] > loss_time) & (events['timestamp'] <= loss_time + time_window) ] for _, future_event in future_events.iterrows(): if (future_event['team'] == team and future_event['event_type'] in ['Tackle', 'Interception', 'Pressure']): # Calculate distance from loss distance = np.sqrt( (future_event['x'] - loss_x)**2 + (future_event['y'] - loss_y)**2 ) counter_presses.append({ 'timestamp': future_event['timestamp'], 'time_to_press': future_event['timestamp'] - loss_time, 'distance': distance, 'x': future_event['x'], 'y': future_event['y'] }) break # Only count first action return pd.DataFrame(counter_presses) def calculate_press_success_rate(self, team): """ Calculate press success rate. Parameters: ----------- team : str Team to analyze """ # Get defensive actions defensive_actions = self.event_data[ (self.event_data['team'] == team) & (self.event_data['event_type'].isin(['Tackle', 'Interception', 'Pressure'])) ].copy() # Sort by timestamp defensive_actions = defensive_actions.sort_values('timestamp') events_sorted = self.event_data.sort_values('timestamp') successful_presses = 0 for idx, action in defensive_actions.iterrows(): action_time = action['timestamp'] # Look for ball recovery within 5 seconds future_events = events_sorted[ (events_sorted['timestamp'] > action_time) & (events_sorted['timestamp'] <= action_time + 5) ] # Check if team regained possession for _, event in future_events.iterrows(): if event['team'] == team and event['event_type'] in ['Pass', 'Carry', 'Shot']: successful_presses += 1 break elif event['team'] != team and event['event_type'] in ['Pass', 'Carry']: # Opponent retained possession break total_presses = len(defensive_actions) success_rate = (successful_presses / total_presses * 100) if total_presses > 0 else 0 return { 'team': team, 'total_presses': total_presses, 'successful_presses': successful_presses, 'success_rate': round(success_rate, 2) } def visualize_pressing_heatmap(self, team, figsize=(12, 8)): """ Visualize pressing intensity heatmap. Parameters: ----------- team : str Team to visualize figsize : tuple Figure size """ # Filter defensive actions defensive_events = self.event_data[ (self.event_data['team'] == team) & (self.event_data['event_type'].isin(['Tackle', 'Interception', 'Pressure'])) ] # Create figure fig, ax = plt.subplots(figsize=figsize) # Draw pitch self._draw_pitch(ax) # Create 2D histogram if len(defensive_events) > 0: heatmap, xedges, yedges = np.histogram2d( defensive_events['x'], defensive_events['y'], bins=(20, 15), range=[[0, 100], [0, 100]] ) # Plot heatmap extent = [xedges[0], xedges[-1], yedges[0], yedges[-1]] im = ax.imshow( heatmap.T, extent=extent, origin='lower', cmap='YlOrRd', alpha=0.6, aspect='auto' ) # Colorbar cbar = plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04) cbar.set_label('Defensive Actions', fontsize=12, fontweight='bold') ax.set_title(f'Pressing Heatmap - {team}', fontsize=16, fontweight='bold', pad=20) plt.tight_layout() return fig def _draw_pitch(self, ax): """Draw soccer pitch.""" # Pitch outline pitch = Rectangle((0, 0), 100, 100, linewidth=2, edgecolor='white', facecolor='#1e8449') ax.add_patch(pitch) # Halfway line ax.plot([50, 50], [0, 100], color='white', linewidth=2) # Center circle circle = plt.Circle((50, 50), 8.7, color='white', fill=False, linewidth=2) ax.add_patch(circle) ax.set_xlim(-2, 102) ax.set_ylim(-2, 102) ax.set_aspect('equal') ax.axis('off') def generate_pressing_report(self, team): """Generate comprehensive pressing report.""" report = { 'ppda_full': self.calculate_ppda(team, opponent_half=False), 'ppda_opponent_half': self.calculate_ppda(team, opponent_half=True), 'press_success': self.calculate_press_success_rate(team), 'intensity_by_zone': self.analyze_pressing_intensity(team) } # Counter-pressing counter_press = self.detect_counter_pressing(team) report['counter_pressing'] = { 'total_instances': len(counter_press), 'avg_time_to_press': counter_press['time_to_press'].mean() if len(counter_press) > 0 else 0, 'avg_distance': counter_press['distance'].mean() if len(counter_press) > 0 else 0 } self.pressing_metrics = report return report # Example Usage if __name__ == "__main__": # Generate sample event data np.random.seed(42) events = [] timestamp = 0 teams = ['Team A', 'Team B'] for _ in range(1000): team = np.random.choice(teams, p=[0.52, 0.48]) # Event type distribution event_type = np.random.choice( ['Pass', 'Carry', 'Shot', 'Tackle', 'Interception', 'Pressure'], p=[0.55, 0.15, 0.05, 0.10, 0.08, 0.07] ) # Position (Team A attacks right) if team == 'Team A': x = np.random.uniform(20, 90) else: x = 100 - np.random.uniform(20, 90) y = np.random.uniform(10, 90) # Outcome if event_type == 'Pass': outcome = np.random.choice(['Complete', 'Incomplete'], p=[0.78, 0.22]) else: outcome = 'Complete' events.append({ 'team': team, 'event_type': event_type, 'x': x, 'y': y, 'timestamp': timestamp, 'outcome': outcome }) timestamp += np.random.uniform(1, 4) event_data = pd.DataFrame(events) # Analyze pressing analyzer = PressingAnalyzer(event_data) # Generate report print("PRESSING ANALYTICS REPORT - Team A\n") report = analyzer.generate_pressing_report('Team A') print(f"PPDA (Full Pitch): {report['ppda_full']['ppda']}") print(f"PPDA (Opponent Half): {report['ppda_opponent_half']['ppda']}") print(f"\nPress Success Rate: {report['press_success']['success_rate']}%") print(f"Total Presses: {report['press_success']['total_presses']}") print(f"Successful Presses: {report['press_success']['successful_presses']}") print(f"\nCounter-Pressing:") print(f" Instances: {report['counter_pressing']['total_instances']}") print(f" Avg Time to Press: {report['counter_pressing']['avg_time_to_press']:.2f}s") print("\nPressing Intensity by Zone:") print(report['intensity_by_zone']) # Visualize fig = analyzer.visualize_pressing_heatmap('Team A') plt.savefig('pressing_heatmap.png', dpi=300, bbox_inches='tight') print("\nPressing heatmap saved as 'pressing_heatmap.png'") ``` ## R Implementation ```r library(tidyverse) library(ggplot2) library(viridis) # Pressing Analyzer PressingAnalyzer <- R6::R6Class("PressingAnalyzer", public = list( event_data = NULL, pressing_metrics = NULL, initialize = function(event_data) { self$event_data <- event_data }, calculate_ppda = function(team, opponent_half = TRUE) { opponent <- unique(self$event_data$team[self$event_data$team != team])[1] if (opponent_half) { opponent_events <- self$event_data %>% filter(team == opponent, x > 50) team_events <- self$event_data %>% filter(team == !!team, x > 50) } else { opponent_events <- self$event_data %>% filter(team == opponent) team_events <- self$event_data %>% filter(team == !!team) } opponent_passes <- opponent_events %>% filter(event_type == 'Pass') %>% nrow() defensive_actions <- team_events %>% filter(event_type %in% c('Tackle', 'Interception', 'Pressure')) %>% nrow() ppda <- if (defensive_actions > 0) { opponent_passes / defensive_actions } else { Inf } list( team = team, ppda = round(ppda, 2), opponent_passes = opponent_passes, defensive_actions = defensive_actions, location = if (opponent_half) 'opponent_half' else 'full_pitch' ) }, analyze_pressing_intensity = function(team) { defensive_events <- self$event_data %>% filter( team == !!team, event_type %in% c('Tackle', 'Interception', 'Pressure') ) %>% mutate( zone_x = cut(x, breaks = c(0, 33, 66, 100), labels = c('Defensive', 'Middle', 'Attacking')), zone_y = cut(y, breaks = c(0, 33, 66, 100), labels = c('Left', 'Center', 'Right')) ) zone_intensity <- defensive_events %>% count(zone_x, zone_y) %>% pivot_wider(names_from = zone_y, values_from = n, values_fill = 0) return(zone_intensity) }, detect_counter_pressing = function(team, time_window = 5) { events <- self$event_data %>% arrange(timestamp) counter_presses <- list() for (i in 1:(nrow(events) - 1)) { event <- events[i, ] if (event$team == team && event$outcome == 'Incomplete') { loss_time <- event$timestamp loss_x <- event$x loss_y <- event$y future_events <- events %>% filter( timestamp > loss_time, timestamp <= loss_time + time_window ) for (j in 1:nrow(future_events)) { future_event <- future_events[j, ] if (future_event$team == team && future_event$event_type %in% c('Tackle', 'Interception', 'Pressure')) { distance <- sqrt((future_event$x - loss_x)^2 + (future_event$y - loss_y)^2) counter_presses[[length(counter_presses) + 1]] <- list( timestamp = future_event$timestamp, time_to_press = future_event$timestamp - loss_time, distance = distance, x = future_event$x, y = future_event$y ) break } } } } bind_rows(counter_presses) }, calculate_press_success_rate = function(team) { defensive_actions <- self$event_data %>% filter( team == !!team, event_type %in% c('Tackle', 'Interception', 'Pressure') ) %>% arrange(timestamp) events_sorted <- self$event_data %>% arrange(timestamp) successful_presses <- 0 for (i in 1:nrow(defensive_actions)) { action <- defensive_actions[i, ] action_time <- action$timestamp future_events <- events_sorted %>% filter( timestamp > action_time, timestamp <= action_time + 5 ) for (j in 1:nrow(future_events)) { event <- future_events[j, ] if (event$team == team && event$event_type %in% c('Pass', 'Carry', 'Shot')) { successful_presses <- successful_presses + 1 break } else if (event$team != team && event$event_type %in% c('Pass', 'Carry')) { break } } } total_presses <- nrow(defensive_actions) success_rate <- if (total_presses > 0) { (successful_presses / total_presses) * 100 } else { 0 } list( team = team, total_presses = total_presses, successful_presses = successful_presses, success_rate = round(success_rate, 2) ) }, visualize_pressing_heatmap = function(team) { defensive_events <- self$event_data %>% filter( team == !!team, event_type %in% c('Tackle', 'Interception', 'Pressure') ) p <- ggplot() + # Pitch geom_rect(aes(xmin = 0, xmax = 100, ymin = 0, ymax = 100), fill = '#1e8449', color = 'white', size = 1) + geom_vline(xintercept = 50, color = 'white', size = 1) + # Heatmap stat_density_2d( data = defensive_events, aes(x = x, y = y, fill = after_stat(level)), geom = 'polygon', alpha = 0.6 ) + scale_fill_viridis(option = 'inferno', name = 'Intensity') + coord_fixed(ratio = 1) + labs(title = paste('Pressing Heatmap -', team)) + theme_void() + theme( plot.title = element_text(face = 'bold', size = 16, hjust = 0.5), legend.position = 'right' ) return(p) }, generate_pressing_report = function(team) { report <- list( ppda_full = self$calculate_ppda(team, opponent_half = FALSE), ppda_opponent_half = self$calculate_ppda(team, opponent_half = TRUE), press_success = self$calculate_press_success_rate(team), intensity_by_zone = self$analyze_pressing_intensity(team) ) counter_press <- self$detect_counter_pressing(team) report$counter_pressing <- list( total_instances = nrow(counter_press), avg_time_to_press = if (nrow(counter_press) > 0) mean(counter_press$time_to_press) else 0, avg_distance = if (nrow(counter_press) > 0) mean(counter_press$distance) else 0 ) self$pressing_metrics <- report return(report) } ) ) # Example usage set.seed(42) # Generate sample data event_data <- tibble( team = sample(c('Team A', 'Team B'), 1000, replace = TRUE, prob = c(0.52, 0.48)), event_type = sample( c('Pass', 'Carry', 'Shot', 'Tackle', 'Interception', 'Pressure'), 1000, replace = TRUE, prob = c(0.55, 0.15, 0.05, 0.10, 0.08, 0.07) ), timestamp = cumsum(runif(1000, 1, 4)) ) %>% mutate( x = if_else(team == 'Team A', runif(1000, 20, 90), 100 - runif(1000, 20, 90)), y = runif(1000, 10, 90), outcome = if_else(event_type == 'Pass', sample(c('Complete', 'Incomplete'), 1000, replace = TRUE, prob = c(0.78, 0.22)), 'Complete') ) # Analyze analyzer <- PressingAnalyzer$new(event_data) cat("PRESSING ANALYTICS REPORT - Team A\n\n") report <- analyzer$generate_pressing_report('Team A') cat(sprintf("PPDA (Full Pitch): %.2f\n", report$ppda_full$ppda)) cat(sprintf("PPDA (Opponent Half): %.2f\n", report$ppda_opponent_half$ppda)) cat(sprintf("\nPress Success Rate: %.2f%%\n", report$press_success$success_rate)) cat(sprintf("Total Presses: %d\n", report$press_success$total_presses)) cat("\nPressing Intensity by Zone:\n") print(report$intensity_by_zone) # Visualize p <- analyzer$visualize_pressing_heatmap('Team A') ggsave('pressing_heatmap_r.png', p, width = 12, height = 8, dpi = 300) cat("\nHeatmap saved\n") ``` ## Interpretation Guidelines 1. **PPDA < 8**: Very high pressing intensity 2. **PPDA 8-12**: Moderate pressing 3. **PPDA > 12**: Low pressing intensity 4. **Success Rate > 35%**: Effective pressing 5. **Counter-Pressing**: Quick reactions indicate well-drilled team ## Applications - **Tactical Planning**: Design pressing strategies - **Opposition Analysis**: Identify pressing triggers - **Performance Evaluation**: Measure defensive work rate - **Player Recruitment**: Find high-intensity players - **Training Design**: Improve pressing coordination

Discussion

Have questions or feedback? Join our community discussion on Discord or GitHub Discussions.
Table of Contents
Quick Actions
Glossary