Introduction

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.

Libraries

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)
  }
}

Reading data

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

Data structure

## 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"

Sample selection : forward roles & Big 5 Leagues only

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

Data filtering

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

Scoring calculation

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.

Data transformation (Normalisation)

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.

Scoring function

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.

Weight assignment

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.

Top 20 Rated Forwards in Europe - Season 24/25
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

Similarity algorithm

Principle

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.

Identifying all outfield metrics

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

Similarity function

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.

Case Study : How Liverpool could have replaced Luis Díaz

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
Top 20 Forwards Similar to Luis Díaz
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

Similarity barplot

Radar graph

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.

Radar drawing utility function

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.