Approach

For Project 1, I will work with the provided chess tournament cross-table text file and transform the semi-structured data into a clean dataset using R. The final dataset will contain each player’s name, state, total points, pre-tournament rating, and average pre-tournament rating of the opponents they played. My plan is to first import the text file and identify the repeating structure of the tournament results. I will then extract the player information and opponent numbers, match the opponent numbers back to the appropriate players and ratings, and calculate the average opponent rating for each player.

One challenge I anticipate is that the original dataset is not organized like a traditional spreadsheet. Each player’s information is spread across two lines, and the round results contain both game outcomes and opponent numbers. I will need to clean and restructure these fields carefully before creating the final dataset and exporting it as a CSV file.

Introduction

The purpose of Project 1 is to practice cleaning and transforming semi-structured data using R. Using chess tournament results, I will extract each player’s name, state, total points, and pre-tournament rating, then calculate the average pre-tournament rating of their opponents. The final cleaned dataset will be organized and exported as a CSV file for further analysis or database use.

Code Base

The chess tournament data is stored in a semi-structured text format rather than a traditional rectangular dataset. I will first import the raw text and inspect its structure before extracting the player information needed for the final dataset. Then we want to see the first several lines of the tournament file for inspection.

data_url <- "https://raw.githubusercontent.com/yeimiperez14/Data_607/refs/heads/main/Project%201/tournamentinfo.txt"

chess_raw <- readLines(data_url, warn = FALSE)
head(chess_raw, 20)
##  [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] "-----------------------------------------------------------------------------------------" 
## [11] "    3 | ADITYA BAJAJ                    |6.0  |L   8|W  61|W  25|W  21|W  11|W  13|W  12|" 
## [12] "   MI | 14959604 / R: 1384   ->1640     |N:2  |W    |B    |W    |B    |W    |B    |W    |" 
## [13] "-----------------------------------------------------------------------------------------" 
## [14] "    4 | PATRICK H SCHILLING             |5.5  |W  23|D  28|W   2|W  26|D   5|W  19|D   1|" 
## [15] "   MI | 12616049 / R: 1716   ->1744     |N:2  |W    |B    |W    |B    |W    |B    |B    |" 
## [16] "-----------------------------------------------------------------------------------------" 
## [17] "    5 | HANSHI ZUO                      |5.5  |W  45|W  37|D  12|D  13|D   4|W  14|W  17|" 
## [18] "   MI | 14601533 / R: 1655   ->1690     |N:2  |B    |W    |B    |W    |B    |W    |B    |" 
## [19] "-----------------------------------------------------------------------------------------" 
## [20] "    6 | HANSEN SONG                     |5.0  |W  34|D  29|L  11|W  35|D  10|W  27|W  21|"

Identify Player Records

The tournament file contains several types of rows, including headers, separator lines, player result rows, and player detail rows. The player result rows begin with a numerical player number, so I will use that pattern to identify them.

player_rows <- chess_raw[
  grepl("^\\s*[0-9]+\\s*\\|", chess_raw)
]

head(player_rows)
## [1] "    1 | GARY HUA                        |6.0  |W  39|W  21|W  18|W  14|W   7|D  12|D   4|"
## [2] "    2 | DAKSHESH DARURI                 |6.0  |W  63|W  58|L   4|W  17|W  16|W  20|W   7|"
## [3] "    3 | ADITYA BAJAJ                    |6.0  |L   8|W  61|W  25|W  21|W  11|W  13|W  12|"
## [4] "    4 | PATRICK H SCHILLING             |5.5  |W  23|D  28|W   2|W  26|D   5|W  19|D   1|"
## [5] "    5 | HANSHI ZUO                      |5.5  |W  45|W  37|D  12|D  13|D   4|W  14|W  17|"
## [6] "    6 | HANSEN SONG                     |5.0  |W  34|D  29|L  11|W  35|D  10|W  27|W  21|"

grepl() searches each line of the text file for a particular pattern: ^\s[0-9]+\s\| means ^ (start of the line),\s (allow spaces), [0-9]+ (look for one or more numbers), and \| (look for the pipe symbol). The matching rows are saved as player_rows.

Checking the Rows Found

I check the number of player rows that were identified to make sure the extraction step returned a reasonable number of records before continuing.

length(player_rows)
## [1] 64

This gives us the number of player records identified and stored in player_rows.

Separating the Player Rows into Separate Fields

Each player row contains several pieces of information separated by the pipe symbol. I split each row at the pipe separators so that the player number, name, points, and round results can be accessed individually.

player_parts <- strsplit(player_rows, "\\|")

strsplit() splits text into separate pieces and the | separator in the tournament file adds individual seprated fields that get stored in player_parts.

Extracting the Player Numner

The first field of each player row contains the player’s tournament number. I extract this value because the player number will later be used to connect each opponent number with that opponent’s pre-tournament rating.

player_number <- sapply(
  player_parts,
  function(x) as.integer(trimws(x[1]))
)

head(player_number)
## [1] 1 2 3 4 5 6

sapply() applies the same operation to every player’s row, selects the first field x[1], removes unnecessary spaces trimws(), and onverts the player number from text into an integer as.integer().

#Extracting Player Names

The second field of each player row contains the player’s name. I extract the second field and remove extra spaces so that each player name is stored cleanly.

player_name <- sapply(
  player_parts,
  function(x) trimws(x[2])
)

head(player_name)
## [1] "GARY HUA"            "DAKSHESH DARURI"     "ADITYA BAJAJ"       
## [4] "PATRICK H SCHILLING" "HANSHI ZUO"          "HANSEN SONG"

#Extracting Total Tournament Points

The third field contains the player’s total tournament points. I extract this field and convert it to numeric format so the values can be treated as numbers rather than text.

total_points <- sapply(
  player_parts,
  function(x) as.numeric(trimws(x[3]))
)

head(total_points)
## [1] 6.0 6.0 6.0 5.5 5.5 5.0

This selects the third field from each player’s row that contains values like 6.0 and converts those values into numeric data, so that we can quantify.

Identifying The Detail Rows

Extracting State and Pre-Tournament Rating

The tournament file stores additional information for each player on a second line. These detail rows begin with a two-letter state abbreviation followed by a pipe symbol. I identify these rows separately so that the state and pre-tournament rating can be extracted.

detail_rows <- chess_raw[
  grepl("^\\s*[A-Z]{2}\\s*\\|", chess_raw)
]

head(detail_rows)
## [1] "   ON | 15445895 / R: 1794   ->1817     |N:2  |W    |B    |W    |B    |W    |B    |W    |"
## [2] "   MI | 14598900 / R: 1553   ->1663     |N:2  |B    |W    |B    |W    |B    |W    |B    |"
## [3] "   MI | 14959604 / R: 1384   ->1640     |N:2  |W    |B    |W    |B    |W    |B    |W    |"
## [4] "   MI | 12616049 / R: 1716   ->1744     |N:2  |W    |B    |W    |B    |W    |B    |B    |"
## [5] "   MI | 14601533 / R: 1655   ->1690     |N:2  |B    |W    |B    |W    |B    |W    |B    |"
## [6] "   OH | 15055204 / R: 1686   ->1687     |N:3  |W    |B    |W    |B    |B    |W    |B    |"

Splitting the Detail Rows

The detail rows also use pipe symbols to separate information. I split these rows into individual fields so that the state and rating information can be extracted separately.

detail_parts <- strsplit(detail_rows, "\\|")

Extracting Player State

The first field of each detail row contains the player’s state or regional abbreviation. I extract this field and remove any extra spaces.

player_state <- sapply(
  detail_parts,
  function(x) trimws(x[1])
)

head(player_state)
## [1] "ON" "MI" "MI" "MI" "MI" "OH"

Isolating the Rating Field

The second field of the detail row contains several values, including a player ID, the pre-tournament rating, and the post-tournament rating. I first isolate this field before extracting only the pre-tournament rating.

rating_field <- sapply(
  detail_parts,
  function(x) trimws(x[2])
)

head(rating_field)
## [1] "15445895 / R: 1794   ->1817" "14598900 / R: 1553   ->1663"
## [3] "14959604 / R: 1384   ->1640" "12616049 / R: 1716   ->1744"
## [5] "14601533 / R: 1655   ->1690" "15055204 / R: 1686   ->1687"

This extracts the second section from every detail row.

Extracting the pre-tournament Rating

The pre-tournament rating appears after the text R: in the rating field. I use a regular expression to capture the number immediately following R: and convert it to numeric format.

pre_rating <- as.numeric(
  sub(
    ".*R:\\s*([0-9]+).*",
    "\\1",
    rating_field
  )
)

head(pre_rating)
## [1] 1794 1553 1384 1716 1655 1686

sub() searches text for a pattern and replaces it, that captures numbers after R, and turns it into a number.

Creating the initial player Table

I’ll combine the information extracted so far into a structured data frame. At this stage, the table contains the player’s tournament number, name, state, total points, and pre-tournament rating.

players <- data.frame(
  player_number,
  player_name,
  player_state,
  total_points,
  pre_rating
)

head(players)
##   player_number         player_name player_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

data.frame() takes the separate vectors created and combines them in one rectangular dataset.

Creating a Function to Extract Opponent numbers

Extracting Opponent Information

Each tournament round contains a result followed by the opponent’s player number. For example, W 39 indicates that the player won against player number 39. I create a function that extracts the numerical opponent IDs from all of the round fields for each player.

extract_opponents <- function(x) {

  fields <- strsplit(x, "\\|")[[1]]

  round_fields <- fields[4:length(fields)]

  opponent_numbers <- sapply(
    round_fields,
    function(field) {

      match_value <- regmatches(
        field,
        regexpr("[0-9]+", field)
      )

      if (length(match_value) == 0 || match_value == "") {
        NA
      } else {
        as.integer(match_value)
      }
    }
  )

  opponent_numbers[!is.na(opponent_numbers)]
}

strsplit(x, “\|”) splits one player’s row into its fields, then fields[4:length(fields)] selects the tournament round information because the first three fields contain player number/name/total points. regexpr(“[0-9]+”, field) looks for a number in each row and !is.na() removed missing values.

Before applying the function to every player, we can test it using the first player, Gary Hua. This allows me to verify that the opponent numbers are being extracted correctly.

gary_opponents <- extract_opponents(player_rows[1])

gary_opponents
## W  39 W  21 W  18 W  14 W   7 D  12 D   4 
##    39    21    18    14     7    12     4

player_rows[1] selects the first player’s row.

Matching Gary Hua’s opponents to their pre-ratings

The opponent numbers identify players rather than ratings. I’ll therefore match each opponent number to the corresponding player number in the player table and retrieve that opponent’s pre-tournament rating.

gary_ratings <- players$pre_rating[
  match(gary_opponents, players$player_number)
]

gary_ratings
## [1] 1436 1563 1600 1610 1649 1663 1716

match() finds where each opponent number appears in players$player_number.

Calculating Gary Hua’s Average Opponent Rating

I’ll calculate the average of Gary Hua’s opponents’ pre-tournament ratings as a validation step. The project instructions state that Gary’s average should equal 1605, so this provides a useful check that the extraction and matching process is working correctly.

round(mean(gary_ratings, na.rm = TRUE))
## [1] 1605

Calculating the Opponent Average for every player

After confirming that the calculation works for the first player, I’ll apply the same process to every player in the tournament. For each player, the code extracts the opponent numbers, matches those opponents to their pre-tournament ratings, and calculates the rounded average.

average_opponent_rating <- sapply(
  player_rows,
  function(row) {

    opponents <- extract_opponents(row)

    opponent_ratings <- players$pre_rating[
      match(opponents, players$player_number)
    ]

    round(mean(opponent_ratings, na.rm = TRUE))
  }
)

head(average_opponent_rating)
##     1 | GARY HUA                        |6.0  |W  39|W  21|W  18|W  14|W   7|D  12|D   4| 
##                                                                                      1605 
##     2 | DAKSHESH DARURI                 |6.0  |W  63|W  58|L   4|W  17|W  16|W  20|W   7| 
##                                                                                      1469 
##     3 | ADITYA BAJAJ                    |6.0  |L   8|W  61|W  25|W  21|W  11|W  13|W  12| 
##                                                                                      1564 
##     4 | PATRICK H SCHILLING             |5.5  |W  23|D  28|W   2|W  26|D   5|W  19|D   1| 
##                                                                                      1574 
##     5 | HANSHI ZUO                      |5.5  |W  45|W  37|D  12|D  13|D   4|W  14|W  17| 
##                                                                                      1501 
##     6 | HANSEN SONG                     |5.0  |W  34|D  29|L  11|W  35|D  10|W  27|W  21| 
##                                                                                      1519

sapply() repeats the same process for every player and produces one average opponent rating for every player.

Final Dataset

Creating the Final Dataset

I’ll combine the required variables into the final dataset. The project requires Player Name, State, Total Points, Pre-Rating, and Average Pre Chess Rating of Opponents.

final_chess_data <- data.frame(
  Player_Name = player_name,
  State = player_state,
  Total_Points = total_points,
  Pre_Rating = pre_rating,
  Average_Opponent_Pre_Rating = average_opponent_rating
)

head(final_chess_data)
##                                                                                                   Player_Name
##     1 | GARY HUA                        |6.0  |W  39|W  21|W  18|W  14|W   7|D  12|D   4|            GARY HUA
##     2 | DAKSHESH DARURI                 |6.0  |W  63|W  58|L   4|W  17|W  16|W  20|W   7|     DAKSHESH DARURI
##     3 | ADITYA BAJAJ                    |6.0  |L   8|W  61|W  25|W  21|W  11|W  13|W  12|        ADITYA BAJAJ
##     4 | PATRICK H SCHILLING             |5.5  |W  23|D  28|W   2|W  26|D   5|W  19|D   1| PATRICK H SCHILLING
##     5 | HANSHI ZUO                      |5.5  |W  45|W  37|D  12|D  13|D   4|W  14|W  17|          HANSHI ZUO
##     6 | HANSEN SONG                     |5.0  |W  34|D  29|L  11|W  35|D  10|W  27|W  21|         HANSEN SONG
##                                                                                           State
##     1 | GARY HUA                        |6.0  |W  39|W  21|W  18|W  14|W   7|D  12|D   4|    ON
##     2 | DAKSHESH DARURI                 |6.0  |W  63|W  58|L   4|W  17|W  16|W  20|W   7|    MI
##     3 | ADITYA BAJAJ                    |6.0  |L   8|W  61|W  25|W  21|W  11|W  13|W  12|    MI
##     4 | PATRICK H SCHILLING             |5.5  |W  23|D  28|W   2|W  26|D   5|W  19|D   1|    MI
##     5 | HANSHI ZUO                      |5.5  |W  45|W  37|D  12|D  13|D   4|W  14|W  17|    MI
##     6 | HANSEN SONG                     |5.0  |W  34|D  29|L  11|W  35|D  10|W  27|W  21|    OH
##                                                                                           Total_Points
##     1 | GARY HUA                        |6.0  |W  39|W  21|W  18|W  14|W   7|D  12|D   4|          6.0
##     2 | DAKSHESH DARURI                 |6.0  |W  63|W  58|L   4|W  17|W  16|W  20|W   7|          6.0
##     3 | ADITYA BAJAJ                    |6.0  |L   8|W  61|W  25|W  21|W  11|W  13|W  12|          6.0
##     4 | PATRICK H SCHILLING             |5.5  |W  23|D  28|W   2|W  26|D   5|W  19|D   1|          5.5
##     5 | HANSHI ZUO                      |5.5  |W  45|W  37|D  12|D  13|D   4|W  14|W  17|          5.5
##     6 | HANSEN SONG                     |5.0  |W  34|D  29|L  11|W  35|D  10|W  27|W  21|          5.0
##                                                                                           Pre_Rating
##     1 | GARY HUA                        |6.0  |W  39|W  21|W  18|W  14|W   7|D  12|D   4|       1794
##     2 | DAKSHESH DARURI                 |6.0  |W  63|W  58|L   4|W  17|W  16|W  20|W   7|       1553
##     3 | ADITYA BAJAJ                    |6.0  |L   8|W  61|W  25|W  21|W  11|W  13|W  12|       1384
##     4 | PATRICK H SCHILLING             |5.5  |W  23|D  28|W   2|W  26|D   5|W  19|D   1|       1716
##     5 | HANSHI ZUO                      |5.5  |W  45|W  37|D  12|D  13|D   4|W  14|W  17|       1655
##     6 | HANSEN SONG                     |5.0  |W  34|D  29|L  11|W  35|D  10|W  27|W  21|       1686
##                                                                                           Average_Opponent_Pre_Rating
##     1 | GARY HUA                        |6.0  |W  39|W  21|W  18|W  14|W   7|D  12|D   4|                        1605
##     2 | DAKSHESH DARURI                 |6.0  |W  63|W  58|L   4|W  17|W  16|W  20|W   7|                        1469
##     3 | ADITYA BAJAJ                    |6.0  |L   8|W  61|W  25|W  21|W  11|W  13|W  12|                        1564
##     4 | PATRICK H SCHILLING             |5.5  |W  23|D  28|W   2|W  26|D   5|W  19|D   1|                        1574
##     5 | HANSHI ZUO                      |5.5  |W  45|W  37|D  12|D  13|D   4|W  14|W  17|                        1501
##     6 | HANSEN SONG                     |5.0  |W  34|D  29|L  11|W  35|D  10|W  27|W  21|                        1519

Before exporting the data, I’ll perform basic checks on the final dataset to confirm its dimensions, review the values, and identify any missing observations.

dim(final_chess_data)
## [1] 64  5
summary(final_chess_data)
##     Player_Name       State     Total_Points     Pre_Rating  
##  Length   :64   Length   :64   Min.   :1.000   Min.   : 377  
##  N.unique :64   N.unique : 3   1st Qu.:2.500   1st Qu.:1227  
##  N.blank  : 0   N.blank  : 0   Median :3.500   Median :1407  
##  Min.nchar: 6   Min.nchar: 2   Mean   :3.438   Mean   :1378  
##  Max.nchar:26   Max.nchar: 2   3rd Qu.:4.000   3rd Qu.:1583  
##                                Max.   :6.000   Max.   :1794  
##  Average_Opponent_Pre_Rating
##  Min.   :1107               
##  1st Qu.:1310               
##  Median :1382               
##  Mean   :1379               
##  3rd Qu.:1481               
##  Max.   :1605
sum(is.na(final_chess_data))
## [1] 0

Exporting the CSV

The final requirement is to generate a CSV file containing the cleaned player information. I export the completed data frame without R row numbers so the resulting file contains only the required variables. Then I verified that the CSV file was successfully successfully created and then read it back into R to confirm that the exported data can be accessed correctly.

write.csv(
  final_chess_data,
  "chess_players_final.csv",
  row.names = FALSE
)
file.exists("chess_players_final.csv")
## [1] TRUE
csv_check <- read.csv("chess_players_final.csv")

head(csv_check)
##           Player_Name State Total_Points Pre_Rating Average_Opponent_Pre_Rating
## 1            GARY HUA    ON          6.0       1794                        1605
## 2     DAKSHESH DARURI    MI          6.0       1553                        1469
## 3        ADITYA BAJAJ    MI          6.0       1384                        1564
## 4 PATRICK H SCHILLING    MI          5.5       1716                        1574
## 5          HANSHI ZUO    MI          5.5       1655                        1501
## 6         HANSEN SONG    OH          5.0       1686                        1519
write.csv(
  final_chess_data,
  "chess_players_final.csv",
  row.names = FALSE
)