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

Încă un experiment, pe orarul unei școli (II)

limbajul R | orar şcolar
2023 oct

[1] Problema orarului școlar echilibrat și limbajul R

[2] V. Bazon - Orare școlare echilibrate și limbajul R https://books.google.ro/books?id=aWrDEAAAQBAJ

[3] Transformarea fișierului PGN în obiect de date (I)

Intenționam să salvăm setul CDL abia în final, după toate simplificările (v. Partea I)
Dar disciplinele sunt mai încâlcite decât credeam inițial (vom vedea că sunt de făcut nu doar „simplificări”) – așa că redenumim programul precedent "adjust_cls.R", salvăm CDL (în urma simplificării denumirii claselor) în fișierul CDL.rds și înființăm un nou program, în care ne vom ocupa de discipline:

# adjust_obj.R
rm(list = ls())  # elimină obiectele din sesiunea precedentă
library(tidyverse)
CDL <- readRDS("CDL.rds")

Dicționarul disciplinelor

Este important, cum denumim disciplinele în cadrul setului de date aferent orarului școlii. Dar nu doar fiindcă denumirile prea lungi (ca aceea oficială "Limba și literatura română") nu încap în orar (chiar dacă-l prezentăm în format "landscape"); trebuie să avem posibilitatea de a face diverse statistici, pe datele respective – deci în primul rând, n-ar trebui să avem mai multe denumiri pentru o aceeași materie.

Avem un număr rezonabil, 35 de discipline:

> CDL$obj %>% unique() %>% sort()
     [1] "BIOLOGIE"                 "CHIMIE"                  
     [3] "CULTURA CIVICA"           "DESEN"                   
     [5] "DIRIGENTIE"               "ECONOMIE"                
     [7] "EDUCATIE ANTREPRENORIALA" "EDUCATIE TEHNOLOGICA"    
     [9] "FILOSOFIE"                "FIZICA"                  
    [11] "GEOGRAFIE"                "INFORMATICA"             
    [13] "ISTORIA HOLOCAUST"        "ISTORIA RELIGIILOR"      
    [15] "ISTORIE"                  "LAB INFORMATICA"         
    [17] "LATINA"                   "LI"                      
    [19] "LIMBA ENGLEZA"            "LIMBA FRANCEZA"          
    [21] "LIMBA GERMANA"            "LIMBA ROMANA"            
    [23] "LITERATURA UNIVERSALA"    "LOGICA"                  
    [25] "M"                        "MATEMATICA"              
    [27] "MATEMATICA U"             "MATEMATIICA"             
    [29] "MUZICA"                   "PSHILOGIE"               
    [31] "RELIGIE"                  "SOCIOLOGIE"              
    [33] "SPORT"                    "STUDII SOCIALE"          
    [35] "TIC"             

"Matematică" are în cazul de aici, trei sau patru denumiri (între care și una stâlcită: "MATEMATIICA"); filtrând CDL pentru disciplina "M" (indicată la indexul 25):

> CDL %>% filter(obj=="M")
      prof obj zi ora cls
    1   NA   M Mi   3  8B
    2   NA   M Mi   4  8B

vedem că "M" se face la o singură clasă 8B, pe două ore, cu un profesor „necunoscut” NA. Filtrând CDL pentru clasa 8B și apoi, pentru comparație, pentru clasa 8A – constatăm diferențe numai între numărul de ore pe "MATEMATICA" (2 ore, cu "NEMES ADRIAN" – versus 5 ore) și respectiv, LIMBA ROMANA (5 ore, versus 4 ore); de aceea, înclinăm să credem că "M" care apare numai la clasa 8B, reprezintă neapărat "MATEMATICA" (altfel, ar însemna că „examenul de evaluare națională” pe care îl au de susținut absolvenții claselor a 8-a, disociază între cei care fac 2 ore și respectiv 4 sau 5 ore de „Matematică” – ceea ce nu este adevărat).

Prin următoarea secvență de comenzi înlocuim denumirile de la indecșii 25:28 cu "MATEMATICA" și zicem că "NA" vine de la "NEMES ADRIAN":

> wh <- which(with(CDL, obj %in% c("MATEMATIICA", "MATEMATICA U", "M")) == TRUE)
> CDL[wh, "obj"] <- "MATEMATICA"
> CDL[which(with(CDL, prof=="NA") == TRUE), "prof"] <- "NEMES ADRIAN"

Procedând analog cazului "M", constatăm că "LI" (de la indexul 18) este o altă denumire pentru "LAB INFORMATICA" – și operăm modificarea necesară în CDL.

Am rămas cu 31 de materii distincte (acceptând că "INFORMATICA" și "LAB INFORMATICA" desemnează și ele, materii „distincte”) și acum ne putem gândi la simplificarea denumirilor: le vom prescurta (dar sugestiv) și vom abandona stilul „cu majuscule”; dar… mai mult, le vom și codifica prin câte două litere – intenționând să desemnăm profesorii după disciplina principală a fiecăruia (și după numărul de ore).

Mai întâi, factorizăm coloana obj, afișăm și înscriem noile denumiri în vectorul levels() asociat factorului respectiv (totodată, le abreviem convenabil):

> CDL <- CDL %>% mutate(obj = factor(obj))
> lev <- str_to_title(levels(CDL$obj))  # "Cultura Civica" pentru "CULTURA CIVICA", etc.
> abb <- abbreviate(lev, minlength=2, strict=TRUE)
> lev <- c("Biologie", "Chimie", "cultCivică", "Desen", "Dirig", # ... #
           "Religie", "Sociologie", "Sport", "stdSociale", "TIC")
> levels(CDL$obj) <- lev

"TIC" este un acronim încetățenit, așa că am păstrat scrierea cu majuscule.
Am preferat să abreviem imediat după transformarea prin str_to_title(), mai „apropiat” de numele inițiale (alte abrevieri obțineam, dacă foloseam abbreviate() abia în final).

Acum transformăm vectorul lev într-un „dicționar”, având ca nume codurile de câte două caractere din vectorul abb și ca valori, denumirile existente în lev:

> names(lev) <- as.vector(abb)  # dicționarul disciplinelor:
             Bl              Ch              CC              Ds              Dr 
     "Biologie"        "Chimie"    "cultCivică"         "Desen"         "Dirig" 
             Ec              EA              ET              Fl              Fz 
     "Economie"      "edAntrep"      "edTehnol"     "Filosofie"        "Fizica" 
             Gg              In              IH              IR              Is 
    "Geografie"   "Informatică"  "istHolocaust" "istReligiilor"       "Istorie" 
             LI              Lt              LE              LF              LG 
    "labInform"        "Latină"       "Engleză"      "Franceză"       "Germană" 
             LR              LU              Lg              Mt              Mz 
       "Românâ"    "litUnivers"        "Logică"    "Matematică"        "Muzică" 
             Ps              Rl              Sc              Sp              SS 
    "Pshilogie"       "Religie"    "Sociologie"         "Sport"    "stdSociale" 
             Tc 
          "TIC" 
> saveRDS(lev, "dict_obj.RDS")

Intern, vom folosi codurile de câte două caractere (și nu coloana obj!); dar când va fi să afișăm orarul unei clase, va trebui să folosim denumirile asociate codurilor respective – așa că am salvat dicționarul respectiv, în fișierul dict_obj.RDS.

Obs. Comenzile din secvențele redate mai sus au fost preluate din consolă (fiind precedate de promptul acesteia '>'); le puteam înscrie ca atare în programul înființat la început adjust_obj.R – sau mai bine (ca în cazul programului adjust_cls.R), într-o funcție pe care să o încorporăm acestuia. Am preferat totuși, să tastăm întâi direct în consolă, fiecare dintre comenzile respective – fiindcă astfel putem controla mai ușor lucrurile…
De exemplu: am avut nevoie de o funcție de transformare a numelor scrise cu majuscule; știm că există asemenea funcții (și nu numai în limbajul R), dar (bineînțeles) nu le știm „pe de-a rostul” – știm doar că numele lor începe de obicei cu "str_"; așa că, lucrând în consolă, am tastat "? str_" și am apăsat tasta tab – obținând pe ecran lista tuturor funcțiilor care încep astfel (și a fost ușor să depistăm funcția care ne trebuia, str_to_title()).
Sau, pentru încă un exemplu: pentru funcția abbreviate() a fost necesar (neștiind-o „pe de-a rostul”) să ne amintim (prin ?) ce parametri putem folosi.
(A nu învăța pe de rost este un principiu de lucru esențial, pentru a putea deprinde și a putea folosi după caz, mai multe limbaje de programare)
Pe de altă parte – nu prea este cazul de a ambala într-o funcție independentă comenzile respective, dat fiind că modificările vizate sunt particulare setului CDL (pentru o altă școală fiind de făcut alte modificări, decât cele de mai sus).

Pentru orice eventualitate, salvăm iarăși în CDL.rds, setul CDL rezultat mai sus prin „simplificarea” disciplinelor.

Codificarea profesorilor (și cuplajelor)

„Simplificăm” și numele profesorilor – dar nu (doar) pentru a evita vreo dispută legată de „drepturile personale”… Profesorii sunt încadrați fiecare pe câte o anumită disciplină (plus eventual, câteva ore pe unele discipline „secundare”) – de unde ideea firească de a le asocia câte un cod care ambalează disciplina principală și (cumva) numărul de ore; vizăm astfel și eficiența programelor (vom putea elimina coloana obj).
Dar vom ambala în codurile asociate profesorilor nu denumirile explicite ale disciplinelor, ci abrevierile la câte două caractere ale acestora:

levels(CDL$obj) <- names(readRDS("dict_obj.RDS"))

În cazul de față, întâi trebuie să verificăm dacă profesorii sunt și bine definiți: mai toți au câte două prenume (separate prin '-') și uneori este menționat numai unul, alteori – ambele prenume. Dar nu facem caz de asta: listăm profesorii (și cuplajele) în ordine alfabetică și reparăm ad-hoc cele două sau trei asemenea situații:

> CDL %>% pull(prof) %>% unique() %>% sort()
    # ... #
    [65] "STANGA MIRELA/RUSET OVIDIU"  "STANGA MIRELA/TEROVAN DIANA"  # cuplaje  
    [67] "STANGA VIORICA-MIRELA"  # ... #
> CDL$prof[CDL$prof=="STANGA VIORICA-MIRELA"] <- "STANGA MIRELA"

Pentru cuplaje întâlnim o situație similară celeia de mai sus: uneori apare "Prof1/Prof2/Prof3", alteori apare "Prof2/Prof1/Prof3"; unificăm cele două denumiri distincte:

> CDL$prof[CDL$prof=="INASEL LAURA/GOSA SIMONA/RITIVOIU ADA"] <- 
                   "GOSA SIMONA/INASEL LAURA/RITIVOIU ADA"

Desigur, după aceste corecturi pe CDL, salvăm iarăși în CDL.rds.

Să despărțim lecțiile obișnuite („cu întreaga clasă”), de cele care decurg „pe grupe” (pentru care în câmpul prof apare ’/’) și să evidențiem profesorii în fiecare caz:

L12 <- CDL %>% split(grepl("/", .$prof))
P1 <- L12[[1]] %>% pull(prof) %>%
       unique() %>% sort() # cei care au și ore proprii ("cu clasa întreagă")
P2 <- L12[[2]] %>% pull(prof) %>% unique() %>%
       strsplit(., "/") %>% unlist() %>% unique() %>% 
       sort() # profesorii angajați în cuplaje ("pe grupe")

setdiff(P2, P1) afișează profesorii care sunt angajați în cuplaje, dar nu au și ore proprii (îi numim „externi”); s-ar fi afișat și "STANGA MIRELA" de exemplu, dacă n-am fi corectat mai sus… În cazul nostru, nu există profesori externi.

Următoarea funcție produce disciplinele pe care este încadrat un profesor (din setul P1, sau din P2), în ordinea descrescătoare a numărului de ore:

prof_objs <- function(P, lss)
    lss %>% filter(grepl(P, prof)) %>% count(obj, sort=TRUE)

Subliniem că prof_objs() păstrează calitatea de factor a câmpului $obj și implicit, toate nivelele acestuia; dacă un profesor este încadrat pe mai multe discipline, ne va interesa acum numai cea „principală” (pe care are cel mai multe ore) și va trebui să folosim droplevels(), pentru a păstra numai nivelele corespunzătoare disciplinelor principale.

Putem obține acum o listă care asociază fiecărei discipline principale, setul profesorilor încadrați pe acea disciplină, ordonați descrescător după numărul de ore:

Pob <- map_dfr(P1, function(P) {
    OB <- prof_objs(P, L12[[1]])[1, ]
    if(P %in% P2) { # decide asupra disciplinei principale
        OB2 <- prof_objs(P, L12[[2]])[1, ]
        if(OB2$n > OB$n) OB <- OB2
    }
    data.frame(prof = P, obj = OB$obj, no = OB$n)
}) %>% droplevels() %>% # ignoră disciplinele secundare
       arrange(desc(no)) %>% split(.$obj)

Și în sfârșit, putem formula un „tabel” care asociază numelor din P1 coduri de câte 3 caractere, formate din codul disciplinei principale și un sufix care indică numărul de ordine după numărul descrescător al orelor fiecăruia:

Pcd <- map_dfr(seq_along(Pob), function(i) {
    N <- nrow(Pob[[i]])  # numărul de profesori pe disciplina curentă (< 10)
    ob <- Pob[[i]]$obj[1]
    obn <- paste0(ob, 1:N)
    data.frame(prof = Pob[[i]]$prof, cod = obn)
})

Dar bineînțeles că în loc de „tabel”, va fi mai convenabil să lucrăm cu dicționare:

prof_cod <- Pcd$cod
names(prof_cod) <- Pcd$prof
CPR <- setNames(names(prof_cod), prof_cod) # dicționar nume_profesor -> COD

Folosind dicționarul prof_cod, putem codifica acum și cuplajele – alipind codurile celor doi sau trei membri (iar în final, reunim toate codurile):

tws <- L12[[2]] %>% pull(prof) %>% unique() %>% 
       sort() # cuplajele de profesori (Prof1/Prof2 sau Prof1/Prof2/Prof3)
cup_cod <- vector("character", length(tws))
for(i in seq_along(tws)) {
    tw <- strsplit(tws[i], "/")[[1]]  # "/" leagă numele din cuplaj
    kod <- as.vector(prof_cod[tw]) # codurile membrilor
    cup_cod[i] <- ifelse(length(kod)==2,
                         paste0(kod[1], kod[2]), # alipește codurile membrilor
                         paste0(kod[1], kod[2], kod[3]))
}
cup_cod <- setNames(cup_cod, tws)
prof_cod <- c(prof_cod, cup_cod)  # dicționar profesor sau cuplaj -> COD
saveRDS(prof_cod, "dict_prof.RDS")  # util când va fi de afișat orarul final

Dicționarul prof_cod va fi util — dacă-l vom inversa — când va fi să afișăm orarul final.

Înregistrăm codurile din vectorul prof_cod, pe toate liniile din setul CDL:

CDL <- CDL %>% mutate(prof = as.vector(prof_cod[prof]))  
> slice_sample(CDL, n=5)  # un eșantion ilustrativ
           prof obj zi ora cls
    1       LG4  LE Ma   6  5A
    2       LR7  LR Jo   6 12D
    3       Tc1  Tc Vi   6  9D
    4 LG3LG1LG4  LG Vi   3 12A
    5       Gg1  Gg Ma   3 10B

Fiecare lecție este reprezentată pe câte o linie din setul CDL; pe linia respectivă, codul din câmpul prof induce pe cel din câmpul obj – dar numai în cazul disciplinelor principale; comparând pe fiecare linie, valorile prof și obj, vom putea depista liniile corespunzătoare disciplinelor secundare.
În mod implicit, operațiile pe un obiect data.frame decurg „pe coloane”; dar funcția dplyr::rowwise() asigură și posibilitatea de a opera „pe linii”, cum am avea nevoie acum:

scd <- CDL %>% rowwise(prof, obj, cls) %>%
       filter(! grepl(obj, prof)) # liniile cu discipline "secundare"
lSC <- scd %>% split(.$prof)
OSE <- map_dfr(seq_along(lSC), function(i) {
           sec <- lSC[[i]] %>% # pe profesorul curent
                  split(.$obj, drop=TRUE) # desparte pe discipline
           map_dfr(seq_along(sec), function(j) {
               Cls <- sec[[j]] %>% pull(cls)
               data.frame(
                   prof = sec[[j]]$prof[1],  # profesorul
                   obj = names(sec)[j], # disciplina secundară
                   cls = paste(Cls, collapse = " "))  # la care clase
           })
})

Obs. În [1] – de unde am cam copiat mai sus, text și secvențe de cod – procedam greșit (totuși fără consecințe, acolo): în [1] aveam sec <- lSC[[i]] (fără despărțire pe discipline, ca aici) și foloseam Obj <- sec$obj[j], unde j era indexul uneia dintre disciplinele secundare ale profesorului respectiv; numai că dacă pentru j=1 de exemplu, există mai multe clase, atunci următorul j (=2) va indica nu linia următoarei discipline secundare, ci linia celei de-a doua dintre clasele de la j-ul precedent!

Să facem și o verificare, pentru unul dintre cei încadrați și pe discipline secundare:

> OSE %>% filter(prof == "CC1")
      prof obj                     cls
    1  CC1  Dr                      5A
    2  CC1  Ec 11A 11B 11C 11E 11E 11F
    3  CC1  EA                 10B 10E
    4  CC1  Sc             11E 11E 11F
> dobj <- readRDS("dict_obj.RDS")
> CDL %>% filter(prof=="CC1") %>% count(obj) %>% 
          mutate(Disciplina = dobj[obj])
      obj n Disciplina
    1  CC 8 cultCivică  # disciplina principală
    2  Dr 1      Dirig
    3  Ec 6   Economie
    4  EA 2   edAntrep
    5  Sc 3 Sociologie

Când va fi de afișat orarul vreunei clase, va trebui să consultăm și OSE:

saveRDS(OSE, "df_obj_sec.RDS")

pentru a stabili ce disciplină afișăm pe lecția curentă a unuia sau altuia dintre profesori.

În final, salvăm iarăși setul CDL (în care acum, profesorii sunt codificați prin câte 3 caractere, sau în cazul cuplajelor, prin 6 sau prin 9 caractere) în CDL.rds.
Să ne dumirim, cât de cât deocamdată, asupra cuplajelor de trei profesori:

> CDL %>% filter(prof %in% "LG3LG1LG4") %>% arrange(zi)
            prof obj zi ora cls
    1  LG3LG1LG4  LG Jo   1 11A  # dobj["LG"]: "Germană"
    2  LG3LG1LG4  LG Jo   1 11B
    3  LG3LG1LG4  LG Jo   1 11D
    4  LG3LG1LG4  LG Mi   5 12A
    5  LG3LG1LG4  LG Mi   5 12B
    6  LG3LG1LG4  LG Mi   5 12D
    7  LG3LG1LG4  LG Vi   2 11A
    8  LG3LG1LG4  LG Vi   2 11B
    9  LG3LG1LG4  LG Vi   2 11D
    10 LG3LG1LG4  LG Vi   3 12A
    11 LG3LG1LG4  LG Vi   3 12B
    12 LG3LG1LG4  LG Vi   3 12D

Elevii din clasele 12A, 12B și 12D sunt repartizați (în ora a 5-a din ziua Mi și apoi, în ora a 3-a din ziua Vi) în trei grupe (poate „începători”, „avansați” și „cultură”), la care intră (pentru câte o lecție de "Germană") respectiv profesorii LG3, LG1 și LG4. La fel, pentru clasele 11A, 11B și 11D, în zilele Jo (ora 1) și Vi (ora 2).

Dicționarele dependențelor

—v. Partea a III-a—

vezi Cărţile mele (de programare)

docerpro | Prev | Next