Throughout this analysis, we will use the FBREF data given to conduct
our study. We will focus on forwards : “Forwards” means any player whose
position contains “FW”. This includes pure forwards (FW),
as well as hybrid positions like FW,MF and
MF,FW because roles and
profiles are more relevant than strict positioning and
some players (eg : Wirtz, Cherki, Olise, etc.) can both play on the
wings or in the midfield. It excludes any position containing
"DF" because I believe they are not relevant.
required_packages <- c(
"tidyverse",
"ggplot2",
"fmsb",
"lsa",
"knitr",
"scales",
"RColorBrewer"
)
# Automatic install if needed
for (pkg in required_packages) {
if (!require(pkg, character.only = TRUE, quietly = TRUE)) {
install.packages(pkg, repos = "https://cran.r-project.org")
library(pkg, character.only = TRUE)
}
}
The first step is to read the CSV file we are going to work with and select the sample of analysis.
## Number of players: 3972
## Number of variables: 72
## Categorical Variables
## [1] "Player" "Squad" "Nation" "Pos" "Competition"
##
## Numeric Variables (integer)
## [1] "Age" "Min" "Gls" "G.PK" "Ast" "MP_Squad"
##
## Numeric Variables (numeric)
## [1] "MP" "xG"
## [3] "xAG" "Gls.90"
## [5] "G.PK.90" "Ast.90"
## [7] "xG.90" "xAG.90"
## [9] "Sh" "Sh.90"
## [11] "SoT.90" "Passes."
## [13] "ShortPasses." "MediumPasses."
## [15] "LongPasses." "A.xAG"
## [17] "TklW.90" "Blocks.90"
## [19] "Int.90" "Tkl.Int.90"
## [21] "Clr.90" "Touches.90"
## [23] "Dribbles.90" "Dribbles."
## [25] "SCA.90" "GCA.90"
## [27] "Aerial." "Points.90"
## [29] "xGA" "OG"
## [31] "PSxG" "PSxG.SoT"
## [33] "PSxG..." "GA"
## [35] "GA.90" "Save."
## [37] "CS." "SoTA.90"
## [39] "SoTA.GA" "SoT.G"
## [41] "G.xG" "PassesCompleted.90"
## [43] "PassesAttempted.90" "ShortPassesCompleted.90"
## [45] "MediumPassesCompleted.90" "LongPassesCompleted.90"
## [47] "TotDistPasses.90" "PrgDistPasses.90"
## [49] "xA.90" "KP.90"
## [51] "FinalThirdPasses.90" "PPA.90"
## [53] "CrsPA.90" "PassesProgressive.90"
## [55] "xGA.90" "Recov.90"
## [57] "Fls.90" "Fld.90"
## [59] "AerialW.90" "xGD"
## [61] "xGD.90"
We filter the dataset to create forwards_pool: all
players whose position contains "FW" (excluding
"DF") as said in the introduction, in the Big 5
leagues.
## Total forwards found (FW, FW/MF, MF/FW): 927
Once we have read the file, we have to narrow down the sample further.
We define the filter_players function that will allow us
to select those players who have played a minimum number of minutes.
We will also keep the metrics we need and the forwards who have played at least 10% of the total minutes over 38 games.
## Forwards after filtering: 655
Once we have selected our study sample, we calculate a value that summarises the performance of these players. This will be our rating.
To do this, as we are working with different metrics measured in different magnitudes, the first step is to normalise the variables so all variables will be in the same range. Then we can assign weights and calculate the final score.
We use the MinMax transformer to normalise the values of the
performance variables. For that, we define a normalise
function that contains the definition of this transformer:
We apply this function to each of the metric columns using
mutate(across(...))
## Goals Assists Goals/90 Goals excl PK/90
## Min. :0.00000 Min. :0.00000 Min. :0.0000 Min. :0.0000
## 1st Qu.:0.04839 1st Qu.:0.05556 1st Qu.:0.1217 1st Qu.:0.1101
## Median :0.09677 Median :0.11111 Median :0.2174 Median :0.2110
## Mean :0.15504 Mean :0.13944 Mean :0.2422 Mean :0.2341
## 3rd Qu.:0.22581 3rd Qu.:0.22222 3rd Qu.:0.3304 3rd Qu.:0.3211
## Max. :1.00000 Max. :1.00000 Max. :1.0000 Max. :1.0000
## xG/90 Shots on Target/90 Shot Creating Act/90 Goal Creating Act/90
## Min. :0.0000 Min. :0.0000 Min. :0.0000 Min. :0.0000
## 1st Qu.:0.1261 1st Qu.:0.2176 1st Qu.:0.2629 1st Qu.:0.1450
## Median :0.2087 Median :0.3098 Median :0.3754 Median :0.2366
## Mean :0.2406 Mean :0.3234 Mean :0.3963 Mean :0.2617
## 3rd Qu.:0.3130 3rd Qu.:0.4118 3rd Qu.:0.5073 3rd Qu.:0.3511
## Max. :1.0000 Max. :1.0000 Max. :1.0000 Max. :1.0000
## Dribbles/90 Key Passes/90 xA/90
## Min. :0.00000 Min. :0.0000 Min. :0.0000
## 1st Qu.:0.06739 1st Qu.:0.1448 1st Qu.:0.1026
## Median :0.12609 Median :0.2357 Median :0.1538
## Mean :0.15434 Mean :0.2670 Mean :0.2063
## 3rd Qu.:0.21087 3rd Qu.:0.3502 3rd Qu.:0.2821
## Max. :1.00000 Max. :1.0000 Max. :1.0000
All variables have a minimum value of 0 and a maximum value of 1 so they are normalised.
Once the variables have been normalised, we calculate the final scoring for each player.
The function calc_scoring takes the MinMax-transformed
dataset, applies a vector of weights to each performance metric, sums
the weighted contributions into a single Final Score on
a 0–10 scale, and returns the top n players.
Our score and weight assignment are based on the following logic : in today’s game, players are heavily judged on output. Goals and assists hold more weight, as they are what is required from forwards. But this view is too diminishing of a player’s creativity and role within a team. So the second distribution of weight gradually goes to creative metrics from dribbling to passing.
| Player | Squad | Competition | Age | Final Score |
|---|---|---|---|---|
| Mohamed Salah | Liverpool | Premier League | 32 | 7.010 |
| Ousmane Dembélé | PSG | Ligue 1 | 27 | 6.775 |
| Omar Marmoush | Eint Frankfurt | Bundesliga | 25 | 6.652 |
| Kylian Mbappé | Real Madrid | La Liga | 25 | 6.292 |
| Michael Olise | Bayern Munich | Bundesliga | 22 | 6.097 |
| Harry Kane | Bayern Munich | Bundesliga | 31 | 5.905 |
| Lamine Yamal | Barcelona | La Liga | 17 | 5.597 |
| Raphinha | Barcelona | La Liga | 27 | 5.571 |
| Florian Wirtz | Leverkusen | Bundesliga | 21 | 5.282 |
| Mateo Retegui | Atalanta | Serie A | 25 | 5.119 |
| Bradley Barcola | PSG | Ligue 1 | 21 | 4.982 |
| Mason Greenwood | Marseille | Ligue 1 | 22 | 4.959 |
| Rayan Cherki | Lyon | Ligue 1 | 20 | 4.951 |
| Bukayo Saka | Arsenal | Premier League | 22 | 4.884 |
| Alexander Isak | Newcastle Utd | Premier League | 24 | 4.865 |
| Amine Gouiri | Marseille | Ligue 1 | 24 | 4.831 |
| Cole Palmer | Chelsea | Premier League | 22 | 4.775 |
| Ademola Lookman | Atalanta | Serie A | 26 | 4.751 |
| Vinicius Júnior | Real Madrid | La Liga | 24 | 4.739 |
| Alexander Sørloth | Atlético Madrid | La Liga | 28 | 4.676 |
The similarity algorithm lets you enter any forward’s name and returns the most similar players based on specific metrics. The logic is based on profiles and roles rather than output. Goals, assists, and other similar numbers are not taken into account because they are too context-based (player is on a poor team, injured, has 1 bad season, etc). In this case, I chose to modify this algorithm so that I have players with similar tendencies and that can fill a role in a team.
If I want to replace a player like Doku, I want a player that has the tendency to take on his man very often. If I want a winger that fits PSG’s system, he has to work defensively and be very strong in dribbling, and so on.
So therefore we take all metrics, remove the goalkeeping-related ones and the ones linked to output, and keep the ones that are relevant to a player’s tendency like dribbling, playing long passes, playing forward, getting fouled often, etc.
Also, if we are looking for a younger prospect, output is something that you can work on. If we are looking for a player that gives us goals and assists now and we need to fill the gap, then we can tweak the algorithm but it is not what we are looking for in this task.
The metric space is built by removing three families of columns from the dataset: identity columns (player, squad, nation, position, minutes…), goalkeeper-specific columns, and every column that measures output rather than behaviour (goals, assists, xG, xAG, shots, penalties and their per-90 versions).
## Number of behavioural metrics used by the similarity model: 36
## Number of output-related metrics deliberately excluded: 15
Each player is described by the vector of his behavioural metrics. Vectors are z-score standardised so that no metric dominates because of its unit, then compared with a cosine similarity (the angle between two vectors, i.e. the shape of the profile, not its magnitude). Similarity is rescaled to a 0–100 index. A minutes threshold is applied per competition (each league has a different number of matchdays) and an age cap can be set to match a club’s recruitment policy.
We will use the similarity algorithm to identify who Liverpool could have brought in place of Luis Diaz who departed last summer. Usually, the club has a philosophy of bringing players around the age of 25, so we change the maximum age to 27 to fit their recruitment strategy.
## Luis Díaz is in the analysis sample.
## Sample size: 487 forwards
| Player | Squad | Competition | Similarity |
|---|---|---|---|
| Khvicha Kvaratskhelia | PSG | Ligue 1 | 89.44 |
| Kenan Yıldız | Juventus | Serie A | 88.50 |
| Arda Güler | Real Madrid | La Liga | 88.14 |
| Fermin López | Barcelona | La Liga | 88.08 |
| Lee Kang-in | PSG | Ligue 1 | 87.99 |
| Désiré Doué | PSG | Ligue 1 | 87.93 |
| Michael Olise | Bayern Munich | Bundesliga | 86.53 |
| Hákon Arnar Haraldsson | Lille | Ligue 1 | 84.91 |
| Amad Diallo | Manchester Utd | Premier League | 84.83 |
| Dani Olmo | Barcelona | La Liga | 84.75 |
| Rayan Cherki | Lyon | Ligue 1 | 84.59 |
| Ousmane Dembélé | PSG | Ligue 1 | 84.57 |
| Vinicius Júnior | Real Madrid | La Liga | 84.47 |
| Antony | Betis | La Liga | 84.27 |
| Florian Wirtz | Leverkusen | Bundesliga | 84.20 |
| Mason Greenwood | Marseille | Ligue 1 | 84.20 |
| Maghnes Akliouche | Monaco | Ligue 1 | 83.99 |
| Rodrygo | Real Madrid | La Liga | 83.58 |
| Lamine Yamal | Barcelona | La Liga | 83.00 |
| Reiss Nelson | Fulham | Premier League | 82.71 |
Finally, we compare the target player with his two most similar forwards on a spider (radar) chart, scaled between the p5 and p95 percentile limits of the forward population.
The best 2 matches according to the algorithm are Kvicha Kvaratskhelia & Kenan Yildiz.
The georgian international was available last winter but Liverpool did not move for him. They could have, knowing that contract negotiations with Diaz were stalling and he was running out of contract very soon. Kvaratskhelia is a tremendous ball carrier with strong workrate, and great finishing ability. He can also play on both wings, offering a lot of flexibility for any coach.
Kenan Yildiz shows also the same traits : a player that can take on his man with ease but also creative, with flair and a finisher. He has also shown willingness to defend : his age and potential make it a costly option, but could be worth it.
Liverpool trusted Cody Gakpo instead who is not similar to Luis Diaz at all as shown by our data. Florian Wirtz who is a strong dribbler and on our list has played in place of Luis Diaz but has been given a much freer role. We can assume that the two options given could have filled the original role given to Luis Diaz much better and helped the club this season.
Physical data could have helped us in order to get a more similar profile in terms of pace, as usually you would want wingers with similar pace (in this case particularly), or if you play with a tall and strong striker, etc.