url="https://raw.githubusercontent.com/dyc-sps/SPS_Data607_Week5B/refs/heads/main/tournamentinfo.csv"SPS_Data607_Week5B
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
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_pointTop 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.