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