Load Libraries

library(dplyr)
library(stringr)

Import Tournament Data

url <- "https://raw.githubusercontent.com/LBoodram26/Data607_-Week-5/refs/heads/main/tournamentinfo.txt"

tournament <- readLines(url)

head(tournament, 20)
##  [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] "-----------------------------------------------------------------------------------------" 
## [11] "    3 | ADITYA BAJAJ                    |6.0  |L   8|W  61|W  25|W  21|W  11|W  13|W  12|" 
## [12] "   MI | 14959604 / R: 1384   ->1640     |N:2  |W    |B    |W    |B    |W    |B    |W    |" 
## [13] "-----------------------------------------------------------------------------------------" 
## [14] "    4 | PATRICK H SCHILLING             |5.5  |W  23|D  28|W   2|W  26|D   5|W  19|D   1|" 
## [15] "   MI | 12616049 / R: 1716   ->1744     |N:2  |W    |B    |W    |B    |W    |B    |B    |" 
## [16] "-----------------------------------------------------------------------------------------" 
## [17] "    5 | HANSHI ZUO                      |5.5  |W  45|W  37|D  12|D  13|D   4|W  14|W  17|" 
## [18] "   MI | 14601533 / R: 1655   ->1690     |N:2  |B    |W    |B    |W    |B    |W    |B    |" 
## [19] "-----------------------------------------------------------------------------------------" 
## [20] "    6 | HANSEN SONG                     |5.0  |W  34|D  29|L  11|W  35|D  10|W  27|W  21|"

Extract Player Information

player_lines <- tournament[seq(5, length(tournament), by = 3)]

players <- data.frame(
  player_number = as.numeric(str_extract(player_lines, "^\\s*\\d+")),
  player_name = str_trim(str_sub(player_lines, 8, 40)),
  total_points = as.numeric(str_extract(str_sub(player_lines, 41, 50), "\\d+\\.?\\d*"))
)

head(players)
##   player_number         player_name total_points
## 1             1            GARY HUA          6.0
## 2             2     DAKSHESH DARURI          6.0
## 3             3        ADITYA BAJAJ          6.0
## 4             4 PATRICK H SCHILLING          5.5
## 5             5          HANSHI ZUO          5.5
## 6             6         HANSEN SONG          5.0

Extract Player Ratings

rating_lines <- tournament[seq(6, length(tournament), by = 3)]

pre_ratings <- as.numeric(
  str_match(rating_lines, "R:\\s*(\\d+)")[, 2]
)

players$pre_rating <- pre_ratings

head(players)
##   player_number         player_name total_points pre_rating
## 1             1            GARY HUA          6.0       1794
## 2             2     DAKSHESH DARURI          6.0       1553
## 3             3        ADITYA BAJAJ          6.0       1384
## 4             4 PATRICK H SCHILLING          5.5       1716
## 5             5          HANSHI ZUO          5.5       1655
## 6             6         HANSEN SONG          5.0       1686

Extract Round Results

round_data <- str_extract_all(
  player_lines,
  "[WLD]\\s*\\d+"
)

head(round_data)
## [[1]]
## [1] "W  39" "W  21" "W  18" "W  14" "W   7" "D  12" "D   4"
## 
## [[2]]
## [1] "W  63" "W  58" "L   4" "W  17" "W  16" "W  20" "W   7"
## 
## [[3]]
## [1] "L   8" "W  61" "W  25" "W  21" "W  11" "W  13" "W  12"
## 
## [[4]]
## [1] "W  23" "D  28" "W   2" "W  26" "D   5" "W  19" "D   1"
## 
## [[5]]
## [1] "W  45" "W  37" "D  12" "D  13" "D   4" "W  14" "W  17"
## 
## [[6]]
## [1] "W  34" "D  29" "L  11" "W  35" "D  10" "W  27" "W  21"

Create Game-Level Data

game_data <- data.frame(
  player_number = rep(players$player_number, lengths(round_data)),
  result = unlist(lapply(round_data, function(x) str_extract(x, "[WLD]"))),
  opponent_number = as.numeric(
    unlist(lapply(round_data, function(x) str_extract(x, "\\d+")))
  )
)

head(game_data, 15)
##    player_number result opponent_number
## 1              1      W              39
## 2              1      W              21
## 3              1      W              18
## 4              1      W              14
## 5              1      W               7
## 6              1      D              12
## 7              1      D               4
## 8              2      W              63
## 9              2      W              58
## 10             2      L               4
## 11             2      W              17
## 12             2      W              16
## 13             2      W              20
## 14             2      W               7
## 15             3      L               8

Add Player and Opponent Ratings

game_data <- game_data %>%
  left_join(
    players %>% select(player_number, pre_rating),
    by = "player_number"
  ) %>%
  rename(player_rating = pre_rating)

game_data <- game_data %>%
  left_join(
    players %>% select(player_number, pre_rating),
    by = c("opponent_number" = "player_number")
  ) %>%
  rename(opponent_rating = pre_rating)

head(game_data, 15)
##    player_number result opponent_number player_rating opponent_rating
## 1              1      W              39          1794            1436
## 2              1      W              21          1794            1563
## 3              1      W              18          1794            1600
## 4              1      W              14          1794            1610
## 5              1      W               7          1794            1649
## 6              1      D              12          1794            1663
## 7              1      D               4          1794            1716
## 8              2      W              63          1553            1175
## 9              2      W              58          1553             917
## 10             2      L               4          1553            1716
## 11             2      W              17          1553            1629
## 12             2      W              16          1553            1604
## 13             2      W              20          1553            1595
## 14             2      W               7          1553            1649
## 15             3      L               8          1384            1641

Calculate Expected Elo Score

game_data <- game_data %>%
  mutate(
    expected_score = 1 / (
      1 + 10^((opponent_rating - player_rating) / 400)
    )
  )

head(game_data, 15)
##    player_number result opponent_number player_rating opponent_rating
## 1              1      W              39          1794            1436
## 2              1      W              21          1794            1563
## 3              1      W              18          1794            1600
## 4              1      W              14          1794            1610
## 5              1      W               7          1794            1649
## 6              1      D              12          1794            1663
## 7              1      D               4          1794            1716
## 8              2      W              63          1553            1175
## 9              2      W              58          1553             917
## 10             2      L               4          1553            1716
## 11             2      W              17          1553            1629
## 12             2      W              16          1553            1604
## 13             2      W              20          1553            1595
## 14             2      W               7          1553            1649
## 15             3      L               8          1384            1641
##    expected_score
## 1       0.8870357
## 2       0.7907981
## 3       0.7533861
## 4       0.7425356
## 5       0.6973451
## 6       0.6800707
## 7       0.6104024
## 8       0.8980683
## 9       0.9749402
## 10      0.2812432
## 11      0.3923389
## 12      0.4271277
## 13      0.4398499
## 14      0.3652567
## 15      0.1855164

Convert Results to Actual Scores

game_data <- game_data %>%
  mutate(
    actual_score = case_when(
      result == "W" ~ 1,
      result == "D" ~ 0.5,
      result == "L" ~ 0
    )
  )

head(game_data, 15)
##    player_number result opponent_number player_rating opponent_rating
## 1              1      W              39          1794            1436
## 2              1      W              21          1794            1563
## 3              1      W              18          1794            1600
## 4              1      W              14          1794            1610
## 5              1      W               7          1794            1649
## 6              1      D              12          1794            1663
## 7              1      D               4          1794            1716
## 8              2      W              63          1553            1175
## 9              2      W              58          1553             917
## 10             2      L               4          1553            1716
## 11             2      W              17          1553            1629
## 12             2      W              16          1553            1604
## 13             2      W              20          1553            1595
## 14             2      W               7          1553            1649
## 15             3      L               8          1384            1641
##    expected_score actual_score
## 1       0.8870357          1.0
## 2       0.7907981          1.0
## 3       0.7533861          1.0
## 4       0.7425356          1.0
## 5       0.6973451          1.0
## 6       0.6800707          0.5
## 7       0.6104024          0.5
## 8       0.8980683          1.0
## 9       0.9749402          1.0
## 10      0.2812432          0.0
## 11      0.3923389          1.0
## 12      0.4271277          1.0
## 13      0.4398499          1.0
## 14      0.3652567          1.0
## 15      0.1855164          0.0

Summarize Player Performance

player_performance <- game_data %>%
  group_by(player_number) %>%
  summarise(
    expected_total = sum(expected_score, na.rm = TRUE),
    actual_total = sum(actual_score, na.rm = TRUE)
  ) %>%
  left_join(
    players %>% select(
      player_number,
      player_name,
      pre_rating,
      total_points
    ),
    by = "player_number"
  ) %>%
  mutate(
    performance_difference = actual_total - expected_total
  ) %>%
  select(
    player_number,
    player_name,
    pre_rating,
    total_points,
    expected_total,
    actual_total,
    performance_difference
  )

head(player_performance, 10)
## # A tibble: 10 × 7
##    player_number player_name pre_rating total_points expected_total actual_total
##            <dbl> <chr>            <dbl>        <dbl>          <dbl>        <dbl>
##  1             1 GARY HUA          1794          6             5.16          6  
##  2             2 DAKSHESH D…       1553          6             3.78          6  
##  3             3 ADITYA BAJ…       1384          6             1.95          6  
##  4             4 PATRICK H …       1716          5.5           4.74          5.5
##  5             5 HANSHI ZUO        1655          5.5           4.38          5.5
##  6             6 HANSEN SONG       1686          5             4.94          5  
##  7             7 GARY DEE S…       1649          5             4.58          5  
##  8             8 EZEKIEL HO…       1641          5             5.03          5  
##  9             9 STEFANO LEE       1411          5             2.29          5  
## 10            10 ANVIT RAO         1365          5             1.94          5  
## # ℹ 1 more variable: performance_difference <dbl>

Top 5 Overperformers

top_overperformers <- player_performance %>%
  arrange(desc(performance_difference)) %>%
  slice_head(n = 5)

top_overperformers
## # A tibble: 5 × 7
##   player_number player_name  pre_rating total_points expected_total actual_total
##           <dbl> <chr>             <dbl>        <dbl>          <dbl>        <dbl>
## 1             3 ADITYA BAJAJ       1384          6           1.95            6  
## 2            15 ZACHARY JAM…       1220          4.5         1.37            4.5
## 3            10 ANVIT RAO          1365          5           1.94            5  
## 4            46 JACOB ALEXA…        377          3           0.0432          3  
## 5             9 STEFANO LEE        1411          5           2.29            5  
## # ℹ 1 more variable: performance_difference <dbl>

Top 5 Underperformers

top_underperformers <- player_performance %>%
  arrange(performance_difference) %>%
  slice_head(n = 5)

top_underperformers
## # A tibble: 5 × 7
##   player_number player_name  pre_rating total_points expected_total actual_total
##           <dbl> <chr>             <dbl>        <dbl>          <dbl>        <dbl>
## 1            25 LOREN SCHWI…       1745          3.5           6.28          3.5
## 2            30 GEORGE AVER…       1522          3.5           6.02          3.5
## 3            54 LARRY HODGE        1270          2             3.40          1  
## 4            42 JARED GE           1332          3             5.01          3  
## 5            31 RISHI SHETTY       1494          3.5           5.09          3.5
## # ℹ 1 more variable: performance_difference <dbl>

Interpretation of Results

The results show that Aditya Bajaj had the largest positive performance difference. Based on his Elo rating, his expected score was approximately 1.95 points, but he earned 6.0 points in his rated games. This means he performed about 4.05 points better than expected.

The other strongest overperformers were Zachary James Houghton, Anvit Rao, Jacob Alexander Lavalley, and Stefano Lee.

Loren Schwiebert had the largest negative performance difference. His expected score was approximately 6.28 points, but he earned 3.5 points, which was about 2.78 points below his expected performance.

The other largest underperformers were George Avery Jones, Larry Hodge, Jared Ge, and Rishi Shetty.

Overall, the Elo calculations show how a player’s actual performance against rated opponents can differ from the performance predicted by their pre-tournament Elo rating.

Sources

  1. singingbanana. (2019, February 15). The Elo Rating System for Chess and Beyond [Video]. YouTube. https://www.youtube.com/watch?v=AsYfbmp0To0

  2. Tournament Information Dataset. GitHub. https://raw.githubusercontent.com/LBoodram26/Data607_-Week-5/refs/heads/main/tournamentinfo.txt

  3. Wickham, H., François, R., Henry, L., Müller, K., & Vaughan, D. dplyr: A Grammar of Data Manipulation. Tidyverse. https://dplyr.tidyverse.org/

  4. Tidyverse. Mutating Joins – dplyr. https://dplyr.tidyverse.org/reference/mutate-joins.html

  5. Tidyverse. summarise(): Summarise Each Group Down to One Row – dplyr. https://dplyr.tidyverse.org/reference/summarise.html

  6. Tidyverse. arrange(): Order Rows Using Column Values – dplyr. https://dplyr.tidyverse.org/reference/arrange.html

  7. Tidyverse. slice(): Subset Rows Using Their Positions – dplyr. https://dplyr.tidyverse.org/reference/slice.html

  8. Wickham, H. stringr: Simple, Consistent Wrappers for Common String Operations. Tidyverse. https://stringr.tidyverse.org/

  9. Tidyverse. str_extract(): Extract the Complete Match – stringr. https://stringr.tidyverse.org/reference/str_extract.html

  10. Tidyverse. str_match(): Extract Components from a Match – stringr. https://stringr.tidyverse.org/reference/str_match.html

  11. Posit. R Markdown: HTML Document. https://rmarkdown.rstudio.com/docs/reference/html_document.html