Chess Tournament Data

Project 1: Code Base

Author

Jocelyn Slater

Introduction

We were tasked with taking a chess rating United States Chess Federation tournament crosstable, parsing the information, calculating each player’s average opponent’s pre-tournament ratings, and exporting that and various other information to a separate .csv file.

Data Import

Import the crosstable as plain text are remove lines that are just the separator rows and headers, just leaving lines with real content.

url <- "https://raw.githubusercontent.com/jocslater-code/DATA607/refs/heads/main/Project1/tournamentinfo.txt"
raw_txt <- suppressWarnings(readLines(url))
# Remove the separator lines and header
content_lines <- raw_txt[!str_detect(raw_txt, "^\\s*---")] 
content_lines <- content_lines[3:length(content_lines)]

Parse Data

Each player has data that spans two lines, so we need to combine the information from those lines into one combined string, and then parse that string by the | delimiter and white space. Then we place the matrix of parsed strings into a dataframe.

# Create a grouping variable for line combination
player_groups <- rep(1:(length(content_lines) / 2), each = 2)

# Combine the two lines for each player into a single string
combined_records <- tapply(content_lines, player_groups, paste0, collapse = " | ")

# Split by '|' whitespace 
split_matrix <- str_split(combined_records, "\\|")
cleaned_data <- lapply(split_matrix, function(row) str_trim(row))

# Convert to a data frame
df_all <- as.data.frame(do.call(rbind, cleaned_data), stringsAsFactors = FALSE)

Tidy Dataframe

To only work with necessary data, we create a tidy data frame with just subselected columns, relabeled for clarity.

# Sub-select the columns
df_tidy <- select(df_all, V1, V2, V3, V4, V5, V6, V7, V8, V9,V10, V12, V13)

# Rename the columns
colnames(df_tidy) <- c(
  "num", "player_name", "total_pts", "R1", "R2", "R3", "R4", "R5", 
  "R6", "R7", "state", "rating_str"
)

Parse Data and Calculate Opponent Information

We create a new column with each player’s pre-tournament rating,

# Parse the rating string to just get the pre-tournament rating
# Extract the digits inside the capture group after 'R:'
df_tidy$pre_rating <- str_match(df_tidy$rating_str, "R:\\s*(\\d+)")[, 2]
# \s is for spaces
# (\\d+) grabs all of the numbers up until the next non-digit character like a P, space, or ->

df_tidy$pre_rating <- as.numeric(df_tidy$pre_rating)

Then we extracted the opponent number for each round and saved them as seven new columns. Opponent numbers were subbed for NA in cases where the game was anything else then a win, lose, or draw since, for the purposes of our exercize, those were the only games that contributed.

# If the game is not won, lost, or draw, fill with NA
df_tidy <- df_tidy %>%
  mutate(across(starts_with("R", ignore.case = FALSE), ~ ifelse(str_detect(., "[WLD]"), ., NA)))

# Create new column that just has the opponent number for each round played
# There is a better way to do this with * or \\d 
df_tidy <- df_tidy %>%
  mutate(across(starts_with("R1", ignore.case = FALSE), 
                ~ as.numeric(str_extract(., "\\d+")), 
                .names = "{.col}_opp_name"))
df_tidy <- df_tidy %>%
  mutate(across(starts_with("R2", ignore.case = FALSE), 
                ~ as.numeric(str_extract(., "\\d+")), 
                .names = "{.col}_opp_name"))
df_tidy <- df_tidy %>%
  mutate(across(starts_with("R3", ignore.case = FALSE), 
                ~ as.numeric(str_extract(., "\\d+")), 
                .names = "{.col}_opp_name"))
df_tidy <- df_tidy %>%
  mutate(across(starts_with("R4", ignore.case = FALSE), 
                ~ as.numeric(str_extract(., "\\d+")), 
                .names = "{.col}_opp_name"))
df_tidy <- df_tidy %>%
  mutate(across(starts_with("R5", ignore.case = FALSE), 
                ~ as.numeric(str_extract(., "\\d+")), 
                .names = "{.col}_opp_name"))
df_tidy <- df_tidy %>%
  mutate(across(starts_with("R6", ignore.case = FALSE), 
                ~ as.numeric(str_extract(., "\\d+")), 
                .names = "{.col}_opp_name"))
df_tidy <- df_tidy %>%
  mutate(across(starts_with("R7", ignore.case = FALSE), 
                ~ as.numeric(str_extract(., "\\d+")), 
                .names = "{.col}_opp_name"))

Once we had an opponent’s name for each round, we then pulled the corresponding pre-rating and saved it as a numeric in another column

# Make new columns for opponent pre-ratings
opp_cols <- paste0("R", 1:7, "_opp_name")

# Loop through each opponent column and pull the matching pre_rating
for (col in opp_cols) {
  new_col <- paste0(col, "_score")
  df_tidy[[new_col]] <- df_tidy$pre_rating[df_tidy[[col]]]
}

Then it was simple to take the mean, removing NAs, of each row to obtain the average opponent pre-rating.

# Calculate average opponent pre-rating, ignoring NAs
score_cols <- paste0("R", 1:7, "_opp_name_score")

# Calculate row-wise mean ignoring NAs
df_tidy$avg_opp_rating <- round(rowMeans(df_tidy[score_cols], na.rm = TRUE))

Summary and Next Steps

With the initial crosstable parsing established, many more calculations could be performed on the chess tournament data to map round by round progress or visualize changes in ranking. The parsing work also stands as a blueprint of how to process other unconventional .csv files.

LLM Model Citations

Google DeepMind. (2026). Gemini 3.6 Flash [Large language model]. https://gemini.google.com. Accessed Sept 19, 2026. Transcript available in Github as Project1_Gemini_Transcript.pdf