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

Modelarea încadrării profesorilor

limbajul R | orar şcolar
2022 jun

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

[2] Abstractizarea datelor orarului şcolar

Fişierul "Lessons.RDS" specifică toate lecţiile prof | obj | cls, de făcut în cursul săptămânii într-o anumită şcoală:

library(tidyverse)
LSS <- readRDS("Lessons.RDS") # 1260 lecţii prof|obj|cls
'data.frame':	1260 obs. of  3 variables:
 $ prof: chr  "Ionela Stan-Cristache" "Alina Teac" "Iulia Salajanu" "Raluca Mangra" ...
 $ obj : chr  "Educatie fizica" "Educatie muzicala" "Limba engleza" "Matematica" ...
 $ cls : Factor w/ 42 levels "10A","10B","10C",..: 28 28 28 28 28 28 28 28 28 28 ...

O lecţie angajează fie un singur profesor, fie (dar mult mai rar) un cuplu de profesori – cum se vede pe acest eşantion de lecţii:

> slice_sample(LSS, n = 3)
                               prof            obj  cls
1                    Daniel Crocnan         Fizica  12H
2 Sorin Giurumescu/Valentina Morman  Limba germana  10A
3                   Maria Ardeleanu        Religie  12E

Numele celor doi profesori care au de partajat într-un acelaşi timp o anumită clasă (lucrând cu câte o grupă a acesteia) sunt separate în coloana prof prin caracterul '/' (ca în linia 2 din eşantionul redat mai sus); spre deosebire de [2], nu asumăm vreo anumită ordine a numelor respective (în general de fapt, demersul de acum este mai „aşezat”).

Investigând disciplinele şcolare din coloana obj, descoperim o compunere de două discipline (separate prin '/'):

> (obj_sep <- unique(LSS$obj) %>% grep("/", ., fixed=TRUE, value=TRUE))
[1] "Educatie muzicala/Educatie vizuala"

Investigând lecţiile pe această disciplină „compusă”, vedem că ele angajează un acelaşi cuplu de profesori, la clasele a 9-a şi a 10-a; extragem aici un eşantion:

> LSS %>% filter(obj == obj_sep) %>% slice_sample(., n=2)
                        prof                                 obj  cls
1 Alina Teac/Gratiela Stoian  Educatie muzicala/Educatie vizuala   9D
2 Alina Teac/Gratiela Stoian  Educatie muzicala/Educatie vizuala  10B

Dar de data aceasta, nu mai este vorba de un cuplaj de profesori obişnuit (pe grupe ale unei aceleiaşi clase), dat fiind că "Educatie muzicala" şi "Educatie vizuala" sunt prevăzute la clasele a 9-a şi a 10-a cu numai câte o jumătate de oră pe săptămână – sau „echivalent”, cu câte o oră la două săptămâni.
Este vorba de clase cuplate într-un acelaşi timp, alternând săptămânal cele două discipline; de exemplu, în săptămânile impare 9D face "Educatie muzicala" şi în acelaşi timp, 10B face "Educatie vizuală", iar în săptămânile pare 9D face "Educatie vizuala" şi în acelaşi timp, 10B face "Educatie muzicala".

Pentru a asigura – prin programele de repartizare pe zile şi ore din [1] – o asemenea alternanţă săptămânală, decupăm din LSS lecţiile pe obiectul compus (deci rămân mai puţine lecţii de repartizat) şi le alocăm anticipat pe zile (anticipând acum şi specificaţia la care vom ajunge mai jos, pentru discipline şi profesori), astfel încât în fiecare zi cei doi profesori implicaţi să poată intra la o clasă a 9-a, respectiv la una de-a 10-a:

cls_cup <- LSS %>% filter(obj == "Educatie muzicala/Educatie vizuala")
LSS <- LSS %>% 
       anti_join(., cls_cup, by=c("prof", "obj", "cls"))  # rămân 1242 lecţii
cls_cup <- cls_cup %>%
           mutate(prof = ifelse(grepl("9", cls), "Em1", "Ev1")) %>%
           mutate(obj = ifelse(grepl("9", cls), "Em", "Ev")) %>%
           arrange(cls) %>%
           mutate(zl = rep(c(1:5, 1:4), 2))
> cls_cup
   prof obj  cls  zl             prof obj  cls  zl
1   Ev1  Ev  10A   1          10  Em1  Em   9A   1
2   Ev1  Ev  10B   2          11  Em1  Em   9B   2
3   Ev1  Ev  10C   3          12  Em1  Em   9C   3
4   Ev1  Ev  10D   4          13  Em1  Em   9D   4
5   Ev1  Ev  10E   5          14  Em1  Em   9E   5
6   Ev1  Ev  10F   1          15  Em1  Em   9F   1
7   Ev1  Ev  10G   2          16  Em1  Em   9G   2
8   Ev1  Ev  10H   3          17  Em1  Em   9H   3
9   Ev1  Ev  10I   4          18  Em1  Em   9I   4

Am preferat să împerechem clasele după litere; de exemplu, clasele 9A şi 10A fac în ziua 1 fie Em şi respectiv Ev, fie invers – după cum săptămâna în curs este de rang par sau impar.
Desigur, ulterior va trebui să asigurăm câte o aceeaşi oră a zilei, pentru ca profesorii Em1 şi Ev1 să intre în paralel (într-un acelaşi timp) la clasele astfel împerecheate pe zile.

Dacă este cazul, putem extinde cls_cup – fixând anumite zile şi pentru alţi vreo doi-trei profesori (şi excluzând lecţiile respective, din LSS)…

Pentru programele de repartizare pe zile şi pe orele zilei a lecţiilor este necesar să constituim anumite seturi de date – precum cls_cup – şi anumite dicţionare prealabile (eventual şi anumite funcţii de conversie); de aceea, se cuvine să vorbim de modelarea încadrării şi nu doar (ca anterior, pe aici) de „matricea de încadrare”.

Dicţionarele inverse unul celuilalt obj_abb.RDS şi abb_obj.RDS vor servi pentru a nota uniform profesorii, după disciplina principală a fiecăruia – permiţând eliminarea câmpului LSS$obj (fără vreo pierdere, dacă vom constitui şi un dicţionar pentru profesorii care au în încadrare şi o disciplină secundară):

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

Am redat aici dicţionarul „direct”, conţinând abrevierile convenite pentru discipline (prin câte două caractere, dacă numărul de profesori de pe disciplina respectivă este mai mic ca 10 şi printr-unul singur, în caz contrar). Subliniem că dObj este de fapt un "vector cu nume", dar l-am redat prin as.data.frame(dObj) (cheile s-au păstrat ca "nume de linii", iar valorile au fost înscrise în singura coloană "dObj" a tabelului).
Dicţionarul „invers” abb_obj.RDS ne va permite să explicităm (când ar fi cazul) disciplina corespunzătoare unui profesor, pe baza „numelui” acestuia; de exemplu, lecţia Em1 | 10A va fi una de "Educatie muzicala", iar Bi2 | 10A de "Biologie".

Vom folosi următoarea funcţie (foarte simplă), pentru a stabili care discipline sunt ”principale” şi care „secundare” şi pentru a introduce apoi notaţia după disciplina principală (sugerată mai sus) pentru profesori:

obj_main_sec <- function(P)
    LSS %>% filter(grepl(P, prof, fixed=TRUE)) %>% count(obj, sort=TRUE)
# Exemplificare:
> print(obj_main_sec("Sorin Giurumescu"))
              obj  n
1   Limba germana  5  # principală
2 Cultură germană  1  # secundară

Profesorul exemplificat aici are disciplina de bază "Limba germana" şi disciplina secundară (cu număr mai mic de ore) "Cultură germană" – încât vom alege ca „nume” al său LgN (şi nu "CgN", cum ne-a rezultat în [2]) unde N ar fi numărul de ordine în lista profesorilor pe "Limba germana", ordonată descrescător după numărul de ore.

Pentru a desemna lecţiile care trebuie să decurgă „pe grupe”, vom compune (prin alipire) numele stabilite pentru cei doi profesori implicaţi (ajungând deci la ceea ce am denumit anterior pe aici, „profesori fictivi”). Deci ne interesează în primul rând, profesorii implicaţi în lecţii care nu decurg pe grupe (altfel spus – cei care au şi ore proprii, la care intră singuri şi nu în vreun cuplaj):

prof_main <- LSS %>% filter(! grepl("/", prof, fixed=TRUE)) %>% 
             pull(prof) %>% unique()  # profesorii care au şi ore proprii
lpm <- map(prof_main, obj_main_sec)
names(lpm) <- prof_main

Aplicând funcţia obj_main_sec() pe vectorul profesorilor cu ore proprii, am obţinut lista lpm care indică pentru fiecare dintre aceştia, disciplina principală şi (când este cazul) pe cea secundară. Să constituim acum un „tabel” (data.frame) care să conţină profesorii cu ore proprii (din vectorul prof_main) împreună cu obiectele principale asociate lor prin lista lpm:

DF_main <- map_df(prof_main, function(P) 
                  data.frame(prof = P, obj = lpm[[P]]$obj[1]))

Numărând pentru fiecare profesor din DF_main, pe câte linii apare în LSS – obţinem numărul de ore ale acestuia şi-l putem integra într-o nouă coloană, pe linia corespunzătoare lui în DF_main; dar coloanele $prof sunt deocamdată de tip character (şi nu factor ordonat după vreun anumit criteriu), deci count(prof) va furniza frecvenţele respective în ordinea alfabetică a numelor.
Prin urmare, întâi aranjăm liniile din DF_main în ordinea alfabetică a numelor din coloana $prof, apoi adăugăm coloana $nh în care înscriem numărul de ore obţinut din LSS prin count(prof), reordonăm liniile descrescător după frecvenţele respective şi în final, transformăm prin split() într-o listă conţinând subseturile de profesori ai câte unei aceleiaşi discipline principale:

DF_main <- DF_main %>% 
           arrange(prof) %>%  # aceeaşi ordine ca după count(prof)
           mutate(nh = LSS %>% 
                       filter(prof %in% DF_main$prof) %>% 
                       count(prof) %>%  # implicit, ordine alfabetică
                       pull(n) # numărul de ore pentru fiecare
           ) %>% arrange(desc(nh)) %>%
           split(.$obj)
# Exemplificare:
 $ Istorie             :'data.frame':	3 obs. of  3 variables:
  ..$ prof: chr [1:3] "Dan Ciuperca" "Veta Grecu" "Vasilica Donciu"
  ..$ obj : chr [1:3] "Istorie" "Istorie" "Istorie"
  ..$ nh  : int [1:3] 20 16 9

Pe Istorie de exemplu, apar 3 profesori, în ordinea descrescătoare a numărului de ore şi urmează să-i notăm respectiv prin Is1, Is2 şi Is3.
Pentru a ajunge la notaţia respectivă (pentru toţi profesorii care au ore proprii), folosim dicţionarul obj_abb.RDS şi pentru profesorii fiecăruia dintre subseturile pe discipline din lista DF_main, alipim abrevierea disciplinei respective cu 1:N unde N este numărul de profesori pe acea disciplină (iar dacă N > 9, intercalăm şi un '0' – încât codurile rezultate să aibă toate, câte 3 caractere). În final, prin map_dfr() obţinem un tabel (data.frame) care asociază numele reale (cele iniţiale, din LSS) ale profesorilor care au ore proprii şi codurile de câte 3 caractere, atribuite lor:

abb <- readRDS("obj_abb.RDS")
DFpc <- map_dfr(1:length(DF_main), function(i) {
            N <- nrow(DF_main[[i]])
            mc <- abb[DF_main[[i]]$obj][[1]]
            obn <- if(N < 10) paste0(mc, 1:N)
                   else c(paste0(mc, "0", 1:9), paste0(mc, 10:N))
            data.frame(prof = DF_main[[i]]$prof, cod = obn)
})
# pentru exemplificare:
> str(DFpc)
'data.frame':	83 obs. of  2 variables:
 $ prof: chr  "Mirela Marinescu" "Doriana Stoica" "Simona Vasilescu" "Felix Lupulescu" ...
 $ cod : chr  "Bi1" "Bi2" "Bi3" "Bi4" ...

Dar este mai simplu să tratăm DFpc nu ca data.frame, ci ca dicţionar (uşor de extins şi – valorile DFpc$cod fiind distincte între ele – de inversat):

prof_cod <- DFpc$cod
names(prof_cod) <- DFpc$prof
cod_prof <- setNames(names(prof_cod), prof_cod)  # dicţionarul invers
## Exemplificare:
> sample(prof_cod, 3)
  Oana Lupascu Alexandra Puiu   Alina Moraru   # chei
         "N14"          "N10"          "Lf2"   # valori (distincte)
> sample(cod_prof, 3)
                 Ec1                   Re2                  N02  # chei
   "Cristina Vasile"  "Veronica Cosacenco"         "Alina Boca"  # valori

"N02" de exemplu, indică (după dicţionarul abb_obj.RDS) un profesor de "Informatica" (al 2-lea, în ordinea descrescătoare a numărului de ore) care are şi ore proprii.
Dar unii profesori care au ore proprii, fac şi o a doua disciplină; să ne amintim că aceasta este indicată pe al doilea loc în vectorul $obj asociat profesorului în lista lpm.
Să constituim un dicţionar prof_sec pentru profesorii cu disciplină secundară:

pr2 <- sapply(lpm, nrow)  # profesor ==> număr discipline
pr2 <- names(pr2[pr2 == 2])  # profesorii cu o disciplină secundară
prof_sec <- sapply(pr2, function(P) lpm[[P]]$obj[2])  # profesor ==> disciplina_2
names(prof_sec) <- prof_cod[names(prof_sec)]
# Exemplificare:
> sample(prof_sec, 3)
              Ge1            Es1                 Lg1 
     "Dirigentie"   "Psihologie"   "Cultură germană"

N-am folosit abrevierile din obj_abb.RDS, fiindcă de disciplinele secundare ar fi nevoie numai la redarea finală a orarelor claselor.
Avem nevoie desigur, de o funcţie care să dea clasele unde profesorul (indicat prin codul său) are şi disciplină secundară:

sec_to_cls <- function(codP, disp)
    LSS %>% filter(prof %in% cod_prof[codP], obj %in% disp) %>%
    pull(cls) %>% 
    as.vector() %>% unique()
# Exemplificare:
> sec_to_cls("Lg1", "Cultură germană")
[1] "11A" "12A"

Folosind '%in%' şi nu '==', ne-am asigurat că putem invoca şi cu argumente de lungime mai mare ca 1; să aplicăm sec_to_cls() pe dicţionarul prof_sec (al profesorilor cu discipline secundare):

cls_sec <- sapply(names(prof_sec), sec_to_cls, prof_sec) 
# Exemplificare: 
> str(cls_sec)
List of 22
 $ Em1: chr [1:3] "7A" "8A" "8B"
 $ Le2: chr "5A"
 $ Ge1: chr "8A"
# etc.

Probabil că s-ar cuveni să strângem într-un singur loc (un obiect data.frame), toate informaţiile (profesorul, disciplina secundară, clasele) obţinute mai sus în vectorul cu nume prof_sec şi în lista cls_sec:

TB_sec <- tibble(prof = names(prof_sec), 
                 disp = as.vector(prof_sec), 
                 cls = as.vector(cls_sec)) %>%
          as.data.frame()
# Exemplificare:
> TB_sec %>% slice_sample(n=4)
  prof            disp          cls
1  Em1  Cultura civica   7A, 8A, 8B
2  Es1      Psihologie     10F, 10G
3  N06             TIC 9F, 10F, 10G
4  Lg2 Cultură germană           9A

Am cam terminat, cu profesorii care au şi ore proprii; din codurile de câte 3 caractere pe care le-am asociat acestora, avem imediat – folosind şi dicţionarul abb_obj.RDS – disciplina principală a fiecăruia; în setul de date TB_sec avem aceia dintre profesori care la anumite clase (indicate şi acestea, în TB_sec), fac şi o disciplină secundară.
De exemplu, cum vedem mai sus, N06 face "Informatica", iar la clasele 9F, 10F şi 10G face (şi) "TIC"; forma indexului, '06', ne spune că avem cel puţin 10 profesori pe "Informatica" (dintre care 5 au tot atâtea sau mai multe ore, decât N06). Rămâne de văzut când va fi cazul, dacă N06 face ambele obiecte la câte o aceeaşi clasă.

Am terminat să zicem, şi cu clasele cuplate – pentru care am rezervat repartizarea pe zile din tabelul cls_cup. A rămas să ne ocupăm de cuplajele de profesori.

Mai întâi, extragem din LSS subsetul lecţiilor care se desfăşoară „pe grupe”:

LSS2 <- LSS %>% filter(grepl("/", prof, fixed=TRUE))
    'data.frame':	115 obs. of  3 variables:
     $ prof: chr  "Alina Boca/Corina Vint" "Alina Boca/Corina Vint" ...
     $ obj : chr  "Informatica" "Informatica" "Informatica" "Informatica" ...
     $ cls : Factor w/ 42 levels "10A","10B","10C",..: 37 37 37 38 38 38 39 ...
> unique(LSS2$obj)
    [1] "Informatica"   "Limba germana" "Limba engleza"

Cele 115 lecţii „pe grupe” (la trei discipline, evidenţiate şi acestea mai sus) angajează câte doi profesori şi pentru aceştia putem avea trei cazuri: fie ambii au şi ore proprii, deci apar ambii în dicţionarul prof_cod (şi n-avem decât să alipim codurile existente aici); fie numai unul dintre ei apare în prof_cod, fie niciunul.

Să determinăm profesorii „externi” – cei care nu apar în prof_cod. Pentru aceasta, obţinem întâi numele unice, dintre cele 115 perechi de nume din LSS2$prof; apoi, din vectorul rezultat reţinem acele nume care nu apar în prof_cod şi prin setNames(), le asociem coduri de forma "ex1", "ex2", etc.:

in_tw <- LSS2 %>% pull(prof) %>%  
         strsplit(.,'/',fixed=TRUE) %>% 
         unlist() %>% unique()
#
> str(in_tw)
 chr [1:23] "Alina Boca" "Corina Vint" "Anca Leuciuc" "Irina Iosupescu" ...
#
pr_ext <- in_tw[! in_tw %in% names(prof_cod)]
pr_ext <- setNames(paste0("ex", 1:length(pr_ext)), pr_ext)
# 
> pr_ext  # profesorii "externi"
  Irina Iosupescu      Elena Dragan Marcel Homorodean      Cezar Mandle 
            "ex1"             "ex2"             "ex3"             "ex4" 

Acum formulăm un dicţionar tw_cod care să asocieze numelor unice din LSS2$prof – aşa cum apar acestea, separate prin '/' – codurile rezultate prin funcţia set_cod() de mai jos, alipind valori din prof_cod sau pr_ext, după caz:

set_cod <- function(P) {
    kd <- if(! is.na(prof_cod[P])) prof_cod[P] else pr_ext[P]
    as.vector(kd) 
}
twins <- LSS2 %>% pull(prof) %>% unique() %>% sort()
tw_cod <- vector("character", length(twins))
for(i in 1:length(twins)) {
    tw <- strsplit(twins[i], "/", fixed=TRUE)[[1]]
    tw_cod[i] <- paste0(set_cod(tw[1]), set_cod(tw[2]))
}
tw_cod <- setNames(tw_cod, twins)
#
> sample(tw_cod, 4)
Elena Dragan/Marcel Homorodean Andreea Dumitru/Iulia Salajanu 
                      "ex2ex3>"                       "Le4Le2" 
   Livia Magureanu/Victor Manz    Cristina Olaru/Elena Dragan 
                      "N13N04"                       "N05ex2" 

Combinând prof_cod (notaţia celor cu ore proprii) şi tw_cod (notaţia cuplajelor) – obţinem codificarea după discipline a tuturor numelor unice de profesori din LSS:

PC <- c(prof_cod, tw_cod)  # profesor (sau cuplu) ==> cod scurt
#
> sample(PC, 4)
   Elena Dragan/Simona Ionescu               Daniel Georgescu 
                      "ex2N01"                          "R10" 
Andreea Dumitru/Iulia Salajanu                  Gina Cojocaru 
                      "Le4Le2"                          "Le5" 

Putem reformula acum LSS, înlocuind numele reale prin cele asociate în dicţionarele PC (pentru profesori şi cuplaje) şi abb (pentru discipline):

LSS <- LSS %>% mutate(prof = as.vector(PC[prof]), 
                      obj = as.vector(abb[obj]))
#
> str(LSS)
'data.frame':	1242 obs. of  3 variables:
 $ prof: chr  "Ef1" "Em1" "Le2" "M07" ...
 $ obj : chr  "Ef" "Em" "Le" "M" ...
 $ cls : Factor w/ 42 levels "10A","10B","10C",..: 28 28 28 28 28 28 ...
#
saveRDS(LSS, file="lessons.RDS")

Am avut grijă să folosim as.vector(), pentru a culege din dicţionarele respective numai valorile (nu şi numele – caz în care LSS ar fi fost de două-trei ori mai voluminos, iar exploatarea ulterioară devenea mai înceată); pentru discipline avem deja obj_abb.RDS şi n-avem decât să salvăm undeva şi PC – pentru a reconstitui ulterior, când ar fi cazul, numele reale.

Mai este de salvat cls_cup, în care avem repartizarea pe zile convenită la început pentru clasele cuplate (eventual şi pentru unii profesori cu ore puţine). Iar pentru a încheia modelarea încadrării – în vederea folosirii acesteia în programele de repartizare pe zile şi ore a lecţiilor, din [1] – rămâne să formulăm încă două-trei „dicţionare” (sau liste) care să evidenţieze de care alţi profesori depinde alocarea pe zile şi ore a lecţiilor unui profesor care este angajat în unul sau mai multe cuplaje; dar listele respective se obţin de-acum exact ca în [1] (Tw1, Tw2 şi Twx) şi aici nu mai avem nimic de adăugat.

Poate doar am avea de subliniat: codificarea pe discipline a profesorilor (introdusă mai sus) este superioară din orice punct de vedere, celeia banale p01...p83, pe care o adoptasem în [1] şi în alte locuri pe aici.
Desigur… apare o întrebare: cum de n-am avut-o în vedere de la bun început (în loc de cea banală)? Explicaţia „păcălelii” este simplă (dar ţine de gusturi şi obiceiuri): orarele de şcoală pe care le-am folosit pentru a dezvolta [1] vizau aşa de prost disciplinele şcolare (spre deosebire de orarul pe care l-am abordat aici, în care notaţiile sunt corecte şi coerente), încât de la bun început, am ignorat mea culpa câmpul obj (în [1] de exemplu, lecţie însemna de la bun început prof | cls, unde prof nu viza în vreun fel, disciplina).

vezi Cărţile mele (de programare)

docerpro | Prev | Next