Assignment 5B Codebase

Author

Theresa Benny

Approach

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:

  1. Join opponent pre-ratings to the long matchup table.

  2. Compute rating difference.

  3. 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

  1. 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.

  2. 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.

  3. 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.

  4. 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 tidyverse
library(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 info

raw_lines <- readLines("tournamentinfo.txt")
Warning in readLines("tournamentinfo.txt"): incomplete final line found on
'tournamentinfo.txt'
#Reuse Project 1 cleanup code

data_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 columns
line1_parts <- str_split(player_line1, "\\|", simplify = TRUE)

#Create the core player table

players_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 columns
line2_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 1
round_cols <- line1_parts[, 4:10]

#Put those rounds into a dataframe and attach pair_num

rounds_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 number
rounds_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 points
games <- 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 row

games <- games %>%
  left_join(
    players %>% select(pair_num, name, pre_rating),
    by = "pair_num"
  ) 

#Add opponent rating

games <- 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.

results <- games %>%
  group_by(name) %>%
  summarise(
    actual_total = sum(actual, na.rm = TRUE),
    expected_total = sum(expected, na.rm = TRUE),
    diff = actual_total - expected_total,
    .groups = "drop"
  )
results
# A tibble: 64 × 4
   name                     actual_total expected_total   diff
   <chr>                           <dbl>          <dbl>  <dbl>
 1 ADITYA BAJAJ                      6            1.02   4.98 
 2 ALAN BUI                          4            1.12   2.88 
 3 ALEX KONG                         1            1.44  -0.440
 4 AMIYATOSH PWNANANDAM              2            0      2    
 5 ANVIT RAO                         5            1.94   3.06 
 6 ASHWIN BALAJI                     1            0.879  0.121
 7 BEN LI                            1            1.29  -0.285
 8 BRADLEY SHAW                      4.5          4.18   0.317
 9 BRIAN LIU                         2.5          2.13   0.370
10 CAMERON WILLIAM MC LEMAN          4.5          5.34  -0.839
# ℹ 54 more rows

Finally, let’s see the top 5 underperformers and overperformers.

top_over <- results %>%
  arrange(desc(diff)) %>%
  slice(1:5)

top_under <- results %>%
  arrange(diff) %>%
  slice(1:5)

# I want the results in a nice table so I will be using GT to display

library(gt)
results_clean <- results %>%
  mutate(
    actual_total = round(actual_total, 2),
    expected_total = round(expected_total, 2),
    diff = round(diff, 2)
  )

top_over %>%
  mutate(
    actual_total = round(actual_total, 2),
    expected_total = round(expected_total, 2),
    diff = round(diff, 2)
  ) %>%
  gt() %>%
  tab_header(
    title = "Top 5 Overperformers",
    subtitle = "Actual Score vs. Elo Expected Score"
  ) %>%
  cols_label(
    name = "Player",
    actual_total = "Actual Score",
    expected_total = "Expected Score",
    diff = "Performance Difference"
  )
Top 5 Overperformers
Actual Score vs. Elo Expected Score
Player Actual Score Expected Score Performance Difference
ADITYA BAJAJ 6.0 1.02 4.98
STEFANO LEE 5.0 1.33 3.67
DAKSHESH DARURI 6.0 2.80 3.20
ZACHARY JAMES HOUGHTON 4.5 1.37 3.13
ANVIT RAO 5.0 1.94 3.06
top_under %>%
  mutate(
    actual_total = round(actual_total, 2),
    expected_total = round(expected_total, 2),
    diff = round(diff, 2)
  ) %>%
  gt() %>%
  tab_header(
    title = "Top 5 Underperformers",
    subtitle = "Actual Score vs. Elo Expected Score"
  ) %>%
  cols_label(
    name = "Player",
    actual_total = "Actual Score",
    expected_total = "Expected Score",
    diff = "Performance Difference"
  )
Top 5 Underperformers
Actual Score vs. Elo Expected Score
Player Actual Score Expected Score Performance Difference
LOREN SCHWIEBERT 3.5 6.28 -2.78
JASON ZHENG 4.0 5.13 -1.13
THOMAS JOSEPH HOSMER 0.5 1.43 -0.93
CAMERON WILLIAM MC LEMAN 4.5 5.34 -0.84
JOSE C YBARRA 1.0 1.72 -0.72