In this assignment, I will calculate each player’s expected score in the Project 1 chess tournament using the Elo rating formula, compare it to their actual score, and identify the five players who most overperformed and the five who most underperformed relative to expectations.
The key idea is that a player’s expected score in each game depends on the rating difference between the player and their opponent. By summing expected scores across all games, I can compute a total expected tournament score for each player and compare it to their actual total points.
Step 1: Use Structured Data from Project 1
I will reuse the cleaned dataset from Project 1, which includes:
Player pair number
Player pre-tournament rating
Opponent pair numbers for each round
Because I already reshaped the data into a long format (one row per player per opponent), this structure is ideal for Elo calculations. Each row already represents a single game matchup, which simplifies computing expected scores game-by-game.
Step 2: Choose and Cite the Elo Formula
I will use the standard Elo expected score formula:
E = 1/ 1+10^((Ropp - Rplayer)/400)
Where:
(E) = expected score for the player
(R_{player}) = player’s pre-rating
(R_{opp}) = opponent’s pre-rating
This formula is widely documented in chess rating literature and on Wikipedia under “Elo rating system.”
Source:
Elo Rating System – Wikipedia (based on Arpad Elo’s original formulation)
This formula gives a value between 0 and 1 representing the probability of winning (with draws effectively contributing 0.5 in actual scoring).
Step 3: Compute Expected Score Per Game
For each player-opponent pair:
Join opponent pre-ratings to the long matchup table.
Compute rating difference.
Apply the Elo formula to calculate expected score per game.
This produces one expected value per game.
Step 4: Compute Total Expected Tournament Score
After computing expected score per game, I will:
Group by player
Sum expected scores across all games
This produces each player’s total expected score (for example, 4.3).
Step 5: Compare to Actual Score
Each player’s actual score is already available from Project 1 (total points column).
I will compute:
Performance Difference = Actual Score - Expected Score
If this value is:
Positive → player overperformed
Negative → player underperformed
Step 6: Identify Top Overperformers and Underperformers
To complete the assignment:
Sort players by performance difference descending → top 5 overperformers
Sort ascending → top 5 underperformers
This will directly answer the required output.
Anticipated Data Challenges
Handling Byes or Unplayed Rounds
Players who had byes or unplayed rounds should not have expected scores calculated for those rounds. Since those rows were already filtered out in Project 1, this should not affect calculations.
Ensuring Pre-Tournament Ratings Are Used
Elo expected score must use pre-tournament ratings only. I will verify that I am not accidentally using post-ratings.
Rounding Differences
Different Elo implementations sometimes vary slightly due to rounding or formula adjustments. I will use the standard 400-point divisor and cite the formula source clearly to ensure transparency.
Verification
I will manually compute at least one expected score using the formula to confirm that my implementation is correct.
Validation Strategy
Before finalizing results, I will:
Hand-calculate expected score for one player against one opponent to verify formula correctness.
Confirm that expected scores are between 0 and 1.
Check that stronger players have higher expected scores against lower-rated opponents.
Confirm that total expected scores are reasonable relative to rating differences.
Summary
This approach builds directly on the cleaned relational structure from Project 1. By calculating expected score per game using the standard Elo formula, aggregating to the tournament level, and comparing to actual results, I will identify which players most exceeded expectations and which fell short.
Codebase
#Load tidyverselibrary(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr 1.2.0 ✔ 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
#Bring in tournament inforaw_lines <-readLines("tournamentinfo.txt")
Warning in readLines("tournamentinfo.txt"): incomplete final line found on
'tournamentinfo.txt'
#Reuse Project 1 cleanup codedata_lines <- raw_lines %>%str_subset("\\|") %>%str_subset("----", negate =TRUE)#Remove first two lines:player_lines <- data_lines[-c(1, 2)]player_line1 <- player_lines[seq(1, length(player_lines), by =2)]player_line2 <- player_lines[seq(2, length(player_lines), by =2)]#Parse line 1 into columnsline1_parts <-str_split(player_line1, "\\|", simplify =TRUE)#Create the core player tableplayers_core <-tibble(pair_num =as.integer(str_trim(line1_parts[, 1])),name =str_trim(line1_parts[, 2]),total_pts =as.numeric(str_trim(line1_parts[, 3])))#Parse Line 2 into columnsline2_parts <-str_split(player_line2, "\\|", simplify =TRUE)player_details <-tibble(state =str_trim(line2_parts[, 1]),pre_rating =as.integer(str_extract(player_line2, "(?<=R:\\s)\\d+")))players <- players_core %>%bind_cols(player_details)
Now we will be adding on to the work from Project 1 by adding a games table.
#Extract the 7 round cells from line 1round_cols <- line1_parts[, 4:10]#Put those rounds into a dataframe and attach pair_numrounds_tbl <-as_tibble(round_cols) %>%mutate(pair_num = players$pair_num) %>%relocate(pair_num)
Warning: The `x` argument of `as_tibble.matrix()` must have unique column names if
`.name_repair` is omitted as of tibble 2.0.0.
ℹ Using compatibility `.name_repair`.
#Pivot to long formmat (one row per game)rounds_long <- rounds_tbl %>%pivot_longer(cols =-pair_num,names_to ="round",values_to ="result" )#Extract opponent pair numberrounds_long <- rounds_long %>%mutate(result =str_trim(result),opp_pair_num =as.integer(str_extract(result, "\\d+")) ) %>%filter(!is.na(opp_pair_num))
Now that our data is tidy. It is time to calculate ELO scoring.
#We need to convert W/L/D to actual pointsgames <- rounds_long %>%mutate(outcome =str_extract(result, "[WLD]"),actual =case_when( outcome =="W"~1, outcome =="D"~0.5, outcome =="L"~0 ) )#Let's add player rating and name to each game rowgames <- games %>%left_join( players %>%select(pair_num, name, pre_rating),by ="pair_num" ) #Add opponent ratinggames <- games %>%left_join( players %>%select(pair_num, pre_rating),by =c("opp_pair_num"="pair_num") ) %>%rename(opp_pre_rating = pre_rating.y,pre_rating = pre_rating.x)
Compute expected score per game (ELO Formula ) Arpad Elo (1978), The Rating of Chess Players, Past and Present
games <- games %>%mutate(expected =1/ (1+10^((opp_pre_rating - pre_rating) /400)) )
Then, let’s use summary statistics for expected and actual totals per player.