How to read PGN data into a DataFrame

Viewed 992

I have a single .pgn (Portable Game Notation) of a large number of chess games. The games are contained in the file like this:

[Event "FIDE World Cup 2017"]
[Site "Tbilisi GEO"]
[Date "2017.09.05"]
[Round "1.1"]
[White "Carlsen, Magnus"]
[Black "Balogun, Oluwafemi"]
[Result "1-0"]
[WhiteTitle "GM"]
[BlackTitle "FM"]
[WhiteElo "2822"]
[BlackElo "2255"]
[ECO "B00"]
[Opening "King's pawn opening"]
[WhiteFideId "1503014"]
[BlackFideId "8501246"]
[EventDate "2017.09.03"]

1. e4 d6 2. d4 g6 3. Bc4 Nf6 4. Qe2 Nc6 5. Nf3 Bg7 6. O-O Bg4 7. c3 O-O         
8. h3 Bxf3 9. Qxf3 e5 10. Rd1 Qe8 11. d5 Ne7 12. Qe2 Nh5 13. Bb5 Qc8 
14. Na3 a6 15. Ba4 f5 16. Bc2 f4 17. Qg4 Qxg4 18. hxg4 Nf6 19. g5 Nd7 
20. Nc4 b6 21. b4 h6 22. gxh6 Bxh6 23. g4 Nf6 24. f3 Bg5 25. Kg2 Kg7 
26. a4 Bh4 27. Bd2 g5 28. Rh1 Ng6 29. Kf1 Rh8 30. Ke2 Bg3 31. a5 b5 32. 
Na3 Ne7 33. c4 c6 34. dxc6 Nxc6 35. Bc3 Rxh1 36. Rxh1 bxc4 37. Nxc4 Rb8 
38. Nxd6 Kg6 39. Nf5 1-0

[Event "FIDE World Cup 2017"]    
etc...

I would like to create a data frame with this data where the column titles are the word to the left of each string, and the data is the string. Then a separate column for the PGN string.

I have attempted this with influence from R: How to read in a PGN as a Data Frame and have come up with:

pgn <- read.table("~/Desktop/GitHub/Chess/test.pgn", quote="", 
stringsAsFactors=FALSE)

# get column names
column_names <- sub("\\[(\\w+).+", "\\1", pgn[1:17,1])
column_names[17] <- "PGN"
#create DF
pgn.df <- data.frame(matrix(sub("\\[\\w+ \\\"(.+)\\\"\\]", "\\1", 
                     pgn[,1]),byrow=TRUE, ncol=17))

names(pgn.df) <- column_names

The issues here is my pgn information is multiple lines. So is there a way to account for this in my regular expression? or a way to automatically change the file to make the pgn a single line?

Thanks!

3 Answers

I still suggest to remove the unwanted breaks in a preparatory step with an (updated) replace RegEx like this:

/(?:[^\[\]\n\S])\s*\n/ /g

You can test it online here (with a PGN as input text). But I had some problems with the escaping of special characters in R like you.
Therefore I decided to go with a Perl instead.

use strict;
use File::Slurp;
my $text = read_file($ARGV[0]);
$text =~ s/(?:[^\[\]\n\S])\s*\n/ /g;
write_file($ARGV[0], $text);

This can be called from R like this

system("perl ~/Desktop/regex.pl ~/Desktop/test.pgn")

I haven't tested this on Windows or Linux yet but the C codebase that the package is based on claims to be very portable. You'll need a R setup that enables compilation from source (i.e. if you're on Windows you need Rtools).

Install:

devtools::install_github("hrbrmstr/pigeon")

Use (tidyverse is not really needed for the package to work but IMO it prints the data frames more cleanly than the built-in base R print functions):

library(pigeon)
library(tidyverse)

Here's a small test with a built-in data set that might be the one you're also working from:

fide <- read_pgn(system.file("extdata", "r7.pgn", package="pigeon"))

fide
## # A tibble: 2 x 12
##            Event    Site       Date Round               White               Black  Result WhiteElo BlackElo   ECO
## *          <chr>   <chr>      <chr> <chr>               <chr>               <chr>   <chr>    <chr>    <chr> <chr>
## 1 World Cup 2017 Tbilisi 2017.09.23  44.1 Aronian Levon (ARM)    Ding Liren (CHN) 1/2-1/2     2799     2777   A18
## 2 World Cup 2017 Tbilisi 2017.09.24  45.1    Ding Liren (CHN) Aronian Levon (ARM) 1/2-1/2     2777     2799   E06
## # ... with 2 more variables: LiveChessVersion <chr>, Moves <list>

glimpse(fide)
## Observations: 2
## Variables: 12
## $ Event            <chr> "World Cup 2017", "World Cup 2017"
## $ Site             <chr> "Tbilisi", "Tbilisi"
## $ Date             <chr> "2017.09.23", "2017.09.24"
## $ Round            <chr> "44.1", "45.1"
## $ White            <chr> "Aronian Levon (ARM)", "Ding Liren (CHN)"
## $ Black            <chr> "Ding Liren (CHN)", "Aronian Levon (ARM)"
## $ Result           <chr> "1/2-1/2", "1/2-1/2"
## $ WhiteElo         <chr> "2799", "2777"
## $ BlackElo         <chr> "2777", "2799"
## $ ECO              <chr> "A18", "E06"
## $ LiveChessVersion <chr> "1.4.8", "1.4.8"
## $ Moves            <list> [c("c4", "Nf6", "Nc3", "e6", "e4", "d5", "cxd5", "exd5", "e5", "Ne4", "Nf3", "Bf5", "Be2"...

This is a larger test:

tf <- tempfile(fileext = ".zip")
td <- tempdir()
download.file("https://www.pgnmentor.com/players/Adams.zip",  tf)
fil <- unzip(tf, exdir = td)

adams <- read_pgn(fil)

adams
## # A tibble: 2,982 x 11
##             Event      Site       Date Round              White              Black  Result WhiteElo BlackElo   ECO
##  *          <chr>     <chr>      <chr> <chr>              <chr>              <chr>   <chr>    <chr>    <chr> <chr>
##  1 Lloyds Bank op    London 1984.??.??     1     Adams, Michael    Sedgwick, David     1-0                     C05
##  2 Lloyds Bank op    London 1984.??.??     3     Adams, Michael  Dickenson, Neil F     1-0              2230   C07
##  3 Lloyds Bank op    London 1984.??.??     4       Hebden, Mark     Adams, Michael     1-0     2480            B10
##  4 Lloyds Bank op    London 1984.??.??     5    Pasman, Michael     Adams, Michael     0-1     2310            D42
##  5 Lloyds Bank op    London 1984.??.??     6     Adams, Michael   Levitt, Jonathan 1/2-1/2              2370   B99
##  6 Lloyds Bank op    London 1984.??.??     9     Adams, Michael Saeed, Saeed Ahmed     1-0              2430   B56
##  7         BCF-ch Edinburgh 1985.??.??     1     Adams, Michael   Singh, Sukh Dave 1/2-1/2     2360     2080   B70
##  8         BCF-ch Edinburgh 1985.??.??     2 Abayasekera, Roger     Adams, Michael     1-0     2200     2360   B13
##  9         BCF-ch Edinburgh 1985.??.??     3     Adams, Michael    Jackson, Sheila 1/2-1/2     2360     2225   C85
## 10         BCF-ch Edinburgh 1985.??.??     4     Muir, Andrew J     Adams, Michael 1/2-1/2     2285     2360   E45
## # ... with 2,972 more rows, and 1 more variables: Moves <list>

glimpse(adams)
## Observations: 2,982
## Variables: 11
## $ Event    <chr> "Lloyds Bank op", "Lloyds Bank op", "Lloyds Bank op", "Lloyds Bank op", "Lloyds Bank op", "Lloyds ...
## $ Site     <chr> "London", "London", "London", "London", "London", "London", "Edinburgh", "Edinburgh", "Edinburgh",...
## $ Date     <chr> "1984.??.??", "1984.??.??", "1984.??.??", "1984.??.??", "1984.??.??", "1984.??.??", "1985.??.??", ...
## $ Round    <chr> "1", "3", "4", "5", "6", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "?", "1", "...
## $ White    <chr> "Adams, Michael", "Adams, Michael", "Hebden, Mark", "Pasman, Michael", "Adams, Michael", "Adams, M...
## $ Black    <chr> "Sedgwick, David", "Dickenson, Neil F", "Adams, Michael", "Adams, Michael", "Levitt, Jonathan", "S...
## $ Result   <chr> "1-0", "1-0", "1-0", "0-1", "1/2-1/2", "1-0", "1/2-1/2", "1-0", "1/2-1/2", "1/2-1/2", "1-0", "1/2-...
## $ WhiteElo <chr> "", "", "2480", "2310", "", "", "2360", "2200", "2360", "2285", "2360", "2250", "2360", "2225", "2...
## $ BlackElo <chr> "", "2230", "", "", "2370", "2430", "2080", "2360", "2225", "2360", "2245", "2360", "2260", "2360"...
## $ ECO      <chr> "C05", "C07", "B10", "D42", "B99", "B56", "B70", "B13", "C85", "E45", "C84", "B10", "C85", "A22", ...
## $ Moves    <list> [c("e4", "e6", "d4", "d5", "Nd2", "Nf6", "e5", "Nfd7", "f4", "c5", "c3", "Nc6", "Ndf3", "cxd4", "...

One nice thing about using a well-established C "library" (it's technically not a library but I shoehorned it into one) is that it does more than just pattern match. If the game file is malformed, it won't parse properly (as it shouldn't).

I need to run it through ASAN/UBSAN/Valgrind to make sure there are no memory leaks, but if this ends up being useful, let me know and I'll round out the corners on the pkg.

Another route you could go is converting .pgn to .csv which is panda's easiest file structure to parse.

https://pypi.org/project/pgn2data/

from converter.pgn_data import PGNData as pgnd
import pandas as pd

# This creates two output files, one for game info 
# (white_elo, black_elo, rating_diff, time_control... etc), 
# and one for moves.
 
filename = 'path to .pgn file'
pgn_data = pgnd(filename)
result = pgn_data.export()
result.print_summary()

# Then read the csv with pandas
# Change path to where your files output

path = 'Documents/github/project/folder/'
df_info = pd.read_csv(path + '_game_info.csv')
df_moves = pd.read_csv(path + '_moves.csv')
Related