Progressive Passes

Beginner 10 min read 0 views Nov 27, 2025
# Progressive Passes ## Overview Progressive passes measure forward ball movement toward the opponent's goal. These metrics identify players and teams who advance play effectively, breaking defensive lines and creating attacking opportunities. ## Definition Criteria A pass is considered **progressive** if it meets one of these criteria: 1. **Distance-based**: Moves ball ≥10 meters toward goal (in own half) or ≥5 meters (in opponent half) 2. **Zone-based**: Advances ball into next attacking zone 3. **Line-breaking**: Bypasses opponent's defensive line ## Key Metrics - **Progressive Passes**: Total forward passes meeting criteria - **Progressive Distance**: Cumulative meters gained - **Progression Rate**: % of passes that are progressive - **Final Third Entries**: Passes into attacking zone ## Python Implementation ```python import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from matplotlib.patches import Rectangle, FancyArrowPatch import matplotlib.patches as mpatches class ProgressivePassAnalyzer: """Analyze progressive passes and forward ball progression.""" def __init__(self, pass_data, pitch_length=105, pitch_width=68): """ Initialize progressive pass analyzer. Parameters: ----------- pass_data : pd.DataFrame Pass data with columns: player, team, x_start, y_start, x_end, y_end, outcome (coordinates normalized 0-100) pitch_length : float Pitch length in meters pitch_width : float Pitch width in meters """ self.pass_data = pass_data.copy() self.pitch_length = pitch_length self.pitch_width = pitch_width self._calculate_distances() def _calculate_distances(self): """Calculate actual distances in meters.""" # Convert normalized coordinates to meters self.pass_data['x_start_m'] = self.pass_data['x_start'] * self.pitch_length / 100 self.pass_data['y_start_m'] = self.pass_data['y_start'] * self.pitch_width / 100 self.pass_data['x_end_m'] = self.pass_data['x_end'] * self.pitch_length / 100 self.pass_data['y_end_m'] = self.pass_data['y_end'] * self.pitch_width / 100 # Calculate pass distance self.pass_data['pass_distance'] = np.sqrt( (self.pass_data['x_end_m'] - self.pass_data['x_start_m'])**2 + (self.pass_data['y_end_m'] - self.pass_data['y_start_m'])**2 ) # Calculate forward progression (x-axis only) self.pass_data['forward_distance'] = ( self.pass_data['x_end_m'] - self.pass_data['x_start_m'] ) def identify_progressive_passes(self, method='distance'): """ Identify progressive passes. Parameters: ----------- method : str 'distance', 'zone', or 'combined' """ if method == 'distance': self._progressive_distance_based() elif method == 'zone': self._progressive_zone_based() else: # combined self._progressive_distance_based() dist_prog = self.pass_data['is_progressive'].copy() self._progressive_zone_based() zone_prog = self.pass_data['is_progressive'].copy() self.pass_data['is_progressive'] = dist_prog | zone_prog return self.pass_data def _progressive_distance_based(self): """Distance-based progressive pass identification.""" conditions = [] # Own half: ≥10m forward own_half = self.pass_data['x_start_m'] < (self.pitch_length / 2) own_half_prog = own_half & (self.pass_data['forward_distance'] >= 10) # Opponent half: ≥5m forward opp_half = self.pass_data['x_start_m'] >= (self.pitch_length / 2) opp_half_prog = opp_half & (self.pass_data['forward_distance'] >= 5) self.pass_data['is_progressive'] = own_half_prog | opp_half_prog def _progressive_zone_based(self): """Zone-based progressive pass identification.""" # Define zones (thirds) def get_zone(x): if x < 35: return 1 # Defensive third elif x < 70: return 2 # Middle third else: return 3 # Attacking third self.pass_data['start_zone'] = self.pass_data['x_start'].apply(get_zone) self.pass_data['end_zone'] = self.pass_data['x_end'].apply(get_zone) # Progressive if advances at least one zone self.pass_data['is_progressive'] = ( self.pass_data['end_zone'] > self.pass_data['start_zone'] ) def calculate_player_metrics(self): """Calculate progressive pass metrics by player.""" # Filter successful passes only successful = self.pass_data[self.pass_data['outcome'] == 'Complete'].copy() player_stats = successful.groupby('player').agg({ 'is_progressive': ['sum', 'count'], 'forward_distance': 'sum', 'pass_distance': 'mean' }).reset_index() player_stats.columns = [ 'player', 'progressive_passes', 'total_passes', 'total_progression_distance', 'avg_pass_distance' ] # Calculate rates player_stats['progression_rate'] = ( player_stats['progressive_passes'] / player_stats['total_passes'] * 100 ).round(2) player_stats['progressive_distance_per_pass'] = ( player_stats['total_progression_distance'] / player_stats['progressive_passes'] ).round(2) # Count final third entries final_third_entries = successful[ (successful['x_end'] >= 70) & (successful['x_start'] < 70) ].groupby('player').size().reset_index(name='final_third_entries') player_stats = player_stats.merge(final_third_entries, on='player', how='left') player_stats['final_third_entries'] = player_stats['final_third_entries'].fillna(0).astype(int) return player_stats.sort_values('progressive_passes', ascending=False) def calculate_team_metrics(self): """Calculate team-level progressive metrics.""" successful = self.pass_data[self.pass_data['outcome'] == 'Complete'].copy() team_stats = successful.groupby('team').agg({ 'is_progressive': ['sum', 'count'], 'forward_distance': ['sum', 'mean'], 'pass_distance': 'mean' }).reset_index() team_stats.columns = [ 'team', 'progressive_passes', 'total_passes', 'total_progression', 'avg_progression', 'avg_pass_distance' ] team_stats['progression_rate'] = ( team_stats['progressive_passes'] / team_stats['total_passes'] * 100 ).round(2) return team_stats def visualize_progressive_passes(self, player=None, team=None, sample_size=50): """ Visualize progressive passes on pitch. Parameters: ----------- player : str, optional Specific player to visualize team : str, optional Specific team to visualize sample_size : int Maximum passes to display """ # Filter data plot_data = self.pass_data[ (self.pass_data['is_progressive'] == True) & (self.pass_data['outcome'] == 'Complete') ].copy() if player: plot_data = plot_data[plot_data['player'] == player] if team: plot_data = plot_data[plot_data['team'] == team] # Sample if too many if len(plot_data) > sample_size: plot_data = plot_data.sample(sample_size, random_state=42) # Create figure fig, ax = plt.subplots(figsize=(14, 10)) # Draw pitch self._draw_pitch(ax) # Draw progressive passes for _, row in plot_data.iterrows(): # Color based on progression distance color_intensity = min(row['forward_distance'] / 30, 1.0) color = plt.cm.YlOrRd(color_intensity) # Arrow width based on total pass distance width = 0.15 + (row['pass_distance'] / 50) * 0.35 arrow = FancyArrowPatch( (row['x_start'], row['y_start']), (row['x_end'], row['y_end']), arrowstyle='->,head_width=0.8,head_length=1.5', color=color, linewidth=width, alpha=0.6, zorder=5 ) ax.add_patch(arrow) # Title title = 'Progressive Passes' if player: title += f' - {player}' elif team: title += f' - {team}' ax.set_title(title, fontsize=16, fontweight='bold', pad=20) # Legend legend_elements = [ mpatches.Patch(facecolor='yellow', edgecolor='black', label='Short progression (< 10m)'), mpatches.Patch(facecolor='orange', edgecolor='black', label='Medium progression (10-20m)'), mpatches.Patch(facecolor='red', edgecolor='black', label='Long progression (> 20m)') ] ax.legend(handles=legend_elements, loc='upper left', fontsize=10) 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) # Thirds ax.plot([33.33, 33.33], [0, 100], color='white', linewidth=1, linestyle='--', alpha=0.5) ax.plot([66.67, 66.67], [0, 100], color='white', linewidth=1, linestyle='--', alpha=0.5) # Center circle circle = plt.Circle((50, 50), 8.7, color='white', fill=False, linewidth=2) ax.add_patch(circle) # Penalty boxes penalty_left = Rectangle((0, 21.1), 15.7, 57.8, linewidth=2, edgecolor='white', facecolor='none') penalty_right = Rectangle((84.3, 21.1), 15.7, 57.8, linewidth=2, edgecolor='white', facecolor='none') ax.add_patch(penalty_left) ax.add_patch(penalty_right) ax.set_xlim(-2, 102) ax.set_ylim(-2, 102) ax.set_aspect('equal') ax.axis('off') def analyze_progression_patterns(self): """Analyze patterns in progressive passing.""" progressive = self.pass_data[self.pass_data['is_progressive'] == True].copy() patterns = { 'total_progressive': len(progressive), 'avg_progression_distance': progressive['forward_distance'].mean().round(2), 'max_progression': progressive['forward_distance'].max().round(2), 'progression_by_zone': progressive.groupby('start_zone')['forward_distance'].agg([ 'count', 'mean' ]).round(2).to_dict() } return patterns # Example Usage if __name__ == "__main__": # Generate sample pass data np.random.seed(42) passes = [] players = [f'Player {i}' for i in range(1, 12)] teams = ['Team A', 'Team B'] for _ in range(500): team = np.random.choice(teams) player = np.random.choice(players) # Start position x_start = np.random.uniform(10, 90) y_start = np.random.uniform(10, 90) # End position (with forward bias) forward_movement = np.random.exponential(15) lateral_movement = np.random.normal(0, 10) x_end = min(x_start + forward_movement, 95) y_end = np.clip(y_start + lateral_movement, 5, 95) outcome = np.random.choice(['Complete', 'Incomplete'], p=[0.80, 0.20]) passes.append({ 'player': player, 'team': team, 'x_start': x_start, 'y_start': y_start, 'x_end': x_end, 'y_end': y_end, 'outcome': outcome }) pass_data = pd.DataFrame(passes) # Analyze progressive passes analyzer = ProgressivePassAnalyzer(pass_data) analyzer.identify_progressive_passes(method='distance') # Player metrics print("Top Progressive Passers:") player_metrics = analyzer.calculate_player_metrics() print(player_metrics.head(5)) print() # Team metrics print("Team Progressive Metrics:") team_metrics = analyzer.calculate_team_metrics() print(team_metrics) print() # Patterns print("Progression Patterns:") patterns = analyzer.analyze_progression_patterns() print(f"Total Progressive Passes: {patterns['total_progressive']}") print(f"Average Progression: {patterns['avg_progression_distance']}m") print(f"Maximum Progression: {patterns['max_progression']}m") # Visualize fig = analyzer.visualize_progressive_passes(team='Team A', sample_size=40) plt.savefig('progressive_passes.png', dpi=300, bbox_inches='tight') print("\nProgressive passes visualization saved") ``` ## R Implementation ```r library(tidyverse) library(ggplot2) # Progressive Pass Analyzer ProgressivePassAnalyzer <- R6::R6Class("ProgressivePassAnalyzer", public = list( pass_data = NULL, pitch_length = 105, pitch_width = 68, initialize = function(pass_data, pitch_length = 105, pitch_width = 68) { self$pass_data <- pass_data self$pitch_length <- pitch_length self$pitch_width <- pitch_width self$calculate_distances() }, calculate_distances = function() { self$pass_data <- self$pass_data %>% mutate( x_start_m = x_start * self$pitch_length / 100, y_start_m = y_start * self$pitch_width / 100, x_end_m = x_end * self$pitch_length / 100, y_end_m = y_end * self$pitch_width / 100, pass_distance = sqrt((x_end_m - x_start_m)^2 + (y_end_m - y_start_m)^2), forward_distance = x_end_m - x_start_m ) }, identify_progressive_passes = function(method = 'distance') { if (method == 'distance') { self$progressive_distance_based() } else if (method == 'zone') { self$progressive_zone_based() } else { # Combined self$progressive_distance_based() dist_prog <- self$pass_data$is_progressive self$progressive_zone_based() self$pass_data$is_progressive <- dist_prog | self$pass_data$is_progressive } return(self$pass_data) }, progressive_distance_based = function() { self$pass_data <- self$pass_data %>% mutate( is_progressive = case_when( x_start_m < (self$pitch_length / 2) & forward_distance >= 10 ~ TRUE, x_start_m >= (self$pitch_length / 2) & forward_distance >= 5 ~ TRUE, TRUE ~ FALSE ) ) }, progressive_zone_based = function() { get_zone <- function(x) { case_when( x < 35 ~ 1, x < 70 ~ 2, TRUE ~ 3 ) } self$pass_data <- self$pass_data %>% mutate( start_zone = get_zone(x_start), end_zone = get_zone(x_end), is_progressive = end_zone > start_zone ) }, calculate_player_metrics = function() { successful <- self$pass_data %>% filter(outcome == 'Complete') player_stats <- successful %>% group_by(player) %>% summarise( progressive_passes = sum(is_progressive), total_passes = n(), total_progression_distance = sum(forward_distance[is_progressive]), avg_pass_distance = mean(pass_distance), final_third_entries = sum(x_end >= 70 & x_start < 70), .groups = 'drop' ) %>% mutate( progression_rate = round(progressive_passes / total_passes * 100, 2), progressive_distance_per_pass = round( total_progression_distance / pmax(progressive_passes, 1), 2 ) ) %>% arrange(desc(progressive_passes)) return(player_stats) }, calculate_team_metrics = function() { successful <- self$pass_data %>% filter(outcome == 'Complete') team_stats <- successful %>% group_by(team) %>% summarise( progressive_passes = sum(is_progressive), total_passes = n(), total_progression = sum(forward_distance[is_progressive]), avg_progression = mean(forward_distance[is_progressive]), avg_pass_distance = mean(pass_distance), .groups = 'drop' ) %>% mutate( progression_rate = round(progressive_passes / total_passes * 100, 2) ) return(team_stats) }, visualize_progressive_passes = function(player = NULL, team = NULL, sample_size = 50) { plot_data <- self$pass_data %>% filter(is_progressive == TRUE, outcome == 'Complete') if (!is.null(player)) { plot_data <- plot_data %>% filter(player == !!player) } if (!is.null(team)) { plot_data <- plot_data %>% filter(team == !!team) } if (nrow(plot_data) > sample_size) { plot_data <- plot_data %>% slice_sample(n = sample_size) } # Create plot 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) + geom_vline(xintercept = c(33.33, 66.67), color = 'white', size = 0.5, linetype = 'dashed', alpha = 0.5) + # Progressive passes geom_segment( data = plot_data, aes(x = x_start, y = y_start, xend = x_end, yend = y_end, color = forward_distance, size = pass_distance), arrow = arrow(length = unit(0.2, 'cm'), type = 'closed'), alpha = 0.6 ) + scale_color_gradient2( low = 'yellow', mid = 'orange', high = 'red', midpoint = 15, name = 'Forward\nProgression (m)' ) + scale_size_continuous(range = c(0.3, 1.5), guide = 'none') + coord_fixed(ratio = 1) + labs( title = if (!is.null(player)) paste('Progressive Passes -', player) else if (!is.null(team)) paste('Progressive Passes -', team) else 'Progressive Passes' ) + theme_void() + theme( plot.title = element_text(face = 'bold', size = 16, hjust = 0.5), legend.position = 'right' ) return(p) }, analyze_progression_patterns = function() { progressive <- self$pass_data %>% filter(is_progressive == TRUE) patterns <- list( total_progressive = nrow(progressive), avg_progression_distance = round(mean(progressive$forward_distance), 2), max_progression = round(max(progressive$forward_distance), 2), by_zone = progressive %>% group_by(start_zone) %>% summarise( count = n(), avg_distance = round(mean(forward_distance), 2), .groups = 'drop' ) ) return(patterns) } ) ) # Example usage set.seed(42) # Generate sample data pass_data <- tibble( player = sample(paste('Player', 1:11), 500, replace = TRUE), team = sample(c('Team A', 'Team B'), 500, replace = TRUE), x_start = runif(500, 10, 90), y_start = runif(500, 10, 90) ) %>% mutate( x_end = pmin(x_start + rexp(500, 1/15), 95), y_end = pmin(pmax(y_start + rnorm(500, 0, 10), 5), 95), outcome = sample(c('Complete', 'Incomplete'), 500, replace = TRUE, prob = c(0.80, 0.20)) ) # Analyze analyzer <- ProgressivePassAnalyzer$new(pass_data) analyzer$identify_progressive_passes(method = 'distance') cat("Top Progressive Passers:\n") player_metrics <- analyzer$calculate_player_metrics() print(head(player_metrics, 5)) cat("\nTeam Progressive Metrics:\n") team_metrics <- analyzer$calculate_team_metrics() print(team_metrics) cat("\nProgression Patterns:\n") patterns <- analyzer$analyze_progression_patterns() cat(sprintf("Total Progressive: %d\n", patterns$total_progressive)) cat(sprintf("Average Progression: %.2fm\n", patterns$avg_progression_distance)) cat(sprintf("Maximum Progression: %.2fm\n", patterns$max_progression)) # Visualize p <- analyzer$visualize_progressive_passes(team = 'Team A', sample_size = 40) ggsave('progressive_passes_r.png', p, width = 14, height = 10, dpi = 300) cat("\nVisualization saved\n") ``` ## Practical Applications 1. **Player Recruitment**: Identify progressive passers 2. **Tactical Analysis**: Assess build-up effectiveness 3. **Opposition Scouting**: Study opponent progression patterns 4. **Performance Evaluation**: Measure attacking contribution 5. **Training Focus**: Improve forward passing quality ## Best Practices - Consider pass completion rate alongside volume - Analyze progression by field zone - Compare against positional benchmarks - Account for tactical context - Combine with chance creation metrics

Discussion

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