In this assignment, I will use the chess tournament data from Project 1 to calculate the expected score for each player using the ELO rating system. I will compare each player’s expected score with their actual score to identify the five players who performed better than expected and the five players who performed worse than expected.
I will use R to analyze the chess tournament data from Project 1. First, I will identify each player’s pre-tournament rating and the ratings of their opponents. Then, I will use the ELO formula to calculate the expected score for each game and add these values to find each player’s total expected score. Next, I will calculate the difference between the actual and expected scores. Finally, I will sort the results to identify the five players who overperformed the most and the five players who underperformed the most.
One challenge may be matching each player with the correct opponents and their ratings. Some players may also have rounds without an opponent, such as a bye. I will need to handle these rounds carefully because they do not have an opponent rating. Another challenge will be making sure the expected scores and actual scores are compared correctly.
The tournament data are stored in a text file available through GitHub. The file is read directly from the GitHub raw URL so that the data remain accessible when the code is run on another computer.
data_url <- "https://raw.githubusercontent.com/OzgeG01/DATA607-Assignment/refs/heads/main/tournamentinfo.txt"
tournament_data <- readLines(data_url)
head(tournament_data, 10)
## [1] "-----------------------------------------------------------------------------------------"
## [2] " Pair | Player Name |Total|Round|Round|Round|Round|Round|Round|Round| "
## [3] " Num | USCF ID / Rtg (Pre->Post) | Pts | 1 | 2 | 3 | 4 | 5 | 6 | 7 | "
## [4] "-----------------------------------------------------------------------------------------"
## [5] " 1 | GARY HUA |6.0 |W 39|W 21|W 18|W 14|W 7|D 12|D 4|"
## [6] " ON | 15445895 / R: 1794 ->1817 |N:2 |W |B |W |B |W |B |W |"
## [7] "-----------------------------------------------------------------------------------------"
## [8] " 2 | DAKSHESH DARURI |6.0 |W 63|W 58|L 4|W 17|W 16|W 20|W 7|"
## [9] " MI | 14598900 / R: 1553 ->1663 |N:2 |B |W |B |W |B |W |B |"
## [10] "-----------------------------------------------------------------------------------------"
Each player’s main record begins with a player number followed by a vertical bar. These rows contain the player’s name, total points, and round-by-round opponent information.
player_rows <- grep("^\\s*\\d+\\s*\\|", tournament_data, value = TRUE)
head(player_rows)
## [1] " 1 | GARY HUA |6.0 |W 39|W 21|W 18|W 14|W 7|D 12|D 4|"
## [2] " 2 | DAKSHESH DARURI |6.0 |W 63|W 58|L 4|W 17|W 16|W 20|W 7|"
## [3] " 3 | ADITYA BAJAJ |6.0 |L 8|W 61|W 25|W 21|W 11|W 13|W 12|"
## [4] " 4 | PATRICK H SCHILLING |5.5 |W 23|D 28|W 2|W 26|D 5|W 19|D 1|"
## [5] " 5 | HANSHI ZUO |5.5 |W 45|W 37|D 12|D 13|D 4|W 14|W 17|"
## [6] " 6 | HANSEN SONG |5.0 |W 34|D 29|L 11|W 35|D 10|W 27|W 21|"
The player number is extracted from the first field of each main player row. These numbers will be used to match each opponent with the corresponding player’s pre-tournament rating.
player_numbers <- as.numeric(
trimws(sapply(strsplit(player_rows, "\\|"), `[`, 1))
)
head(player_numbers)
## [1] 1 2 3 4 5 6
The player name is extracted from each main player row by splitting the row at the vertical bar. The second field contains the player’s name, and trimws() is used to remove any extra spaces.
player_names <- trimws(
sapply(strsplit(player_rows, "\\|"), `[`, 2)
)
head(player_names)
## [1] "GARY HUA" "DAKSHESH DARURI" "ADITYA BAJAJ"
## [4] "PATRICK H SCHILLING" "HANSHI ZUO" "HANSEN SONG"
The total number of points for each player is stored in the third field of the main player row. The rows are split using the vertical bar as the delimiter, and the extracted values are converted to numeric format.
total_points <- as.numeric(
trimws(sapply(strsplit(player_rows, "\\|"), `[`, 3))
)
head(total_points)
## [1] 6.0 6.0 6.0 5.5 5.5 5.0
The second line of each player record contains the player’s state and rating information. These rows begin with a two-letter state abbreviation followed by a vertical bar.
detail_rows <- grep("^\\s*[A-Z]{2}\\s*\\|", tournament_data, value = TRUE)
head(detail_rows)
## [1] " ON | 15445895 / R: 1794 ->1817 |N:2 |W |B |W |B |W |B |W |"
## [2] " MI | 14598900 / R: 1553 ->1663 |N:2 |B |W |B |W |B |W |B |"
## [3] " MI | 14959604 / R: 1384 ->1640 |N:2 |W |B |W |B |W |B |W |"
## [4] " MI | 12616049 / R: 1716 ->1744 |N:2 |W |B |W |B |W |B |B |"
## [5] " MI | 14601533 / R: 1655 ->1690 |N:2 |B |W |B |W |B |W |B |"
## [6] " OH | 15055204 / R: 1686 ->1687 |N:3 |W |B |W |B |B |W |B |"
The player’s state is stored in the first field of the detail row. The detail rows are split using the vertical bar as the delimiter, and extra whitespace is removed from the extracted state values.
player_states <- trimws(
sapply(strsplit(detail_rows, "\\|"), `[`, 1)
)
head(player_states)
## [1] "ON" "MI" "MI" "MI" "MI" "OH"
The player’s pre-tournament rating appears in the detail row immediately after R:. A regular expression is used to extract this value, and the result is converted to numeric format.
pre_ratings <- as.numeric(
sub(".*R:\\s*(\\d+).*", "\\1", detail_rows)
)
head(pre_ratings)
## [1] 1794 1553 1384 1716 1655 1686
The round information begins with the fourth field of each main player row. First, the round fields are separated from the rest of the player information. Only games recorded as wins (W), losses (L), or draws (D) are included. Byes and other unplayed rounds are excluded.
round_fields <- lapply(
strsplit(player_rows, "\\|"),
function(x) trimws(x[4:length(x)])
)
opponent_numbers <- lapply(round_fields, function(rounds) {
played_games <- rounds[grepl("^[WLD]\\s+\\d+$", rounds)]
as.numeric(sub("^[WLD]\\s+", "", played_games))
})
head(opponent_numbers)
## [[1]]
## [1] 39 21 18 14 7 12 4
##
## [[2]]
## [1] 63 58 4 17 16 20 7
##
## [[3]]
## [1] 8 61 25 21 11 13 12
##
## [[4]]
## [1] 23 28 2 26 5 19 1
##
## [[5]]
## [1] 45 37 12 13 4 14 17
##
## [[6]]
## [1] 34 29 11 35 10 27 21
I will use the ELO formula to calculate each player’s expected score against their opponents. I will calculate the expected score for each game based on the difference between the two players’ pre-tournament ratings. Then, I will add these values to find each player’s total expected score.
expected_scores <- sapply(seq_along(player_numbers), function(i) {
opponent_ratings <- pre_ratings[
match(opponent_numbers[[i]], player_numbers)
]
expected <- 1 / (1 + 10^((opponent_ratings - pre_ratings[i]) / 400))
sum(expected)
})
head(expected_scores)
## [1] 5.161574 3.778825 1.945088 4.741764 4.382484 4.944596
I will calculate the actual score from the games each player played. I will assign 1 point for a win, 0.5 points for a draw, and 0 points for a loss. I will exclude byes and unplayed rounds to make a fair comparison with the expected scores.
actual_scores <- sapply(round_fields, function(rounds) {
played_games <- rounds[grepl("^[WLD]\\s+\\d+$", rounds)]
results <- substr(played_games, 1, 1)
sum(results == "W") + 0.5 * sum(results == "D")
})
head(actual_scores)
## [1] 6.0 6.0 6.0 5.5 5.5 5.0
I will subtract each player’s expected score from their actual score to calculate the performance difference. A positive difference means the player performed better than expected, while a negative difference means the player performed worse than expected.
performance_difference <- actual_scores - expected_scores
head(performance_difference)
## [1] 0.83842636 2.22117517 4.05491209 0.75823568 1.11751602 0.05540355
I will create a data frame that includes each player’s name, actual score, expected score, and performance difference. I will round the expected scores and differences to two decimal places to make the results easier to read.
elo_results <- data.frame(
PlayerName = player_names,
ActualScore = actual_scores,
ExpectedScore = round(expected_scores, 2),
Difference = round(performance_difference, 2)
)
head(elo_results)
## PlayerName ActualScore ExpectedScore Difference
## 1 GARY HUA 6.0 5.16 0.84
## 2 DAKSHESH DARURI 6.0 3.78 2.22
## 3 ADITYA BAJAJ 6.0 1.95 4.05
## 4 PATRICK H SCHILLING 5.5 4.74 0.76
## 5 HANSHI ZUO 5.5 4.38 1.12
## 6 HANSEN SONG 5.0 4.94 0.06
I will sort the players by their performance differences from highest to lowest. Then, I will select the five players with the largest positive differences to identify who performed the most above their expected scores.
top_overperformers <- head(
elo_results[order(-elo_results$Difference), ],
5
)
top_overperformers
## PlayerName ActualScore ExpectedScore Difference
## 3 ADITYA BAJAJ 6.0 1.95 4.05
## 15 ZACHARY JAMES HOUGHTON 4.5 1.37 3.13
## 10 ANVIT RAO 5.0 1.94 3.06
## 46 JACOB ALEXANDER LAVALLEY 3.0 0.04 2.96
## 9 STEFANO LEE 5.0 2.29 2.71
Aditya Bajaj had the largest positive difference, scoring 4.05 points above his expected score. Zachary James Houghton followed with 3.13 points, and Anvit Rao with 3.06 points. All five players performed better than expected based on their ELO ratings.
I will sort the players by their performance differences from lowest to highest. Then, I will select the five players with the largest negative differences to identify who performed the most below their expected scores.
top_underperformers <- head(
elo_results[order(elo_results$Difference), ],
5
)
top_underperformers
## PlayerName ActualScore ExpectedScore Difference
## 25 LOREN SCHWIEBERT 3.5 6.28 -2.78
## 30 GEORGE AVERY JONES 3.5 6.02 -2.52
## 54 LARRY HODGE 1.0 3.40 -2.40
## 42 JARED GE 3.0 5.01 -2.01
## 31 RISHI SHETTY 3.5 5.09 -1.59
Loren Schwiebert had the largest negative difference, scoring 2.78 points below his expected score. George Avery Jones followed with 2.52 points below expectations, and Larry Hodge with 2.40 points below expectations. All five players performed worse than expected based on their ELO ratings.
In this assignment, I used the ELO rating system to calculate the expected scores of 64 chess players and compared them with their actual scores.
The results showed that Aditya Bajaj had the largest positive difference, scoring 6.0 points compared to an expected score of 1.95. This means he performed 4.05 points better than expected. On the other hand, Loren Schwiebert had the largest negative difference, scoring 3.5 points compared to an expected score of 6.28.
Overall, this analysis shows how ELO ratings can be used to compare expected and actual performance in a chess tournament. It also shows that players can perform much better or worse than their ratings suggest.
I used ChatGPT (GPT-5.6 Luna), developed by OpenAI, to help me understand some of the functions I used in this assignment and to check my work.
OpenAI. (2026). ChatGPT (GPT-5.6 Luna) [Large language model]. [https://chatgpt.com/ Accessed October 11, 2026].
Link: