<jupyter_start><jupyter_text>Serie A 2021-22 ```8 matches to go and many battles to come``` ![serieA](data/images/serieA.png) I've scraped data from the _FBref_ website to analyze the current state-of-doing of the Serie A Italian championship of football. The break for the national teams leaves the league as is, with 8-9 games to play and with many situations to be determined.These last matches could make the difference between an extraordinary season and an undertone season, or a season where the team achieves its goals.xG stands for expected goals. It correspond to the probability that a shot will result in a goal based on the characteristics of that shot and the events leading up to it. Some of these characteristics/variables include:- _Location of shooter_: How far was it from the goal and at what angle on the pitch?- _Body part_: Was it a header or off the shooter's foot?- _Type of pass_: Was it from a through ball, cross, set piece, etc?- _Type of attack_: Was it from an established possession? Was it off a rebound? Did the defense have time to get in position? Did it follow a dribble?An xG of 0 is a certain miss, while an xG of 1 is a certain goal. An xG of .5 would indicate that if identical shots were attempted 10 times, 5 would be expected to result in a goal.There are a number of xG models that use similar techniques and variables, which attempt to reach the same conclusion. The model that FBref uses is provided by StatsBomb. What sets StatsBomb's xG model apart from others is their use of freeze frames. A freeze frame is the location of all players on the pitch at the moment the shot was taken. _Imports and Setup_<jupyter_code>import numpy as np
import pandas as pd
df = pd.read_csv('data/serieA.csv')
df.head()<jupyter_output><empty_output><jupyter_text>_Data Cleaning_<jupyter_code># drop unnecessary columns
df = df.drop(['Unnamed: 0', 'xG_diff_per90'], axis=1)
df.head(2)
# sort teams by alphabetic order
df = df.sort_values(by='Team')
df.head()
# check data types
df.dtypes
# remove whitespaces
df.Team = df.Team.str.replace(' ', '')
# df.columns = df.columns.str.strip()
# except for Hellas Verona
# df.loc[9, 'Team'] = "Hellas Verona"<jupyter_output><empty_output><jupyter_text>_Data Manipulation_<jupyter_code># add xG diff per 90 column
df['xG_diff_per90'] = df['xG_diff'] / df['Games']
df['xG_diff_per90'] = round(df['xG_diff_per90'], 2)
df.head(2)
# add points per game column
df['Points_per_game'] = df['Points'] / df['Games']
df['Points_per_game'] = round(df['Points_per_game'], 2)
df.head(2)
# add squad value column
# from transfermarkt in alphabetic order
squad_value = [384900000, 146000000, 128550000, 91100000, 237000000, 112800000, 125830000, 528900000, 555600000, 267950000, 480100000, 493850000, 369350000, 54850000, 95900000, 233100000, 71300000, 221080000, 104250000, 82530000]
df['Squad_value'] = squad_value
df.head()
# average xG for and average xG against
df['Avg_xG_for'] = df['xG_for'] / df['Games']
df['Avg_xG_for'] = round(df['Avg_xG_for'], 2)

df['Avg_xG_against'] = df['xG_against'] / df['Games']
df['Avg_xG_against'] = round(df['Avg_xG_against'], 2)

df.head()
# add salary column
# couldn't find data atm<jupyter_output><empty_output><jupyter_text>_Data Visualization_<jupyter_code>import matplotlib.pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
%matplotlib inline
df = df.sort_values(by='Points', ascending=False)
df.head(2)
df['path'] = './data/images/' + df['Team'] + '.png'
# set font and background colour
# plt.rcParams.update({'font.family':'Avenir'})
bgcol = '#fafafa'

# create initial plot
fig, ax = plt.subplots(figsize=(6, 4), dpi=120)
fig.set_facecolor(bgcol)
ax.set_facecolor(bgcol)
ax.scatter(df['Avg_xG_for'], df['Avg_xG_against'], c=bgcol)

# change plot spines
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
ax.spines['left'].set_color('#ccc8c8')
ax.spines['bottom'].set_color('#ccc8c8')

# change ticks
plt.tick_params(axis='x', labelsize=12, color='#ccc8c8')
plt.tick_params(axis='y', labelsize=12, color='#ccc8c8')

# plot badges
def getImage(path):
    return OffsetImage(plt.imread(path), zoom=.10, alpha=1)

for index, row in df.iterrows():
    ab = AnnotationBbox(getImage(row['path']), (row['Avg_xG_for'], row['Avg_xG_against']), frameon=False)
    ax.add_artist(ab)

# add average lines
plt.hlines(df['Avg_xG_against'].mean(), df['Avg_xG_for'].min(), df['Avg_xG_for'].max(), color='#c2c1c0')
plt.vlines(df['Avg_xG_for'].mean(), df['Avg_xG_against'].min(), df['Avg_xG_against'].max(), color='#c2c1c0')

# text

## title & comment
fig.text(.15,.98,'Average xG Performance',size=20)
fig.text(.15,.93,'Weeks 1-30', size=12)

## avg line explanation
fig.text(.03,.14,'xG Against', size=9, color='#575654',rotation=90)
fig.text(.12,0.03,'xG For', size=9, color='#575654')

## axes titles
fig.text(.76,.55,'Avg. xG Against', size=6, color='#c2c1c0')
fig.text(.4,.17,'Avg. xG For', size=6, color='#c2c1c0',rotation=90)

## save plot
plt.savefig('./figures/xGChart1.png', dpi=1200, bbox_inches = "tight")<jupyter_output><empty_output><jupyter_text>**Findings**:A team's xG comparison (xG for - xG against) can indicate how a team should be performing. A negative goal difference but a positive xG difference might indicate a team has experienced poor luck or has below average finishing ability.Basically, teams in the upper-right quadrant are dangerous when attacking, but they concede a lot.Teams in the lower-right quadrant should stand in the first places of the standing as they are the ones most dangerous with a very good defense.Teams in the lower-left quadrant are solid in defense, but they struggle at threaten the opponent's defense.Teams in the upper-left quadrant should be the ones that occupy the lower places of the league as the concede a lot to their opponents and struggle to construct serious occasions.We can observe from the plot that Inter is the most dangerous team while Napoli is the most solid one. This came as no surprise as the two teams are in the first three places of the standing.AC Milan is the current leader of the league even if they aren't brilliant, or leaders, in the attacking phase or in the defensive phase. Will they be able to maintain their current position? From the plot it seems they are balancing from the two phases, but in the long-run as the history if this league teaches us, Napoli, with the better defense and a similar attack, could prevail.Sassuolo has always been a reckless team and this plot doesn't lie about it. They are the only team in the upper-right corner of the quadrant as they build a lot of threat to opponents, but they fatigue to control attacks from them.Considering the lower part of the league, Genoa is surprising because they grant few chances to the opponents, but they struggle too much to be a threat to them.<jupyter_code># plot for top 7 teams - the 7 sisters
df_top7 = df.head(7)
df_top7
# plot points - squad value
# use points per game
# set font and background colour
# plt.rcParams.update({'font.family':'Avenir'})
bgcol = '#fafafa'

# create initial plot
fig, ax = plt.subplots(figsize=(6, 4), dpi=120)
fig.set_facecolor(bgcol)
ax.set_facecolor(bgcol)
ax.scatter(df_top7['Points_per_game'], df_top7['Squad_value'], c=bgcol)

# change plot spines
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
ax.spines['left'].set_color('#ccc8c8')
ax.spines['bottom'].set_color('#ccc8c8')

# change ticks
plt.tick_params(axis='x', labelsize=12, color='#ccc8c8')
plt.tick_params(axis='y', labelsize=12, color='#ccc8c8')

# plot badges
def getImage(path):
    return OffsetImage(plt.imread(path), zoom=.10, alpha=1)

for index, row in df_top7.iterrows():
    ab = AnnotationBbox(getImage(row['path']), (row['Points_per_game'], row['Squad_value']), frameon=False)
    ax.add_artist(ab)

# add average lines
plt.hlines(df_top7['Squad_value'].mean(), df_top7['Points_per_game'].min(), df_top7['Points_per_game'].max(), color='#c2c1c0')
plt.vlines(df_top7['Points_per_game'].mean(), df_top7['Squad_value'].min(), df_top7['Squad_value'].max(), color='#c2c1c0')

# text

## title & comment
fig.text(.15,.98,'Average Points per Game based on Squad Value',size=20)
fig.text(.15,.93,'Weeks 1-30', size=12)

## avg line explanation
fig.text(.03,.14,'Squad Value (MLN)', size=9, color='#575654',rotation=90)
fig.text(.12,0.03,'Points per Game', size=9, color='#575654')

## axes titles
fig.text(.74,.54,'Avg. Squad Value', size=6, color='#c2c1c0')
fig.text(.5,.17,'Avg. Points per Game', size=6, color='#c2c1c0',rotation=90)

# y-axis ticks label
ax.set_yticklabels(['250','300','350','400','450','500','550','600'])

## save plot
plt.savefig('./figures/xGChart2.png', dpi=1200, bbox_inches = "tight")<jupyter_output>C:\Users\FC\AppData\Local\Temp\ipykernel_15424\268208850.py:50: UserWarning: FixedFormatter should only be used together with FixedLocator
  ax.set_yticklabels(['250','300','350','400','450','500','550','600'])<jupyter_text>**Findings**This figure plots the squad value (in million euros) on the y-axis and the average of points per game on the x-axis, and allows us to observe some interesting results.We can start by saying that the league standing doesn't represent the teams in order of squad value. Juventus, for instance, has the most valued team, but it's only at the fourth place for average points per game. So, Juventus is deluding since they have the squad with the maximum value in million euros, but they are only the 4th team in points per game.AC Milan, on the other side, is the best performing team, considering its lowest squad value between the top four contenders.The last 'three sisters', Atalanta, Roma and Lazio, follow instead their ordered values.<jupyter_code># plot for relegation battle
df_relegation = df.tail(6)
df_relegation
# plot xG diff per 90 - squad value
# set font and background colour
# plt.rcParams.update({'font.family':'Avenir'})
bgcol = '#fafafa'

# create initial plot
fig, ax = plt.subplots(figsize=(6, 4), dpi=120)
fig.set_facecolor(bgcol)
ax.set_facecolor(bgcol)
ax.scatter(df_relegation['xG_diff_per90'], df_relegation['Squad_value'], c=bgcol)

# change plot spines
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
ax.spines['left'].set_color('#ccc8c8')
ax.spines['bottom'].set_color('#ccc8c8')

# change ticks
plt.tick_params(axis='x', labelsize=12, color='#ccc8c8')
plt.tick_params(axis='y', labelsize=12, color='#ccc8c8')

# plot badges
def getImage(path):
    return OffsetImage(plt.imread(path), zoom=.10, alpha=1)

for index, row in df_relegation.iterrows():
    ab = AnnotationBbox(getImage(row['path']), (row['xG_diff_per90'], row['Squad_value']), frameon=False)
    ax.add_artist(ab)

# add average lines
plt.hlines(df_relegation['Squad_value'].mean(), df_relegation['xG_diff_per90'].min(), df_relegation['xG_diff_per90'].max(), color='#c2c1c0')
plt.vlines(df_relegation['xG_diff_per90'].mean(), df_relegation['Squad_value'].min(), df_relegation['Squad_value'].max(), color='#c2c1c0')

# text

## title & comment
fig.text(.15,.98,'Average xG Difference x90min based on Squad Value',size=20)
fig.text(.15,.93,'Weeks 1-30', size=12)

## avg line explanation
fig.text(.03,.14,'Squad Value (MLN)', size=9, color='#575654',rotation=90)
fig.text(.12,0.03,'xG_diff_per90', size=9, color='#575654')

## axes titles
fig.text(.74,.52,'Avg. Squad Value', size=6, color='#c2c1c0')
fig.text(.5,.17,'Avg. xG diff per90', size=6, color='#c2c1c0',rotation=90)

# y-axis ticks label
ax.set_yticklabels(['50','60','70','80','90','100','110','120','130','140'])

## save plot
plt.savefig('./figures/xGChart3.png', dpi=1200, bbox_inches = "tight")<jupyter_output>C:\Users\FC\AppData\Local\Temp\ipykernel_15424\2444717810.py:49: UserWarning: FixedFormatter should only be used together with FixedLocator
  ax.set_yticklabels(['50','60','70','80','90','100','110','120','130','140'])
{'fraction_non_alphanumeric': 0.13755146626333922, 'fraction_numerical': 0.053776993529955464, 'mean_word_length': 5.984741784037559, 'pattern_counts': {'":': 0, '<': 23, '<?xml version=': 0, '>': 23, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '6', 'dir': 'jupyter-structured-clean-dedup', 'id': '543435', 'license': 'RSA-MD', 'n_tokens_mistral': 4512, 'n_tokens_neox': 4020, 'n_words': 1248, 'path': 'serieA.ipynb', 'repo_name': 'fabriziocominetti/serie-A', 'size': '11901'}