momente şi schiţe de informatică şi matematică
anti point—and—click

Repartizarea pe zile a încadrării profesorilor (III)

R | orar şcolar
2021 jan

Procurarea datelor

Până acum ne-am folosit de un singur set de date, extras dintr-un fişier Excel (v. [1]); căutând cu Google "orar liceu", putem obţine orarele pentru multe şcoli – dar nu ca fişier Excel, ci fie ca document PDF, fie (mai rar) ca fişier HTML (slavă Domnului – cazul grosolan, când orarul este furnizat printr-o poză JPG este din ce în ce mai rar). Extragerea datelor necesare dintr-un document PDF este dificilă (spre deosebire de cazul HTML); în ultimul timp s-au pus totuşi la punct metode (suficient de exacte) pentru aceasta şi s-au înfiinţat site-uri care îţi returnează un fişier Excel conţinând tabelele existente în documentul PDF pe care l-ai postat.

În vederea unor noi experimente – folosind R – am ales un orar bine făcut, prezentat decent prin două documente PDF, unul pentru profesori şi unul pentru clase; fiecare pagină expune orarul unui profesor, respectiv al unei clase. Pagina profesorului are ca titlu "Profesor Nume Prenume" şi conţine un tabel cu 5 linii (pentru zilele săptămânii) şi 12 coloane (pentru cele 12 ore din fiecare zi); datele din acest tabel reprezintă clasele (notate sub forma "12.A", sau "5") la care trebuie să intre acel profesor în ziua şi ora respective şi obiectul pe care îl predă (obiectul şi clasa fiind separate în celula în care apar împreună, prin "\r\n"). Pagina clasei are o structură asemănătoare, conţinând ca date obiectele programate pe zile şi ore (împreună cu numele profesorilor corespunzători acestora la acea clasă); în plus—iar aceasta este o idee foarte bună (şi n-am întâlnit-o în alte locuri)—este anexat un mic tabel conţinând „planul-cadru” corespunzător profilului clasei respective: ce discipline sunt prevăzute şi cu câte ore pe săptămână.

Este suficient să extragem datele de pe fişierul PDF pentru profesori; deocamdată, nu ne interesează orarul propriu-zis, ci (ca şi în [1]) doar încadrarea: clasele repartizate fiecărui profesor şi numărul de ore corespunzător.

În principiu, evităm să depindem de vreun serviciu extern („de digitalizare”); căutăm mai degrabă vreun program utilitar potrivit, de invocat fie direct (de pe linia de comandă), fie indirect (dintr-un alt program). Prin Camelot, cu opţiunea "lattice" am obţinut un fişier Excel cu mai multe foi, câte una pentru fiecare pagină PDF, conţinând tabelele propriu-zise, dar nu şi titlurile acestora (încât numele profesorilor ar trebui montate ulterior, probabil manual), iar numele coloanelor fac parte efectiv din tabel (constituind primul rând de celule al acestuia); cu opţiunea "stream", tabelul din foaia Excel corespunzătoare uneia sau alteia dintre paginile PDF include şi titlul (dar mai include şi toate celelalte înscrisuri din pagina PDF: numele şcolii, data şi numele programului folosit pentru producerea orarului), apărând în plus acest defect: caracterele de „sfârşit de rând” "\r\n" existente în celulele tabelului PDF au fost interpretate la formularea în Excel ca ţinând de rânduri consecutive ale acelui tabel.
"Camelot" este un produs mai nou şi probabil va fi pus la punct, cu timpul; dar eu nu-l voi mai folosi, după ce am văzut că se bazează pe o serie de pachete Python (numpy, pandas, pdfminer, PyPDF2) de care chiar nu am nevoie, câtă vreme folosesc R (şi folosesc direct Ghostscript).

Până la urmă, am folosit serviciile de conversie (deocamdată gratuite) de la iLovePDF.com şi am fost foarte mulţumit de rezultat: fişierul .xlsx returnat conţine 118 foi ("Table 1"..."Table 118"); pe foile impare avem câte un tabel cu o singură linie, conţinând în celula "A" numele şcolii, iar în celula "B" numele unui profesor; pe foile pare avem câte un tabel cu exact aceeaşi structură şi conţinut ca tabelul din PDF, conţinând încadrarea corespunzătoare profesorului numit în foaia precedentă (iar înscrisurile „de subsol” existente în paginile PDF au fost ignorate).

Prin următorul program R obţinem din fişierul Excel respectiv, un obiect de tip data.frame (un „set de date”) – folosind pentru aceasta pachetul readxl:

library(tidyverse)
library(readxl)

path <- "orarP.xlsx"  # fişierul Excel (cu 118 "sheets") conţinând orarul şcolii
orarDF <- path %>%
          excel_sheets() %>%  # lista foilor Excel conţinute în fişier
          set_names() %>%
          map(read_excel, path = path)  # citeşte fiecare foaie, în câte un "tibble"
# 'orarDF' este o listă conţinând cele 118 obiecte "tibble" asociate foilor

numePr <- c(paste0("P0", (1:9)), paste0("P", (10:59)))  # anonimizează numele
for(i in (1:59)) {
    nrr <- nrow(orarDF[[2*i]])
    orarDF[[(2*i)]]$prof <- rep(numePr[i], nrr)
}  # adaugă în obiectele "tibble" de la indicii pari din lista 'orarDF', coloana numelor

orarDF <- seq(2, 118, by=2) %>%
          map_df(., function(i) orarDF[[i]]) %>%  # reuneşte obiectele "tibble"
          relocate(., prof, before=1) %>%  # coloana 'prof' devine prima coloană
          rename(., zl = `...1`)  # redenumeşte prin 'zl', coloana zilelor
       
saveRDS(orarDF, file="orar.rds")  # serializează şi salvează obiectul "tibble" final

Pentru a nu ofensa cumva reglementările europene privitoare la „datele personale”, am anonimizat numele profesorilor din orarul iniţial (şi probabil exagerând, am evitat să indic numele şcolii al cărei orar l-am preluat de pe Internet). Obiectul R final (salvat ca atare în fişierul "orar.rds") este un tibble care ca structură de date arată astfel:

> str(orarDF, vec.len=7, give.length=FALSE)
tibble [300 × 14] (S3: tbl_df/tbl/data.frame)
 $ prof: chr  "P01" "P01" "P01" "P01" "P01" "P02" "P02" ...
 $ zl  : chr  "Lu" "Ma" "Mi" "Jo" "Vi" "Lu" "Ma" ...
 $ 1   : chr  NA NA NA NA "Rom\r\n11.D" NA NA ...
 $ 2   : chr  NA NA NA NA "Rom\r\n11.D" "Rom\r\n11.B" NA ...
 $ 3   : chr  NA NA NA NA NA "Rom\r\n12.C" NA ...
 $ 4   : chr  NA NA NA NA NA "Rom\r\n12.C" NA ...
 $ 5   : chr  NA NA NA NA NA "Rom\r\n12.E" "Rom\r\n12.C" ...
 $ 6   : chr  NA "Rom\r\n11.D" NA NA NA NA "Rom\r\n12.E" ...
 $ 7   : chr  "Rom\r\n9.C" "Rom\r\n10.C" NA "Rom\r\n10.D" NA NA "Rom\r\n9.G" ...
 $ 8   : chr  "Rom\r\n10.C" "Rom\r\n9.C" NA "Rom\r\n10.C" NA NA NA ...
 $ 9   : chr  "Rom\r\n10.D" "Rom\r\n10.D" NA NA NA NA NA ...
 $ 10  : chr  NA NA NA "Rom\r\n9.C" NA NA NA ...
 $ 11  : chr  NA NA NA "Rom\r\n9.C" NA NA NA ...
 $ 12  : chr  NA NA NA NA NA NA NA ...

În termeni obişnuiţi, orarDF este un „tabel” cu 300 de linii, fiecare conţinând câte 14 „date”; toate datele sunt valori de tip chr („şir de caractere”) – dar acele date care „lipsesc” sunt desemnate prin valoarea specială NA, însemnând în cazul nostru, că profesorul este „liber” în ziua indicată în coloana $zl şi ora indicată de una dintre coloanele $`1`..$`12` (numele de coloană care încep cu cifră trebuie ambalate cu backtick („accent grav”)). Desigur, datele referitoare la discipline (precum "Rom", mai sus) vor trebui separate (într-o coloană proprie) de cele referitoare la clase.

Datele pot fi selectate şi apoi redate în diverse forme; de exemplu, putem folosi operatorul de transpunere t() pentru a reda ultimele 5 linii din tabel (cu tail()) în forma folosită mai sus de către funcţia str():

> t(tail(orarDF, 5))
     [,1]  [,2]             [,3]  [,4]             [,5] 
prof "P59" "P59"            "P59" "P59"            "P59"
zl   "Lu"  "Ma"             "Mi"  "Jo"             "Vi" 
1    NA    NA               NA    NA               NA   
2    NA    NA               NA    NA               NA   
3    NA    NA               NA    NA               NA   
4    NA    NA               NA    NA               NA   
5    NA    NA               NA    NA               NA   
6    NA    NA               NA    NA               NA   
7    NA    NA               NA    NA               NA   
8    NA    NA               NA    NA               NA   
9    NA    "Ed.Fiz\r\n9.E"  NA    "Ed.Fiz\r\n10.B" NA   
10   NA    "Ed.Fiz\r\n10.B" NA    "Ed.Fiz\r\n9.B"  NA   
11   NA    NA               NA    NA               NA   
12   NA    NA               NA    NA               NA   

Vedem astfel că profesorul "P59" are ore pe disciplina "Ed.Fiz" în zilele "Ma" şi "Jo", la clasele "9.E" şi "10.B" şi este liber în celelalte zile (când probabil, are ore la o altă şcoală); în orarul propriu-zis, orele respective sunt într-o anumită ordine, a 9-a şi a 10-a din zi – dar subliniem că aici ne interesează doar „încadrarea”, adică valorile pentru profesor, disciplină şi clasă, împreună cu numărul săptămânal de ore aferent acestora (urmând să experimentăm asupra alocării pe zile a orelor respective).

Desigur, nu ne scapă acest aspect: fiind 59 de profesori şi 5 zile, ar trebui să avem exact 5×59=295 de linii – ori tabelul obţinut are 300 de linii; de ce apar încă 5 linii, în tabel? Constatăm că în coloana $zl apar şi 5 valori NA:

> orarDF[is.na(orarDF$zl), (1:2)]
# A tibble: 5 x 2
  prof  zl   
  <chr> <chr>
1 P49   NA   
2 P49   NA   
3 P50   NA   
4 P51   NA   
5 P51   NA 

şi selectând datele cuvenite, constatăm că "P49" este cuplat în două zile (pe 3 şi respectiv 5 ore) cu "P51", făcând cu câte o grupă din clasele respective disciplinele "Ed.Muz" şi respectiv "Ed.Viz" şi deasemenea, "P50" este şi el cuplat cu "P51", într-o altă zi (pe 3 ore) şi la alte clase (iarăşi, pentru "Ed.Muz" şi respectiv "Ed.Viz").

În [1], am evitat contextul – rar, dar posibil – în care la unele clase, o aceeaşi oră poate fi partajată între doi profesori, considerând că aspectele particulare sau artificiale – dacă nu pot fi ocolite – trebuie luate în seamă la sfârşit (altfel, ar însemna să intrăm într-un joc nesfârşit de chichiţe). Dar este drept că în învăţământul nostru, din diverse motive (care ar merita discutate), sunt de repartizat profesorilor nu numai clase şi ore din zi, dar şi jumătăţi de clasă şi oră; rămâne de văzut cum să ţinem totuşi seama de aceasta, în cursul procedurii iniţiate în [1] pentru repartizarea omogenă a orelor pe zilele săptămânii.

vezi Cărţile mele (de programare)

docerpro | Prev | Next