What Factor is the most Important in Determining Success in the Premier League

Author

Hamza Nuru

What factor is the most important in determining success in the English Premier League?

Success in the Premier League is associated with having world-class player, a strong squad depth, and effective tactics. In this project will discover how these factors contribute to success in the English Premier League. 

I anazlyed data for the 2024/2025 Premier League season, by combining individual player statistics with match results to find a relationship between performance and league points.

This project involved combining multiple variables to create new ones, and they are listed as follows:

Star-player Dependence: (Team, player names, goals, assists) how much of the goals were scored by the top 3 goalscorers.

Squad depth: (Team, players, minutes) how many players have played over 900 minutes. 

Attacking Score: (shots on target, successful final third passes, assists, carries ending with shots) Score to observe the tactics

Defense Score: (Tackles, interceptions, blocks, possession won,) score to observe the tactics.

Hypothesis: Star players, squad depth and tactics will determine number og league points ofr teams in the premier league.

We start off by loading the libraries necessary, as well as the datasets.

library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.2.1     ✔ readr     2.2.0
✔ forcats   1.0.1     ✔ stringr   1.6.0
✔ ggplot2   4.0.3     ✔ tibble    3.3.1
✔ lubridate 1.9.5     ✔ tidyr     1.3.2
✔ purrr     1.2.1     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(plotly)

Attaching package: 'plotly'

The following object is masked from 'package:ggplot2':

    last_plot

The following object is masked from 'package:stats':

    filter

The following object is masked from 'package:graphics':

    layout
library(ggplot2)
library(dplyr)
library(ggimage)

obtained from Football-Data.co.uk

matches <- readr::read_csv("season-2425.csv") 
Rows: 380 Columns: 22
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr   (5): HomeTeam, AwayTeam, FTR, HTR, Referee
dbl  (16): FTHG, FTAG, HTHG, HTAG, HS, AS, HST, AST, HF, AF, HC, AC, HY, AY,...
date  (1): Date

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
head(matches)
# A tibble: 6 × 22
  Date       HomeTeam AwayTeam  FTHG  FTAG FTR    HTHG  HTAG HTR   Referee    HS
  <date>     <chr>    <chr>    <dbl> <dbl> <chr> <dbl> <dbl> <chr> <chr>   <dbl>
1 2024-08-16 Man Uni… Fulham       1     0 H         0     0 D     R Jones    14
2 2024-08-17 Ipswich  Liverpo…     0     2 A         0     0 D     T Robi…     7
3 2024-08-17 Arsenal  Wolves       2     0 H         1     0 H     J Gill…    18
4 2024-08-17 Everton  Brighton     0     3 A         0     1 A     S Hoop…     9
5 2024-08-17 Newcast… Southam…     1     0 H         1     0 H     C Paws…     3
6 2024-08-17 Nott'm … Bournem…     1     1 D         1     0 H     M Oliv…    14
# ℹ 11 more variables: AS <dbl>, HST <dbl>, AST <dbl>, HF <dbl>, AF <dbl>,
#   HC <dbl>, AC <dbl>, HY <dbl>, AY <dbl>, HR <dbl>, AR <dbl>

Obtained from: Ahmed Gaballah, sources obtained from Premier League official statistics, Opta/StatsPerform and Fbref.

players <- readr::read_csv("epl_player_stats_24_25.csv")
Rows: 562 Columns: 57
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (11): Player Name, Club, Nationality, Position, Conversion %, Passes%, C...
dbl (46): Appearances, Minutes, Goals, Assists, Shots, Shots On Target, Big ...

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
head(players)
# A tibble: 6 × 57
  `Player Name` Club    Nationality Position Appearances Minutes Goals Assists
  <chr>         <chr>   <chr>       <chr>          <dbl>   <dbl> <dbl>   <dbl>
1 Ben White     Arsenal England     DEF               17    1198     0       2
2 Bukayo Saka   Arsenal England     MID               25    1735     6      10
3 David Raya    Arsenal Spain       GKP               38    3420     0       0
4 Declan Rice   Arsenal England     MID               35    2833     4       7
5 Ethan Nwaneri Arsenal England     MID               26     889     4       0
6 Gabriel Jesus Arsenal Brazil      FWD               17     603     3       0
# ℹ 49 more variables: Shots <dbl>, `Shots On Target` <dbl>,
#   `Conversion %` <chr>, `Big Chances Missed` <dbl>, `Hit Woodwork` <dbl>,
#   Offsides <dbl>, Touches <dbl>, Passes <dbl>, `Successful Passes` <dbl>,
#   `Passes%` <chr>, Crosses <dbl>, `Successful Crosses` <dbl>,
#   `Crosses %` <chr>, `fThird Passes` <dbl>, `Successful fThird Passes` <dbl>,
#   `fThird Passes %` <chr>, `Through Balls` <dbl>, Carries <dbl>,
#   `Progressive Carries` <dbl>, `Carries Ended with Goal` <dbl>, …

Then, we sort the clubs, from the two datasets, and make sure they are named the same way.

sort(unique(matches$HomeTeam))
 [1] "Arsenal"        "Aston Villa"    "Bournemouth"    "Brentford"     
 [5] "Brighton"       "Chelsea"        "Crystal Palace" "Everton"       
 [9] "Fulham"         "Ipswich"        "Leicester"      "Liverpool"     
[13] "Man City"       "Man United"     "Newcastle"      "Nott'm Forest" 
[17] "Southampton"    "Tottenham"      "West Ham"       "Wolves"        
sort(unique(players$Club))
 [1] "Arsenal"                 "Aston Villa"            
 [3] "Bournemouth"             "Brentford"              
 [5] "Brighton"                "Brighton & Hove Albion" 
 [7] "Chelsea"                 "Crystal Palace"         
 [9] "Everton"                 "Fulham"                 
[11] "Ipswich Town"            "Leicester City"         
[13] "Liverpool"               "Manchester City"        
[15] "Manchester United"       "Newcastle United"       
[17] "Nottingham Forest"       "Southampton"            
[19] "Tottenham Hotspur"       "West Ham United"        
[21] "Wolverhampton Wanderers"

This chunk is important as the player dataset has a variable called club, and it has to be renamed to Team to match the variable with the matches datasets.

players <- players %>%
  rename(Team = Club)

Here, we make sure the values of the variables are named consistently.

players <- players %>%
  mutate(
    Team = recode(
      Team,
      "Brighton" = "Brighton & Hove Albion",
      "Ipswich" = "Ipswich Town",
      "Leicester" = "Leicester City",
      "Man City" = "Manchester City",
      "Man United" = "Manchester United",
      "Newcastle" = "Newcastle United",
      "Nott'm Forest" = "Nottingham Forest",
      "Tottenham" = "Tottenham Hotspur",
      "West Ham" = "West Ham United",
      "Wolves" = "Wolverhampton Wanderers"
    )
  )
matches <- matches %>%
  mutate(
    HomeTeam = recode(
      HomeTeam,
      "Brighton" = "Brighton & Hove Albion",
      "Ipswich" = "Ipswich Town",
      "Leicester" = "Leicester City",
      "Man City" = "Manchester City",
      "Man United" = "Manchester United",
      "Newcastle" = "Newcastle United",
      "Nott'm Forest" = "Nottingham Forest",
      "Tottenham" = "Tottenham Hotspur",
      "West Ham" = "West Ham United",
      "Wolves" = "Wolverhampton Wanderers"
    ),
 AwayTeam = recode(
      AwayTeam,
      "Brighton" = "Brighton & Hove Albion",
      "Ipswich" = "Ipswich Town",
      "Leicester" = "Leicester City",
      "Man City" = "Manchester City",
      "Man United" = "Manchester United",
      "Newcastle" = "Newcastle United",
      "Nott'm Forest" = "Nottingham Forest",
      "Tottenham" = "Tottenham Hotspur",
      "West Ham" = "West Ham United",
      "Wolves" = "Wolverhampton Wanderers"
 )
)

Once renaming is done, we check if we missed any names, since it returns character(0), all the team names have been synced.

setdiff(
  unique(players$Team),
  unique(matches$HomeTeam)
)
character(0)

Now, that we have cleaned and combined our datasets, we will start calculating star player dependence and squad depth variable.

On this chunk, we are adding a new variable called “GA”, which is the sum of goals and assists., then displaying the 5 players with the most contribution. These are our star players.

players <- players %>%
  mutate(
    GA = Goals + Assists
  )

players %>%
  arrange(desc(GA)) %>%
  select(`Player Name`, Team, Goals, Assists, GA) %>%
  head(5)
# A tibble: 5 × 5
  `Player Name`  Team            Goals Assists    GA
  <chr>          <chr>           <dbl>   <dbl> <dbl>
1 Mohamed Salah  Liverpool          29      18    47
2 Bryan Mbeumo   Brentford          20       7    27
3 Erling Haaland Manchester City    22       3    25
4 Ollie Watkins  Aston Villa        16       8    24
5 Cole Palmer    Chelsea            15       8    23
ggplot(
  players %>% arrange(desc(GA)) %>% head(5), 
  aes(
  x = reorder(`Player Name`, GA),
  y = GA,
  fill = `Player Name`
)) + 
  geom_col() + 
  geom_text(aes(label = GA), hjust = -0.2) +
  coord_flip() +
  scale_fill_brewer(palette = "Set2") 

Now, we answer first research question.

Do teams that depend heavily on a few star players perform better than balanced squads?

We’re gonna find that out by calculating the percentage of a team’s total players goals scored by its three highest scoring-players.

star_dependence <- players %>%
  group_by(Team) %>%
  arrange(desc(Goals), .by_group = TRUE) %>%
  summarise(
    TotalGoals = sum(Goals, na.rm = TRUE),
    Top3Goals = sum(head(Goals, 3), na.rm = TRUE),
    StarDependence = if_else(
      TotalGoals > 0,
      (Top3Goals/TotalGoals) * 100,
      NA_real_
    ),
    .groups = "drop"
  )

star_dependence
# A tibble: 20 × 4
   Team                    TotalGoals Top3Goals StarDependence
   <chr>                        <dbl>     <dbl>          <dbl>
 1 Arsenal                         67        25           37.3
 2 Aston Villa                     61        31           50.8
 3 Bournemouth                     24        13           54.2
 4 Brentford                       54        44           81.5
 5 Brighton & Hove Albion          63        30           47.6
 6 Chelsea                         63        32           50.8
 7 Crystal Palace                  51        30           58.8
 8 Everton                         41        21           51.2
 9 Fulham                          41        23           56.1
10 Ipswich Town                    23        10           43.5
11 Leicester City                  28        18           64.3
12 Liverpool                       72        45           62.5
13 Manchester City                 71        36           50.7
14 Manchester United               38        22           57.9
15 Newcastle United                66        40           60.6
16 Nottingham Forest               57        33           57.9
17 Southampton                     23         9           39.1
18 Tottenham Hotspur               50        25           50  
19 West Ham United                 31        18           58.1
20 Wolverhampton Wanderers         39        22           56.4

Now that we have our star dependence variable,

Do teams with greater squad depth achieve more success?

squad_depth <- players %>%
  group_by(Team) %>%
  summarise(
    SquadDepth = sum(Minutes >= 900, na.rm = TRUE),
    TotalPlayers = n(),
    .groups = "drop"
  )

squad_depth
# A tibble: 20 × 3
   Team                    SquadDepth TotalPlayers
   <chr>                        <int>        <int>
 1 Arsenal                         16           25
 2 Aston Villa                     16           27
 3 Bournemouth                     15           29
 4 Brentford                       14           28
 5 Brighton & Hove Albion          17           30
 6 Chelsea                         16           28
 7 Crystal Palace                  15           26
 8 Everton                         18           26
 9 Fulham                          16           26
10 Ipswich Town                    17           30
11 Leicester City                  17           31
12 Liverpool                       15           24
13 Manchester City                 18           31
14 Manchester United               16           30
15 Newcastle United                15           23
16 Nottingham Forest               13           23
17 Southampton                     16           34
18 Tottenham Hotspur               19           31
19 West Ham United                 16           28
20 Wolverhampton Wanderers         16           32
squad_stats <- star_dependence %>%
  left_join(
    squad_depth,
    by = "Team"
  )

squad_stats
# A tibble: 20 × 6
   Team              TotalGoals Top3Goals StarDependence SquadDepth TotalPlayers
   <chr>                  <dbl>     <dbl>          <dbl>      <int>        <int>
 1 Arsenal                   67        25           37.3         16           25
 2 Aston Villa               61        31           50.8         16           27
 3 Bournemouth               24        13           54.2         15           29
 4 Brentford                 54        44           81.5         14           28
 5 Brighton & Hove …         63        30           47.6         17           30
 6 Chelsea                   63        32           50.8         16           28
 7 Crystal Palace            51        30           58.8         15           26
 8 Everton                   41        21           51.2         18           26
 9 Fulham                    41        23           56.1         16           26
10 Ipswich Town              23        10           43.5         17           30
11 Leicester City            28        18           64.3         17           31
12 Liverpool                 72        45           62.5         15           24
13 Manchester City           71        36           50.7         18           31
14 Manchester United         38        22           57.9         16           30
15 Newcastle United          66        40           60.6         15           23
16 Nottingham Forest         57        33           57.9         13           23
17 Southampton               23         9           39.1         16           34
18 Tottenham Hotspur         50        25           50           19           31
19 West Ham United           31        18           58.1         16           28
20 Wolverhampton Wa…         39        22           56.4         16           32

Now, we will be calculating wins, losses, and league points of the teams.

We start of by calculating the results of teams from home. FTR stands for Full-Time Result, with H indicating the home team winning, D for Draw, and A for away team winning.

home_results <- matches %>%
  group_by(Team = HomeTeam) %>%
  summarise(
    HomeWins = sum(FTR == "H", na.rm = TRUE),
    HomeDraws = sum(FTR == "D", na.rm = TRUE),
    HomeLosses = sum(FTR == "A", na.rm = TRUE),
    .groups = "drop"
  )

home_results
# A tibble: 20 × 4
   Team                    HomeWins HomeDraws HomeLosses
   <chr>                      <int>     <int>      <int>
 1 Arsenal                       11         6          2
 2 Aston Villa                   11         7          1
 3 Bournemouth                    8         4          7
 4 Brentford                      9         4          6
 5 Brighton & Hove Albion         8         8          3
 6 Chelsea                       12         5          2
 7 Crystal Palace                 6         7          6
 8 Everton                        5         9          5
 9 Fulham                         7         5          7
10 Ipswich Town                   1         4         14
11 Leicester City                 4         3         12
12 Liverpool                     14         4          1
13 Manchester City               13         3          3
14 Manchester United              7         3          9
15 Newcastle United              12         2          5
16 Nottingham Forest              9         5          5
17 Southampton                    1         3         15
18 Tottenham Hotspur              6         3         10
19 West Ham United                5         5          9
20 Wolverhampton Wanderers        6         3         10

Then the results of teams when they’re playing away from home.

away_results <- matches %>%
  group_by(Team = AwayTeam) %>%
  summarise(
    AwayWins = sum(FTR == "A", na.rm = TRUE),
    AwayDraws = sum(FTR == "D", na.rm = TRUE),
    AwayLosses = sum(FTR == "H", na.rm = TRUE),
    .groups = "drop"
  )

away_results
# A tibble: 20 × 4
   Team                    AwayWins AwayDraws AwayLosses
   <chr>                      <int>     <int>      <int>
 1 Arsenal                        9         8          2
 2 Aston Villa                    8         2          9
 3 Bournemouth                    7         7          5
 4 Brentford                      7         4          8
 5 Brighton & Hove Albion         8         5          6
 6 Chelsea                        8         4          7
 7 Crystal Palace                 7         7          5
 8 Everton                        6         6          7
 9 Fulham                         8         4          7
10 Ipswich Town                   3         6         10
11 Leicester City                 2         4         13
12 Liverpool                     11         5          3
13 Manchester City                8         5          6
14 Manchester United              4         6          9
15 Newcastle United               8         4          7
16 Nottingham Forest             10         3          6
17 Southampton                    1         3         15
18 Tottenham Hotspur              5         2         12
19 West Ham United                6         5          8
20 Wolverhampton Wanderers        6         3         10

We then combine home and awat results by using left_join() which combines both tables and mutate() to calculate total wins, draws, losses,and points

team_results <- home_results %>%
  left_join(away_results, by = "Team") %>%
  mutate(
    Wins = HomeWins + AwayWins, 
    Draws = HomeDraws + AwayDraws,
    Losses = HomeLosses + AwayLosses, 
    
    Points = (Wins * 3) + Draws, 
    
    MatchesPlayed = Wins + Draws + Losses, 
    
    WinPercentage = (Wins/MatchesPlayed) * 100
  ) %>%
  arrange(desc(Points))
team_results %>%
  select(Team, Wins, Draws, Losses, Points, WinPercentage) %>%
  arrange(desc(Points)) %>%
  print(n=20)
# A tibble: 20 × 6
   Team                     Wins Draws Losses Points WinPercentage
   <chr>                   <int> <int>  <int>  <dbl>         <dbl>
 1 Liverpool                  25     9      4     84         65.8 
 2 Arsenal                    20    14      4     74         52.6 
 3 Manchester City            21     8      9     71         55.3 
 4 Chelsea                    20     9      9     69         52.6 
 5 Aston Villa                19     9     10     66         50   
 6 Newcastle United           20     6     12     66         52.6 
 7 Nottingham Forest          19     8     11     65         50   
 8 Brighton & Hove Albion     16    13      9     61         42.1 
 9 Bournemouth                15    11     12     56         39.5 
10 Brentford                  16     8     14     56         42.1 
11 Fulham                     15     9     14     54         39.5 
12 Crystal Palace             13    14     11     53         34.2 
13 Everton                    11    15     12     48         28.9 
14 West Ham United            11    10     17     43         28.9 
15 Manchester United          11     9     18     42         28.9 
16 Wolverhampton Wanderers    12     6     20     42         31.6 
17 Tottenham Hotspur          11     5     22     38         28.9 
18 Leicester City              6     7     25     25         15.8 
19 Ipswich Town                4    10     24     22         10.5 
20 Southampton                 2     6     30     12          5.26

Now, We combine team results with player statistics. This will finally allow us to view the players contribution to their teams, so factors like Star Dependence and squad Depth can be feasible to study.

premier_league <- team_results %>%
  left_join(squad_stats, by = "Team") %>%
  arrange(desc(Points))

premier_league %>%
  select(Team, Wins, Losses, Points, StarDependence, SquadDepth, TotalGoals,
  ) %>%
  print(n=20, width = Inf)
# A tibble: 20 × 7
   Team                     Wins Losses Points StarDependence SquadDepth
   <chr>                   <int>  <int>  <dbl>          <dbl>      <int>
 1 Liverpool                  25      4     84           62.5         15
 2 Arsenal                    20      4     74           37.3         16
 3 Manchester City            21      9     71           50.7         18
 4 Chelsea                    20      9     69           50.8         16
 5 Aston Villa                19     10     66           50.8         16
 6 Newcastle United           20     12     66           60.6         15
 7 Nottingham Forest          19     11     65           57.9         13
 8 Brighton & Hove Albion     16      9     61           47.6         17
 9 Bournemouth                15     12     56           54.2         15
10 Brentford                  16     14     56           81.5         14
11 Fulham                     15     14     54           56.1         16
12 Crystal Palace             13     11     53           58.8         15
13 Everton                    11     12     48           51.2         18
14 West Ham United            11     17     43           58.1         16
15 Manchester United          11     18     42           57.9         16
16 Wolverhampton Wanderers    12     20     42           56.4         16
17 Tottenham Hotspur          11     22     38           50           19
18 Leicester City              6     25     25           64.3         17
19 Ipswich Town                4     24     22           43.5         17
20 Southampton                 2     30     12           39.1         16
   TotalGoals
        <dbl>
 1         72
 2         67
 3         71
 4         63
 5         61
 6         66
 7         57
 8         63
 9         24
10         54
11         41
12         51
13         41
14         31
15         38
16         39
17         50
18         28
19         23
20         23

Now, we are going to visualize our star players’ contributions to their teams. with an interactive scatterplot that displays team name, star dependence, and league points.

p <- ggplot(
  premier_league,
  aes(
    x = StarDependence,
    y = Points,
    color = Points,
    text = paste(
      "Team:", Team,
      "<br>Points:", Points,
      "<br>Star Dependence:", round(StarDependence, 1), "%"
    )
  )
) + 
  geom_point(size = 3.5) + 
  scale_color_gradient(
    low = "red",
    high = "blue"
  )

ggplotly(p, tooltip = "text")

For this visualization, I’m using the crests of the teams to show their star dependence. I went out of my way to do this to use team crests as points. I used a folder full of png pictures in my file path, and named them the same name as the team names to identify which on was which

premier_league <- premier_league %>%
  mutate(
    Crest = file.path("Crest", paste0(Team, ".png"))
  )
crest_plot <- ggplot(
  premier_league,
  aes(
    x = StarDependence,
    y = Points,
  )
) + 
  geom_image(
    aes(image = Crest),
    size = 0.12
  ) + 
  geom_smooth(
    method = "lm",
    se = FALSE,
    color = "gray",
    linetype = "dashed"
  )

crest_plot
`geom_smooth()` using formula = 'y ~ x'

Do teams with greater squad depth earn more league points?

Players who have played more than 900 minutes of football will be automatically part of the players usual squad. The higher the number, the bigger the depth. The more players available, the less injuries, and the better the performance.

Below is an interactive graph showing squad depth, as well as star dependence.

depth_plot <- plot_ly(
  data = premier_league,
  x = ~SquadDepth,
  y = ~Points,
  type = "scatter",
  mode = "markers",
  color = ~StarDependence,
  colors = c("blue", "green", "red"),
  marker = list(size = 13),
  text = ~paste0(
    "<b>", Team, "</b>",
    "<br>League Points: ", Points,
    "<br>Squad Depth: ", SquadDepth,
    "<br>Star Dependence: ",
    round(StarDependence, 1), "%"
  ),
  hoverinfo = "text"
)
depth_plot

Now that we have reviewed squad depth, we move on to tactics.

We will try to figure out if attacking or defensive tactics are prefferable to win more matches.

From the players data set, we will use the following variables: Attacking Variables: Shots On Targetm Succesful fThird Passes, Carries Ended with Shot, Assists.

Defensive Variables: Interceptions, Tackles, Blocks, Possession won

Now, we set up the variables.

team_performance <- players %>%
  group_by(Team) %>%
  summarise(
    ShotsOnTarget = sum(`Shots On Target`, na.rm = TRUE),
    FinalThirdPasses = sum(
      `Successful fThird Passes`, na.rm = TRUE
    ),
    ShotCarries = sum(
      `Carries Ended with Shot`, na.rm = TRUE
    ),
    TotalAssists = sum(Assists, na.rm = TRUE),
    
    Interceptions = sum(Interceptions, na.rm = TRUE),
    Tackles = sum(Tackles, na.rm = TRUE),
    Blocks = sum(Blocks, na.rm = TRUE),
    PossessionWon = sum(`Possession Won`, na.rm = TRUE),
    
    .groups = "drop"
  )

team_performance
# A tibble: 20 × 9
   Team    ShotsOnTarget FinalThirdPasses ShotCarries TotalAssists Interceptions
   <chr>           <dbl>            <dbl>       <dbl>        <dbl>         <dbl>
 1 Arsenal           139             3760          81           50           271
 2 Aston …           126             3052          84           46           295
 3 Bourne…           189             3329         116           28           315
 4 Brentf…           154             3041         114           33           205
 5 Bright…           138             3549         106           24           294
 6 Chelsea           189             3376          99           32           301
 7 Crysta…           104             1882          37           37           224
 8 Everton           175             3287         119           17           255
 9 Fulham            165             3743         133           37           201
10 Ipswic…           104             2380          88           20           225
11 Leices…           161             2961         113           21           324
12 Liverp…           165             2857         141           60           250
13 Manche…           137             3195          80           31           367
14 Manche…           171             3088         122           27           329
15 Newcas…           210             2820         154           37           180
16 Nottin…           149             2536          96           39           175
17 Southa…           168             3877         104           10           284
18 Totten…           174             3749          87           36           324
19 West H…           142             2715          87           25           300
20 Wolver…           139             3943          72           34           304
# ℹ 3 more variables: Tackles <dbl>, Blocks <dbl>, PossessionWon <dbl>

For our attacking scores, we will use z scores to find out of they teams are above or below the league average.

Here, we will add all the values from our variables and divide them with the number of our variables. that will be our attack/defense score.

team_performance <- team_performance %>%
  mutate(
    AttackScore = (
      as.numeric(scale(ShotsOnTarget)) + as.numeric(scale(FinalThirdPasses))+as.numeric(scale(ShotCarries))+as.numeric(scale(TotalAssists))
      )/4,
      
    DefenseScore = (
      as.numeric(scale(Interceptions)) + as.numeric(scale(Tackles))+as.numeric(scale(Blocks))+as.numeric(scale(PossessionWon))
      )/4
    )

team_performance %>%
  select(
    Team,
    AttackScore,
    DefenseScore
  ) %>%
  arrange(desc(DefenseScore)) %>%
  print(n=20)
# A tibble: 20 × 3
   Team                    AttackScore DefenseScore
   <chr>                         <dbl>        <dbl>
 1 Manchester United           0.195         0.830 
 2 Brighton & Hove Albion     -0.109         0.795 
 3 Manchester City            -0.376         0.733 
 4 Wolverhampton Wanderers    -0.0221        0.630 
 5 Tottenham Hotspur           0.393         0.598 
 6 Leicester City             -0.169         0.538 
 7 Chelsea                     0.384         0.386 
 8 Aston Villa                -0.184         0.370 
 9 Bournemouth                 0.437         0.270 
10 Everton                     0.0810        0.266 
11 Southampton                -0.00100       0.243 
12 Arsenal                     0.321        -0.0281
13 West Ham United            -0.616        -0.0951
14 Crystal Palace             -1.56         -0.157 
15 Fulham                      0.762        -0.418 
16 Liverpool                   0.920        -0.737 
17 Nottingham Forest          -0.250        -0.976 
18 Newcastle United            0.944        -1.01  
19 Brentford                   0.0709       -1.04  
20 Ipswich Town               -1.22         -1.19  

Now we’re going to classify playing styles

team_performance <- team_performance %>%
  mutate(
    StyleIndex = AttackScore - DefenseScore,
    
    PlayingStyle = case_when(
      StyleIndex > 0.5 ~ "Attack-Leaning",
      StyleIndex < -0.5 ~ "Defense-Leaning",
      TRUE ~ "Balanced"
    )
  )

team_performance %>%
  select(
    Team,
    AttackScore,
    DefenseScore,
    StyleIndex,
    PlayingStyle
  ) %>%
  arrange(desc(StyleIndex)) %>%
  print(n=20)
# A tibble: 20 × 5
   Team                    AttackScore DefenseScore StyleIndex PlayingStyle   
   <chr>                         <dbl>        <dbl>      <dbl> <chr>          
 1 Newcastle United            0.944        -1.01      1.96    Attack-Leaning 
 2 Liverpool                   0.920        -0.737     1.66    Attack-Leaning 
 3 Fulham                      0.762        -0.418     1.18    Attack-Leaning 
 4 Brentford                   0.0709       -1.04      1.11    Attack-Leaning 
 5 Nottingham Forest          -0.250        -0.976     0.727   Attack-Leaning 
 6 Arsenal                     0.321        -0.0281    0.349   Balanced       
 7 Bournemouth                 0.437         0.270     0.167   Balanced       
 8 Chelsea                     0.384         0.386    -0.00155 Balanced       
 9 Ipswich Town               -1.22         -1.19     -0.0260  Balanced       
10 Everton                     0.0810        0.266    -0.185   Balanced       
11 Tottenham Hotspur           0.393         0.598    -0.205   Balanced       
12 Southampton                -0.00100       0.243    -0.244   Balanced       
13 West Ham United            -0.616        -0.0951   -0.521   Defense-Leaning
14 Aston Villa                -0.184         0.370    -0.554   Defense-Leaning
15 Manchester United           0.195         0.830    -0.635   Defense-Leaning
16 Wolverhampton Wanderers    -0.0221        0.630    -0.652   Defense-Leaning
17 Leicester City             -0.169         0.538    -0.707   Defense-Leaning
18 Brighton & Hove Albion     -0.109         0.795    -0.904   Defense-Leaning
19 Manchester City            -0.376         0.733    -1.11    Defense-Leaning
20 Crystal Palace             -1.56         -0.157    -1.41    Defense-Leaning

Visualization, Attacking vs Defensive football

premier_league <- premier_league %>%
  select(-any_of(c(
    "AttackScore", "DefenseScore",
    "StyleIndex", "PlayingStyle"
  )))%>%
  left_join(
    team_performance %>%
      select(
        Team, AttackScore, DefenseScore, 
        StyleIndex, PlayingStyle
      ),
    by = "Team"
  )
tactics_plot <- plot_ly(
  data = premier_league,
  x = ~AttackScore,
  y = ~DefenseScore,
  type = "scatter",
  mode = "markers",
  color =~PlayingStyle,
  colors = c(
    "Attack-Leaning" = "red",
    "Balanced" = "purple",
    "Defense-Leaning" = "blue"
  ),
  marker = list(
    size = 14,
    line = list(
      color = "white",
      width = 1
    )
  ),
  text = ~paste0(
    "<b>", Team, "</b>",
    "<br>Attack Score: ", round(AttackScore, 2),
    "<br>Defense Score: ", round(DefenseScore, 2),
    "<br>Style Index:  ", round(StyleIndex, 2),
    "<br>Playing Style: ", PlayingStyle
  ),
  hoverinfo = "text"
  )

tactics_plot
ggplot(
  premier_league,
  aes(x = AttackScore, y = DefenseScore)
) +
  ggimage::geom_image(
    aes(image = Crest),
    size = 0.13
  )

Now, we’re gonna build our regression model.

pl_model <- lm(
  Points~ StarDependence + SquadDepth + AttackScore + DefenseScore, 
  data=premier_league
)

summary(pl_model)

Call:
lm(formula = Points ~ StarDependence + SquadDepth + AttackScore + 
    DefenseScore, data = premier_league)

Residuals:
    Min      1Q  Median      3Q     Max 
-41.399  -7.423  -1.170  12.731  30.252 

Coefficients:
                Estimate Std. Error t value Pr(>|t|)
(Intercept)    125.73719   77.92494   1.614    0.127
StarDependence  -0.03529    0.49679  -0.071    0.944
SquadDepth      -4.45267    4.00240  -1.112    0.283
AttackScore     10.48135    6.72827   1.558    0.140
DefenseScore     1.21760    7.78917   0.156    0.878

Residual standard error: 18.33 on 15 degrees of freedom
Multiple R-squared:  0.2316,    Adjusted R-squared:  0.02674 
F-statistic: 1.131 on 4 and 15 DF,  p-value: 0.3794

Regression Coefficients

Star Dependence: Almost no relationship with league points

Squad depth: players with 900 minute threshold earned fewer points, evidence is weak though.

Attacking Performance: Strongest coefficient,one unit increase corresponds to 10 points.

Defense: Small positive relationship.

Results and Interpretaion

The model produced an R-squared value of 0.2316, meaning 23.16 of the value in league points was explained by the four predictors. However, adjusted R-squared was 0.0267, suggesting that the model explains very variation.

Attacking performance had the strongest postitive coefficient(10.48), suggesting teams with more attacking tactic tended to earn more points compared to the other variables. Squad depth had a negative coefficient, (-4.45), while star dependence (-0.035) and defensive ativity(1.22) showed relatively small estimated coefficients.

The overall model was not statistically significant, as we had a p-value of 0.3794

Overall, while there are heavy associations with these factors, the findings suggest otherwise. Thes findings suggest that these variables alone are not reliable to explain success in the premier league during the 2024/2025 season.

Challenges Encountered

Combining the two datasets was the first hurdle. Although both datasets contained the same teams, several clubs had different names, for example “Manchester City” vs “Man City”. I used recode to standardize the team names, then I finally combined them with left_join

Another challenge was calculating the league results. The match dataset used “H” “A” and “D” to represent results, and when calculating i kept on getting the wrong league standings, and in the end, I figured it was a typo in the way I renamed one of the clubs.

A limitation to the attacking and defensive performance scores was that, the variables used to determine the scores were broad, so deciding which variables I should use was hard. I could’ve added more, but there were statistics measured on different scales.

Another limitaiton was the image plots, as they had background, as opposed to having just the crest. I discovered later that I can only use png files and not vector files.

The interpretation was also challenging because the model didn’t identify any statistically significant predictors. Although attack heavy tactics showed a positive relationship, the model explained only a little amount of variation.

Future Research Notes

In the future, I would expand the analysis to multiple league seasons instead of one.

I would also like to include additional variables such as player injuries, expected goals (xG), transfer spending, and so much more.

I would also study how football stat developers develop and find their stats, and what factors they consider. Some examples of these include OptaAnalyst, and the xG philosophy

Finally, I would strive to improve my regression model by testing different and more accurate combinations of variables.

Citations

https://search.r-project.org/CRAN/refmans/ggimage/html/geom_image.html
https://gianluca-sottile.github.io/An-R-Tutorial-for-Beginners/lessons/02-data/10-grouped-summaries.html
https://www.rpubs.com/shubh2565/pvalue-rsquared