5B Elo Calculation

Introduction

We will be calculating the elo scores of the players from our first project and compare the expected score to the actual elo change that each player experienced. To do this, we will first get the original data from github used in project 1 and perform the same transformations as the original project.

library(tidyverse)
library(gt)

lines <- readLines("https://raw.githubusercontent.com/Keenan-Roe/DATA-607/main/Project%201/ELO.txt")
Warning in
readLines("https://raw.githubusercontent.com/Keenan-Roe/DATA-607/main/Project%201/ELO.txt"):
incomplete final line found on
'https://raw.githubusercontent.com/Keenan-Roe/DATA-607/main/Project%201/ELO.txt'
elo <- read.delim(text = grep("^-+\\s*$", lines, value = TRUE, invert = TRUE), sep = "|", header = TRUE, strip.white = TRUE, row.names = NULL)


elo <- elo |> 
  slice(-1) |>
  select(-X)

odd_rows <- elo |> 
  filter( row_number() %% 2 == 1)

even_rows <- elo |> 
  filter(row_number() %% 2 == 0)

eloSplit <- data.frame( 
  
  str_split_fixed(
    
    str_split_fixed( even_rows[,2],"[R:](.)", 2)[,2] , 
    
    "->", 2 )
) 

names(eloSplit) <- c("starting_elo", "ending_elo")

eloSplit <- eloSplit |> 
  mutate(
    across( starting_elo:ending_elo, 
            \(x) str_remove(x, "P.*")
    )
  )

even_rows <- even_rows |> 
  mutate( eloSplit ) 

New Dataframes

In order to perform our calculations we will create two new data sets that we will use to keep track of the opponents as well as the results of matches separately. We will need to replace the win, lose, and draw indicators with 1, 0, and .5 respectively. This will allow us to us the results data set directly in our calculations later on.

opponents <- odd_rows |> 
  mutate( across( Round:Round.6, \(x) gsub("\\D", "", x) ) ) |> 
  mutate(start_elo = even_rows$starting_elo) |> 
  mutate(start_elo = as.numeric(str_trim(start_elo))) |>
  select(Player.Name, Round:Round.6)

results <- odd_rows |> 
  mutate( across( Round:Round.6, \(x) gsub("[0-9]", "", x) ) ) |>
  mutate(across( Round:Round.6, trimws)) |>
  mutate(across(Round:Round.6, ~ recode(.x, "W" = 1, "L" = 0, "D" = 0.5, .default = NA_real_))) |>
  select(Player.Name, Round:Round.6) |>
  mutate(starting_elo = as.numeric(even_rows$starting_elo))

Functions

In order to perform elo changes we will be using two formulas derived from this video. The basics of the algorithm states that a player facing an opponent with 400 more elo than them means that the opponent has a 10 times higher chance to win. A K-value of 32 is used which will be how much the elo changes with each match. a higher K-value introduces more volatility and change to elo while a lower K-value will result in lower elo changes overall. The previously produced data set with our win/loss/draw numbers will be used in place of score.

E_value <- function(p2, p1) {
  E <- 1/(1+10^((p2-p1)/400))
}

point_change <- function( elo, score, E ){
  newElo <- elo + 32*( score - E )
}

Analysis

Now that we have all the pieces we need we can put it all together and even keep track of elo changes in a dataframe. We will create a new data set called elo_results and then update the column starting_elo in our players new starting elo for each match so we will be using their results in the tournament in the calculation. We will need the players elo and their opponent’s elo to calculate the expected score. We will them use that as well as their starting elo and whether they won, lost, or drew to determine the player’s new score. Any NA value can be ignored.

elo_results <- results |>
  select(Player.Name, Round:starting_elo) |>
  mutate( expected_score = 0 )


for ( i in 2:(length(elo_results) - 2)){
  
  for ( j in 1:length(elo_results$Round)){
    
    if(!is.na(elo_results[j,i])){
      
      pElo <- elo_results[j,"starting_elo"]
      opponent <- opponents[j, i]
      opElo <- results[opponent, "starting_elo"]
      s<-as.numeric(elo_results[j,i])
      
      expected <- E_value( opElo, pElo)
      
      newElo <- round(point_change(pElo, s, expected))
      
      elo_results[j,"expected_score"] <- expected + elo_results[j,"expected_score"]
      elo_results[j,i] <- newElo
      elo_results[j,"starting_elo"] <- newElo
        
    }
    
  }
  
}

With these results we see that our final score is different from the final elo score stated in the original file, but this can be down to a different k-value or a different calculation altogether. No provisional rating was used in our calculations either. We can start comparing the expected score vs the actual score each player achieved. We can see there was a large spread of performances, but looking at the top ten performing players compared to their expected score we can see that Aditya Bajaj outperformed themselves by almost 4 whole points with each point mapping onto an unexpected win based on the elo differences.

score_compare <- elo_results |> 
  select(final_elo = starting_elo, expected_score) |>
  mutate( players = odd_rows$Player.Name) |>
  mutate(starting_elo = trimws(even_rows$starting_elo) ) |>
  mutate( real_score = odd_rows$Total) |> 
  mutate( performance = as.numeric(real_score) - as.numeric(expected_score)) |>
  relocate(starting_elo, .before = final_elo) |> 
  relocate(players, .before = starting_elo) |>
  arrange(desc(performance))


score_compare |>
  gt() |>
  tab_header(title = "Player Performance")
Player Performance
players starting_elo final_elo expected_score real_score performance
ADITYA BAJAJ 1384 1508 2.16867362 6.0 3.831326380
JACOB ALEXANDER LAVALLEY 377 472 0.05438498 3.0 2.945615022
ZACHARY JAMES HOUGHTON 1220 1314 1.58776204 4.5 2.912237960
ANVIT RAO 1365 1458 2.09050207 5.0 2.909497927
AMIYATOSH PWNANANDAM 980 1017 0.82654413 3.5 2.673455867
STEFANO LEE 1411 1490 2.53208421 5.0 2.467915791
ETHAN GUO 935 1003 0.38392238 2.5 2.116077622
DAKSHESH DARURI 1553 1620 3.91304514 6.0 2.086954862
SEAN M MC CORMICK 853 872 0.42438080 2.0 1.575619196
VIRAJ MOHILE 917 933 0.48121465 2.0 1.518785354
MICHAEL R ALDRICH 1229 1275 2.56234674 4.0 1.437653262
TEJAS AYYAGARI 1011 1056 1.13199025 2.5 1.368009749
HANSHI ZUO 1655 1690 4.43968368 5.5 1.060316318
SHIVAM JHA 1056 1075 1.45682747 2.5 1.043172534
JUSTIN D SCHILLING 1199 1196 2.08099360 3.0 0.919006404
MARISA RICCI 1153 1150 1.09722875 2.0 0.902771250
JULIA SHEN 967 980 0.61279349 1.5 0.887206512
SIDDHARTH JHA 1355 1364 2.71093584 3.5 0.789064157
PATRICK H SCHILLING 1716 1739 4.80496759 5.5 0.695032412
BRIAN LIU 1423 1430 2.30817863 3.0 0.691821368
GARY HUA 1794 1818 5.31125667 6.0 0.688743335
MICHAEL LU 1092 1082 1.31894138 2.0 0.681058617
KYLE WILLIAM MURPHY 1403 1391 2.35964011 3.0 0.640359888
ALEX KONG 1186 1171 1.43119149 2.0 0.568808511
JEZZEL FARKAS 955 971 1.03985539 1.5 0.460144612
KENNETH J TACK 1663 1657 4.17359481 4.5 0.326405194
JOSE C YBARRA 1393 1371 1.69355416 2.0 0.306445845
GARY DEE SWATHELL 1649 1658 4.71577996 5.0 0.284220041
BRADLEY SHAW 1610 1619 4.25707436 4.5 0.242925643
MIKE NIKITIN 1604 1595 3.77906283 4.0 0.220937174
ASHWIN BALAJI 1530 1534 0.87870495 1.0 0.121295049
MICHAEL JEFFERY THOMAS 1399 1402 3.40318973 3.5 0.096810271
HANSEN SONG 1686 1690 4.90331271 5.0 0.096687288
ALAN BUI 1363 1364 3.94740377 4.0 0.052596234
SOFIA ADINA STANESCU-BELLU 1507 1508 3.45623347 3.5 0.043766532
EZEKIEL HOUGHTON 1641 1641 4.98020667 5.0 0.019793331
DANIEL KHAIN 1382 1350 2.48699897 2.5 0.013001027
MICHAEL J MARTIN 1291 1275 2.49431675 2.5 0.005683253
FOREST ZHANG 1348 1348 3.00448145 3.0 -0.004481449
JOSHUA PHILIP MATHEWS 1441 1435 3.70759243 3.5 -0.207592430
DINH DANG BUI 1563 1553 4.30016844 4.0 -0.300168444
DIPANKAR ROY 1564 1554 4.32131587 4.0 -0.321315867
EUGENE L MCCLURE 1555 1527 4.37064681 4.0 -0.370646814
THOMAS JOSEPH HOSMER 1175 1148 1.38637425 1.0 -0.386374246
TORRANCE HENRY JR 1666 1652 4.95932794 4.5 -0.459327945
GAURAV GIDWANI 1552 1536 3.97479215 3.5 -0.474792155
RONALD GRZEGORCZYK 1629 1608 4.61919372 4.0 -0.619193717
DAVID SUNDEEN 1600 1580 4.63922173 4.0 -0.639221732
MAX ZHU 1579 1558 4.16397110 3.5 -0.663971097
ERIC WRIGHT 1362 1341 3.16767053 2.5 -0.667670530
JOEL R HENDON 1436 1414 3.67349820 3.0 -0.673498202
CAMERON WILLIAM MC LEMAN 1712 1689 5.24309850 4.5 -0.743098499
CHIEDOZIE OKORIE 1602 1568 4.53555264 3.5 -1.035552639
JASON ZHENG 1595 1560 5.05829470 4.0 -1.058294698
JADE GE 1449 1414 4.60393487 3.5 -1.103934873
DEREK YAN 1242 1205 4.12477935 3.0 -1.124779346
ROBERT GLEN VASEY 1283 1246 4.17559653 3.0 -1.175596525
LARRY HODGE 1270 1201 3.18362413 2.0 -1.183624134
JOSHUA DAVID LEE 1438 1399 4.71504968 3.5 -1.215049677
BEN LI 1163 1123 2.22311786 1.0 -1.223117855
RISHI SHETTY 1494 1451 4.85276303 3.5 -1.352763035
JARED GE 1332 1278 4.72778870 3.0 -1.727788703
GEORGE AVERY JONES 1522 1450 5.77312629 3.5 -2.273126293
LOREN SCHWIEBERT 1745 1661 6.10105844 3.5 -2.601058445
ggplot(score_compare, aes(x = reorder(players, -performance), y = performance)) +
  geom_col(fill = "steelblue") +
  theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 3)) +
  labs(x = "Player")

top10 <- score_compare |> 
  arrange(desc(performance)) |>
  slice_head(n=10)

ggplot( top10, aes( x = reorder ( players, -performance ), y = performance)) + 
  geom_col( fill = "firebrick")+
  theme( axis.text.x = element_text( angle = 45, hjust = 1 )) + 
  labs( x = "Players")

bottom10 <- score_compare |> 
  arrange(performance) |>
  slice_head(n=10)

ggplot( bottom10, aes( x = reorder ( players, -performance ), y = performance)) + 
  geom_col( fill = "orangered")+
  theme( axis.text.x = element_text( angle = 45, hjust = 1 )) + 
  labs( x = "Players")

Conclusion

the results are a fairly even spread when looking at a 5 to 6 game average. The total difference between the top performing player and the lowest performing player is 6.4323848. A max differential in this format would be a number approaching 12 with one player winning all 6 games when they are expected to lose all 6 and another player losing all 6 games when they are expected to win all six games.