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
Related Topics
Quick Actions