Introduction
Objective
Data
Loading
Identifying
Player Information Lines
Extracting
Player Details
Overall
Approach
Calculating
the Average Opponent Rating
Valid
Games
Validation
Automated
Validation
Manual Verification-Step 1
Manual
Verification-Step 2
Test
Case 1: Player Who Played All Games
Test
Case 2: Player Who Played Fewer Games
Final
Output
Reproducibility
Data
Parsing Challenges
Code
For this project, I worked with a chess tournament cross-table that was not in a tidy format. My goal was to convert the original data into a clean dataset that could be used for analysis.
The final dataset has one row for each player and includes these columns:
• Player Name
• Player State
• Total Points
•
Pre-Tournament Rating
• Average Pre-Tournament Rating of Opponents
I used the tournament crosstable provided for the assignment. I think this dataset was complex enough to demonstrate the data cleaning and parsing skills required for this project. Each player is represented across multiple lines, and the round information includes opponent numbers and game results.
The main goal was to extract the player information from the original crosstable and determine the average pre-tournament rating of each player’s opponents.
The most difficult part of the project was not calculating an average. The main challenge was correctly identifying each opponent and connecting that opponent to the correct pre-tournament rating.
I also needed to handle situations where a player did not play a particular round, such as a bye or an unplayed game. Finally, I wanted the entire process to be reproducible so that the same code could be run again and produce the same results.
I loaded the tournament information directly from my GitHub repository using the raw text file URL. I used readLines() to read the file into R and then checked the number of lines and the first 10 rows to make sure the data was loaded correctly.
# Read the tournament information from GitHub
url <- "https://raw.githubusercontent.com/BIKASHBHOWMIK15/Data-607/main/Project-1/tournamentinfo.txt"
tournament_data <- readLines(url, warn = FALSE)
# Check the data
length(tournament_data)[1] 196
[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] "-----------------------------------------------------------------------------------------"
Identifying Player Information Lines
I identified the two types of lines containing player information in the raw tournament data. I then separated these lines so that the player details could be extracted and processed correctly.
# Identify the two types of player information lines
is_player_line1 <- str_detect(tournament_data, "^\\s*\\d+\\s*\\|")
is_player_line2 <- str_detect(tournament_data, "^\\s*[A-Z]{2}\\s*\\|")
# Extract the player information lines
player_line1 <- tournament_data[is_player_line1]
player_line2 <- tournament_data[is_player_line2]
# Check the number of lines found
length(player_line1)[1] 64
[1] 64
# Extract information from the first player line
pair_number <- as.integer(
str_match(player_line1, "^\\s*(\\d+)\\s*\\|")[, 2]
)
player_name <- str_match(
player_line1, "^\\s*\\d+\\s*\\|\\s*(.*?)\\s*\\|"
)[, 2] %>%
str_squish()
total_points <- as.numeric(
str_match(player_line1, "\\|\\s*([0-9]+\\.?[0-9]*)\\s*\\|")[, 2]
)
# Store the extracted information in a data frame
player_data <- tibble(
pair_number = pair_number,
player_name = str_to_title(player_name),
total_points = total_points,
line1_raw = player_line1
)
# Display the first five players
player_data %>%
slice(1:5)# A tibble: 5 × 4
pair_number player_name total_points line1_raw
<int> <chr> <dbl> <chr>
1 1 Gary Hua 6 " 1 | GARY HUA …
2 2 Dakshesh Daruri 6 " 2 | DAKSHESH DARURI …
3 3 Aditya Bajaj 6 " 3 | ADITYA BAJAJ …
4 4 Patrick H Schilling 5.5 " 4 | PATRICK H SCHILLING …
5 5 Hanshi Zuo 5.5 " 5 | HANSHI ZUO …
I extracted the player’s pair number, name, and total points from the first player information line. I stored these values in a tidy data frame for use in the next steps of the analysis.
# Extract the state abbreviation
state <- str_match(
player_line2, "^\\s*([A-Z]{2})\\s*\\|"
)[, 2]
# Extract the pre-tournament rating
# The pattern captures the numeric value after "R:"
pre_rating <- as.integer(
str_match(player_line2, "R:\\s*([0-9]+)")[, 2]
)
# Store the extracted information
player_details <- tibble(
state = state,
pre_rating = pre_rating,
line2_raw = player_line2
)
# Display the first five records
player_details %>%
slice(1:5)# A tibble: 5 × 3
state pre_rating line2_raw
<chr> <int> <chr>
1 ON 1794 " ON | 15445895 / R: 1794 ->1817 |N:2 |W |B |…
2 MI 1553 " MI | 14598900 / R: 1553 ->1663 |N:2 |B |W |…
3 MI 1384 " MI | 14959604 / R: 1384 ->1640 |N:2 |W |B |…
4 MI 1716 " MI | 12616049 / R: 1716 ->1744 |N:2 |W |B |…
5 MI 1655 " MI | 14601533 / R: 1655 ->1690 |N:2 |B |W |…
# Combine the two sets of player information
players <- bind_cols(
player_data %>%
select(pair_number, player_name, total_points, line1_raw),
player_details %>%
select(state, pre_rating, line2_raw)
) %>%
arrange(pair_number)
# Display the first 10 players
players %>%
slice(1:10)# A tibble: 10 × 7
pair_number player_name total_points line1_raw state pre_rating line2_raw
<int> <chr> <dbl> <chr> <chr> <int> <chr>
1 1 Gary Hua 6 " 1 |… ON 1794 " ON |…
2 2 Dakshesh Daruri 6 " 2 |… MI 1553 " MI |…
3 3 Aditya Bajaj 6 " 3 |… MI 1384 " MI |…
4 4 Patrick H Schi… 5.5 " 4 |… MI 1716 " MI |…
5 5 Hanshi Zuo 5.5 " 5 |… MI 1655 " MI |…
6 6 Hansen Song 5 " 6 |… OH 1686 " OH |…
7 7 Gary Dee Swath… 5 " 7 |… MI 1649 " MI |…
8 8 Ezekiel Hought… 5 " 8 |… MI 1641 " MI |…
9 9 Stefano Lee 5 " 9 |… ON 1411 " ON |…
10 10 Anvit Rao 5 " 10 |… MI 1365 " MI |…
1. Data Ingestion
I read the tournament text file from a public URL so that the project did not depend on a file stored only on my computer.
I read the file line by line and preserved the original formatting. This allowed me to work with the structure of the original cross-table.
2. Reconstructing Player Records
Each player is represented by two lines in the cross-table.
The first line contains information such as:
• Pair number
• Player name
• Total points
• Results for
each round
The second line contains information such as:
• State
• USCF ID
• Pre-tournament and post-tournament
ratings
I identified the two types of player information separately and combined them in their original order after verifying that 64 records of each type were found.
Regular expressions and string manipulation were used to extract the information I needed.
3. Extracting the Variables
For each player, I extracted:
• pair_number
• player_name
• state
• total_points
•
pre_rating
• Opponent pair numbers for each round
The round results may look like:
• W 39
• L 21
• D 12
The letter shows the result of the game, while the number identifies the opponent. I only needed the opponent number for calculating the average opponent rating.
Calculating the Average Opponent Rating
To calculate the average opponent rating, I created a lookup table that connected each player’s pair number with their pre-tournament rating.
For example:
pair_number → pre_rating
Then, for each player, I collected the opponent pair numbers from all rounds and used the lookup table to obtain the corresponding pre-tournament ratings.
I counted a round as a game when it contained a result indicator
(W, L, or D) followed by an opponent number. I did not count entries
such as:
• H – half-point bye
• B – bye
• U – unplayed
• X –
forfeit or win by absence
These entries do not provide an actual opponent whose pre-tournament
rating should be included in the average.
After identifying the
valid opponents, I used their pair numbers to look up their
pre-tournament ratings and calculate the mean.
This means that a player who played all seven rounds had the average
calculated from all of their opponents, while a player who played fewer
rounds had the average calculated only from the opponents they actually
played.
I performed manual checks to make sure the program produced the correct results.
Test Case 1: Player Who Played All Games
I selected a player who played all rounds and manually identified their opponents. I identified the opponents, found the pre-tournament rating of each opponent, and calculated the average by hand. I then compared this result with the value produced by my program.
Test Case 2: Player Who Played Fewer Games
I selected a player who did not play every round. I identified the rounds that were actually played and excluded byes or other non-game entries. I then manually calculated the average opponent rating and compared it with the program’s result.
These checks helped confirm that the opponent numbers were parsed correctly and that the correct denominator was used when calculating the average.
I stored all of the code in the Quarto file so that the complete process can be reproduced from beginning to end. The data was read from a public URL rather than from a local file. There was no manual editing of the data.
I also included checks to make sure that:
• Exactly 64 players
were processed.
• Opponent pair numbers were between 1 and 64.
• Opponent pair numbers successfully matched a player in the rating
lookup table.
• No missing pre-tournament ratings were created
during the lookup process.
• The final dataset contained one row
per player.
The main challenges in processing the data included:
• Correctly combining the two lines for each player.
• Dealing
with inconsistent spacing in the original text.
• Correctly
extracting opponent numbers from the round results.
•
Distinguishing actual games from byes and unplayed rounds.
• Making
sure that opponent numbers are connected to the correct player ratings.
• Avoiding parsing errors that could shift information from one
player to another.
# Extract opponent pair numbers from each player's tournament line
extract_opponents <- function(line) {
matches <- str_match_all(line, "(W|L|D)\\s*(\\d+)")[[1]]
if (nrow(matches) == 0) {
return(integer(0))
}
as.integer(matches[, 3])
}
# Add opponents and number of games played
players <- players %>%
mutate(
opponents = map(line1_raw, extract_opponents),
games_played = map_int(opponents, length)
)
# Display the first 12 players
players %>%
select(
pair_number,
player_name,
total_points,
games_played,
opponents
) %>%
slice(1:12)# A tibble: 12 × 5
pair_number player_name total_points games_played opponents
<int> <chr> <dbl> <int> <list>
1 1 Gary Hua 6 7 <int [7]>
2 2 Dakshesh Daruri 6 7 <int [7]>
3 3 Aditya Bajaj 6 7 <int [7]>
4 4 Patrick H Schilling 5.5 7 <int [7]>
5 5 Hanshi Zuo 5.5 7 <int [7]>
6 6 Hansen Song 5 7 <int [7]>
7 7 Gary Dee Swathell 5 7 <int [7]>
8 8 Ezekiel Houghton 5 7 <int [7]>
9 9 Stefano Lee 5 7 <int [7]>
10 10 Anvit Rao 5 7 <int [7]>
11 11 Cameron William Mc Leman 4.5 7 <int [7]>
12 12 Kenneth J Tack 4.5 6 <int [6]>
# Create a lookup table for each player's pre-tournament rating
rating_lookup <- players %>%
select(pair_number, pre_rating)
# Match each opponent with their pre-tournament rating
players <- players %>%
mutate(
opponent_pre_ratings = map(
opponents,
~ rating_lookup %>%
filter(pair_number %in% .x) %>%
pull(pre_rating)
),
# Calculate the average pre-tournament rating of the opponents
avg_opp_pre_rating = map_dbl(
opponent_pre_ratings,
~ if (length(.x) == 0) {
NA_real_
} else {
round(mean(.x), 0)
}
)
)
# Display the results for the first 12 players
players %>%
select(
pair_number,
player_name,
pre_rating,
opponents,
opponent_pre_ratings,
avg_opp_pre_rating
) %>%
slice(1:12)# A tibble: 12 × 6
pair_number player_name pre_rating opponents opponent_pre_ratings
<int> <chr> <int> <list> <list>
1 1 Gary Hua 1794 <int [7]> <int [7]>
2 2 Dakshesh Daruri 1553 <int [7]> <int [7]>
3 3 Aditya Bajaj 1384 <int [7]> <int [7]>
4 4 Patrick H Schilling 1716 <int [7]> <int [7]>
5 5 Hanshi Zuo 1655 <int [7]> <int [7]>
6 6 Hansen Song 1686 <int [7]> <int [7]>
7 7 Gary Dee Swathell 1649 <int [7]> <int [7]>
8 8 Ezekiel Houghton 1641 <int [7]> <int [7]>
9 9 Stefano Lee 1411 <int [7]> <int [7]>
10 10 Anvit Rao 1365 <int [7]> <int [7]>
11 11 Cameron William Mc Lem… 1712 <int [7]> <int [7]>
12 12 Kenneth J Tack 1663 <int [6]> <int [6]>
# ℹ 1 more variable: avg_opp_pre_rating <dbl>
# Check the opponent ratings for player 1
players %>%
filter(pair_number == 1) %>%
select(
pair_number,
player_name,
pre_rating,
opponents,
opponent_pre_ratings,
avg_opp_pre_rating
)# A tibble: 1 × 6
pair_number player_name pre_rating opponents opponent_pre_ratings
<int> <chr> <int> <list> <list>
1 1 Gary Hua 1794 <int [7]> <int [7]>
# ℹ 1 more variable: avg_opp_pre_rating <dbl>
For Player 1 (Pair #1), the opponents played were:
39, 21, 18, 14, 7, 12, 4
Their corresponding pre-ratings are:
1436, 1563, 1600, 1610, 1649, 1663, 1716
Hand calculation:
Sum = 1436 + 1563 + 1600 + 1610 + 1649 + 1663 + 1716
Sum = 11237
Number of games played = 7
Average = 11237 / 7 = 1605.2857
Rounded to nearest integer = 1605
The program output for Player 1 is 1605, confirming correct extraction, mapping, and denominator logic.
# Compare two selected players
players %>%
filter(pair_number %in% c(12, 16)) %>%
select(
pair_number,
player_name,
pre_rating,
total_points,
games_played,
opponents,
opponent_pre_ratings,
avg_opp_pre_rating
)# A tibble: 2 × 8
pair_number player_name pre_rating total_points games_played opponents
<int> <chr> <int> <dbl> <int> <list>
1 12 Kenneth J Tack 1663 4.5 6 <int [6]>
2 16 Mike Nikitin 1604 4 5 <int [5]>
# ℹ 2 more variables: opponent_pre_ratings <list>, avg_opp_pre_rating <dbl>
For Player 16 (Pair #16), the valid games were against:
10, 15, 39, 2, 36
The entries “H” and “U” are excluded because they do not represent valid opponents.
Their corresponding pre-ratings are:
1365, 1220, 1436, 1553, 1355
Hand calculation:
Sum = 1365 + 1220 + 1436 + 1553 + 1355
Sum = 6929
Number of valid games played = 5
Average = 6929 / 5 = 1385.8
Rounded to nearest integer = 1386
The program output (1386) matches the hand calculation, confirming correct handling of partial participation and exclusion of non-games.
# Automated Validation
# Validation 1: Confirm that the tournament has exactly 64 players
stopifnot(nrow(players) == 64)
# Verify that the expected number of player records was extracted
stopifnot(length(player_line1) == 64)
stopifnot(length(player_line2) == 64)
# Validation 2: Confirm that all opponent numbers are valid
all_opponent_numbers <- unlist(players$opponents)
stopifnot(
all(
all_opponent_numbers >= 1 &
all_opponent_numbers <= 64
)
)
# Validation 3: Confirm that every opponent has a valid pre-tournament rating
all_opponent_ratings <- unlist(players$opponent_pre_ratings)
stopifnot(!any(is.na(all_opponent_ratings)))
cat("All automated validation checks passed successfully.")All automated validation checks passed successfully.
Min. 1st Qu. Median Mean 3rd Qu. Max.
1107 1310 1382 1379 1481 1605
The final dataset contains exactly one row for each player. The columns are:
• Player_Name
• State
• Total_Points
• Pre_Rating
•
Average_Opponent_Pre_Rating
The complete final dataset was exported as a CSV file.
# Create the final summary dataset
final_df <- players %>%
transmute(
Player_Name = str_to_title(player_name),
State = state,
Total_Points = total_points,
Pre_Rating = pre_rating,
Average_Opponent_Pre_Rating = round(avg_opp_pre_rating, 0)
)
# Display the first 10 players as a table
final_df %>%
slice(1:10) %>%
knitr::kable(
caption = "Chess Tournament Data Summary"
)| 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 |
# Check that the final dataset contains exactly 64 players
stopifnot(nrow(final_df) == 64)
# Check that each player appears only once in the final dataset
stopifnot(!anyDuplicated(final_df$Player_Name))
# Verify that the expected number of player records was extracted
stopifnot(length(player_line1) == 64)
stopifnot(length(player_line2) == 64)
# Save the complete dataset as a CSV file
write_csv(final_df, "Project1_Chess_Data_Summary.csv")In this project, I worked with a semi-structured chess tournament cross-table and converted it into a tidy dataset using R. I extracted player names, states, total points, pre-tournament ratings, and opponent information from the original text file. I then matched each opponent with their pre-tournament rating and calculated the average pre-tournament rating of each player’s opponents.
I also used manual checks and automated validation to make sure the data was parsed correctly. The final dataset contains 64 players and provides the main information needed for further analysis. I created a summary table and saved the cleaned data as a CSV file. Overall, this project gave me practical experience working with semi-structured data, string parsing, data transformation, validation, and reproducible analysis in R.