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.