In [1]:
#Loading all the packages that we need 
import matplotlib
import pandas as pd
import numpy as np
import warnings
import urllib
from PIL import Image
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import matplotlib.colors as mcolors
from matplotlib.colors import LinearSegmentedColormap
from matplotlib.patches import RegularPolygon
import matplotlib.patheffects as path_effects
from mplsoccer import Pitch, VerticalPitch, lines
from scipy.ndimage import gaussian_filter

import socceraction
import socceraction.atomic.spadl as atomicspadl
In [2]:
#Importing the fonts
fe_regular = fm.FontEntry(
    fname='/Users/davidegualano/Documents/Python FTBLData/SourceSansPro-Regular.ttf',
    name='SourceSansPro-Regular'
)
fe_semibold = fm.FontEntry(
    fname='/Users/davidegualano/Documents/Python FTBLData/SourceSansPro-SemiBold.ttf',
    name='SourceSansPro-SemiBold'
)

fe_medium = fm.FontEntry(
    fname='/Users/davidegualano/Documents/Python FTBLData/Shentox-W01-Medium.ttf',
    name='Shentox-Medium'
)
fe_bold = fm.FontEntry(
    fname='/Users/davidegualano/Documents/Python FTBLData/Shentox-W01-Bold.ttf',
    name='Shentox-Bold'
)

# Insert both fonts into the font manager
fm.fontManager.ttflist.insert(0, fe_regular)
fm.fontManager.ttflist.insert(1, fe_semibold)
fm.fontManager.ttflist.insert(2, fe_medium)
fm.fontManager.ttflist.insert(3, fe_bold)

# Set the font family
matplotlib.rcParams['font.family'] = fe_regular.name  # Default to Regular
In [3]:
#Choosing the season for which we want to look at the data
season = 2425
In [4]:
#Loading the data containing files
fb = pd.read_csv("teamsFOTMOB.csv", index_col = 0)
players = pd.read_csv(f"players{season}.csv", index_col = 0)
games = pd.read_csv(f"games{season}.csv", index_col = 0)
actions = pd.read_csv(f"atomic_actions{season}.csv", index_col = 0)
positions = pd.read_csv("clustered_position.csv", index_col = 0)
VAEP = pd.read_csv("aVAEPactions.csv", index_col = 0)
In [5]:
#Filtering players and games files so to have only important features
games = games[["game_id", "game_date", "competition_id", "season_id"]]
games['game_date'] = pd.to_datetime(games['game_date'])
players_info = players[['game_id', 'team_id', 'player_id', 'player_name', 'season_id', 'competition_id']]
In [6]:
#Using the spadl framework to add names to the actions
actions = atomicspadl.add_names(actions)
In [7]:
#Merging all the files to the actions so to have all the relevant informations
df = (
    actions
    .merge(VAEP, how="left")
    .merge(fb, how="left")
    .merge(games, how="left")
    .merge(players_info, how="left")
)
In [8]:
#Creating other features we want from scratch
df["angle"] = np.arctan2(df["end_y"] - df["y_a0"], df["end_x"] - df["x_a0"])
df['angle_degrees'] = np.degrees(df['angle']) % 360
df["action_distance"] = np.sqrt((df["end_x"] - df["x_a0"])**2 + (df["end_y"] - df["y_a0"])**2).round(2)
df['duration'] = df['time_seconds'].shift(-1) - df['time_seconds']
In [9]:
#Adding features from next/previosu actions we'll use later
df["next_team_name"] = df["team_name"].shift(-1, fill_value=0)

df["next_player_id"] = df["player_id"].shift(-1, fill_value=0)
df["prev_player_id"] = df["player_id"].shift(+1, fill_value=0)

df["next_type_name"] = df["type_name"].shift(-1, fill_value=0)
df["prev_type_name"] = df["type_name"].shift(+1, fill_value=0)
In [10]:
# Create a mask for the condition we want to filter the data with
mask = (df['type_name'] == 'pass') & (df['team_name'] == df['next_team_name']) & (df['player_id'] != df['next_player_id'])

# Shift the mask backward to select the following rows
shifted_mask = mask.shift(1).fillna(False)

# Apply the shifted mask
first_time0 = df[shifted_mask]

# Filter for the action types I want
first_time1 = first_time0[first_time0['type_name'].isin(['dribble', 'take_on', 'pass', 'shot', 'goal', 'cross'])]
/var/folders/ns/3wxdg4g57h77vxwmr4wzmvt40000gn/T/ipykernel_43404/610884589.py:5: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
  shifted_mask = mask.shift(1).fillna(False)
In [11]:
#Checking that we actually kept only certain actions that follow a pass
first_time1.prev_type_name.unique()
Out[11]:
array(['pass'], dtype=object)
In [12]:
# Create a mask for the condition we want to filter the data with but to focus on after receival and not first time actions
mask2 = (df['type_name'] == 'pass') & (df['team_name'] == df['next_team_name']) & (df['next_type_name'] == 'receival') & (df['player_id'] != df['next_player_id'])

# Shift the mask backward to select the following rows
shifted_mask2 = mask2.shift(2).fillna(False)

# Apply the shifted mask
after_receival0 = df[shifted_mask2]

# Filter for events by the same player who received the ball
after_receival1 = after_receival0[after_receival0['player_id'] == after_receival0['prev_player_id']]
/var/folders/ns/3wxdg4g57h77vxwmr4wzmvt40000gn/T/ipykernel_43404/3710332761.py:5: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
  shifted_mask2 = mask2.shift(2).fillna(False)
In [13]:
#Checking that we actually kept only certain actions that follow a reception
after_receival1.prev_type_name.unique()
Out[13]:
array(['receival'], dtype=object)
In [14]:
#Checking all the type of actions there are
after_receival1.type_name.unique()
Out[14]:
array(['dribble', 'receival', 'pass', 'foul', 'cross', 'bad_touch',
       'shot', 'clearance', 'take_on', 'goalkick', 'corner', 'freekick'],
      dtype=object)
In [15]:
# Filter for the action types I want
after_receival = after_receival1[after_receival1['type_name'].isin(
    ['dribble', 'pass', 'cross', 'shot', 'take_on'])]
In [16]:
# Creating a function to keep only those actions that are forward more or less
def is_forward_angle(angle_deg, forward_range=45):
    """
    Determine if an angle is considered "forward" (left to right)
    
    Parameters:
    - angle_deg: Angle in degrees (0-360)
    - forward_range: How many degrees on either side of straight forward to consider forward
    """
    # Forward is toward the right of the pitch (around 0 degrees)
    return (angle_deg >= 360 - forward_range) or (angle_deg <= forward_range)

# Define your thresholds
forward_angle_range = 60  # Degrees to consider as "forward" on either side
quick_time_threshold = 1.5  # Seconds (adjust based on your definition of "very narrow short range")

# Creating the final first time actions dataframe
first_time = first_time1[first_time1['angle_degrees'].apply(
    lambda x: is_forward_angle(x, forward_range=forward_angle_range)
)]

# Creating the after receival actions dataframe
after_receival_fwd = after_receival[after_receival['angle_degrees'].apply(
    lambda x: is_forward_angle(x, forward_range=forward_angle_range)
)]

# Further filter for those action that happens quickly after reception as we want both possibilities 
after_receival_quick = after_receival_fwd[after_receival_fwd['duration'] <= quick_time_threshold]
In [17]:
# Convert 'minutes_played' to total minutes with error handling
def convert_to_minutes(time_str):
    try:
        # Convert to string in case it's a float (e.g., NaN)
        time_str = str(time_str)
        # Split the time string into minutes and seconds
        minutes, seconds = map(int, time_str.split(':'))
        # Convert total time to minutes (seconds converted to fraction of minutes)
        return minutes + seconds / 60
    except (ValueError, AttributeError):
        # Handle cases where the conversion fails (e.g., NaN or bad format)
        return 0  # or use `np.nan` if you prefer to mark as missing

# Apply the conversion function to the 'minutes_played' column
players['minutes_played_converted'] = players['minutes_played'].apply(convert_to_minutes)
In [18]:
#We look at the duration of each game in the dataset
minutesadj = players.groupby(["game_id", "game_duration"], observed=True)['is_starter'].count().reset_index(name='is_starter')

# Apply the conversion function to the 'minutes_played' column
minutesadj['game_duration_converted'] = minutesadj['game_duration'].apply(convert_to_minutes)

#We find the median duration of games in the dataset to normalize for that instead of 90'
minutesadj = minutesadj.game_duration_converted.median()
minutesadj
Out[18]:
98.5
In [19]:
#Creating a table in which each player has his total of minutes played in the season and merge with team_name column
mp = players.groupby(["player_id", "player_name", "team_id"])["minutes_played_converted"].sum().reset_index(name='minutes_played')
mp = mp.merge(fb[['team_id', 'team_name']])
In [20]:
#Creating three dataframes from each of the created one to have each player and his metrics from that type of actions
A0 = (after_receival_fwd.groupby(["player_id", "player_name", "team_id", "team_name"], observed=True)
              .agg(
                  after_receival=("type_name", "count"),
                  after_receival_vaep=("vaep_value", "sum")
              )
              .reset_index())

A1 = (after_receival_quick.groupby(["player_id", "player_name", "team_id", "team_name"], observed=True)
              .agg(
                  after_receival_q=("type_name", "count"),
                  after_receival_q_vaep=("vaep_value", "sum")
              )
              .reset_index())

A2 = (first_time.groupby(["player_id", "player_name", "team_id", "team_name"], observed=True)
              .agg(
                  first_time=("type_name", "count"),
                  first_time_vaep=("vaep_value", "sum")
              )
              .reset_index())

#Merging together the dataframes
AX = (A0
        .merge(A1, how='left')
        .merge(A2, how='left')).fillna(0)
In [21]:
#Creating columns of metrics we want to look at
AX['non_quick'] = AX['after_receival'] - AX['after_receival_q']
AX['quick'] = AX['first_time'] + AX['after_receival_q']
AX['overall'] = AX['first_time'] + AX['after_receival']

AX['non_quick_vaep'] = AX['after_receival_vaep'] - AX['after_receival_q_vaep']
AX['quick_vaep'] = AX['first_time_vaep'] + AX['after_receival_q_vaep']
AX['overall_vaep'] = AX['first_time_vaep'] + AX['after_receival_vaep']
In [22]:
#Filtering the dataframe to keep only the columns we have created
AZ = AX[['player_id', 'player_name', 'team_name', 'non_quick', 'quick', 'overall', 'non_quick_vaep', 'quick_vaep', 'overall_vaep']]
In [23]:
#Filtering the position dataframe to keep only those position mapping for players of the selected season
#So to not have doubles
positions0 = positions[positions['season_id'] == int(season)]

#Merging togther position, minutes played and metrics dataframe
A = (AZ
        .merge(positions0)
        .merge(mp))

#Creating normalized metrics now that we have minutes played
A["overall_98"] = A.overall * minutesadj / A.minutes_played
A["non_quick_98"] = A.non_quick * minutesadj / A.minutes_played
A["quick_98"] = A.quick * minutesadj / A.minutes_played

A["overall_vaep_98"] = (A.overall_vaep * minutesadj / A.minutes_played).round(3)
A["non_quick_vaep_98"] = (A.non_quick_vaep * minutesadj / A.minutes_played).round(3)
A["quick_vaep_98"] = (A.quick_vaep * minutesadj / A.minutes_played).round(3)

#Keep only players with at least 1000 miuntes played
A_final = A[A['minutes_played'] > 999]
In [24]:
#Filtering for columns we want to keep and looking at the unique position group inside
X = A_final[['player_id', 'player_name', 'team_name', 'season_id', 'position', 'position_group', 'minutes_played', 'overall_98',
            'non_quick_98', 'quick_98', 'overall_vaep_98', 'non_quick_vaep_98', 'quick_vaep_98']]

X.position_group.unique()
Out[24]:
array(['AMW', 'CB', 'GK', 'ST', 'WB', 'CDM'], dtype=object)
In [25]:
#Selecting a position group as we don't want to compare apples with pears
Z = X[X["position_group"] == 'CDM']
In [26]:
#Selecting a metric to explore the results for
metric = 'quick_98'
In [27]:
#Creating the dataframe for the vsiualization
W = Z[['player_id', 'player_name', 'team_name', 'season_id', 'position', 
       'position_group', 'minutes_played', metric]].sort_values(
       by=[metric], ascending=False).reset_index(drop=True).head(10)

#Rounding the selected metric for making it pleasing to the eye
W[metric] = W[metric].round(3)

#Sorting the top 10 to make it work inside the visualization
Y = W.sort_values(by = [metric], ascending = True)

Y
Out[27]:
player_id player_name team_name season_id position position_group minutes_played quick_98
9 295768.0 Claudinho Zenit 2425.0 CM CDM 1282.200000 19.973
8 384887.0 Vitinha PSG 2425.0 CM CDM 3494.933333 20.236
7 296321.0 Florian Tardieu Saint-Etienne 2425.0 HB CDM 1069.883333 20.439
6 273539.0 Manuel Locatelli Juventus 2425.0 DM CDM 3344.116667 21.325
5 144890.0 Dani Ceballos Real Madrid 2425.0 CM CDM 1426.550000 21.612
4 20874.0 Luka Modric Real Madrid 2425.0 DM CDM 2326.566667 22.015
3 481079.0 Aleksandar Pavlovic Bayern 2425.0 CM CDM 1630.083333 22.116
2 422197.0 Adem Zorgane Sporting Charleroi 2425.0 DM CDM 3506.633333 22.359
1 283323.0 Joshua Kimmich Bayern 2425.0 CM CDM 4132.116667 22.813
0 101859.0 Pierre-Emile Højbjerg Marseille 2425.0 CM CDM 2631.983333 23.166
In [28]:
#Setting the figure, the axes and the dimension of the figure to make it all fit pleasingly
fig = plt.figure(figsize=(1800/500, 1800/500), dpi=500)
ax = plt.subplot()

ncols = Y.shape[1]
nrows = Y.shape[0]

ax.set_xlim(0, ncols + 1)
ax.set_ylim(0, nrows + 1)

position = [0.1, 5, 7.5]
columns = ['player_name', 'team_name', metric]

#Conditioning for names in different columns
for i in range(nrows):
    for j, column in enumerate(columns):
        if j == 0:
            ha = 'left'
        else:
            ha = 'center'
        if column == metric:
            fontsize = 10
            color = '#FFFFFF'
            fontname = fe_semibold.name
        elif column == 'team_name':
            fontsize = 4  
            color = '#4E616C' 
            fontname = fe_regular.name
        else:
            fontsize = 8
            color = '#000000' 
            fontname = fe_semibold.name
        ax.annotate(
            xy=(position[j], i + .5), text=str(Y[column].iloc[i]), ha=ha, va='center', fontsize=fontsize, color=color, fontname=fontname)

# Add dividing lines and color for the column to highlight
ax.plot([ax.get_xlim()[0], ax.get_xlim()[1]], [nrows, nrows], lw=1.5, color='black', marker='', zorder=4)
ax.plot([ax.get_xlim()[0], ax.get_xlim()[1]], [0, 0], lw=1.5, color='black', marker='', zorder=4)
for x in range(1, nrows):
    ax.plot([ax.get_xlim()[0], ax.get_xlim()[1]], [x, x], lw=0.5, color='gray', ls='-', zorder=3 , marker='')
    
    ax.fill_between(x=[6.5, 8.5], y1=nrows, y2=0, color='#D32F2F', alpha=0.5, ec='None')

# Adding titles and notes with conditioning based on the metric we use
if metric == 'overall_98':
    plt.text(0.5, 0.89, 'Number of actions', transform=fig.transFigure,
         horizontalalignment='center', fontsize=10, fontfamily='SourceSansPro-SemiBold')
    plt.text(0.5, 0.86, 'Performed after receiving a pass', transform=fig.transFigure,
         horizontalalignment='center', fontsize=6)
elif metric == 'non_quick_98':
    plt.text(0.5, 0.89, 'Number of actions performed', transform=fig.transFigure,
         horizontalalignment='center', fontsize=10, fontfamily='SourceSansPro-SemiBold')
    plt.text(0.5, 0.86, 'Performed in more than 1.5s after receiving a pass', transform=fig.transFigure,
         horizontalalignment='center', fontsize=6)
elif metric == 'quick_98':
    plt.text(0.5, 0.89, 'Number of actions performed', transform=fig.transFigure,
         horizontalalignment='center', fontsize=10, fontfamily='SourceSansPro-SemiBold')
    plt.text(0.5, 0.86, 'Performed in less than 1.5s or first time after receiving a pass', transform=fig.transFigure,
         horizontalalignment='center', fontsize=6)
elif metric == 'overall_vaep_98':
    plt.text(0.5, 0.89, 'Value of actions', transform=fig.transFigure,
         horizontalalignment='center', fontsize=10, fontfamily='SourceSansPro-SemiBold')
    plt.text(0.5, 0.86, 'Performed after receiving a pass', transform=fig.transFigure,
         horizontalalignment='center', fontsize=6)
elif metric == 'non_quick_vaep_98':
    plt.text(0.5, 0.89, 'Value of actions', transform=fig.transFigure,
         horizontalalignment='center', fontsize=10, fontfamily='SourceSansPro-SemiBold')
    plt.text(0.5, 0.86, 'Performed in more than 1.5s after receiving a pass', transform=fig.transFigure,
         horizontalalignment='center', fontsize=6)
elif metric == 'quick_vaep_98':
    plt.text(0.5, 0.89, 'Value of actions', transform=fig.transFigure,
         horizontalalignment='center', fontsize=10, fontfamily='SourceSansPro-SemiBold')
    plt.text(0.5, 0.86, 'Performed in less than 1.5s or first time after receiving a pass', transform=fig.transFigure,
         horizontalalignment='center', fontsize=6)

plt.text(0.5, 0.83, f'Central and Defensive Midfielders | Minimum 1000 minutes played | Normalized per 98 minutes',
         transform=fig.transFigure, horizontalalignment='center', fontsize = 4, color = '#4E616C')
fig.suptitle(f'X: @gualanodavide | Bluesky: @gualanodavide.bsky.social | Linkedin: www.linkedin.com/in/davide-gualano-a2454b187 | Newsletter: the-cutback.beehiiv.com',
             horizontalalignment='center', x = 0.5125, y = 0.09, fontsize=3, color = "#000000")

#Saving and showing
ax.set_axis_off()
plt.savefig(f'TOP_AFTER_RECEPTIONS.png', dpi=500, facecolor = "#D7D1CF", bbox_inches = "tight", transparent = True)
plt.show()
No description has been provided for this image
In [ ]:
 
In [ ]:
 
In [ ]:
 
In [29]:
#Selecting a player for which to create a visualization of all the actions
player_dfa = pd.concat([first_time, after_receival_fwd])
player_df0 = player_dfa[player_dfa['player_name'] == 'Dani Ceballos']
player_df0.team_name.unique()
Out[29]:
array(['Real Madrid'], dtype=object)
In [30]:
#Filtering for the team of the player - useful if the player changed team
player_df = player_df0[player_df0['team_name'] == 'Real Madrid']
In [31]:
# Function to format season ID into a readable format
def format_season_id(season_id):
    # Convert to integer if it's a float
    season_id = int(season_id)
    # Extract the last two digits of the year
    start_year = str(season_id - 1)[-2:]
    # Calculate the end year
    end_year = str(season_id)[-2:]
    # Format as 20/21
    formatted_season = f"{start_year}/{end_year}"
    return formatted_season

#Apply the function
player_df['formatted_season'] = player_df['season_id'].apply(format_season_id)
In [32]:
#Creating the different dataframes to use for the plotting
carries = player_df[player_df["type_name"] == 'dribble']
dribbles = player_df[player_df["type_name"] == 'take_on']

passes = player_df[player_df["type_name"].isin(['cross', 'pass'])]
shots = player_df[player_df["type_name"] == 'shot']

heatmap1 = player_df[player_df["type_name"].isin(['cross', 'pass', 'shot'])]
heatmap2 = player_df[player_df["type_name"].isin(['dribble', 'take_on'])]
In [33]:
# Set up gridspec figure
fig = plt.figure(figsize=(16, 12))
gs = fig.add_gridspec(6, 6, wspace=0.1, hspace=0.1)

# Create the axes
ax1 = fig.add_subplot(gs[:, :3]) 
ax2 = fig.add_subplot(gs[:, 3:]) 

#Create the pitches
pitch1 = VerticalPitch(pitch_type='custom', pitch_width=68, pitch_length=105, goal_type='box', linewidth=1.25, line_color='#000000')
pitch2 = VerticalPitch(pitch_type='custom', pitch_width=68, pitch_length=105, goal_type='box', linewidth=1.25, line_color='#000000')

pitch1.draw(ax=ax1)
pitch2.draw(ax=ax2)

# plot the heatmap - darker colors = more actions originating from that square
bins = (18, 12)
cmap1 = mcolors.LinearSegmentedColormap.from_list("custom_red", ["#D7D1CF", "#FF0000"])
cmap2 = mcolors.LinearSegmentedColormap.from_list("custom_red", ["#D7D1CF", "#FF0000"])

bs_heatmap1 = pitch1.bin_statistic(heatmap1.x_a0, heatmap1.y_a0, statistic='count', bins=bins)
hm = pitch1.heatmap(bs_heatmap1, ax=ax1, cmap=cmap1, zorder = 4, alpha = 0.8)

bs_heatmap2 = pitch2.bin_statistic(heatmap2.x_a0, heatmap2.y_a0, statistic='count', bins=bins)
hm = pitch2.heatmap(bs_heatmap2, ax=ax2, cmap=cmap2, zorder = 4, alpha = 0.8)

# Scatter plots and arrows for the passes and carries in the two pitches
pitch1.arrows(passes.x_a0, passes.y_a0, passes.end_x, passes.end_y, width=1, alpha = 0.5, zorder = 1,
             headwidth = 10, headlength = 8, color = '#000000', ax=ax1)
pitch1.scatter(passes.x_a0, passes.y_a0, c='#000000', marker='o', s=30, ax=ax1, zorder=1, ec='#000000', alpha=0.5)
pitch1.scatter(shots.x_a0, shots.y_a0, c='#000000', marker='*', s=100, ax=ax1, zorder=2, ec='#000000')

pitch2.plot([carries.x_a0, carries.end_x], 
           [carries.y_a0, carries.end_y], 
           linestyle='--', linewidth=1, color='#000000', markersize=2, 
           zorder=1, ax=ax2)
pitch2.scatter(carries.end_x, carries.end_y, c='#000000', s=50, ax=ax2, zorder=2, alpha=0.5)
pitch2.scatter(dribbles.end_x, dribbles.end_y, c='#000000', marker='X', s=100, ax=ax2, zorder=1)

# Variables that store elements to use in titles
team_name = player_df['team_name'].iloc[0]
player_name = player_df['player_name'].iloc[0]
competition_ids = ', '.join(player_df['competition_id'].unique())
formatted_season = player_df['formatted_season'].iloc[0]
season_id = player_df['season_id'].iloc[0]

# Titles
ax1.text(0.5, 1.01,
         f"Shots : Stars ({len(shots)})  |  Passes and Cross : Dots + Arrows ({(passes.shape[0])})",
         color='#000000', va='center', ha='center', fontsize=11, transform=ax1.transAxes)
ax1.text(0.5, 1.03, f"Passes, Crosses and Shots", color='#000000',
        va='center', ha='center', fontsize=11, transform=ax1.transAxes)

ax2.text(1.52, 1.03, f"Carries and Take Ons", color='#000000',
        va='center', ha='center', fontsize=11, transform=ax1.transAxes)
ax1.text(1.52, 1.01,
         f"Carries : Lines + Dots [at the end] ({len(carries)})  |  Take Ons : Cross ({(dribbles.shape[0])})",
         color='#000000', va='center', ha='center', fontsize=11, transform=ax1.transAxes)

fig.text(0.22, 0.96, f'{player_name} forward actions after receiving a pass',
         fontsize=30, va='center', ha='left', fontfamily='SourceSansPro-SemiBold')
fig.text(0.22, 0.93, f'{competition_ids} {formatted_season}  |  Heatmap: Amount at starting coordinates',
         fontsize=20, va='center', ha='left')
fig.text(0.15, 0.1, 'X: @gualanodavide | Bluesky: @gualanodavide.bsky.social | Linkedin: www.linkedin.com/in/davide-gualano-a2454b187 | Newsletter: the-cutback.beehiiv.com', va='center', ha='left', fontsize=12)

#Adding the club logo
DC_to_FC = ax1.transData.transform
FC_to_NFC = fig.transFigure.inverted().transform
# -- Take data coordinates and transform them to normalized figure coordinates
DC_to_NFC = lambda x: FC_to_NFC(DC_to_FC(x))

ax_size = 0.12
y = 95
# Get the data coordinates for the specific x and y values
data_coords = DC_to_FC((-4, (y * 1.18) - 2.25))  # This returns a tuple
ax_coords = FC_to_NFC(data_coords)  # Transform to figure coordinates

# Adjust the x-coordinate
adjusted_x = 0.1
ax_coords = (adjusted_x, ax_coords[1])  # Create new ax_coords with adjusted x

# Retrieve the team_id and team_name from the DataFrame
team_id = fb[fb['team_name'] == team_name]['fotmob_id'].iloc[0]

# Add an axis for the image
image_ax = fig.add_axes([ax_coords[0], ax_coords[1], ax_size, ax_size], fc='None', anchor='C')
fotmob_url = 'https://images.fotmob.com/image_resources/logo/teamlogo/'

try:
    player_face = Image.open(urllib.request.urlopen(f"{fotmob_url}{team_id}.png")).convert('RGBA')
    image_ax.imshow(player_face)
except Exception as e:
    print(f"Error loading image for team {team_name}: {e}")
    # If an error occurs, you might want to exit or handle it differently.
    # 'continue' is removed because it's not in a loop.

image_ax.axis("off")

# Save the figure
plt.savefig(f'{player_name}-afterreceivingmap-{season_id}.png', dpi=500, facecolor="#D7D1CF", bbox_inches="tight", transparent=True)
plt.show()
No description has been provided for this image
In [ ]:
 
In [ ]:
 
In [ ]: