Loading libraries:

library(tidyverse)
library(readr)
library(data.table)
library(kableExtra)

This code cell loading tournamentinfo.txt from GitHub URL and stores it into data

URL <- "https://raw.githubusercontent.com/farhodibr/CUNY-SPS-MSDS/main/DATA607/LAB5/tournamentinfo.txt"
data <- readLines(URL)
## Warning in readLines(URL): incomplete final line found on
## 'https://raw.githubusercontent.com/farhodibr/CUNY-SPS-MSDS/main/DATA607/LAB5/tournamentinfo.txt'
head(data)
## [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    |"

This code cell creates a messy table from data and stores it into elo_table by doing:

  1. merging_linesfunction iterates through each line and merges two lines into one. Function grabs first line and appends next line to it, then goes to next line and does same merging as in previous step. The reason i use this because each player’s obseravtion is in two lines all over data set.
  2. replacing all "|" with ","
data[length(data)] <- paste0(data[length(data)], "\n")
data <- data[!grepl("^[-]+$", data)]
head(data)
## [1] " Pair | Player Name                     |Total|Round|Round|Round|Round|Round|Round|Round| "
## [2] " Num  | USCF ID / Rtg (Pre->Post)       | Pts |  1  |  2  |  3  |  4  |  5  |  6  |  7  | "
## [3] "    1 | GARY HUA                        |6.0  |W  39|W  21|W  18|W  14|W   7|D  12|D   4|" 
## [4] "   ON | 15445895 / R: 1794   ->1817     |N:2  |W    |B    |W    |B    |W    |B    |W    |" 
## [5] "    2 | DAKSHESH DARURI                 |6.0  |W  63|W  58|L   4|W  17|W  16|W  20|W   7|" 
## [6] "   MI | 14598900 / R: 1553   ->1663     |N:2  |B    |W    |B    |W    |B    |W    |B    |"
merging_lines <- function(data) {
  merged_data <- character(0)
    for (i in seq(1, length(data) - 1, by = 2)) {
        merged_line <- paste0(data[i], "|", data[i + 1])
        merged_data <- c(merged_data, merged_line)
  }
  return(merged_data)
}
merged_data <- merging_lines(data)
merged_data <- gsub("|", ",", merged_data, fixed = TRUE) 
head(merged_data)
## [1] " Pair , Player Name                     ,Total,Round,Round,Round,Round,Round,Round,Round, , Num  , USCF ID / Rtg (Pre->Post)       , Pts ,  1  ,  2  ,  3  ,  4  ,  5  ,  6  ,  7  , "
## [2] "    1 , GARY HUA                        ,6.0  ,W  39,W  21,W  18,W  14,W   7,D  12,D   4,,   ON , 15445895 / R: 1794   ->1817     ,N:2  ,W    ,B    ,W    ,B    ,W    ,B    ,W    ,"  
## [3] "    2 , DAKSHESH DARURI                 ,6.0  ,W  63,W  58,L   4,W  17,W  16,W  20,W   7,,   MI , 14598900 / R: 1553   ->1663     ,N:2  ,B    ,W    ,B    ,W    ,B    ,W    ,B    ,"  
## [4] "    3 , ADITYA BAJAJ                    ,6.0  ,L   8,W  61,W  25,W  21,W  11,W  13,W  12,,   MI , 14959604 / R: 1384   ->1640     ,N:2  ,W    ,B    ,W    ,B    ,W    ,B    ,W    ,"  
## [5] "    4 , PATRICK H SCHILLING             ,5.5  ,W  23,D  28,W   2,W  26,D   5,W  19,D   1,,   MI , 12616049 / R: 1716   ->1744     ,N:2  ,W    ,B    ,W    ,B    ,W    ,B    ,B    ,"  
## [6] "    5 , HANSHI ZUO                      ,5.5  ,W  45,W  37,D  12,D  13,D   4,W  14,W  17,,   MI , 14601533 / R: 1655   ->1690     ,N:2  ,B    ,W    ,B    ,W    ,B    ,W    ,B    ,"

This cell does cleaning and formatting for column names. It creates elo_table data frame from merged_data.

column_names <- strsplit(merged_data[1], split = ",", fixed = TRUE)
column_names <- trimws(column_names)
column_names <- gsub("\\s+", " ", column_names)


print(column_names, quote = FALSE)
## [1] c(" Pair ", " Player Name ", "Total", "Round", "Round", "Round", "Round", "Round", "Round", "Round", " ", " Num ", " USCF ID / Rtg (Pre->Post) ", " Pts ", " 1 ", " 2 ", " 3 ", " 4 ", " 5 ", " 6 ", " 7 ", " ")
print(typeof(column_names))
## [1] "character"
#print(length(column_names))
merged_data <- merged_data[-1]


column_names <- c(" Pair ", " Player Name ", "Total", "Round 1", "Round 2", "Round 3", "Round 4", "Round 5", "Round 6", "Round 7", " ", " State ", " USCFID \ Rtg \ Pre \ Post ", " Pts ", " 1 ", " 2 ", " 3 ", " 4 ", " 5 ", " 6 ", " 7 ", " __ ")
print(length(column_names))
## [1] 22
#merged_data <- merged_data[-1]
elo_table <- data.frame(merged_data)

elo_table <- elo_table |>
  separate(col = 1,
           into = column_names,
           sep = ",",
           extra = "merge")


head(elo_table)
##    Pair                       Player Name  Total Round 1 Round 2 Round 3
## 1     1   GARY HUA                         6.0     W  39   W  21   W  18
## 2     2   DAKSHESH DARURI                  6.0     W  63   W  58   L   4
## 3     3   ADITYA BAJAJ                     6.0     L   8   W  61   W  25
## 4     4   PATRICK H SCHILLING              5.5     W  23   D  28   W   2
## 5     5   HANSHI ZUO                       5.5     W  45   W  37   D  12
## 6     6   HANSEN SONG                      5.0     W  34   D  29   L  11
##   Round 4 Round 5 Round 6 Round 7    State            USCFID  Rtg  Pre  Post 
## 1   W  14   W   7   D  12   D   4       ON   15445895 / R: 1794   ->1817     
## 2   W  17   W  16   W  20   W   7       MI   14598900 / R: 1553   ->1663     
## 3   W  21   W  11   W  13   W  12       MI   14959604 / R: 1384   ->1640     
## 4   W  26   D   5   W  19   D   1       MI   12616049 / R: 1716   ->1744     
## 5   D  13   D   4   W  14   W  17       MI   14601533 / R: 1655   ->1690     
## 6   W  35   D  10   W  27   W  21       OH   15055204 / R: 1686   ->1687     
##    Pts     1     2     3     4     5     6     7   __ 
## 1 N:2   W     B     W     B     W     B     W         
## 2 N:2   B     W     B     W     B     W     B         
## 3 N:2   W     B     W     B     W     B     W         
## 4 N:2   W     B     W     B     W     B     B         
## 5 N:2   B     W     B     W     B     W     B         
## 6 N:3   W     B     W     B     B     W     B
#elo_table$pair <- as.numeric(unlist(elo_table$pair))
elo_table <- Filter(function(x)!(all(x == "")), elo_table)
elo_table <- elo_table |>
  mutate(across(everything(), ~trimws(.x))) |>
  mutate(across(everything(), ~gsub("\\s+", " ", .x))) |>
  mutate(across(everything(), ~gsub(" +", " ", .x))) |>
  mutate(across(everything(), ~gsub(" +$", "", .x))) |>
  mutate(across(everything(), ~gsub("^ +", "", .x)))

glimpse(elo_table)
## Rows: 64
## Columns: 20
## $ ` Pair `                   <chr> "1", "2", "3", "4", "5", "6", "7", "8", "9"…
## $ ` Player Name `            <chr> "GARY HUA", "DAKSHESH DARURI", "ADITYA BAJA…
## $ Total                      <chr> "6.0", "6.0", "6.0", "5.5", "5.5", "5.0", "…
## $ `Round 1`                  <chr> "W 39", "W 63", "L 8", "W 23", "W 45", "W 3…
## $ `Round 2`                  <chr> "W 21", "W 58", "W 61", "D 28", "W 37", "D …
## $ `Round 3`                  <chr> "W 18", "L 4", "W 25", "W 2", "D 12", "L 11…
## $ `Round 4`                  <chr> "W 14", "W 17", "W 21", "W 26", "D 13", "W …
## $ `Round 5`                  <chr> "W 7", "W 16", "W 11", "D 5", "D 4", "D 10"…
## $ `Round 6`                  <chr> "D 12", "W 20", "W 13", "W 19", "W 14", "W …
## $ `Round 7`                  <chr> "D 4", "W 7", "W 12", "D 1", "W 17", "W 21"…
## $ ` State `                  <chr> "ON", "MI", "MI", "MI", "MI", "OH", "MI", "…
## $ ` USCFID  Rtg  Pre  Post ` <chr> "15445895 / R: 1794 ->1817", "14598900 / R:…
## $ ` Pts `                    <chr> "N:2", "N:2", "N:2", "N:2", "N:2", "N:3", "…
## $ ` 1 `                      <chr> "W", "B", "W", "W", "B", "W", "W", "B", "W"…
## $ ` 2 `                      <chr> "B", "W", "B", "B", "W", "B", "B", "W", "B"…
## $ ` 3 `                      <chr> "W", "B", "W", "W", "B", "W", "W", "B", "W"…
## $ ` 4 `                      <chr> "B", "W", "B", "B", "W", "B", "B", "W", "B"…
## $ ` 5 `                      <chr> "W", "B", "W", "W", "B", "B", "B", "B", "W"…
## $ ` 6 `                      <chr> "B", "W", "B", "B", "W", "W", "W", "W", "B"…
## $ ` 7 `                      <chr> "W", "B", "W", "B", "B", "B", "W", "W", "B"…
head(elo_table)
##    Pair         Player Name  Total 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
## 2      2     DAKSHESH DARURI   6.0    W 63    W 58     L 4    W 17    W 16
## 3      3        ADITYA BAJAJ   6.0     L 8    W 61    W 25    W 21    W 11
## 4      4 PATRICK H SCHILLING   5.5    W 23    D 28     W 2    W 26     D 5
## 5      5          HANSHI ZUO   5.5    W 45    W 37    D 12    D 13     D 4
## 6      6         HANSEN SONG   5.0    W 34    D 29    L 11    W 35    D 10
##   Round 6 Round 7  State    USCFID  Rtg  Pre  Post   Pts   1   2   3   4   5 
## 1    D 12     D 4      ON 15445895 / R: 1794 ->1817   N:2   W   B   W   B   W
## 2    W 20     W 7      MI 14598900 / R: 1553 ->1663   N:2   B   W   B   W   B
## 3    W 13    W 12      MI 14959604 / R: 1384 ->1640   N:2   W   B   W   B   W
## 4    W 19     D 1      MI 12616049 / R: 1716 ->1744   N:2   W   B   W   B   W
## 5    W 14    W 17      MI 14601533 / R: 1655 ->1690   N:2   B   W   B   W   B
## 6    W 27    W 21      OH 15055204 / R: 1686 ->1687   N:3   W   B   W   B   B
##    6   7 
## 1   B   W
## 2   W   B
## 3   B   W
## 4   B   B
## 5   W   B
## 6   W   B

This cell cleans and format elo_table variable names, splits uscfid__rtg__pre__post into separate variables.

colnames(elo_table) <- tolower(colnames(elo_table))
colnames(elo_table) <- trimws(colnames(elo_table))
colnames(elo_table) <- str_replace_all(colnames(elo_table), " ", "_")
print(colnames(elo_table))
##  [1] "pair"                   "player_name"            "total"                 
##  [4] "round_1"                "round_2"                "round_3"               
##  [7] "round_4"                "round_5"                "round_6"               
## [10] "round_7"                "state"                  "uscfid__rtg__pre__post"
## [13] "pts"                    "1"                      "2"                     
## [16] "3"                      "4"                      "5"                     
## [19] "6"                      "7"
elo_table$pair <- elo_table$pair |>
  as.numeric(unlist(elo_table$pair)) 
  

head(elo_table)
##   pair         player_name total 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
## 2    2     DAKSHESH DARURI   6.0    W 63    W 58     L 4    W 17    W 16
## 3    3        ADITYA BAJAJ   6.0     L 8    W 61    W 25    W 21    W 11
## 4    4 PATRICK H SCHILLING   5.5    W 23    D 28     W 2    W 26     D 5
## 5    5          HANSHI ZUO   5.5    W 45    W 37    D 12    D 13     D 4
## 6    6         HANSEN SONG   5.0    W 34    D 29    L 11    W 35    D 10
##   round_6 round_7 state    uscfid__rtg__pre__post pts 1 2 3 4 5 6 7
## 1    D 12     D 4    ON 15445895 / R: 1794 ->1817 N:2 W B W B W B W
## 2    W 20     W 7    MI 14598900 / R: 1553 ->1663 N:2 B W B W B W B
## 3    W 13    W 12    MI 14959604 / R: 1384 ->1640 N:2 W B W B W B W
## 4    W 19     D 1    MI 12616049 / R: 1716 ->1744 N:2 W B W B W B B
## 5    W 14    W 17    MI 14601533 / R: 1655 ->1690 N:2 B W B W B W B
## 6    W 27    W 21    OH 15055204 / R: 1686 ->1687 N:3 W B W B B W B

In this code cell I’m creating tables for players info an players ratings. Players rating table created by splitting uscfid__rtg__pre__post column and its values into separate variables.

players_info_table <- elo_table |>
  select(pair, player_name, state, total)



players_rating_table <- elo_table |>
  select(pair, uscfid__rtg__pre__post) |>
  extract(
    col = uscfid__rtg__pre__post,
    into = c("uscf_id", "rating", "new_rating"),
    regex = "(\\d*)\\s*/\\s*R:\\s*(\\d*P?\\d*)\\s*->\\s*(\\d*P?\\d*)"
  )|>
  mutate(
    rating = gsub("P.*", "", rating),            
    new_rating = gsub("P.*", "", new_rating),  
    across(c("rating", "new_rating"), as.numeric)  
  )
#|>
 # mutate(across(c("uscf_id", "rating", "new_rating"), as.numeric))

Here I’m creating new rounds and rounds results tables. Rounds results table extracts only characters from rounds table values, such as “W” -won the game, “L” - lost, “D” - draw, and with the rest of characters for not played games.

players_rating_table$pair <- elo_table$pair
glimpse(players_rating_table)
## Rows: 64
## Columns: 4
## $ pair       <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, …
## $ uscf_id    <chr> "15445895", "14598900", "14959604", "12616049", "14601533",…
## $ rating     <dbl> 1794, 1553, 1384, 1716, 1655, 1686, 1649, 1641, 1411, 1365,…
## $ new_rating <dbl> 1817, 1663, 1640, 1744, 1690, 1687, 1673, 1657, 1564, 1544,…
head(elo_table$player_name[5])
## [1] "HANSHI ZUO"
new_row <- data.frame(pair = 0, uscf_id = "0", rating = 0, new_rating = 0, round_1 = 1,
                      round_2 = 1, round_3 = 1,
                      round_4 = 1, round_5 = 1,
                      round_6 = 1, round_7 = 1)


rounds_table <- elo_table |>
  select(pair, starts_with("round")) |>
  mutate(across(starts_with("round"), ~as.numeric(gsub("[^0-9]", "",.))))
  
#print(rounds_table)
#print(elo_table)

round_results_table <- elo_table |>
  select(pair, starts_with("round")) |>
  mutate(across(starts_with("round"), ~gsub("[^A-Z]", "",.x)))
  



rounds_table <- bind_rows(rounds_table, new_row[1])

This cell creates opponents_average_rating_table:

  1. joining with players_rating_table
  2. creates average variable for average ratings of played games opponent players.(this looks little bit complicated. I’m learning to get same results in less amount of code in future)
  3. creates final_ratings_table with pair#, players name, player’s state, total game points, player’s rating, average rating of played opponents.
opponents_average_rating_table <- left_join(players_rating_table, 
                                            rounds_table,
                                            by = "pair")
opponents_average_rating_table <- rbind(opponents_average_rating_table, new_row)



opponents_average_rating_table <- opponents_average_rating_table |>
  rowwise() |>
  mutate(average = round(mean( 
           c(
           opponents_average_rating_table$rating[
             opponents_average_rating_table$round_1[pair]
             ],
           opponents_average_rating_table$rating[
             opponents_average_rating_table$round_2[pair]
             ],
           opponents_average_rating_table$rating[
             opponents_average_rating_table$round_3[pair]
             ],
           opponents_average_rating_table$rating[
             opponents_average_rating_table$round_4[pair]
             ],
           opponents_average_rating_table$rating[
             opponents_average_rating_table$round_5[pair]
             ],
           opponents_average_rating_table$rating[
             opponents_average_rating_table$round_6[pair]
             ],
           opponents_average_rating_table$rating[
             opponents_average_rating_table$round_7[pair]
             ]
           
                     ),
           na.rm = TRUE))) |>
  ungroup()

glimpse(opponents_average_rating_table)
## Rows: 65
## Columns: 12
## $ pair       <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, …
## $ uscf_id    <chr> "15445895", "14598900", "14959604", "12616049", "14601533",…
## $ rating     <dbl> 1794, 1553, 1384, 1716, 1655, 1686, 1649, 1641, 1411, 1365,…
## $ new_rating <dbl> 1817, 1663, 1640, 1744, 1690, 1687, 1673, 1657, 1564, 1544,…
## $ round_1    <dbl> 39, 63, 8, 23, 45, 34, 57, 3, 25, 16, 38, 42, 36, 54, 19, 1…
## $ round_2    <dbl> 21, 58, 61, 28, 37, 29, 46, 32, 18, 19, 56, 33, 27, 44, 16,…
## $ round_3    <dbl> 18, 4, 25, 2, 12, 11, 13, 14, 59, 55, 6, 5, 7, 8, 30, NA, 2…
## $ round_4    <dbl> 14, 17, 21, 26, 13, 35, 11, 9, 8, 31, 7, 38, 5, 1, 22, 39, …
## $ round_5    <dbl> 7, 16, 11, 5, 4, 10, 1, 47, 26, 6, 3, NA, 33, 27, 54, 2, 23…
## $ round_6    <dbl> 12, 20, 13, 19, 14, 27, 9, 28, 7, 25, 34, 1, 3, 5, 33, 36, …
## $ round_7    <dbl> 4, 7, 12, 1, 17, 21, 2, 19, 20, 18, 26, 3, 32, 31, 38, NA, …
## $ average    <dbl> 1605, 1469, 1564, 1574, 1501, 1519, 1372, 1468, 1523, 1554,…
new_rating_table <- opponents_average_rating_table|>
  select(pair, average) |>
  slice(-n())

final_ratings_table <- players_info_table|>
  left_join(players_rating_table) |>
  left_join(new_rating_table, by = "pair")|>
  select(-new_rating, -uscf_id)
## Joining with `by = join_by(pair)`
head(final_ratings_table)
##   pair         player_name state total rating average
## 1    1            GARY HUA    ON   6.0   1794    1605
## 2    2     DAKSHESH DARURI    MI   6.0   1553    1469
## 3    3        ADITYA BAJAJ    MI   6.0   1384    1564
## 4    4 PATRICK H SCHILLING    MI   5.5   1716    1574
## 5    5          HANSHI ZUO    MI   5.5   1655    1501
## 6    6         HANSEN SONG    OH   5.0   1686    1519
final_ratings_table |>
  head(10) |>
  kable() |>
  kable_styling(full_width = F)
pair player_name state total rating average
1 GARY HUA ON 6.0 1794 1605
2 DAKSHESH DARURI MI 6.0 1553 1469
3 ADITYA BAJAJ MI 6.0 1384 1564
4 PATRICK H SCHILLING MI 5.5 1716 1574
5 HANSHI ZUO MI 5.5 1655 1501
6 HANSEN SONG OH 5.0 1686 1519
7 GARY DEE SWATHELL MI 5.0 1649 1372
8 EZEKIEL HOUGHTON MI 5.0 1641 1468
9 STEFANO LEE ON 5.0 1411 1523
10 ANVIT RAO MI 5.0 1365 1554

This analysis will examine how the outcomes of chess games are influenced by the color of the pieces each player uses. I want to if there is any advantage or disadvantage associated with playing with white or black pieces.

round_results_table_long <- round_results_table |>
  pivot_longer(
    cols = (!pair),
    names_to = "round",
    names_transform = list(round = ~gsub("[^0-9]", "", .x)),
    values_to = "result"
  ) |>
  mutate(result = trimws(result))

rounds_pcs_color_table <- elo_table |>
  select(-(player_name:pts)) |>
  pivot_longer(
    cols = (!pair),
    names_to = "round",
    values_to = "pieces_color"
  ) |>
  mutate(pieces_color = trimws(pieces_color))

wins_by_pcs_color_table <- round_results_table_long |>
  left_join(rounds_pcs_color_table, by = c("pair", "round"))

wins_by_pcs_color_table <- wins_by_pcs_color_table |>
  mutate(white_win = ifelse(result == "W" & pieces_color == "W", TRUE,
                            ifelse(result == "L" & pieces_color == "W", FALSE, NA)))

table(wins_by_pcs_color_table$white_win)
## 
## FALSE  TRUE 
##    85    90
ggplot(wins_by_pcs_color_table, aes(x = white_win, fill = white_win)) +
  geom_bar(stat = "count") +
  labs(
    title = "Distibution of white wins",
    x = "Game results",
    y = "Count"
  ) +
  theme_minimal() +
  geom_text(stat = "count", aes(x = white_win,
                                label = after_stat(count)),
                                vjust = -0.5)

As the bar plot shows, there is very small advantage of the color of starting pieces according to results of this dataset. As a chess players myself I believed that starting with white pieces is advantage. But this analysis puts doubt in it.

Extra Credit - ELO calculations

The expected score for a player in ELO rating system is calculated by this formula :

\[ E = \sum_{i=1}^{n} \frac{1}{1 + 10^{(R_{\text{opponent}_i} - R_{\text{player}})/400}} \]

where:

(source: Wikipedia https://en.wikipedia.org/wiki/Elo_rating_system )

Below, we compute the expected score for each player in the tournament, compare it to their actual total score, and identify who most overperformed and underperformed players relative to expectations.

First let’s create a function to compute expected score for a single player:

compute_expected_score <- function(player_rating, opponent_ratings) {
  expected_scores <- 1 / (1 + 10 ^ ((opponent_ratings - player_rating) / 400))
  sum(expected_scores, na.rm = TRUE)
}

Here we will pass each player’s rating and extract opponent ratings based on round pairings into function.

# this is a function to calculate expected score
compute_expected_score <- function(player_rating, opponent_ids, ratings_vector) {
  opponent_ratings <- ratings_vector[opponent_ids]
  expected_scores <- 1 / (1 + 10 ^ ((opponent_ratings - player_rating) / 400))
  sum(expected_scores, na.rm = TRUE)
}

# Added a dummy row at the end for NAs (opponent might be blank)
players_rating_vector <- players_rating_table$rating
players_rating_vector[0] <- NA  # allow for index 0 to be NA
players_rating_vector <- c(players_rating_vector, NA)  # padding for out-of-bound safety

# Calculating expected score for each player
expected_score_table <- rounds_table |>
  left_join(players_rating_table, by = "pair") |>
  rowwise() |>
  mutate(
    expected_score = compute_expected_score(
      player_rating = rating,
      opponent_ids = c(round_1, round_2, round_3, round_4, round_5, round_6, round_7),
      ratings_vector = players_rating_vector
    )
  ) |>
  ungroup()

# Combining with actual total points
performance_table <- expected_score_table |>
  select(pair, expected_score, rating) |>   # Keep original rating from players_rating_table
  left_join(select(final_ratings_table, pair, player_name, total), by = "pair") |>
  mutate(
    total = as.numeric(total),
    score_diff = total - expected_score
  )


# Top 5 overperformers
top_overperformers <- performance_table |>
  arrange(desc(score_diff)) |>
  slice(1:5)

# Top 5 underperformers
top_underperformers <- performance_table |>
  arrange(score_diff) |>
  slice(1:5)

Top 5 overperforming players

# Overperforming players table
top_overperformers |>
  kable(caption = "Top 5 overperforming players (Actual total - Expected)") |>
  kable_styling(full_width = FALSE)
Top 5 overperforming players (Actual total - Expected)
pair expected_score rating player_name total score_diff
3 1.9450879 1384 ADITYA BAJAJ 6.0 4.054912
15 1.3733089 1220 ZACHARY JAMES HOUGHTON 4.5 3.126691
10 1.9448541 1365 ANVIT RAO 5.0 3.055146
46 0.0432498 377 JACOB ALEXANDER LAVALLEY 3.0 2.956750
37 0.7734529 980 AMIYATOSH PWNANANDAM 3.5 2.726547

This table shows the five players who scored way higher than what Elo rating expected from them. For example, Aditya Bajaj had an expected score of only 1.95 but finished with 6.0 points, that’s over 4 points above expected. Another big jump is Jacob Alexander Lavalley with a rating of just 377 and expected score of 0.05 still managed to earn 3.0 points, which is nearly 3 points higher than expected.

Top 5 underperforming players

# Underperforming players table
top_underperformers |>
  kable(caption = "Top 5 underperforming players (Actual total - Expected)") |>
  kable_styling(full_width = FALSE)
Top 5 underperforming players (Actual total - Expected)
pair expected_score rating player_name total score_diff
25 6.275650 1745 LOREN SCHWIEBERT 3.5 -2.775650
30 6.018220 1522 GEORGE AVERY JONES 3.5 -2.518220
42 5.010416 1332 JARED GE 3.0 -2.010416
31 5.092465 1494 RISHI SHETTY 3.5 -1.592465
35 4.957890 1438 JOSHUA DAVID LEE 3.5 -1.457890

This table shows players who didn’t perform as expected based on their ratings. Loren Schwiebert had the highest expected score (6.28) but only scored 3.5 points. That’s a difference of -2.78, which is the biggest drop in the whole tournament. Same goes for George Avery Jones and Jared Ge, both fell short by over 2 points.