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

Abstractizarea datelor orarului şcolar

limbajul R | orar şcolar
2022 jun

[1] De capul meu prin problema orarului şcolar (pe Google Play)

[2] Înapoi, de la orar (PDF) la matricea de încadrare

În [1] (şi adesea, pe aici), am „abstractizat” cumva, profesorii implicaţi pe orarele şcolare pe care le-am abordat – dar şi cu alte gânduri, decât acela obişnuit, de a proteja „drepturile personale”…

Ultimul exemplu îl avem în [2], unde profesorii „de bază” au fost desemnaţi prin p01, p02, ..., p83 (în ordinea descrescătoare a numărului de ore); rezultă astfel formulări concise pentru matricea de încadrare şi pentru orare – iar pe de altă parte, devine firească modelarea cuplajelor de profesori (v. [1]): dacă de exemplu, p55 şi p71 trebuie să partajeze anumite clase (în câte o aceeaşi zi şi oră), atunci înfiinţăm „profesorul” fictiv "p55p71" căruia îi atribuim orele „pe grupe” ale celor doi profesori.

Totuşi, notaţia amintită mai sus este chiar săracă: "p" este o literă oarecare (putea fi "t" de la "teacher", sau "y" de la "учитель", etc.), iar "55" ar spune doar că p55 are mai multe ore decât p71 (şi mai puţine decât p25); parcă şi „notaţia” vulgară "profu' de mate" este mai consistentă…

Îmbunătăţirea de făcut este străvezie, dar realizarea acesteia este chiar instructivă (reflectând într-un context real, o seamă de aspecte specifice programării în limbajul R).

Considerăm setul de lecţii constituit la început în [2], plecând de la fişierul PDF care prezenta orarul final al unei anumite şcoli:

library(tidyverse)
LSS <- readRDS("TV-org.RDS")  # 1260 lecţii prof|obj|zi|ora|cls

Subliniem că nu prea ne interesează părţile zi|ora (din care deducem totuşi, cuplajele de profesori), având intenţia de a genera – prin programele R din [1] – orare echilibrate corespunzătoare matricei de încadrare deduse din orarul iniţial.

În $prof avem numele reale, separate prin '/' în cazul în care lecţia respectivă angajează „pe grupe” ale clasei, cei doi profesori – ca în acest eşantion de lecţii:

> slice_sample(LSS, n = 5)
                              prof         obj zi ora cls
1                      Rica Zamfir  Matematica Vi   2 12f
2                   Claudia Anghel      Chimie Jo   8  9d
3 Marcel Homorodean/Simona Ionescu Informatica Lu  12 10i
4                  Maria Ardeleanu     Religie Lu   1 11g
5                    Ovidiu Sontea  Matematica Vi   7 10h

Ne propunem să „abstractizăm” numele profesorilor indicând cumva disciplina şi un număr de ordine între ceilalţi angajaţi pe disciplina respectivă.

Dicţionarul disciplinelor

Bineînţeles că întâi, ne procurăm – folosind abbreviate(), cu unele ajustări manuale – un set de abrevieri convenabile pentru discipline (numele unice din LSS$obj):

abb <- readRDS("abb_obj.RDS")
Biologie    Chimie    Cultura civica    Cultură germană    Economie
    "Bi"      "Ch"              "Cv"               "Cg"        "Ec"
Dezbatere    Dirigentie    Educatie antreprenoriala    Educatie fizica
     "Dz"          "Dr"                        "Ea"               "Ef"
Educatie muzicala    Educatie sociala    Educatie tehnologica    Educatie vizuala
             "Em"                "Es"                    "Et"                "Ev"
Filosofie    Fizica    Geografie    Informatica    Istorie
     "Fs"      "Fz"         "Ge"            "N"       "Is"
Limba engleza    Limba franceza    Limba germana    Limba latina    Limba romana
         "Le"              "Lf"             "Lg"            "Ll"             "R"
Logica    Matematica    Psihologie    Religie     TIC
  "Lo"           "M"          "Ps"       "Re"    "Ti" 

Dacă avem trei profesori de "Biologie", ei vor fi desemnaţi prin "Bi1", "Bi2" şi "Bi3"; având mai mult de 9 profesori de "Matematica", îi vom desemna prin "M01", ..., "M09", "M10", etc. Indecşii 1, 2, etc. reflectă ordinea descrescătoare a numărului de ore ale profesorilor de pe disciplina respectivă.

abb este un vector cu nume, sau cum se zice parcă mai firesc, în alte limbaje – un dicţionar, având drept "chei" denumirile obişnuite ale disciplinelor şcolare şi drept "valori", abrevierile convenite pentru acestea ("Biologie" ⇒ "Bi", etc.).
Cheile trebuie să fie (şi sunt) distincte între ele; în cazul de faţă şi valorile asociate cheilor, sunt distincte între ele – încât ne putem gândi şi la dicţionarul "invers":

iabb <- names(abb)  # cheile din 'abb' devin valorile lui 'iabb'
names(iabb) <- as.vector(abb)  # valorile din 'abb' devin cheile lui 'iabb'
> head(iabb, 4)  # exemplificare
               Bi            Ch                Cv                 Cg 
       "Biologie"      "Chimie"  "Cultura civica"  "Cultură germană" 

Obs. Avem totuşi această diferenţă între "dicţionar" (ca în Python) şi "vector cu nume" (specific lui R): selectarea unui element dintr-un vector cu nume – de exemplu, prin iabb["Bi"], sau prin iabb[1] – ne dă şi cheia şi valoarea (… tot un "vector cu nume"), pe când selectarea dintr-un dicţionar ne dă numai valoarea asociată cheii indicate.

Prin iabb vom putea explicita discipline, plecând de la numele profesorilor:

show_obj <- function(Prof)
    gsub("[\\d]", "", Prof, perl=TRUE) %>%  # fără cifre, rămâne o cheie din 'iabb'
    iabb[.] %>%  # selectează cheia şi valoarea
    as.vector(.)  # păstrează numai valoarea (şi o returnează)
# exemplificări:
> show_obj("Bi2")
[1] "Biologie"
> show_obj(c("M12", "Bi2", "N09"))  # aplicare pe o coloană de profesori
[1] "Matematica"  "Biologie"  "Informatica"

Dacă avem un obiect data.frame conţinând orarul „pe profesori” al unei clase (indicând profesorii care intră la clasă în fiecare oră), atunci aplicând pe coloanele sale funcţia show_obj(), am obţine imediat (fără vreo altă investigare) orarul obişnuit, „pe discipline”.

Profesorii disciplinei, în ordinea numărului de ore

Păstrăm din LSS numai datele de încadrare, $prof, $obj şi $cls; renunţăm deocamdată la liniile pe care avem '/' în câmpul $prof, sau în $obj (acestea reprezintă o lecţie care se desfăşoară „pe grupe” – v. [2]). Împărţim apoi pe discipline:

lso <- LSS %>% select(prof, obj, cls) %>%
       filter(! (grepl("/", prof, fixed=TRUE) | 
                 grepl("/", obj, fixed=TRUE))) %>% 
       split(.$obj)  # seturile de lecţii pe câte o aceeaşi disciplină

De exemplu, pe "Geografie" avem 44 de lecţii şi putem constata prin count() că acestea sunt repartizate pe doi profesori, cu câte 24 şi 20 de ore:

> str(lso$Geografie)
'data.frame':	44 obs. of  3 variables:
 $ prof: chr  "Diana Petculescu" "Florentin Rotea" "Florentin Rotea" ...
 $ obj : chr  "Geografie" "Geografie" "Geografie" ...
 $ cls : chr  "5" "9d" "9e" ...
# Câte ore are fiecare:
> lso$Geografie %>% count(prof, sort=TRUE)
              prof  n
1 Diana Petculescu 24
2  Florentin Rotea 20

Pentru fiecare set de lecţii pe o aceeaşi disciplină, contorizăm apariţiile câte unui aceluiaşi profesor – obţinând de fapt, numărul de ore ale acestuia – şi producem vectorul profesorilor respectivi, în ordinea descrescătoare a numărului de ore:

LO <- map(1:length(lso), function(i) 
          lso[[i]] %>%
          count(prof, sort=TRUE) %>%
          pull(prof))
names(LO) <- names(lso)
#Exemplificare:
> LO[16]
$Geografie
[1] "Diana Petculescu" "Florentin Rotea" 

Folosind acum abrevierile din abb, urmează să desemnăm profesorii de "Geografie" prin "Ge1" (cel cu mai multe ore) şi "Ge2".
Să observăm că puteam scurta puţin lucrurile: names(LO) <- abb[names(lso)] (atunci, nu mai aveam "Geografie" de exemplu, ci direct "Ge").

Constituirea notaţiei profesorilor de bază

Pentru fiecare set de lecţii (pe o aceeaşi disciplină) dintre cele 28 din lista LO, constituim câte un obiect data.frame conţinând profesorii din acel set şi codificarea rezultată pentru aceştia prin alipirea numelui abreviat al obiectului, cu indexul profesorului în cadrul setului respectiv – având grijă să folosim "01", "02" etc. în loc de 1, 2 etc., dacă numărul profesorilor pe obiectul respectiv este mai mare ca 9.
În final „reunim” obiectele data.frame constituite astfel, folosind map_dfr():

LOP <- map_dfr(1:length(LO), function(i) {
           n <- length(LO[[i]])
           obj <- if(n < 10) paste0(abb[[i]], 1:n)
                  else c(paste0(abb[[i]], "0", 1:9), paste0(abb[[i]], 10:n))
           data.frame(prof = LO[[i]], obp = obj)
})
> str(LOP)
'data.frame':	103 obs. of  2 variables:
 $ prof: chr  "Mirela Marinescu" "Doriana Stoica" "Simona Vasilescu" ...
 $ obp : chr  "Bi1" "Bi2" "Bi3" ...

Am obţinut codificările prin câte 3 caractere – indicând disciplina şi cumva, numărul de ore – pentru 103 profesori; de exemplu, la întâmplare:

> slice_sample(LOP, n = 5)
               prof  obp
1         Sena Azis  M11
2      Mirela Marcu  Ch2
3 Monica Dumitrache  M06
4     Raluca Mangra  M07
5   Livia Magureanu  N13

"M11" (evidenţiat în selecţia redată mai sus) este al 11-lea profesor, în ordinea descrescătoare a numărului de ore, pe disciplina "Matematica".

Dar să observăm că avem acum 103 „profesori”, în loc de 83 câţi erau „de bază” în notaţia anterioară ("p01"..."p83"); aceasta înseamnă că avem cazuri când un acelaşi profesor apare la mai multe discipline (şi i s-au asociat codificări distincte):

po2 <- LOP %>% 
       group_by(prof) %>% 
       filter(n() > 1) %>%  # cei cu mai multe codificări
       arrange(prof)
> str(po2)
tibble [40 × 2] (S3: grouped_df/tbl_df/tbl/data.frame)
 $ prof: chr [1:40] "Alina Boca" "Alina Boca" "Alina Teac" "Alina Teac" ...
 $ obp : chr [1:40] "Dr1" "N02" "Cv1" "Em1" ...

po2 având 40 de linii, deducem că avem (cel mai probabil – dar se verifică, listând po2) 20 de profesori încadraţi pe câte două discipline; de exemplu, profesorul Em1 (de "Educatie muzicala") are şi "Cultura civica", fiind codificat şi prin Cv1. Dintre cele două codificări vom alege (fireşte) pe aceea care asociază mai multe ore (ţinând cont cumva şi de cealaltă, atunci când va fi să formulăm în final, orarele „pe discipline”, ale claselor).

Reducerea de la cei 103 la cei 103-20=83 „de bază”, ar putea decurge astfel:

N2 <- map_dbl(1:nrow(po2), function(i)
              LSS %>% 
              filter(prof == po2$prof[i] & 
                     obj == show_obj(po2$obp[i])) %>% 
              nrow(.)
)  
# num [1:40] 1 15 3 6 8 6 1 14 6 1 ...
idx <- map_dbl(seq(1, nrow(po2), by=2), function(i)
               if(N2[i] > N2[i+1]) i else i+1)
# num [1:20] 2 4 5 8 9 12 14 15 17 20 ...
LOP <- LOP %>% 
       anti_join(po2) %>% 
       full_join(po2[idx, ])   

Vectorul N2 a înregistrat numărul de ore pe fiecare dintre cele două discipline ale profesorilor din po2 (de exemplu, vedem că Cv1 are 3 ore, iar Em1 are 6 ore); dintre cele câte două linii corespunzătoare în po2 unui aceluiaşi profesor, am înscris în vectorul idx indexul aceleia pe care numărul de ore este mai mare (de exemplu, pentru Em1 am păstrat linia 4). În final, prin anti_join() am exclus din LOP cele 40 de linii existente în po2 şi apoi prin full_join(), am inclus cele 20 de linii de pe rangurile idx, ale lui po2.

Acum cei 83 profesori „de bază” sunt:

> LOP$obp %>% sort()
 [1] "Bi1" "Bi2" "Bi3" "Bi4" "Cg2" "Cg3" "Ch1" "Ch2" "Ch3" "Ch4" "Ea1" "Ef1"
[13] "Ef2" "Ef3" "Ef4" "Em1" "Es1" "Et1" "Ev1" "Fz1" "Fz2" "Fz3" "Fz4" "Fz5"
[25] "Fz6" "Fz7" "Fz8" "Ge1" "Ge2" "Is1" "Is2" "Is3" "Le1" "Le2" "Le3" "Le4"
[37] "Le5" "Lf1" "Lf2" "Lf3" "Lf4" "Lf5" "Lg1" "Lo1" "M01" "M02" "M03" "M04"
[49] "M05" "M06" "M07" "M08" "M09" "M10" "M11" "N01" "N02" "N03" "N04" "N05"
[61] "N06" "N07" "N08" "N09" "N10" "N11" "N12" "N13" "N14" "Ps1" "R01" "R02"
[73] "R03" "R04" "R05" "R06" "R07" "R08" "R09" "R10" "Re1" "Re2" "Ti2"

Prin funcţia show_obj() putem stabili direct, disciplina principală a fiecăruia; dacă profesorul are şi o disciplină secundară, atunci aceasta (cu număr de ore mai mic) a fost indicată în po2 fie în faţa, fie după cea principală – deci o putem regăsi astfel:

sec_obj <- function(Prof) {  # un singur 'Prof' (NU o coloană!)
    id <- match(Prof, po2$obp, 0)
    if(id == 0) return(NULL)  # NU are disciplină secundară
    id <- if(id %% 2 == 0) id - 1 else id + 1 
    show_obj(po2$obp[id])
}
# exemplificare:
> sec_obj("Em1")
[1] "Cultura civica"

Subliniem că if() evaluează o singură valoare (nu este o funcţie „vectorizată”), încât sec_obj() trebuie folosită pentru câte un singur profesor (nu şi pentru o coloană de profesori, ca în cazul funcţiei show_obj()).

Notaţia cuplajelor

Vom avea de înlocuit numele reale din LSS prin codificările asociate acestora în LOP (după ce vom fi extins LOP, cuprinzând şi notaţia „scurtă” pentru cuplaje); în [2] am procedat în maniera tipică, transformând coloanele $prof în factori la fel ordonaţi şi folosind levels(). Dar putem proceda şi direct (scăpând de ordonări): transformăm „tabelul” LOP în vector cu nume şi prevedem o funcţie care să dea codul scurt asociat numelui, dacă există:

Dpc <- LOP$obp
names(Dpc) <- LOP$prof
short_name <- function(Prof) 
    as.vector(ifelse(is.na(Dpc[Prof]), Prof, Dpc[Prof]))
# Exemplificare:
    > short_name(c("Marcel Homorodean", "Simona Ionescu"))
    [1] "Marcel Homorodean" "N01"

Fiindcă am formulat cu ifelse() (nu cu if()) – funcţia short_name() poate fi apelată şi pentru o coloană de profesori, cum am şi exemplificat mai sus.
Următoarea funcţie primeşte un cuplaj cum avem în LSS (de exemplu "Marcel Homorodean/Simona Ionescu") şi angajează short_name() pe vectorul celor două nume, returnând prin alipire un prim cod scurt al cuplajului (păstrând separatorul '/'), sau dacă unul dintre nume nu este găsit în Dpc – însuşi numele respectiv, separat de celălalt (eventual „scurtat”) iarăşi prin '/' (dar astfel încât codul scurt, dacă există unul, să fie înainte de '/'):

st2name <- function(P2) {
    s2 <- short_name(strsplit(P2, "/", fixed=TRUE)[[1]])
    if(nchar(s2[1]) > 3) 
        s2 <- rev(s2)  # codul scurt să fie înainte de '/'
    paste0(s2, collapse="/")
}
# Exemplificare:
    > st2name("Isabela Coman/Victor Manz")
    [1] "N10/N03"
    > st2name("Marcel Homorodean/Simona Ionescu")
    [1] "N01/Marcel Homorodean"

Acum culegem într-un „tabel” DFcup, cuplajele din LSS (liniile care în $prof conţin '/') şi-i montăm o coloană $obp pe care înscriem valorile date de st2name() (eliminând separatorul '/' din codurile deja scurte – cele de câte 7 caractere):

DFcup <- LSS %>% select(prof, obj, cls) %>%
         filter(grepl("/", prof)) %>% 
         count(prof, sort=TRUE) %>%
         mutate(obp = "")
for(i in 1:nrow(DFcup)) {
    cup <- st2name(DFcup$prof[i])
    DFcup$obp[i] <- if(nchar(cup)==7) sub("/", "", cup) else cup
}
DFcup$n <- NULL  # nu avem nevoie de numărul de ore (introdus de count())
# Exemplificare
    > str(DFcup)
    'data.frame':	26 obs. of  2 variables:
     $ prof: chr  "Alina Teac/Gratiela Stoian" "Alexandra Tudor/Valentina Morman" 
                  "Alina Boca/Corina Vint" "Anca Leuciuc/Irina Iosupescu" ...
     $ obp : chr  "Em1Ev1" "Cg2Lg1" "N02N12" "N04/Irina Iosupescu" ...

Numele din DFcup$obp care mai conţin '/' corespund cuplajelor cu profesori „externi” (neaflaţi printre cei 83 „de bază”); pentru aceştia – doar câţiva – prevedem (după ce excludem partea din faţa separatorului '/') codurile "e01", "e02" etc.:

C3 <- DFcup$obp 
cpx <- C3[grepl("/", C3)]  # reţine cuplajele cu profesor extern
Pex <- unique(gsub(".*/", "", cpx))  # profesorii externi
dct_x <- paste0("e0", 1:length(Pex))
names(dct_x) <- Pex  # dicţionar {Prof_extern ==> e0N}

Folosind dicţionarul dct_x, definitivăm codurile de câte 6 caractere şi pentru cuplajele cu profesori externi, din DFcup$obp:

for(i in 1:nrow(DFcup)) {
    if(nchar(DFcup$obp[i]) == 6) next
    vct <- strsplit(DFcup$obp[i], "/", fixed=TRUE)[[1]]
    p1 <- vct[1]
    if(nchar(p1) > 3)
        p1 <- dct_x[p1]
    p2 <- dct_x[vct[2]]
    DFcup$obp[i] <- paste0(p1, p2)
}

Acum putem finaliza lucrurile: reunim LOP (unde avem codurile scurte pentru profesorii „de bază”) şi DFcup (unde avem codurile cuplajelor) şi transformăm iarăşi în "vector cu nume", pe care îl folosim pentru a converti numele reale din LSS$prof:

DFn <- LOP %>% full_join(DFcup, by=c("prof", "obp"))
Dpc <- DFn$obp
names(Dpc) <- DFn$prof
LSS <- LSS %>% select(prof, cls) %>%
       mutate(prof = as.vector(Dpc[prof]))
# Exemplificare
    > str(LSS)
    'data.frame':	1260 obs. of  2 variables:
     $ prof: chr  "Ef1" "Em1" "Le2" "M07" ...
     $ cls : Factor w/ 42 levels "10A","10B","10C",..: 28 28 28 28 ...

(desigur, între timp am ajustat ca în [2], notaţia claselor din LSS$cls)

Dacă foloseam mutate(prof=Dcp[prof]) (fără as.vector()), atunci s-ar fi păstrat cumva şi numele reale (accesibile prin construcţii incomode, ca attr(LSS$prof[10], "names")):

 $ prof: Named chr  "Ef1" "Em1" "Le2" "M07" ...  # vector cu nume
  ..- attr(*, "names")= chr [1:1260] "Ionela Stan-Cristache" "Alina Teac" ...

ceea ce ar fi însă inutil (dacă ar fi cazul de a regăsi numele reale, putem accesa dicţionarul Dpc) şi s-ar putea să ne şi încurce ulterior, când ar fi să aplicăm programele din [1] pentru a repartiza lecţiile prof | cls pe zile şi pe orele zilei.

Dacă vrem, putem formula (tipări) şi matricea de încadrare, ca în [2]; dar acum putem evidenţia imediat (fără investigări suplimentare) şi matricele de încadrare pe discipline:

emv <- LSS %>% filter(grepl("Em|Ev", prof)) 
frem <- addmargins(table(emv[c('prof', 'cls')]), 2)
frem <- frem[, -(10:27)]  # exclude clasele 11 şi 12
frem[frem == 0] <- '.'
    > frem
            cls
            10________________ 5 6__ 7 8__ 9________________ 
    prof     A B C D E F G H I A A B A A B A B C D E F G H I +
      Em1    . . . . . . . . . 1 1 1 2 2 2 . . . . . . . . . 9 
      Em1Ev1 1 1 1 1 1 1 1 1 1 . . . . . . 1 1 1 1 1 1 1 1 1 18  # jumătăţi de oră
      Ev1    . . . . . . . . . 1 1 1 1 1 1 . . . . . . . . . 6 

Am redat încadrarea pe "Educatie muzicala" şi "Educatie vizuala"; fiindcă Em1 apare şi în dicţionarul po2 al profesorilor care au şi discipline secundare – deducem că una din cele câte două ore ale sale de la clasele a 7-a şi a 8-a este de "Educatie civica".
Cele 18 ore alocate fictiv lui Em1Ev1 angajează cei doi profesori „pe grupe”, la clasele respective şi se pot desfăşura fie ca atare (18 ore cu câte o jumătate de clasă), fie cuplând câte o grupă de la două clase (pe 9 ore, cu clase „întregi”).

Bineînţeles că putem „abstractiza”, inventând o funcţie frame_obj(), care să producă matricea de încadrare pe disciplinele primite ca argument; de exemplu, tabelul redat mai sus s-ar obţine prin frame_obj(c("Em", "Ev")).

vezi Cărţile mele (de programare)

docerpro | Prev | Next