SPS_Data607_Week5B

Author

David Chen

Chess ELO calculations

Based on difference in ratings between the chess players and each of their opponents in our Project 1 tournament, calculate each player’s expected score (e.g. 4.3) and the difference from their actual score (e.g 4.0).  List the five players who most overperformed relative to their expected score, and the five players that most underperformed relative to their expected score.

You’ll find some small differences in different implementation of ELO formulas.  You may use any reasonably-sourced formula, but please cite your source.  

Image source (and potentially useful reference!): The Elo Rating System for Chess and Beyond.  Feb 15, 2019.  [video, 7m]

The point system is W=1 D=0.5 L=0 B=1 H=0.5 U=0

Approach

The new rating is calculated as:

R_new=R_old+K×(Score−Expected Score)

R_old = your current rating

K = a factor that determines how fast ratings change (often 10–40)

Score = 1 for a win, 0.5 for a draw, 0 for a loss

Expected Score = a probability based on rating difference

The formula for expected score:

If Player A has rating R_a and Player B has rating R-b

E_a= 1 / (1+10^(R_b−R_a)/400)

Running Code

url="https://raw.githubusercontent.com/dyc-sps/SPS_Data607_Week5B/refs/heads/main/tournamentinfo.csv"
library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.6
✔ forcats   1.0.1     ✔ stringr   1.6.0
✔ ggplot2   4.0.1     ✔ tibble    3.3.1
✔ lubridate 1.9.4     ✔ 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
df_eol <- read.csv(url)

df_eol <-df_eol[,c(1:10,12)]
df_eol$R_number <- as.numeric(str_match(df_eol[,11], "R:\\s*(\\d+)")[,2])
head(df_eol)
  Pair         Player.Name Total Round Round.1 Round.2 Round.3 Round.4 Round.5
1    1            GARY HUA   6.0 W  39   W  21   W  18   W  14   W   7   D  12
2    2     DAKSHESH DARURI   6.0 W  63   W  58   L   4   W  17   W  16   W  20
3    3        ADITYA BAJAJ   6.0 L   8   W  61   W  25   W  21   W  11   W  13
4    4 PATRICK H SCHILLING   5.5 W  23   D  28   W   2   W  26   D   5   W  19
5    5          HANSHI ZUO   5.5 W  45   W  37   D  12   D  13   D   4   W  14
6    6         HANSEN SONG   5.0 W  34   D  29   L  11   W  35   D  10   W  27
  Round.6   USCF.ID...Rtg..Pre..Post. R_number
1   D   4 15445895 / R: 1794   ->1817     1794
2   W   7 14598900 / R: 1553   ->1663     1553
3   W  12 14959604 / R: 1384   ->1640     1384
4   D   1 12616049 / R: 1716   ->1744     1716
5   W  17 14601533 / R: 1655   ->1690     1655
6   W  21 15055204 / R: 1686   ->1687     1686
library(stringr)
df_eol$expected_point  <- NA

for (row_num in 1:nrow(df_eol)){
expected_point=0
k_play_num = df_eol[row_num,"R_number"]
#print(k_play_num)

for (col in 4:10){
  
  # from col 4 to col 10 , remove letter "W" "L" or others strings.
  pair_num=str_match(df_eol[,col], " \\s*(\\d+)")[,2]
  #print(df[pair_num,"R_number"][row_num])
  # add row_num to only targeting single number and is.na function to work.
  if(is.na(df_eol[pair_num,"R_number"][row_num])) {  
     #na_num<-na_num+1
    first_chr<-substring(df_eol[,col][row_num],1,1)
    #print(first_chr)
    BHU_point = case_when(
      first_chr == "X" ~ 1,
      first_chr == "B" ~ 1,
      first_chr == "H" ~ 0.5,
      first_chr == "U" ~ 0
    )
    #print(BHU_point)
    expected_point <- expected_point+BHU_point
   } else {
    k_pair_num=df_eol[pair_num,"R_number"][row_num]
    power_num=(k_pair_num - k_play_num)/400
    ep_score=1/(1+10^power_num)
    WDL_point =  case_when(
      ep_score > 0.5 ~ 1,
      ep_score == 0.5 ~ 0.5,
      ep_score < 0.5 ~ 0
    )
    #print(WDL_point)
    expected_point <- expected_point+WDL_point
    
    
    }
    #print(df[pair_num,"R_number"][row_num])
  
  
}
#print(pre_r_num)
#print(na_num)
#if (na_num ==0){ played_num = 7}else{played_num <- 7- na_num}
#avg_pre_r_num=round(pre_r_num/played_num,0)
df_eol[row_num,"expected_point"]<- expected_point
#print(expected_point)
}
df_eol$diff <- df_eol$Total-df_eol$expected_point

Top 5 Outperformers

df_eol[,c(1:3,12:14)] %>%
  arrange(desc(diff)) %>%
  slice_head(n = 5)
  Pair            Player.Name Total R_number expected_point diff
1    3           ADITYA BAJAJ   6.0     1384              1  5.0
2   15 ZACHARY JAMES HOUGHTON   4.5     1220              0  4.5
3    2        DAKSHESH DARURI   6.0     1553              2  4.0
4    9            STEFANO LEE   5.0     1411              1  4.0
5   10              ANVIT RAO   5.0     1365              1  4.0

Top 5 Underperformers

df_eol[,c(1:3,12:14)] %>%
  arrange(diff) %>%
  slice_head(n = 5)
  Pair        Player.Name Total R_number expected_point diff
1   25   LOREN SCHWIEBERT   3.5     1745              7 -3.5
2   30 GEORGE AVERY JONES   3.5     1522              7 -3.5
3   20        JASON ZHENG   4.0     1595              7 -3.0
4   42           JARED GE   3.0     1332              6 -3.0
5   45          DEREK YAN   3.0     1242              6 -3.0

Conclusion

Compared each player’s rating and applied the ELO expected score formula to calculate the differences. A positive number indicates the player outperformed expectations, while a negative number means the player underperformed relative to the expected score.

LLMS used:

• OpenAI. (2025). ChatGPT (Version 5.2) [Large language model]. https://chat.openai.com. Accessed Feb 28, 2026.