Assignment 5B: ELO Calculations

Author

Yeimi Perez

Approach

I will use the cleaned chess tournament data from Project 1 and compare each player’s pre-tournament rating with the ratings of their opponents. For each matchup, I will calculate the player’s expected Elo score and then add the expected scores across all games, to then compare the total expected score with the player’s actual tournament points. Finally, I will sort the differences to identify the five players who most overperformed and the five players who most underperformed expectations.

Introduction

This assignment builds on the chess tournament data used in Project 1. The goal is to calculate each player’s expected tournament score using the Elo rating system and compare it with the player’s actual tournament points. The difference between actual and expected performance will then be used to identify the five players who most over-performed and the five players who most under-performed.

library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.2.1     ✔ readr     2.2.0
✔ forcats   1.0.1     ✔ stringr   1.6.0
✔ ggplot2   4.0.3     ✔ tibble    3.3.1
✔ lubridate 1.9.5     ✔ tidyr     1.3.2
✔ purrr     1.2.2     
── 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
tournament_url <- "https://raw.githubusercontent.com/yeimiperez14/Data_607/main/Project%201/tournamentinfo.txt"

tournament_lines <- readLines(tournament_url)
Warning in readLines(tournament_url): incomplete final line found on
'https://raw.githubusercontent.com/yeimiperez14/Data_607/main/Project%201/tournamentinfo.txt'
head(tournament_lines, 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] "-----------------------------------------------------------------------------------------" 

Here we imported the tournament file from github and checked the import.

Extracting the Player Results Only

player_lines <- tournament_lines[
  str_detect(tournament_lines, "^\\s*\\d+\\s*\\|")
]
head(player_lines)
[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|"
length(player_lines)
[1] 64

This pattern ^\s\d+\s\| find lines that begin with a number followed by |. We then confirmed we have all 64 players.

Separating Each Player into Separate Pieces

player_split <- str_split_fixed(
  player_lines,
  "\\|",
  n = 11
)

Here we separate information and split into for each player.

Creating the Player Table

players <- tibble(
  player_number = as.numeric(str_trim(player_split[, 1])),
  player_name = str_trim(player_split[, 2]),
  total_points = as.numeric(str_trim(player_split[, 3])),
  round1 = str_trim(player_split[, 4]),
  round2 = str_trim(player_split[, 5]),
  round3 = str_trim(player_split[, 6]),
  round4 = str_trim(player_split[, 7]),
  round5 = str_trim(player_split[, 8]),
  round6 = str_trim(player_split[, 9]),
  round7 = str_trim(player_split[, 10])
)
names(players)
 [1] "player_number" "player_name"   "total_points"  "round1"       
 [5] "round2"        "round3"        "round4"        "round5"       
 [9] "round6"        "round7"       
dim(players)
[1] 64 10
names(players)
 [1] "player_number" "player_name"   "total_points"  "round1"       
 [5] "round2"        "round3"        "round4"        "round5"       
 [9] "round6"        "round7"       
head(players)
# A tibble: 6 × 10
  player_number player_name      total_points round1 round2 round3 round4 round5
          <dbl> <chr>                   <dbl> <chr>  <chr>  <chr>  <chr>  <chr> 
1             1 GARY HUA                  6   W  39  W  21  W  18  W  14  W   7 
2             2 DAKSHESH DARURI           6   W  63  W  58  L   4  W  17  W  16 
3             3 ADITYA BAJAJ              6   L   8  W  61  W  25  W  21  W  11 
4             4 PATRICK H SCHIL…          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   W  34  D  29  L  11  W  35  D  10 
# ℹ 2 more variables: round6 <chr>, round7 <chr>

Extracting the Rating Lines

rating_lines <- tournament_lines[
  str_detect(tournament_lines, "^\\s*[A-Z]{2}\\s*\\|")
]
head(rating_lines)
[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    |"

Extracting each player’s rating

ratings <- tibble(
  player_number = 1:length(rating_lines),
  pre_rating = as.numeric(
    str_match(rating_lines, "R:\\s*(\\d+)")[, 2]
  )
)
head(ratings)
# A tibble: 6 × 2
  player_number pre_rating
          <int>      <dbl>
1             1       1794
2             2       1553
3             3       1384
4             4       1716
5             5       1655
6             6       1686

These are the pre-tournament ratings shown in the source.

Bringing together the Ratings to the Player Table

players <- players %>%
  left_join(
    ratings,
    by = "player_number"
  )
head(players)
# A tibble: 6 × 11
  player_number player_name      total_points round1 round2 round3 round4 round5
          <dbl> <chr>                   <dbl> <chr>  <chr>  <chr>  <chr>  <chr> 
1             1 GARY HUA                  6   W  39  W  21  W  18  W  14  W   7 
2             2 DAKSHESH DARURI           6   W  63  W  58  L   4  W  17  W  16 
3             3 ADITYA BAJAJ              6   L   8  W  61  W  25  W  21  W  11 
4             4 PATRICK H SCHIL…          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   W  34  D  29  L  11  W  35  D  10 
# ℹ 3 more variables: round6 <chr>, round7 <chr>, pre_rating <dbl>
players %>%
  filter(player_name == "GARY HUA")
# A tibble: 1 × 11
  player_number player_name total_points round1 round2 round3 round4 round5
          <dbl> <chr>              <dbl> <chr>  <chr>  <chr>  <chr>  <chr> 
1             1 GARY HUA               6 W  39  W  21  W  18  W  14  W   7 
# ℹ 3 more variables: round6 <chr>, round7 <chr>, pre_rating <dbl>

Changing the 7 round columns into rows

games <- players %>%
  pivot_longer(
    cols = round1:round7,
    names_to = "round",
    values_to = "round_result"
  )
head(games, 10)
# A tibble: 10 × 6
   player_number player_name     total_points pre_rating round  round_result
           <dbl> <chr>                  <dbl>      <dbl> <chr>  <chr>       
 1             1 GARY HUA                   6       1794 round1 W  39       
 2             1 GARY HUA                   6       1794 round2 W  21       
 3             1 GARY HUA                   6       1794 round3 W  18       
 4             1 GARY HUA                   6       1794 round4 W  14       
 5             1 GARY HUA                   6       1794 round5 W   7       
 6             1 GARY HUA                   6       1794 round6 D  12       
 7             1 GARY HUA                   6       1794 round7 D   4       
 8             2 DAKSHESH DARURI            6       1553 round1 W  63       
 9             2 DAKSHESH DARURI            6       1553 round2 W  58       
10             2 DAKSHESH DARURI            6       1553 round3 L   4       

Extract the result code

games <- games %>%
  mutate(
    result_code = str_extract(
      round_result,
      "^[A-Z]"
    )
  )
head(games)
# A tibble: 6 × 7
  player_number player_name total_points pre_rating round  round_result
          <dbl> <chr>              <dbl>      <dbl> <chr>  <chr>       
1             1 GARY HUA               6       1794 round1 W  39       
2             1 GARY HUA               6       1794 round2 W  21       
3             1 GARY HUA               6       1794 round3 W  18       
4             1 GARY HUA               6       1794 round4 W  14       
5             1 GARY HUA               6       1794 round5 W   7       
6             1 GARY HUA               6       1794 round6 D  12       
# ℹ 1 more variable: result_code <chr>

Extracting the opponents number

games <- games %>%
  mutate(
    opponent_number = as.numeric(
      str_extract(round_result, "\\d+")
    )
  )
head(games)
# A tibble: 6 × 8
  player_number player_name total_points pre_rating round  round_result
          <dbl> <chr>              <dbl>      <dbl> <chr>  <chr>       
1             1 GARY HUA               6       1794 round1 W  39       
2             1 GARY HUA               6       1794 round2 W  21       
3             1 GARY HUA               6       1794 round3 W  18       
4             1 GARY HUA               6       1794 round4 W  14       
5             1 GARY HUA               6       1794 round5 W   7       
6             1 GARY HUA               6       1794 round6 D  12       
# ℹ 2 more variables: result_code <chr>, opponent_number <dbl>

Remove rounds without an Opponent

played_games <- games %>%
  filter(!is.na(opponent_number))

Those rounds do not contain an opponent number, so there is no opponent rating available for an Elo calculation.

Checking the Matchup Table

played_games %>%
  select(
    player_name,
    pre_rating,
    round,
    round_result,
    opponent_number
  ) %>%
  head(15)
# A tibble: 15 × 5
   player_name     pre_rating round  round_result opponent_number
   <chr>                <dbl> <chr>  <chr>                  <dbl>
 1 GARY HUA              1794 round1 W  39                     39
 2 GARY HUA              1794 round2 W  21                     21
 3 GARY HUA              1794 round3 W  18                     18
 4 GARY HUA              1794 round4 W  14                     14
 5 GARY HUA              1794 round5 W   7                      7
 6 GARY HUA              1794 round6 D  12                     12
 7 GARY HUA              1794 round7 D   4                      4
 8 DAKSHESH DARURI       1553 round1 W  63                     63
 9 DAKSHESH DARURI       1553 round2 W  58                     58
10 DAKSHESH DARURI       1553 round3 L   4                      4
11 DAKSHESH DARURI       1553 round4 W  17                     17
12 DAKSHESH DARURI       1553 round5 W  16                     16
13 DAKSHESH DARURI       1553 round6 W  20                     20
14 DAKSHESH DARURI       1553 round7 W   7                      7
15 ADITYA BAJAJ          1384 round1 L   8                      8

Creating an opponent rating lookup table

rating_lookup <- players %>%
  select(
    opponent_number = player_number,
    opponent_rating = pre_rating
  )
head(rating_lookup)
# A tibble: 6 × 2
  opponent_number opponent_rating
            <dbl>           <dbl>
1               1            1794
2               2            1553
3               3            1384
4               4            1716
5               5            1655
6               6            1686

Attaching the Opponents Ratings

played_games <- played_games %>%
  left_join(
    rating_lookup,
    by = "opponent_number"
  )
played_games %>%
  select(
    player_name,
    pre_rating,
    opponent_number,
    opponent_rating
  ) %>%
  head(15)
# A tibble: 15 × 4
   player_name     pre_rating opponent_number opponent_rating
   <chr>                <dbl>           <dbl>           <dbl>
 1 GARY HUA              1794              39            1436
 2 GARY HUA              1794              21            1563
 3 GARY HUA              1794              18            1600
 4 GARY HUA              1794              14            1610
 5 GARY HUA              1794               7            1649
 6 GARY HUA              1794              12            1663
 7 GARY HUA              1794               4            1716
 8 DAKSHESH DARURI       1553              63            1175
 9 DAKSHESH DARURI       1553              58             917
10 DAKSHESH DARURI       1553               4            1716
11 DAKSHESH DARURI       1553              17            1629
12 DAKSHESH DARURI       1553              16            1604
13 DAKSHESH DARURI       1553              20            1595
14 DAKSHESH DARURI       1553               7            1649
15 ADITYA BAJAJ          1384               8            1641

Making sure every opponent got a rating

sum(is.na(played_games$opponent_rating))
[1] 0

0 means every opponent number successfully matched a player rating.

Planning on the Elo formula

# source: https://handbook.fide.com/chapter/B022017

1 / (1 + 10^((opponent_rating - pre_rating) / 400))

Testing the Formula on Gary Hua’s first opponent

gary_expected <- 1 / (
  1 + 10^((1436 - 1794) / 400)
)

gary_expected
[1] 0.8870357

Calculating Elo expectation for every matchup

played_games <- played_games %>%
  mutate(
    expected_score =
      1 / (
        1 + 10^(
          (opponent_rating - pre_rating) / 400
        )
      )
  )
played_games %>%
  select(
    player_name,
    pre_rating,
    opponent_rating,
    expected_score
  ) %>%
  head(15)
# A tibble: 15 × 4
   player_name     pre_rating opponent_rating expected_score
   <chr>                <dbl>           <dbl>          <dbl>
 1 GARY HUA              1794            1436          0.887
 2 GARY HUA              1794            1563          0.791
 3 GARY HUA              1794            1600          0.753
 4 GARY HUA              1794            1610          0.743
 5 GARY HUA              1794            1649          0.697
 6 GARY HUA              1794            1663          0.680
 7 GARY HUA              1794            1716          0.610
 8 DAKSHESH DARURI       1553            1175          0.898
 9 DAKSHESH DARURI       1553             917          0.975
10 DAKSHESH DARURI       1553            1716          0.281
11 DAKSHESH DARURI       1553            1629          0.392
12 DAKSHESH DARURI       1553            1604          0.427
13 DAKSHESH DARURI       1553            1595          0.440
14 DAKSHESH DARURI       1553            1649          0.365
15 ADITYA BAJAJ          1384            1641          0.186

Adding the expected scores for each Player

expected_totals <- played_games %>%
  group_by(
    player_number,
    player_name
  ) %>%
  summarise(
    expected_score = sum(expected_score),
    .groups = "drop"
  )
head(expected_totals)
# A tibble: 6 × 3
  player_number player_name         expected_score
          <dbl> <chr>                        <dbl>
1             1 GARY HUA                      5.16
2             2 DAKSHESH DARURI               3.78
3             3 ADITYA BAJAJ                  1.95
4             4 PATRICK H SCHILLING           4.74
5             5 HANSHI ZUO                    4.38
6             6 HANSEN SONG                   4.94

Bringing back the actual tournament score

Joining it:

actual_scores <- players %>%
  select(
    player_number,
    total_points
  )
player_results <- expected_totals %>%
  left_join(
    actual_scores,
    by = "player_number"
  )

renaming it:

player_results <- player_results %>%
  rename(
    actual_score = total_points
  )

and checking:

head(player_results)
# A tibble: 6 × 4
  player_number player_name         expected_score actual_score
          <dbl> <chr>                        <dbl>        <dbl>
1             1 GARY HUA                      5.16          6  
2             2 DAKSHESH DARURI               3.78          6  
3             3 ADITYA BAJAJ                  1.95          6  
4             4 PATRICK H SCHILLING           4.74          5.5
5             5 HANSHI ZUO                    4.38          5.5
6             6 HANSEN SONG                   4.94          5  

I am using total_points here because the assignment asks to compare expected score with the player’s actual tournament score.

Some players have special rounds such as B or H, for which there is no rated opponent. Those rounds therefore do not generate an Elo expected value, as that limitation comes from the structure of the tournament file itself.

Calculating performance difference

player_results <- player_results %>%
  mutate(
    performance_difference =
      actual_score - expected_score
  )
head(player_results)
# A tibble: 6 × 5
  player_number player_name   expected_score actual_score performance_difference
          <dbl> <chr>                  <dbl>        <dbl>                  <dbl>
1             1 GARY HUA                5.16          6                   0.838 
2             2 DAKSHESH DAR…           3.78          6                   2.22  
3             3 ADITYA BAJAJ            1.95          6                   4.05  
4             4 PATRICK H SC…           4.74          5.5                 0.758 
5             5 HANSHI ZUO              4.38          5.5                 1.12  
6             6 HANSEN SONG             4.94          5                   0.0554

A positive difference means that the player over-performed and a negative difference that they under-performed.

Rounding the results

player_results <- player_results %>%
  mutate(
    expected_score =
      round(expected_score, 2),
    
    performance_difference =
      round(performance_difference, 2)
  )

The 5 greatest over-performers

top_overperformers <- player_results %>%
  arrange(
    desc(performance_difference)
  ) %>%
  slice_head(n = 5)
top_overperformers
# A tibble: 5 × 5
  player_number player_name   expected_score actual_score performance_difference
          <dbl> <chr>                  <dbl>        <dbl>                  <dbl>
1             3 ADITYA BAJAJ            1.95          6                     4.05
2            15 ZACHARY JAME…           1.37          4.5                   3.13
3            10 ANVIT RAO               1.94          5                     3.06
4            46 JACOB ALEXAN…           0.04          3                     2.96
5            37 AMIYATOSH PW…           0.77          3.5                   2.73

The 5 greatest under-performers

top_underperformers <- player_results %>%
  arrange(
    performance_difference
  ) %>%
  slice_head(n = 5)
top_underperformers
# A tibble: 5 × 5
  player_number player_name   expected_score actual_score performance_difference
          <dbl> <chr>                  <dbl>        <dbl>                  <dbl>
1            25 LOREN SCHWIE…           6.28          3.5                  -2.78
2            30 GEORGE AVERY…           6.02          3.5                  -2.52
3            42 JARED GE                5.01          3                    -2.01
4            31 RISHI SHETTY            5.09          3.5                  -1.59
5            35 JOSHUA DAVID…           4.96          3.5                  -1.46

Cleaning the final tables

For over-performers

top_overperformers %>%
  select(
    player_name,
    actual_score,
    expected_score,
    performance_difference
  )
# A tibble: 5 × 4
  player_name              actual_score expected_score performance_difference
  <chr>                           <dbl>          <dbl>                  <dbl>
1 ADITYA BAJAJ                      6             1.95                   4.05
2 ZACHARY JAMES HOUGHTON            4.5           1.37                   3.13
3 ANVIT RAO                         5             1.94                   3.06
4 JACOB ALEXANDER LAVALLEY          3             0.04                   2.96
5 AMIYATOSH PWNANANDAM              3.5           0.77                   2.73

For under-performers

top_underperformers %>%
  select(
    player_name,
    actual_score,
    expected_score,
    performance_difference
  )
# A tibble: 5 × 4
  player_name        actual_score expected_score performance_difference
  <chr>                     <dbl>          <dbl>                  <dbl>
1 LOREN SCHWIEBERT            3.5           6.28                  -2.78
2 GEORGE AVERY JONES          3.5           6.02                  -2.52
3 JARED GE                    3             5.01                  -2.01
4 RISHI SHETTY                3.5           5.09                  -1.59
5 JOSHUA DAVID LEE            3.5           4.96                  -1.46

Reviewing the entire ranking

player_results %>%
  arrange(
    desc(performance_difference)
  )
# A tibble: 64 × 5
   player_number player_name  expected_score actual_score performance_difference
           <dbl> <chr>                 <dbl>        <dbl>                  <dbl>
 1             3 ADITYA BAJAJ           1.95          6                     4.05
 2            15 ZACHARY JAM…           1.37          4.5                   3.13
 3            10 ANVIT RAO              1.94          5                     3.06
 4            46 JACOB ALEXA…           0.04          3                     2.96
 5            37 AMIYATOSH P…           0.77          3.5                   2.73
 6             9 STEFANO LEE            2.29          5                     2.71
 7             2 DAKSHESH DA…           3.78          6                     2.22
 8            52 ETHAN GUO              0.3           2.5                   2.2 
 9            59 SEAN M MC C…           0.41          2                     1.59
10            58 VIRAJ MOHILE           0.43          2                     1.57
# ℹ 54 more rows

Checking the number of players

nrow(player_results)
[1] 64

The performance difference was calculated by subtracting each player’s Elo expected score from the player’s actual tournament score. Positive differences indicate that the player scored more points than expected based on the ratings of their opponents, while negative differences indicate that the player scored fewer points than expected.

Conclusion

The Elo calculations provided a way to compare each player’s actual tournament performance with the performance expected from the rating differences between the player and their opponents. After calculating and summing the expected scores, the difference between actual and expected performance was used to identify the five greatest over-performers and five greatest underperformers in the tournament.