Player Radar Charts and Comparison Visualizations
Beginner
10 min read
0 views
Nov 27, 2025
# Player Radar Charts and Comparison Visualizations
## Overview
Player radar charts (also called spider charts or web charts) provide a multi-dimensional visualization of player performance across various attributes. They enable quick visual comparison between players and help identify strengths, weaknesses, and playing styles.
## Key Components
### Selecting Attributes
Choose 5-10 relevant attributes based on position:
**Forwards/Strikers:**
- Goals per 90
- xG per 90
- Shot accuracy
- Aerial duels won
- Touches in box
**Midfielders:**
- Key passes per 90
- Progressive passes
- Pass completion %
- Tackles won
- Distance covered
**Defenders:**
- Tackles won
- Interceptions
- Aerial duels won
- Pass completion %
- Clearances
## Python Implementation
```python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from math import pi
from scipy import stats
# Sample player data
player_data = pd.DataFrame({
'Player': ['Player A', 'Player B', 'Player C'],
'Goals_p90': [0.65, 0.42, 0.55],
'xG_p90': [0.58, 0.48, 0.52],
'Shot_Accuracy': [65, 72, 58],
'Aerial_Duels_Won': [68, 55, 75],
'Key_Passes_p90': [1.2, 2.1, 1.5],
'Dribbles_Success': [58, 45, 62],
'Pass_Completion': [78, 82, 75]
})
# Normalize data to 0-100 scale for better visualization
def normalize_stats(df, columns):
"""
Normalize statistics to 0-100 scale using percentile ranking
"""
df_norm = df.copy()
for col in columns:
if col != 'Player':
# Convert to percentile (0-100)
df_norm[col] = stats.rankdata(df[col], method='average') / len(df) * 100
return df_norm
stat_columns = [col for col in player_data.columns if col != 'Player']
player_data_norm = normalize_stats(player_data, stat_columns)
print("Normalized Player Data:")
print(player_data_norm.round(2))
# Create radar chart function
def create_radar_chart(player_name, data, attributes, ax=None, color='blue'):
"""
Create a radar chart for a single player
"""
if ax is None:
fig, ax = plt.subplots(figsize=(8, 8), subplot_kw=dict(projection='polar'))
# Number of attributes
num_vars = len(attributes)
# Compute angle for each axis
angles = [n / float(num_vars) * 2 * pi for n in range(num_vars)]
angles += angles[:1] # Complete the circle
# Get player values
player_row = data[data['Player'] == player_name]
values = player_row[attributes].values.flatten().tolist()
values += values[:1] # Complete the circle
# Plot
ax.plot(angles, values, 'o-', linewidth=2, label=player_name, color=color)
ax.fill(angles, values, alpha=0.25, color=color)
ax.set_xticks(angles[:-1])
ax.set_xticklabels(attributes, size=10)
ax.set_ylim(0, 100)
ax.set_yticks([25, 50, 75, 100])
ax.set_yticklabels(['25', '50', '75', '100'], size=8)
ax.grid(True)
return ax
# Create individual radar chart
fig, ax = plt.subplots(figsize=(10, 10), subplot_kw=dict(projection='polar'))
create_radar_chart('Player A', player_data_norm, stat_columns, ax, color='blue')
plt.title('Player A Performance Profile', size=16, y=1.08)
plt.legend(loc='upper right', bbox_to_anchor=(1.3, 1.1))
plt.tight_layout()
plt.savefig('radar_single_player.png', dpi=300, bbox_inches='tight')
plt.show()
# Create comparison radar chart (multiple players)
def create_comparison_radar(data, attributes, players, colors=None):
"""
Create a radar chart comparing multiple players
"""
if colors is None:
colors = ['blue', 'red', 'green', 'orange', 'purple']
fig, ax = plt.subplots(figsize=(12, 12), subplot_kw=dict(projection='polar'))
for idx, player in enumerate(players):
create_radar_chart(player, data, attributes, ax, colors[idx])
plt.title('Player Comparison', size=18, y=1.08, fontweight='bold')
plt.legend(loc='upper right', bbox_to_anchor=(1.3, 1.1), fontsize=12)
return fig, ax
# Compare all three players
fig, ax = create_comparison_radar(
player_data_norm,
stat_columns,
['Player A', 'Player B', 'Player C'],
colors=['#2E86AB', '#A23B72', '#F18F01']
)
plt.tight_layout()
plt.savefig('radar_comparison.png', dpi=300, bbox_inches='tight')
plt.show()
# Advanced: Position-specific radar charts
def create_position_radar(player_data, position_attributes, player_name):
"""
Create position-specific radar chart with benchmarks
"""
fig, ax = plt.subplots(figsize=(10, 10), subplot_kw=dict(projection='polar'))
# Calculate league average (50th percentile)
num_vars = len(position_attributes)
angles = [n / float(num_vars) * 2 * pi for n in range(num_vars)]
angles += angles[:1]
# League average line
avg_values = [50] * (num_vars + 1)
ax.plot(angles, avg_values, 'k--', linewidth=1, alpha=0.5, label='League Average')
# Player data
create_radar_chart(player_name, player_data, position_attributes, ax, color='#2E86AB')
plt.title(f'{player_name} vs League Average', size=16, y=1.08, fontweight='bold')
plt.legend(loc='upper right', bbox_to_anchor=(1.3, 1.1))
return fig, ax
# Create position-specific chart
striker_attributes = ['Goals_p90', 'xG_p90', 'Shot_Accuracy',
'Aerial_Duels_Won', 'Dribbles_Success']
fig, ax = create_position_radar(player_data_norm, striker_attributes, 'Player A')
plt.tight_layout()
plt.savefig('radar_position_specific.png', dpi=300, bbox_inches='tight')
plt.show()
# Create percentile table alongside radar
def create_radar_with_table(player_name, data_raw, data_norm, attributes):
"""
Create radar chart with accompanying percentile table
"""
fig = plt.figure(figsize=(16, 8))
# Radar chart
ax_radar = plt.subplot(121, projection='polar')
create_radar_chart(player_name, data_norm, attributes, ax_radar, color='#2E86AB')
# Table
ax_table = plt.subplot(122)
ax_table.axis('tight')
ax_table.axis('off')
# Prepare table data
player_raw = data_raw[data_raw['Player'] == player_name][attributes].values[0]
player_percentiles = data_norm[data_norm['Player'] == player_name][attributes].values[0]
table_data = []
for i, attr in enumerate(attributes):
table_data.append([
attr.replace('_', ' '),
f'{player_raw[i]:.2f}',
f'{player_percentiles[i]:.0f}%'
])
table = ax_table.table(
cellText=table_data,
colLabels=['Attribute', 'Raw Value', 'Percentile'],
cellLoc='left',
loc='center',
colWidths=[0.4, 0.2, 0.2]
)
table.auto_set_font_size(False)
table.set_fontsize(10)
table.scale(1, 2)
# Style header
for i in range(3):
table[(0, i)].set_facecolor('#2E86AB')
table[(0, i)].set_text_props(weight='bold', color='white')
plt.suptitle(f'{player_name} Performance Profile',
size=16, fontweight='bold', y=0.98)
plt.tight_layout()
plt.savefig('radar_with_table.png', dpi=300, bbox_inches='tight')
plt.show()
create_radar_with_table('Player A', player_data, player_data_norm, stat_columns)
# Similarity score based on radar profiles
def calculate_player_similarity(data, player1, player2, attributes):
"""
Calculate similarity score between two players
"""
p1_values = data[data['Player'] == player1][attributes].values[0]
p2_values = data[data['Player'] == player2][attributes].values[0]
# Euclidean distance
distance = np.sqrt(np.sum((p1_values - p2_values) ** 2))
# Convert to similarity (0-100, where 100 is identical)
max_distance = np.sqrt(len(attributes) * 100**2)
similarity = (1 - distance / max_distance) * 100
return similarity
# Calculate all pairwise similarities
players = player_data_norm['Player'].tolist()
similarity_matrix = pd.DataFrame(index=players, columns=players)
for p1 in players:
for p2 in players:
similarity_matrix.loc[p1, p2] = calculate_player_similarity(
player_data_norm, p1, p2, stat_columns
)
print("\nPlayer Similarity Matrix:")
print(similarity_matrix.astype(float).round(2))
```
## R Implementation
```r
library(tidyverse)
library(fmsb)
library(scales)
library(gridExtra)
# Sample player data
player_data <- data.frame(
Player = c("Player A", "Player B", "Player C"),
Goals_p90 = c(0.65, 0.42, 0.55),
xG_p90 = c(0.58, 0.48, 0.52),
Shot_Accuracy = c(65, 72, 58),
Aerial_Duels_Won = c(68, 55, 75),
Key_Passes_p90 = c(1.2, 2.1, 1.5),
Dribbles_Success = c(58, 45, 62),
Pass_Completion = c(78, 82, 75)
)
# Normalize data to 0-100 scale
normalize_stats <- function(df) {
df %>%
mutate(across(-Player, ~ percent_rank(.) * 100))
}
player_data_norm <- normalize_stats(player_data)
print("Normalized Player Data:")
print(player_data_norm)
# Create radar chart for single player
create_radar_chart <- function(player_name, data, color = "blue") {
# Filter player data
player_stats <- data %>%
filter(Player == player_name) %>%
select(-Player)
# Prepare data for fmsb (needs max and min rows)
radar_data <- rbind(
rep(100, ncol(player_stats)), # Max
rep(0, ncol(player_stats)), # Min
player_stats
)
# Create radar chart
radarchart(
radar_data,
axistype = 1,
pcol = color,
pfcol = alpha(color, 0.3),
plwd = 2,
cglcol = "grey",
cglty = 1,
axislabcol = "grey",
caxislabels = seq(0, 100, 25),
cglwd = 0.8,
vlcex = 0.8,
title = paste(player_name, "Performance Profile")
)
}
# Plot single player
par(mar = c(1, 1, 2, 1))
create_radar_chart("Player A", player_data_norm, color = "#2E86AB")
# Create comparison radar chart
create_comparison_radar <- function(data, players, colors = NULL) {
if (is.null(colors)) {
colors <- c("#2E86AB", "#A23B72", "#F18F01", "#C73E1D", "#6A994E")
}
# Prepare data
comparison_data <- data %>%
filter(Player %in% players) %>%
select(-Player)
# Add max and min rows
radar_data <- rbind(
rep(100, ncol(comparison_data)),
rep(0, ncol(comparison_data)),
comparison_data
)
# Create plot
radarchart(
radar_data,
axistype = 1,
pcol = colors[1:length(players)],
pfcol = alpha(colors[1:length(players)], 0.3),
plwd = 2,
cglcol = "grey",
cglty = 1,
axislabcol = "grey",
caxislabels = seq(0, 100, 25),
cglwd = 0.8,
vlcex = 0.8,
title = "Player Comparison"
)
# Add legend
legend(
x = "topright",
legend = players,
col = colors[1:length(players)],
lty = 1,
lwd = 2,
bty = "n"
)
}
# Compare all players
par(mar = c(1, 1, 2, 1))
create_comparison_radar(
player_data_norm,
c("Player A", "Player B", "Player C"),
colors = c("#2E86AB", "#A23B72", "#F18F01")
)
# Position-specific radar with league average
create_position_radar <- function(player_name, data, attributes) {
# Filter data
player_stats <- data %>%
filter(Player == player_name) %>%
select(all_of(attributes))
# League average (50th percentile)
league_avg <- rep(50, length(attributes))
# Prepare data
radar_data <- rbind(
rep(100, length(attributes)),
rep(0, length(attributes)),
player_stats,
league_avg
)
colnames(radar_data) <- attributes
# Create plot
radarchart(
radar_data,
axistype = 1,
pcol = c("#2E86AB", "black"),
pfcol = c(alpha("#2E86AB", 0.3), alpha("grey", 0.1)),
plwd = c(2, 2),
plty = c(1, 2),
cglcol = "grey",
cglty = 1,
axislabcol = "grey",
caxislabels = seq(0, 100, 25),
cglwd = 0.8,
vlcex = 0.8,
title = paste(player_name, "vs League Average")
)
legend(
x = "topright",
legend = c(player_name, "League Average"),
col = c("#2E86AB", "black"),
lty = c(1, 2),
lwd = 2,
bty = "n"
)
}
# Position-specific attributes
striker_attributes <- c("Goals_p90", "xG_p90", "Shot_Accuracy",
"Aerial_Duels_Won", "Dribbles_Success")
par(mar = c(1, 1, 2, 1))
create_position_radar("Player A", player_data_norm, striker_attributes)
# Calculate player similarity
calculate_player_similarity <- function(data, player1, player2, attributes) {
p1_values <- data %>%
filter(Player == player1) %>%
select(all_of(attributes)) %>%
as.numeric()
p2_values <- data %>%
filter(Player == player2) %>%
select(all_of(attributes)) %>%
as.numeric()
# Euclidean distance
distance <- sqrt(sum((p1_values - p2_values)^2))
# Convert to similarity (0-100)
max_distance <- sqrt(length(attributes) * 100^2)
similarity <- (1 - distance / max_distance) * 100
return(similarity)
}
# Create similarity matrix
players <- player_data_norm$Player
stat_columns <- setdiff(names(player_data_norm), "Player")
similarity_matrix <- matrix(
nrow = length(players),
ncol = length(players),
dimnames = list(players, players)
)
for (i in 1:length(players)) {
for (j in 1:length(players)) {
similarity_matrix[i, j] <- calculate_player_similarity(
player_data_norm,
players[i],
players[j],
stat_columns
)
}
}
print("Player Similarity Matrix:")
print(round(similarity_matrix, 2))
# Create heatmap of similarity
similarity_df <- as.data.frame(similarity_matrix) %>%
rownames_to_column("Player1") %>%
pivot_longer(-Player1, names_to = "Player2", values_to = "Similarity")
similarity_plot <- ggplot(similarity_df,
aes(x = Player1, y = Player2, fill = Similarity)) +
geom_tile(color = "white") +
geom_text(aes(label = sprintf("%.1f", Similarity)), color = "white", size = 5) +
scale_fill_gradient2(low = "#A23B72", mid = "#F18F01", high = "#2E86AB",
midpoint = 75, limit = c(0, 100)) +
labs(
title = "Player Similarity Heatmap",
x = "",
y = "",
fill = "Similarity %"
) +
theme_minimal() +
theme(
plot.title = element_text(hjust = 0.5, size = 14, face = "bold"),
axis.text.x = element_text(angle = 45, hjust = 1)
)
print(similarity_plot)
```
## Best Practices
### Attribute Selection
1. **Position-Relevant**: Choose metrics appropriate for the player's role
2. **Independent Metrics**: Avoid highly correlated attributes
3. **Balanced Coverage**: Include offensive, defensive, and technical attributes
4. **Standardized Metrics**: Use per-90-minute or percentage stats
### Normalization Methods
1. **Percentile Ranking**: Rank against peer group (preferred)
2. **Z-Score**: Standardize to mean=0, std=1
3. **Min-Max Scaling**: Scale to 0-100 range
4. **League-Specific**: Compare within same league/competition
### Visualization Tips
- Use 5-8 attributes for clarity (too many = cluttered)
- Consistent color schemes for easy comparison
- Include league average reference line
- Label axes clearly
- Provide raw values alongside percentiles
## Interpretation Guidelines
### Reading Radar Charts
- **Large Area**: Well-rounded player
- **Elongated Shape**: Specialist with specific strengths
- **Small Area**: Below-average performer or young player
- **Spiky Pattern**: Clear strengths and weaknesses
### Comparison Analysis
- Overlapping areas show similar attributes
- Non-overlapping areas highlight differences
- Use to identify playing style and role fit
- Consider tactical system requirements
## Use Cases
1. **Recruitment**: Compare transfer targets visually
2. **Squad Balance**: Identify gaps in team profiles
3. **Player Development**: Track improvement over time
4. **Tactical Fit**: Match player profiles to system requirements
5. **Contract Negotiations**: Objective performance visualization
Discussion
Have questions or feedback? Join our community discussion on
Discord or
GitHub Discussions.
Table of Contents
Related Topics
Quick Actions