Introduction

In this project, I work with a text file containing the results of a chess tournament with 64 players. The file contains each player’s pair number, name, state, total points, USCF ID, pre and post tournament ratings, and the result and opponent of each of the seven rounds. However, the file is designed to be read by people, not by software: each player’s information is split across two lines, fields are separated by pipe characters (|) and padded with spaces and dashed lines separate the records.

In other words, the data is not tidy. According to Wickham, Çetinkaya-Rundel, and Grolemund (2023), a dataset is tidy when each variable is a column, each observation is a row, and each value is a cell. In this file, a single observation (one player) spans two lines, and some cells hold more than one value. For example, in player 8’s record (Ezekiel Houghton), the cell “W 3” holds both the result and the opponent’s pair number, and “R: 1641P17->1657P24” holds the pre-rating, the post-rating, and provisional rating markers all in one place.

The goal is to transform this file into a tidy CSV file with one row per player and five columns: player’s name, state, total points, pre-tournament rating, and average pre-tournament rating of opponents. The last column does not exist in the file and must be calculated.

The chess rating system developed by Arpad Elo has been used in many other contexts, including evaluating job candidates. A player’s total points alone do not tell the full story, because scoring 5 points against strong opponents is more impressive than scoring 5 points against weak ones. The average opponent rating adds this context. As an extension, I will compare each player’s own rating with the average rating of their opponents.

Planned Approach

I will read the text file into R from GitHub and reshape it so that each player’s information is in a single row. Then I will extract the player’s name, state, total points, and pre-rating, along with the opponents they played.

To calculate the average opponent rating, I will look up each opponent’s pre-rating and average it over the games actually played. Finally, I will check the results against the assignment example, export the table as a CSV file, and compare each player’s rating with the average rating of their opponents.

Anticipated Challenges

The main challenge is calculating the average opponent rating correctly. Byes and unplayed rounds have no opponent, so they must be left out, and players who played fewer than seven games should be averaged only over the games they actually played. Since each opponent’s rating is stored in that opponent’s own row, all ratings need to be extracted before any averages can be calculated.

Cleaning the file also has its own difficulties. Each player’s information is split across two lines that must be matched correctly, and some cells hold more than one value as I mentioned before.

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
library(gganimate)
chess_raw <- read_lines("https://raw.githubusercontent.com/esradogan3/data607-project1/refs/heads/main/tournamentinfo.txt")
head(chess_raw)
## [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    |"
length(chess_raw)
## [1] 196
url <- "https://raw.githubusercontent.com/esradogan3/data607-project1/refs/heads/main/tournamentinfo.txt"

chess_raw <- readLines(url, warn = FALSE) # hide the warning while reading lines
chess_raw <- chess_raw[!grepl("^-+\\s*$", chess_raw)] # drop the dashed separator lines
chess_raw <- chess_raw[-(1:2)] # drop the two header lines (Pair/Num rows)

row1 <- chess_raw[seq(1, length(chess_raw), by = 2)] # pair num, name, points, rounds
row2 <- chess_raw[seq(2, length(chess_raw), by = 2)] # state, USCF ID / ratings

p1 <- read.table(text = row1, sep = "|", strip.white = TRUE,
                 quote = "", stringsAsFactors = FALSE)
p2 <- read.table(text = row2, sep = "|", strip.white = TRUE,
                 quote = "", stringsAsFactors = FALSE)

chess <- data.frame(
  id         = p1$V1,
  name       = p1$V2,
  state      = p2$V1,
  points     = as.numeric(p1$V3),
  pre_rating = as.numeric(sub(".*R:\\s*(\\d+).*", "\\1", p2$V2))
)

print(chess)
##    id                       name state points pre_rating
## 1   1                   GARY HUA    ON    6.0       1794
## 2   2            DAKSHESH DARURI    MI    6.0       1553
## 3   3               ADITYA BAJAJ    MI    6.0       1384
## 4   4        PATRICK H SCHILLING    MI    5.5       1716
## 5   5                 HANSHI ZUO    MI    5.5       1655
## 6   6                HANSEN SONG    OH    5.0       1686
## 7   7          GARY DEE SWATHELL    MI    5.0       1649
## 8   8           EZEKIEL HOUGHTON    MI    5.0       1641
## 9   9                STEFANO LEE    ON    5.0       1411
## 10 10                  ANVIT RAO    MI    5.0       1365
## 11 11   CAMERON WILLIAM MC LEMAN    MI    4.5       1712
## 12 12             KENNETH J TACK    MI    4.5       1663
## 13 13          TORRANCE HENRY JR    MI    4.5       1666
## 14 14               BRADLEY SHAW    MI    4.5       1610
## 15 15     ZACHARY JAMES HOUGHTON    MI    4.5       1220
## 16 16               MIKE NIKITIN    MI    4.0       1604
## 17 17         RONALD GRZEGORCZYK    MI    4.0       1629
## 18 18              DAVID SUNDEEN    MI    4.0       1600
## 19 19               DIPANKAR ROY    MI    4.0       1564
## 20 20                JASON ZHENG    MI    4.0       1595
## 21 21              DINH DANG BUI    ON    4.0       1563
## 22 22           EUGENE L MCCLURE    MI    4.0       1555
## 23 23                   ALAN BUI    ON    4.0       1363
## 24 24          MICHAEL R ALDRICH    MI    4.0       1229
## 25 25           LOREN SCHWIEBERT    MI    3.5       1745
## 26 26                    MAX ZHU    ON    3.5       1579
## 27 27             GAURAV GIDWANI    MI    3.5       1552
## 28 28 SOFIA ADINA STANESCU-BELLU    MI    3.5       1507
## 29 29           CHIEDOZIE OKORIE    MI    3.5       1602
## 30 30         GEORGE AVERY JONES    ON    3.5       1522
## 31 31               RISHI SHETTY    MI    3.5       1494
## 32 32      JOSHUA PHILIP MATHEWS    ON    3.5       1441
## 33 33                    JADE GE    MI    3.5       1449
## 34 34     MICHAEL JEFFERY THOMAS    MI    3.5       1399
## 35 35           JOSHUA DAVID LEE    MI    3.5       1438
## 36 36              SIDDHARTH JHA    MI    3.5       1355
## 37 37       AMIYATOSH PWNANANDAM    MI    3.5        980
## 38 38                  BRIAN LIU    MI    3.0       1423
## 39 39              JOEL R HENDON    MI    3.0       1436
## 40 40               FOREST ZHANG    MI    3.0       1348
## 41 41        KYLE WILLIAM MURPHY    MI    3.0       1403
## 42 42                   JARED GE    MI    3.0       1332
## 43 43          ROBERT GLEN VASEY    MI    3.0       1283
## 44 44         JUSTIN D SCHILLING    MI    3.0       1199
## 45 45                  DEREK YAN    MI    3.0       1242
## 46 46   JACOB ALEXANDER LAVALLEY    MI    3.0        377
## 47 47                ERIC WRIGHT    MI    2.5       1362
## 48 48               DANIEL KHAIN    MI    2.5       1382
## 49 49           MICHAEL J MARTIN    MI    2.5       1291
## 50 50                 SHIVAM JHA    MI    2.5       1056
## 51 51             TEJAS AYYAGARI    MI    2.5       1011
## 52 52                  ETHAN GUO    MI    2.5        935
## 53 53              JOSE C YBARRA    MI    2.0       1393
## 54 54                LARRY HODGE    MI    2.0       1270
## 55 55                  ALEX KONG    MI    2.0       1186
## 56 56               MARISA RICCI    MI    2.0       1153
## 57 57                 MICHAEL LU    MI    2.0       1092
## 58 58               VIRAJ MOHILE    MI    2.0        917
## 59 59          SEAN M MC CORMICK    MI    2.0        853
## 60 60                 JULIA SHEN    MI    1.5        967
## 61 61              JEZZEL FARKAS    ON    1.5        955
## 62 62              ASHWIN BALAJI    MI    1.0       1530
## 63 63       THOMAS JOSEPH HOSMER    MI    1.0       1175
## 64 64                     BEN LI    MI    1.0       1163

Checking if everything looks good:

nrow(chess)
## [1] 64
View(chess)

Step by Step Calculating Average Pre-Ratings of Opponent’s:

gsub("\\D", "", unlist(p1[1, 4:10]))
##   V4   V5   V6   V7   V8   V9  V10 
## "39" "21" "18" "14"  "7" "12"  "4"
opponents <- as.integer(gsub("\\D", "", unlist(p1[1, 4:10])))
opponents
## [1] 39 21 18 14  7 12  4
opp_ratings <- chess$pre_rating[opponents]
opp_ratings
## [1] 1436 1563 1600 1610 1649 1663 1716
mean(opp_ratings, na.rm = TRUE)
## [1] 1605.286
opp_avg <- numeric(64)

for (i in 1:64) {
  opponents   <- as.integer(gsub("\\D", "", unlist(p1[i, 4:10])))
  opp_ratings <- chess$pre_rating[opponents]
  opp_avg[i]  <- mean(opp_ratings, na.rm = TRUE)
}
chess <- mutate(chess, opp_avg_prerating = opp_avg)

head(chess)
##   id                name state points pre_rating opp_avg_prerating
## 1  1            GARY HUA    ON    6.0       1794          1605.286
## 2  2     DAKSHESH DARURI    MI    6.0       1553          1469.286
## 3  3        ADITYA BAJAJ    MI    6.0       1384          1563.571
## 4  4 PATRICK H SCHILLING    MI    5.5       1716          1573.571
## 5  5          HANSHI ZUO    MI    5.5       1655          1500.857
## 6  6         HANSEN SONG    OH    5.0       1686          1518.714
print(chess[c("pre_rating", "opp_avg_prerating")])
##    pre_rating opp_avg_prerating
## 1        1794          1605.286
## 2        1553          1469.286
## 3        1384          1563.571
## 4        1716          1573.571
## 5        1655          1500.857
## 6        1686          1518.714
## 7        1649          1372.143
## 8        1641          1468.429
## 9        1411          1523.143
## 10       1365          1554.143
## 11       1712          1467.571
## 12       1663          1506.167
## 13       1666          1497.857
## 14       1610          1515.000
## 15       1220          1483.857
## 16       1604          1385.800
## 17       1629          1498.571
## 18       1600          1480.000
## 19       1564          1426.286
## 20       1595          1410.857
## 21       1563          1470.429
## 22       1555          1300.333
## 23       1363          1213.857
## 24       1229          1357.000
## 25       1745          1363.286
## 26       1579          1506.857
## 27       1552          1221.667
## 28       1507          1522.143
## 29       1602          1313.500
## 30       1522          1144.143
## 31       1494          1259.857
## 32       1441          1378.714
## 33       1449          1276.857
## 34       1399          1375.286
## 35       1438          1149.714
## 36       1355          1388.167
## 37        980          1384.800
## 38       1423          1539.167
## 39       1436          1429.571
## 40       1348          1390.571
## 41       1403          1248.500
## 42       1332          1149.857
## 43       1283          1106.571
## 44       1199          1327.000
## 45       1242          1152.000
## 46        377          1357.714
## 47       1362          1392.000
## 48       1382          1355.800
## 49       1291          1285.800
## 50       1056          1296.000
## 51       1011          1356.143
## 52        935          1494.571
## 53       1393          1345.333
## 54       1270          1206.167
## 55       1186          1406.000
## 56       1153          1414.400
## 57       1092          1363.000
## 58        917          1391.000
## 59        853          1319.000
## 60        967          1330.200
## 61        955          1327.286
## 62       1530          1186.000
## 63       1175          1350.200
## 64       1163          1263.000

The table above places each player’s pre-rating next to their opponents’ average pre-rating, so the two can be compared directly. I discuss this comparison in more detail in the video.

write.csv(chess, "chess_ratings.csv", row.names = FALSE)

Additional Exploration: How the Standings Changed Round by Round

Beyond the main task, I wanted to see how the tournament unfolded over time. Using each player’s result in every round, I calculated their running point total after each of the 7 rounds and animated the top 10 players’ standings with gganimate. As a check, each player’s total after round 7 matches their final points value in the data.

# Step 1: Get each round's result letter (W, L, D, ...) from columns V4 to V10
rounds <- p1[, c("V4", "V5", "V6", "V7", "V8", "V9", "V10")]

for (i in 1:7) {
  rounds[[i]] <- substr(trimws(rounds[[i]]), 1, 1)
}

rounds$id     <- 1:nrow(rounds)
rounds$player <- chess$name

# Step 2: Convert letters to points (win = 1, draw = 0.5, loss = 0)
points_df <- rounds

for (i in 1:7) {
  letter <- points_df[[i]]
  points_df[[i]] <- ifelse(letter %in% c("W", "X", "B"), 1,
                    ifelse(letter %in% c("D", "H"), 0.5, 0))
}

# Step 3: Running total of points after each round
cum_df <- points_df

for (i in 2:7) {
  cum_df[[i]] <- cum_df[[i - 1]] + points_df[[i]]
}

# Step 4: Reshape to long format (one row per player per round)
long <- pivot_longer(cum_df,
                     cols = V4:V10,
                     names_to = "round",
                     values_to = "total")

long$round <- as.numeric(substr(long$round, 2, 3)) - 3

# Step 5: Keep the top 10 players in each round
top <- NULL

for (r in 1:7) {
  this_round <- long[long$round == r, ]
  this_round <- this_round[order(-this_round$total), ]
  this_round$rank <- 1:nrow(this_round)
  top <- rbind(top, this_round[1:10, ])
}

# Step 6: Animated bar chart of the standings, round by round
p <- ggplot(top, aes(x = rank, y = total, group = player, fill = player)) +
  geom_col(show.legend = FALSE) +
  geom_text(aes(label = player), hjust = -0.05, size = 3) +
  coord_flip(clip = "off") +
  scale_x_reverse() +
  scale_y_continuous(limits = c(0, 9)) +
  labs(title = "Standings after round {closest_state}",
       x = NULL, y = "Total points") +
  transition_states(round, transition_length = 2, state_length = 1)

animate(p, nframes = 100, fps = 10)

``` ## Using LLM: I used Claude (Anthropic, 2026) as a tutor while working on this project, to understand R functions and regular expressions, debug errors, and plan the calculation and animation steps. I wrote the code myself with its guidance. The link to the chat: https://claude.ai/share/ef57a130-5f58-4300-a86c-ca45e3cfdd47

References

Anthropic. (2026). Claude (Opus 5.5 version) [Large language model]. https://claude.ai Wickham, H., Çetinkaya-Rundel, M., & Grolemund, G. (2023). R for Data Science (2nd ed.). O’Reilly Media. https://r4ds.hadley.nz