momente şi schiţe de informatică şi matematică
To attain knowledge, write. To attain wisdom, rewrite.

Transformarea fișierului PGN în obiect de date (III)

PGN | limbajul R
2023 apr

Exemple de folosire a specificației Lichess API

Dacă te interesează statisticile ca atare, chiar și pentru un altul dintre cei înscriși pe Lichess, atunci este suficient să folosești browser-ul; de exemplu, tastând în bara de adresă https://lichess.org/@/{username}, obținem o pagină Web conținând statistici (redate grafic prin javaScript și SVG) asupra partidelor jucătorului "username":

Însă dacă te interesează datele, cu scopul de a construi tu însuți diverse statistici (lucrând de exemplu în R, sau în Python) – atunci ai de folosit interfața de programare oferită de Lichess (și în principiu, în loc de /@/{username} vei folosi /api/user/{username}).

Datele dorite pot fi obținute și direct, folosind programele utilitare wget (destinat pentru descărcarea ne-interactivă de fișiere de pe Web) sau curl (pentru transfer bilateral de fișiere, de pe sau pe server); de exemplu, prin:

vb@Home:~/23apr$ wget https://lichess.org/api/user/vlbz/rating-history
... 
Connecting to lichess.org (lichess.org)|37.187.205.99|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 9566 (9,3K) [application/json]
Saving to: ‘rating-history’

obținem fișierul text "rating-history", din care selectăm aici:

[{"name":"Bullet","points":[]},{"name":"Blitz","points":[]},{"name":"Rapid","points":[[2019,11,30,2139],[2020,0,1,1993],[2020,3,6,2012], ... , ,[2020,3,4,2240]]},{"name":"Correspondence","points":[]},{"name":"Chess960","points":[]},{"name":"King of the Hill","points":[]}, ... , {"name":"UltraBullet","points":[]}]

Datele cerute au fost furnizate în format JSON; parantezele pătrate delimitează un "tablou" (sau "array") de valori; acoladele delimitează "obiecte" (sau "dicționare" cheie : valoare). Am obținut deci un tablou de dicționare cu câte două chei, "name" și "points"; doar în cel cu numele "Rapid", avem în "points" un tablou nevid, conținând subtablouri de câte 4 numere, a căror interpretare nu este greu de intuit: anul, luna (cu 0 pentru Ianuarie), ziua și coeficientul Elo rezultat pentru vlbz în urma partidei respective.

În principiu, formatul JSON (ca orice format textual) este ușor de analizat; putem extrage subtablourile într-un data.frame și apoi putem formula (ca în programul stats.R din [1]) diverse statistici privitoare la variația coeficienților Elo respectivi.

Dar bineînțeles că există pachete pentru diverse limbaje, prin care putem substitui utilizarea directă a programului utilitar curl (de exemplu, pachetul R httr) și deasemenea, putem evita analiza și extragerea manuală dintr-un fișier JSON (pachetul R jsonlite):

# api_get.R  (using Lichess-API from R)
library(tidyverse)
library(httr)
library(jsonlite)
st_p <- GET("https://lichess.org/api/user/vlbz/perf/rapid")  # statistici performanță
print( http_error(st_p) )  # [1] FALSE
print( http_type(st_p) )  # [1] "application/json"
stp <- fromJSON( rawToChar(st_p $content) )
glimpse(stp)

Rezultatul (afișat prin glimpse()) este o listă stp care conține sub anumite chei, diverși indicatori de performanță calculați deja de către Lichess (la momentul primirii cererii), pentru vlbz (pe categoria "șah Rapid"). De exemplu, pentru coeficientul Elo al lui vlbz:

 > stp $perf $glicko  # sau: stp[["perf"]][["glicko"]]
$rating  [1] 2259.22  # media Elo (actuală)
$deviation  [1] 45.14  # abaterea standard

Subliniem că de data aceasta am primit „de-a gata” anumite statistici (și nu date brute, pe seama cărora să facem noi, diverse statistici); iar cea mai bună organizare și vizualizare o obținem formulând direct în browser, cererea https://lichess.org/@/vlbz/perf/rapid.

Aplicație (partenerul cel mai frecvent)

Ne pot interesa și alte statistici decât cele oferite direct de către Lichess
Imaginăm un exemplu simplu, dar potrivit pentru a clarifica diverse aspecte de folosire a specificației Lichess API.

vlbz a jucat un număr de partide cu câte un același partener; să identificăm pe cei cu care a jucat măcar câte 10 partide și să confruntăm rezultatele unuia și altuia.

Am putea refolosi lichess_vlbz.RDS obținut deja în [1] – dar „tabelul” respectiv conține partidele jucate până acum vreo lună, ori între timp, vlbz a făcut pe Lichess încă un anumit număr de partide.
Prin /api/games/user/vlbz putem obține partidele lui vlbz jucate până la momentul curent.

Fiindcă viteza de exportare a partidelor respective depinde de autorizarea cererii, am folosit întâi formularul din Lichess create token pentru a obține un "token" personal de access API (pe de altă parte, unele cereri API trebuie „semnate” în mod obligatoriu, cu un asemenea "token"). De observat că pe Lichess poți șterge la un moment dat, token-ul respectiv și poți crea, în diverse scopuri, mai mulți tokeni.

În principiu, tokenul obținut trebuie ținut pe cât posibil, secret; în acest scop, l-am înscris sub un anumit nume ("lichessToken") în fișierul local ".env"; incluzând pachetul dotenv, tokenul respectiv va fi produs când va fi cazul, prin Sys.getenv("lichessToken").

Inițiem programul prin care vom rezolva chestiunea propusă mai sus:

library(tidyverse)
library(httr)
library(jsonlite)
library(dotenv)  # Sys.getenv("lichessToken")

Ne putem asigura întâi, că tokenul nostru este valid – transmițându-l la api/token/test, prin POST() (ca text/plain, în "body"):

TT <- POST("https://lichess.org/api/token/test", 
           body = Sys.getenv("lichessToken"), 
           content_type('text/plain'))  # , verbose()
print(prettify(rawToChar(TT$content)))
    {
        "xxx_xxxxxxxxxxxxxxxxxxxx": {  # am mascat aici, "lichessToken"
            "userId": "vlbz",  # posesorul tokenului
            "scopes": "email:read,preference:read,preference:write, ...",
            "expires": null
        }
    }

Răspunsul este un obiect JSON care conține "userId", etc. dacă tokenul este recunoscut (sau null, în caz contrar); eliminăm TT și continuăm programul:

G <- GET("https://lichess.org/api/games/user/vlbz", add_headers(
         Authorization = Sys.getenv("lichessToken")),
         query = list(perfType = "rapid", moves = 0))
saveRDS(rawToChar(G$content), "pgn_tags.RDS")

Am vrut numai partidele din clasa "Rapid"; nu ne interesează, de data aceasta, secțiunile de mutări – ci doar tagurile partidelor. Am salvat rezultatul PGN, pentru a evita repetarea eventuală a cererii respective; mai departe, avem de analizat datele salvate în pgn_tags.RDS (și n-am mai avea nevoie de pachetele httr și jsonlite):

library(tidyverse)
Tgs <- readRDS("pgn_tags.RDS")

Tgs este un vector conținând un singur element de tip chr; redăm partea finală, evidențiind faptul că pentru fiecare partidă s-au păstrat numai tagurile, nu și secțiunea de mutări (încât lungimea este cam jumătate din cea a fișierului PGN complet):

> nchar(Tgs)  # [1] 803743  (lungimea șirului de caractere din Tgs)
"...
[Event \"Rated Rapid game\"]\n[Site \"https://lichess.org/gMsTUXyy\"]\n[Date \"2019.12.30\"]\n[White \"regis64\"]\n[Black \"vlbz\"]\n[Result \"0-1\"]\n[UTCDate \"2019.12.30\"]\n[UTCTime \"17:50:12\"]\n[WhiteElo \"1908\"]\n[BlackElo \"2017\"]\n[WhiteRatingDiff \"-4\"]\n[BlackRatingDiff \"+122\"]\n[Variant \"Standard\"]\n[TimeControl \"540+9\"]\n[ECO \"B21\"]\n[Termination \"Normal\"]\n\n0-1\n\n\n[Event \"Rated Rapid game\"]\n[Site \"https://lichess.org/AT4mamOx\"]\n[Date \"2019.12.30\"]\n[White \"vlbz\"]\n[Black \"Pichiruliev\"]\n[Result \"1-0\"]\n[UTCDate \"2019.12.30\"]\n[UTCTime \"17:33:00\"]\n[WhiteElo \"1500\"]\n[BlackElo \"1972\"]\n[WhiteRatingDiff \"+517\"]\n[BlackRatingDiff \"-57\"]\n[Variant \"Standard\"]\n[TimeControl \"900+8\"]\n[ECO \"D31\"]\n[Termination \"Normal\"]\n\n1-0\n\n\n"

Extrăgând din șirul de caractere Tgs, conținuturile tagurilor Black și White și excluzând din rezultat "vlbz", obținem un vector BW care conține partenerii lui vlbz, în fiecare dintre partidele respective; atunci table(BW) ne va da frecvența apariției fiecăruia – încât putem afla pe cei cu care vlbz a făcut măcar câte 10 partide:

Bl <- str_extract_all(Tgs, "(?<=Black \")([^\"]+)")[[1]]
Wh <- str_extract_all(Tgs, "(?<=White \")([^\"]+)")[[1]]
BW <- c(Bl[Bl != 'vlbz'], Wh[Wh != 'vlbz'])
BW <- sort(table(BW), decreasing=TRUE)
p10 <- names(BW[BW > 9])  # cei cu care 'vlbz' are măcar câte 10 partide

Precizăm că șablonul "(?<=prefix)(expr)" depistează prefix (dar fără a-l reține, ca în cazul parantezelor de grupare obișnuite) și culege grupul expr care îi urmează; în cazul nostru, expr este [^\"]+ – acoperind caractere diferite de ghilimele – dar dacă am ști că numele respective sunt compuse numai din litere, cifre și eventual '_', atunci puteam folosi \\w+.

Am reținut din BW numai numele, nu și frecvențele corespunzătoare – fiindcă de la bun început am intenționat obținerea prin Lichess API a statisticilor respective (numărul de partide cu fiecare și scorul total); de exemplu, prin:

> GET(paste0("https://lichess.org/api/crosstable/vlbz/Ay1010"))$content |>
     rawToChar() |> fromJSON()
$users
    $users$ay1010  # [1] 18.5
    $users$vlbz  # [1] 16.5
$nbGames  # [1] 35

obținem o listă cu două elemente, $users și $nbGames, unde $users este o listă care indică scorul pentru fiecare dintre cei doi jucători, iar $nbGames are ca valoare numărul de partide jucate împreună.
De observat că numele inițiale (de exemplu, "Ay1010") au fost transformate (prin tolower()) când au fost folosite ca nume de câmpuri ale listei $users ($ay1010 – fără majuscule).

Aplicând cererea exemplificată mai sus, pe fiecare nume P din vectorul p10, obținem prin map_dfr() un tabel care sintetizează datele primite:

DF <- map_dfr(p10, function(P) {
        Res <- GET(paste0("https://lichess.org/api/crosstable/vlbz/", P))
        JS <- fromJSON(rawToChar(Res$content))
        data.frame(game = paste("--", P),
                   nGames = JS$nbGames,
                   score = paste(JS$users[["vlbz"]], JS$users[[tolower(P)]], 
                                 sep="--"))
})
                       game nGames   score
        1         -- Ay1010     35   16.5--18.5
        2   -- justlik3that     21   12--9
        3         -- zellap     14   9.5--4.5
        4     -- CharlyXYZ5     13   8.5--4.5
        5       -- makeacer     12   6.5--5.5
        6       -- zenonwaw     12   5.5--6.5
        7 -- darkunorthodox     11   3.5--7.5
        8        -- embodas     10   6.5--3.5

Deci vlbz a jucat cel mai multe partide (35 la momentul curent) cu Ay1010, iar scorul total este 16.5–18.5 (în favoarea partenerului). Cel mai defavorabil scor (cu 4 puncte mai puțin ca partenerul) este la darkunorthodox
Dar repetăm – aici ne-a interesat nu statisticile și interpretarea lor, ci modul în care obținem datele necesare folosind R și interfața de programare oferită de Lichess.

vezi Cărţile mele (de programare)

docerpro | Prev | Next