LoL Worlds 2021: PCA and Radar Charts

The Fall 2021 semester is over, which means it’s time for one thing: keeping up with R coding.

In my previous semester, I had to very little to almost no coding (not including exams) for my classes. So rather than let my R coding rust, I should work on making sure I am still familiar with R.

Once again, like last fall, the dataset will be related to League of Legends. This time I will look at the World Championship that occurred this past fall. I got the data from gol.gg.

Sports stats is interesting and radar charts look pretty cool, so I wanted to make this for the players that participated in the tournament.

library(dplyr)
library(ggplot2)
library(hablar)
library(readxl)
library(fmsb)

worlds <- read_xlsx("worlds2021.xlsx")

worlds <- worlds %>% 
  convert(fct(region, position), num(solo)) %>% 
  filter(games> 3)

## Warning in as_reliable_num(.): NAs introduced by coercion

head(worlds)

## # A tibble: 6 x 25
##   player     region position games    wr   kda avgkill avgdeaths avgassist   csm
##   <chr>      <fct>  <fct>    <dbl> <dbl> <dbl>   <dbl>     <dbl>     <dbl> <dbl>
## 1 Abbedagge  LCS    MID          6 0.5     2.6     2.5       2.7       4.3   8.7
## 2 Adam       LEC    TOP          6 0.167   1.4     4.3       6.5       5     7.6
## 3 Ale        LPL    TOP          7 0.429   2.7     3.4       2.9       4.1   8.8
## 4 Alphari    LCS    TOP          7 0.429   2.4     2.7       2.4       3.1   8.6
## 5 Aria       LJL    MID          6 0       2.6     2.3       2.2       3.3   7.9
## 6 Armut      LEC    TOP         11 0.364   2.1     3.2       4.2       5.5   7.8
## # ... with 15 more variables: gpm <dbl>, kpp <dbl>, dmgp <dbl>, dpm <dbl>,
## #   vspm <dbl>, avgwpm <dbl>, avgwcpm <dbl>, avgvpm <dbl>, gd15 <dbl>,
## #   csd15 <dbl>, xpd15 <dbl>, fbp <dbl>, fbv <dbl>, penta <dbl>, solo <dbl>

The data has various aspects of the players. There’s the usual kills, deaths, and assists, but also there’s gold and creep score differences, and finally there’s vision stats.

PCA

Let’s do something similar to what we did last year. Let’s take the two largest components in PCA, and see if there’s anything different from last year.

worlds.pca <- prcomp(worlds[, 4:24],
                     center = TRUE,
                     scale = TRUE)

PC1 <- worlds.pca$x[, "PC1"]
PC2 <- worlds.pca$x[, "PC2"]

data_pca <- tibble(PC1=PC1, PC2=PC2, position = worlds$position)
variance <- worlds.pca$sdev^2 / sum(worlds.pca$sdev^2)
v1 <- paste0("variance: ",signif(variance[1] * 100,3), "%")
v2 <- paste0("variance: ",signif(variance[2] * 100,3), "%")

data_pca %>% 
  ggplot() +
  aes(x=PC1,y=PC2,color=position) + 
  geom_point() + 
  labs(x = v1, y=v2) +    
  theme_bw() +
  labs(title = "PCA of Worlds 2021 Player Data (by position)")

Result looks very similar to last year. Top, mid, and ADC overlap with each other, while jungle and support on their own areas. As usual, this suggests that jungle and support play different roles compared to their carry counterparts.

Now let’s color the dots by region:

worlds.pca <- prcomp(worlds[, 4:24],
                     center = TRUE,
                     scale = TRUE)

PC1 <- worlds.pca$x[, "PC1"]
PC2 <- worlds.pca$x[, "PC2"]

data_pca <- tibble(PC1=PC1, PC2=PC2, region = worlds$region)
variance <- worlds.pca$sdev^2 / sum(worlds.pca$sdev^2)
v1 <- paste0("variance: ",signif(variance[1] * 100,3), "%")
v2 <- paste0("variance: ",signif(variance[2] * 100,3), "%")

data_pca %>% 
  ggplot() +
  aes(x=PC1,y=PC2,color=region) + 
  geom_point() + 
  labs(x = v1, y=v2) +    
  theme_bw() +
  labs(title = "PCA of Worlds 2021 Player Data (by region)")

We can see that LPL and LCK are relatively together on the top, which means the players from these regions play similarly. On the other hand, the LJL have the lowest at the bottom. This is maybe because they are the minor region, and their performances aren’t as great as the major regions.

However, another minor region, PCS, is in the graph. But the PCS has been known to be underdogs with relatively good performances. The team in particular, PSG Talon, made it to semi-finals in the Mid-Season Invitational.

Radar Chart

The real goal of this coding to make radar charts. I had to do a little searching, but there is the fmsb package that allows us to make it.

Let’s test the code with the mid-lane players. I will select eight stats to create the chart: KDA, CS per minute, gold per minute, kill participation percentage, damage per minute, gold difference at 15 minutes, CS difference at 15 minutes, and XP difference at 15 minutes.

These 8 stats tell us the general idea of how well the player is doing. Mid-lane is heavily focused on during the game because it is quickly way around the map.

Testing the code:

worlds_mid <- worlds %>% 
  filter(position == "MID") %>% 
  select(player, kda, csm, gpm, kpp, dpm, gd15, csd15, xpd15)

worlds_mid <- as.data.frame(worlds_mid)

rownames(worlds_mid) <- worlds_mid$player

worlds_mid[,2:9] <- scale(worlds_mid[,2:9])
col_max <- apply(worlds_mid[,2:9], 2, max)
col_min <- apply(worlds_mid[,2:9], 2, min)
col_mean <- apply(worlds_mid[,2:9], 2, mean)

col_summary <- t(data.frame(Max = col_max, 
                            Min = col_min, 
                            average = col_mean))

worlds_mid2 <- data.frame(rbind(col_summary, worlds_mid[,2:9]))

opar <- par() 

par(mar = rep(1,4))
par(mfrow = c(4,4))


for (i in 4:nrow(worlds_mid2)) {
  radarchart(
    worlds_mid2[c(1:3, i), ],
    pfcol = c("#99999980",NA),
    pcol= c("gray","red"), plty = c(1,1), plwd = c(.5,2),
    pty = c(NA, NA),
    cglcol = "black", cglty=1, axislabcol="grey", cglwd=1,
    title = row.names(worlds_mid2)[i]
  )
}

mtext("MID", outer= TRUE, line = -2)

par <- par(opar) 

Oh great, it worked.

I will go use it for the other positions now through a for loop:

for (i in levels(worlds$position)[c(1:2, 5)]) {
  worlds_position <- worlds %>% 
    filter(position == i) %>% 
    select(player, kda, csm, gpm, kpp, dpm, gd15, csd15, xpd15)

  worlds_position <- as.data.frame(worlds_position)
  
  rownames(worlds_position) <- worlds_position$player
  
  worlds_position[,2:9] <- scale(worlds_position[,2:9])
  col_max <- apply(worlds_position[,2:9], 2, max)
  col_min <- apply(worlds_position[,2:9], 2, min)
  col_mean <- apply(worlds_position[,2:9], 2, mean)
  
  col_summary <- t(data.frame(Max = col_max, 
                              Min = col_min, 
                              average = col_mean))
  
  worlds_position2 <- data.frame(rbind(col_summary, worlds_position[,2:9]))
  
  opar <- par() 

  par(mar = rep(1,4))
  par(mfrow = c(4,4))
  
  for (j in 4:nrow(worlds_position2)) {
    radarchart(
      worlds_position2[c(1:3, j), ],
      pfcol = c("#99999980",NA),
      pcol= c("gray","red"), plty = c(1,1), plwd = c(.5,2),
      pty = c(NA, NA),
      cglcol = "black", cglty=1, axislabcol="grey", cglwd=1,
      title = row.names(worlds_position2)[j]
    )
  }
  
  mtext(i, outer= TRUE, line = -2)
  
  par <- par(opar) 
}

I have to do supports differently because they’re more about vision and assisting.

I will keep the usual KDA and kill participation, but I will add average, assists, vision score per minute, average wards per minute, average wards cleared per minute, and average vision wards per minute.

worlds_supp <- worlds %>% 
  filter(position == "SUPPORT") %>% 
  select(player, kda, kpp, avgassist, vspm, avgwpm, avgwcpm, avgvpm)

worlds_supp <- as.data.frame(worlds_supp)

rownames(worlds_supp) <- worlds_supp$player

worlds_supp[,2:8] <- scale(worlds_supp[,2:8])
col_max <- apply(worlds_supp[,2:8], 2, max)
col_min <- apply(worlds_supp[,2:8], 2, min)
col_mean <- apply(worlds_supp[,2:8], 2, mean)

col_summary <- t(data.frame(Max = col_max, Min = col_min, average = col_mean))

worlds_supp2 <- data.frame(rbind(col_summary, worlds_supp[,2:8]))

opar <- par() 

par(mar = rep(1,4))
par(mfrow = c(4,4))

for (i in 4:nrow(worlds_supp2)) {
  radarchart(
    worlds_supp2[c(1:3, i), ],
    pfcol = c("#99999980",NA),
    pcol= c("gray","red"), plty = c(1,1), plwd = c(.5,2),
    pty = c(NA, NA),
    cglcol = "black", cglty=1, axislabcol="grey", cglwd=1,
    title = row.names(worlds_supp2)[i]
  )
}

mtext("SUPPORT", outer= TRUE, line = -2)

par <- par(opar) 

Updated: