Code
data_url <- paste0(
"https://raw.githubusercontent.com/dillonleeper/",
"DATA-607/main/assignments/week04/tournament-results.txt"
)
tournament_lines <- readLines(data_url, warn = FALSE)
length(tournament_lines)[1] 196
This project uses a text file containing USCF chess tournament results for 64 players across seven rounds. The goal is to use R to transform the raw tournament data into a clean CSV containing each player’s name, state, total points, pre-tournament rating, and average pre-tournament rating of their opponents.
Data source: Tournament results hosted on GitHub
I began by reading the raw text file into R and identifying the two lines associated with each player. From the first line, I extracted the player number, name, total points, and opponent numbers from the seven round fields. From the second line, I extracted the player’s state and pre-tournament Elo rating.
Next, I created a lookup table that connects each player number to that player’s pre-tournament rating. I used the opponent numbers from each round to retrieve the corresponding ratings and calculate each player’s average opponent rating. Rounds without an actual opponent, including byes and other entries marked B, H, U, or X, were excluded from the calculation.
One challenge was that some ratings contain provisional-game indicators, such as 1641P17. I extracted only the numerical rating portion. I also accounted for inconsistent spacing in the text file by using patterns rather than relying entirely on fixed character positions.
To verify the transformation, I confirmed that Gary Hua’s calculated average opponent rating rounds to 1605, as shown in the assignment instructions. I also checked that the completed dataset contains one row for each of the 64 players before exporting it as a CSV file. The raw data is stored at a publicly accessible GitHub URL so that the analysis can run without relying on a local file path.
The analysis reads the tournament report from its public GitHub URL. This makes the document reproducible without requiring a file stored on my computer.
data_url <- paste0(
"https://raw.githubusercontent.com/dillonleeper/",
"DATA-607/main/assignments/week04/tournament-results.txt"
)
tournament_lines <- readLines(data_url, warn = FALSE)
length(tournament_lines)[1] 196
Each player occupies two lines in the source report. A player line begins with a numeric pair number, and the following line contains the player’s state and rating information. Splitting the lines at the vertical bars makes it possible to extract the fields without depending on exact character positions.
The two lines contain different but aligned information. For example, W 39 on Gary Hua’s first line means that he won against the player whose pair number is 39. The W directly below that result on the second line means that Gary played with the White pieces; a B in that position means Black. The first line therefore provides the player’s pair number, name, total points, results, and opponent numbers, while the second provides the state, USCF ID, rating information, and color played in each round.
The pattern ^\\s*[0-9]+\\s*\\| identifies the beginning of a player record. In plain English, it looks for a line that starts with optional blank spaces, followed by one or more digits, more optional spaces, and a vertical bar. This selects lines beginning like 1 | GARY HUA | while ignoring headings and divider lines. Because the report follows a consistent format, the code can then retrieve the matching player’s detail line from the line immediately below it.
player_line_numbers <- grep("^\\s*[0-9]+\\s*\\|", tournament_lines)
extract_first_integer <- function(value) {
match_position <- regexpr("[0-9]+", value)
if (match_position[1] == -1) {
return(NA_integer_)
}
as.integer(regmatches(value, match_position))
}
parse_player <- function(line_number) {
player_fields <- trimws(strsplit(
tournament_lines[line_number],
"|",
fixed = TRUE
)[[1]])
detail_fields <- trimws(strsplit(
tournament_lines[line_number + 1],
"|",
fixed = TRUE
)[[1]])
round_fields <- player_fields[4:10]
list(
player_number = as.integer(player_fields[1]),
player_name = player_fields[2],
state = detail_fields[1],
total_points = as.numeric(player_fields[3]),
pre_rating = as.integer(sub(
"^.*R:\\s*([0-9]+).*$",
"\\1",
detail_fields[2]
)),
opponents = vapply(
round_fields,
extract_first_integer,
integer(1)
)
)
}
player_records <- lapply(player_line_numbers, parse_player)
players <- data.frame(
player_number = vapply(player_records, `[[`, integer(1), "player_number"),
player_name = vapply(player_records, `[[`, character(1), "player_name"),
state = vapply(player_records, `[[`, character(1), "state"),
total_points = vapply(player_records, `[[`, numeric(1), "total_points"),
pre_rating = vapply(player_records, `[[`, integer(1), "pre_rating"),
stringsAsFactors = FALSE
)
opponents <- do.call(
rbind,
lapply(player_records, `[[`, "opponents")
)
colnames(opponents) <- paste0("round_", 1:7)
head(players) player_number player_name state total_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
The helper function parse_player() organizes one player’s two-line record. lapply() then runs that same function for every player record found in the report, ensuring that all 64 players are processed consistently. Next, vapply() extracts one expected field, such as the name or rating, from every parsed record and verifies the type of the returned values. Finally, do.call(rbind, ...) stacks the seven opponent-number vectors into a matrix with one row per player and one column per round. The opponent-to-rating lookup occurs later; vapply() itself is not performing that lookup.
Codes such as B, H, U, and X represent rounds without a normal numbered opponent, including byes, unplayed rounds, or forfeits. Because these entries do not provide an opponent whose rating can be averaged, the parsing function records them as missing values and excludes them from the opponent-rating calculation. They are not converted to zero because doing so would incorrectly reduce the average.
Some pre-tournament ratings include provisional-status information. For example, 1641P17 means that the numeric rating is 1641 and that it is provisional based on 17 games. The code extracts 1641 because that is the portion needed for arithmetic; attempting to convert the complete text value to a number would produce a missing value or warning.
The named vector below connects each pair number to its pre-tournament rating. It acts like a dictionary: a pair number is the key, and that player’s rating is the corresponding value. For Gary Hua, the opponent numbers 39, 21, 18, 14, 7, 12, 4 are used to retrieve only those seven players’ ratings. The calculation then takes their mean and rounds it to produce Gary’s average opponent pre-rating of 1605.
rating_lookup <- setNames(players$pre_rating, players$player_number)
valid_opponent_numbers <- opponents[!is.na(opponents)]
average_opponent_rating <- apply(opponents, 1, function(opponent_numbers) {
opponent_numbers <- opponent_numbers[!is.na(opponent_numbers)]
opponent_ratings <- rating_lookup[as.character(opponent_numbers)]
round(mean(opponent_ratings))
})
project1_results <- data.frame(
player_name = players$player_name,
state = players$state,
total_points = players$total_points,
pre_rating = players$pre_rating,
average_opponent_pre_rating = as.integer(average_opponent_rating),
stringsAsFactors = FALSE
)
knitr::kable(
head(project1_results, 10),
caption = "First 10 rows of the completed tournament dataset"
)| player_name | state | total_points | pre_rating | average_opponent_pre_rating |
|---|---|---|---|---|
| GARY HUA | ON | 6.0 | 1794 | 1605 |
| DAKSHESH DARURI | MI | 6.0 | 1553 | 1469 |
| ADITYA BAJAJ | MI | 6.0 | 1384 | 1564 |
| PATRICK H SCHILLING | MI | 5.5 | 1716 | 1574 |
| HANSHI ZUO | MI | 5.5 | 1655 | 1501 |
| HANSEN SONG | OH | 5.0 | 1686 | 1519 |
| GARY DEE SWATHELL | MI | 5.0 | 1649 | 1372 |
| EZEKIEL HOUGHTON | MI | 5.0 | 1641 | 1468 |
| STEFANO LEE | ON | 5.0 | 1411 | 1523 |
| ANVIT RAO | MI | 5.0 | 1365 | 1554 |
The following checks confirm that the parser found all 64 players, that every recorded opponent maps to a player in the tournament, and that Gary Hua’s values match the example in the assignment. The final check also confirms that none of the five required output columns contains a missing value.
gary_hua <- project1_results[
project1_results$player_name == "GARY HUA",
]
stopifnot(
nrow(project1_results) == 64,
identical(players$player_number, 1:64),
all(valid_opponent_numbers %in% players$player_number),
nrow(gary_hua) == 1,
gary_hua$state == "ON",
gary_hua$total_points == 6,
gary_hua$pre_rating == 1794,
gary_hua$average_opponent_pre_rating == 1605,
!anyNA(project1_results)
)
write.csv(
project1_results,
"project1-results.csv",
row.names = FALSE
)
gary_hua player_name state total_points pre_rating average_opponent_pre_rating
1 GARY HUA ON 6 1794 1605
The completed dataset is available on GitHub: project1-results.csv.
The analysis successfully transformed the semi-structured tournament report into a clean dataset containing all 64 players. Gary Hua earned 6.0 points with a pre-tournament rating of 1794, and his seven opponents had an average pre-tournament rating of 1605, which matches the assignment’s expected result.
The result can support further questions about tournament performance, such as whether players who faced stronger opponents earned fewer points or which players performed above expectations relative to their starting ratings. A useful extension would be to include post-tournament ratings and calculate rating changes, allowing performance in this tournament to be compared with each player’s change in USCF rating.
The raw tournament report was supplied by the course instructor and is stored in this repository as tournament-results.txt.