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

Noul orar (partea a doua)

limbajul R | orar şcolar
2024 oct

Forma normală a lecțiilor și repartizarea pe zile

Pănă acum (v. "partea întâia") am folosit consecvent forma normală a lecțiilor: fiecare lecție este reprezentată pe câte o linie de date, care conține (într-o aceeași ordine) valori ale unor variabile independente (nu se amestecă într-o aceeași celulă, valori de natură diferită). Orice investigație dorim, va decurge folosind operații de filtrare (pentru linii de valori) și de selectare (de variabile, sau coloane), iar pentru facilitare, putem transforma o coloană sau alta în factor (ceea ce asigură și operații de grupare și „splitare” după diverse criterii) — în [1] avem deja numeroase exemplificări.

Forma normală stă la baza unui algoritm elementar care promite obținerea de repartizări „echilibrate” (pe zile, poate și pe orele zilei) a unui set de lecții prof|cls (doar "promite": repartizarea produsă nu este neapărat, acceptabilă și pot fi necesare diverse retușări).

Pentru exemplu, să considerăm lecțiile de "Română"; să vedem întâi cum sunt distribuite acestea pe zile, în orarul lăsat de [1]:

library(tidyverse)
orr8 <- readRDS("orar_8.RDS") %>% # 826 lecții cls|prof|zi|ora
        mutate(prof = factor(prof))
Zile <- orr8$zi %>% unique() %>% as.factor()

Din capul locului, am transformat prof în factor; pe lângă posibilitățile de grupare și splitare care apar astfel, va fi ușor (folosind droplevels()) să „excludem” pe cei care nu satisfac un anumit criteriu (nu au lecții de "Ro"; nu au lecții în ziua curentă; etc.).

Selectăm lecțiile de "Română" și vedem distribuția pe zile a acestora:

> pRo <- orr8 %>% filter(grepl("Ro", prof)) %>% droplevels()
> table(pRo[c("prof", "zi")])
         zi
    prof  Lu Ma Mi Jo Vi
      Ro1  3  6  6  7  0
      Ro2  4  3  6  5  0
      Ro3  3  4  2  5  4
      Ro4  0  6  5  5  5
      Ro5  4  4  5  3  0
      Ro6  3  0  0  3  0

Subliniem că fără droplevels(), comanda table() ar fi produs și liniile (cu valori zero peste tot) aferente profesorilor care nu au "Română" ca obiect principal.

Exceptând poate pe Ro6 (fiindcă are numai 6 ore), toți acești profesori au în orarul curent, distribuții neuniforme de lecții pe zile; iar distribuția zilnică a celor 101 ore de Ro este extrem de neomogenă: Jo sunt 28 ore, iar Vi sunt numai 9 ore. Înghesuind astfel lecțiile, se ajunge aproape inevitabil în situația în care clasele fac câte două ore/zi un același obiect.

Nu este chiar obligatoriu, dar noi urmărim consecvent principiul care pare a fi cel mai favorabil pentru procesul de învățare: distribuim lecțiile în mod echilibrat pe zile, clase și profesori; la fiecare clasă, lecțiile pe un același obiect sunt repartizate câte una pe zi; toate clasele își încep programul zilnic la o aceeași oră a zilei.

Aranjăm liniile din pRo după prof și cls — adică, lecțiile fiecărui profesor sunt pe linii consecutive, iar dintre acestea, cele de la o aceeași clasă sunt și ele, pe linii consecutive; apoi, (re)etichetăm toate lecțiile, de sus în jos, plasând succesiv în coloana zi vectorul Zile (sau dacă vrem, o permutare fixată a acestuia):

> Dlz <- pRo %>% select(prof, cls) %>% arrange(prof) %>%
         mutate(zi = rep_len(Zile, nrow(.)))
> Dlz  # distribuția pe zile rezultată (pentru cele 101 lecții de "Ro")
        prof cls zi            prof cls zi            prof cls zi
    1    Ro1 10B Lu        11   Ro1 11E Lu        21   Ro1  7A Lu
    2    Ro1 10B Ma        12   Ro1 11E Ma        22   Ro1  7A Ma
    3    Ro1 10B Mi        13   Ro1  6A Mi        23   Ro2 10E Mi
    4    Ro1 11B Jo        14   Ro1  6A Jo        24   Ro2 10E Jo 
    5    Ro1 11B Vi        15   Ro1  6A Vi        ... ...
    6    Ro1 11B Lu        16   Ro1  6A Lu        97   Ro6 12B Ma
    7    Ro1 11D Ma        17   Ro1  7A Ma        98   Ro6 12B Mi
    8    Ro1 11D Mi        18   Ro1  7A Mi        99   Ro6 12B Jo
    9    Ro1 11D Jo        19   Ro1  7A Jo        100  Ro6 12B Vi
    10   Ro1 11E Vi        20   Ro1  7A Vi        101  Ro6  9F Lu

Procedând astfel, lecțiile unui profesor la o aceeași clasă cad în zile diferite (exceptând desigur, cazul când are mai mult de 5 ore la acea clasă), iar distribuțiile orare ale profesorilor respectivi sunt toate, uniforme; deasemenea, distribuția pe zile a celor 101 lecții de Ro este și ea, uniformă (cu 20 sau 21 ore/zi):

> addmargins(table(Dlz[c("prof", "zi")]))
         zi
    prof   Lu  Ma  Mi  Jo  Vi Sum
      Ro1   5   5   4   4   4  22
      Ro2   3   3   4   4   4  18
      Ro3   4   4   4   3   3  18
      Ro4   4   4   4   5   4  21
      Ro5   3   3   3   3   4  16
      Ro6   2   1   1   1   1   6
      Sum  21  20  20  20  20 101

Înlocuind vectorul Zile cu o permutare oarecare a sa și repetând etichetarea în coloana zi, obținem alte repartizări pe zile, care păstrează caracterul de uniformitate.

Matricea orară a lecțiilor zilei și socotirea ferestrelor

Câte lecții sunt în orarul curent orr8, pe fiecare zi în parte ? Formulăm întâi o listă care asociază fiecărei zile, subsetul lecțiilor pe acea zi:

Lz <- map(Zile, function(z)
          orr8 %>% filter(zi == z) %>%
          select(-c(zi))  # excludem câmpul 'zi'
      ) %>% setNames(Zile)

Aplicând nrow() pe fiecare subset, avem distribuția numărului de lecții pe fiecare zi (dar fără lecțiile cuplate pe "viz/muz", pentru care făcusem în [1] un "orar" separat):

map_int(Zile, function(z) nrow(Lz[[z]])) %>% setNames(Zile)
     Lu  Ma  Mi  Jo  Vi
    169 163 176 161 157

Cum era de așteptat (având în vedere „înghesuirea” evidențiată mai sus), avem o distribuție neomogenă; o distribuție uniformă (sau "omogenă") a celor 826 de lecții ar fi avut 4 zile cu câte 165 și una cu 166 lecții.

Desigur, subseturile respective sunt și ele în "formă normală"; de exemplu, Lz[["Lu"]] este coșcogeamite listă, un tabel cu 169 linii cls|prof|ora… Folosind direct această formă, nu-i ușor să formulezi o secvență de comenzi prin care să găsești câte ferestre are fiecare profesor, în ziua respectivă.

Pentru calculul numărului de ferestre ne-ar conveni mai mult un format matriceal, reprezentând pe fiecare linie orarul câte unui profesor; am avea numai atâtea linii câți profesori au ore în acea zi și pe de altă parte, văzând șabloanele orare de pe linii putem determina (nu chiar „ușor”, trebuind să ținem seama de cuplaje) numărul de ferestre din ziua respectivă.

Dacă în K am avea lecțiile dintr-o aceeași zi ale unuia dintre profesori, atunci prin pivot_wider(K,...) am ajunge la un format matriceal: valorile 1:8 din coloana ora ar deveni coloanele „matricei”, iar valorile din câmpul cls ar fi plasate corespunzător pe aceste coloane (folosind constanta NA, când în ora respectivă profesorul este liber).

Următoarea funcție produce matricea orară a lecțiilor unei zile (din lista Lz):

hourly_matrix <- function(Dorar) { # prof|cls|ora (pe ziua curentă)
    orz <- Dorar %>% droplevels() %>%
           split(.$prof) %>%
           map_df(., function(K)
               pivot_wider(K, names_from="ora", values_from="cls"))
    M <- orz[, c('prof', sort(colnames(orz)[-1]))] %>%
         replace(is.na(.), '-') %>% as.matrix()
    row.names(M) <- M[, 1]  # profesorii devin numele de linie ale matricei
    M[, 2:ncol(M)]
} # prof: 1|2|3|4|5|6|7 (linia claselor alocate pe ore)

De exemplu, pentru prima zi, transformând matricea pentru a afișa datele într-un tabel cu două coloane (ordonăm liniile în ordinea alfabetică a numelor de linie, adică în ordinea alfabetică a profesorilor; apoi integrăm numele de linie drept coloană, și constituim un "tabel" cu trei coloane, prima pentru prima jumătate a liniilor matricei, a doua drept separator de coloane și a treia, pentru a doua jumătate a liniilor matricei):

Mz <- hourly_matrix(Lz[["Lu"]])  # matrice [1:45, 1:7] (45 profesori)
Mz <- Mz[order(rownames(Mz)), ]  # liniile în ordinea alfabetică a profesorilor
Mz <- cbind(rownames(Mz), Mz)   # copiază numele de linie în coloana V1
data.frame(Mz[1:23, 2:8], rep("   ", 23), rbind(Mz[24:45, ], " "),
           check.names = FALSE, fix.empty.names = FALSE) %>% print()

             1   2   3   4   5   6   7     V1   1   2   3   4   5   6   7
    Bl2    10A  5A  6A  6A  9F  7A  9E    In2   -   -   -   - 10B  8A  9D
    Bl3      - 12E 10C 10B 10D 12E   -    In4 11C 12C  7A 11C  7A  9E   -
    Ch1     7A  9A  8A   - 12E 10E  8A    Is1   -   -   - 10F  5A 10C   -
    Ch2     9D 12D   - 11E 12C 11D 11A    Is2   -   -   -  9B  8A   -   -
    Ec1    10F 11F 12F   - 11D 11F 10C    Lg1 12A  9D  9E 12B 12F   -   -
    En1      -   -   -  9E 10C  9B 11E    Mt1 12D   - 12B 10A 11A 12A   -
    En2      -   - 11B   - 12D 12B   -    Mt2 12C 10C 10E 12C   -   -   -
    En3    11F   -   -  5A   -   -   -    Mt3  6A  7A  5A  8A   -   -   -
    En4En2   - 10A   - 12A   -   -   -    Mt4 10B 11D 11C 12E   -   -   -
    En4En3   -   -  9A   -  9A  9A   -    Mt5   -  9B  9D  9D   -   -   -
    En5      -   -  9F   -   -   -   -    Mt6   - 10D 10D   -   -   -   -
    En5En1 11A 11A   -   -   -   -   -    Mt7 11B 11B   -   -   -   -   -
    eS1     5A  6A   -   - 10A 10B   -    Rl2  9A 11E 11F   - 11C 11A 11D
    Fr1     9E  9E   -   -   -   -   -    Ro1   -   - 10B 11B   -  6A   -
    Fz1    11D 11C 11E   -  9E 12C 12D    Ro2   -   -   -  9F 11F 10F 10E
    Fz3    10C  9F 10A 10E 10F 10D 10A    Ro3  9B 12F   -  9A   -   -   -
    Fz4    12E 10B   -   -   -   -   -    Ro5   -   - 12D 10C 12A  9D   -
    Fz5    12B   -  9B   -   -   -   -    Ro6  9F 12B 10F   -   -   -   -
    Gg1    12F  8A 12C 12F  9B   -   -    Sp1   -   - 12E   -   - 11E  7A
    Gg3      -   -   - 12D 11E   -   -    Sp2 10E 10F 12A 10D   -   -   -
    Gr2Gr1   -   -   - 11D  9D 12D 10D    Sp3   -   -   -   - 12B  9F  9B
    Gr3      -   - 11A  7A  6A 10A  9A    TI1   - 10E 11D 11F 10E   -   -
    In1      - 12A   - 11A 11B   -   -                                   

Pentru primele 23 de linii numele de linie sunt cele din rowname(Mz)[1:23] și am ignorat copia acestora din coloana "V1" (reținând numai coloanele 2:8); pentru celelalte linii (completate cu o linie "vidă", pentru a avea tot 23 de linii), numele profesorilor sunt cele din coloana "V1" (care corespund acestor linii).

Pe matricea orară a zilei este ușor de văzut ferestrele; de exemplu (pe matricea redată mai sus) Bl2 și Bl3 nu au ferestre, iar Ch1 are o singură fereastră, în ora a 4-a. Nu este greu nici de numărat totalul de ferestre din ziua respectivă — trebuie contorizate aparițiile caracterului "-" între lecții; doar că în cazul celor angajați în cuplaje, o asemenea apariție nu constituie neapărat, o fereastră…

Să vedem orarul celor angajați în cuplajele pe "En", existente în ziua Lu:

> print.table(Mz[grepl('En', rownames(Mz)), 2:8])
           1   2   3   4   5   6   7  
    En1    -   -   -   9E  10C 9B  11E
    En2    -   -   11B -   12D 12B -  
    En3    11F -   -   5A  -   -   -  
    En4En2 -   10A -   12A -   -   -  
    En4En3 -   -   9A  -   9A  9A  -  
    En5    -   -   9F  -   -   -   -  
    En5En1 11A 11A -   -   -   -   -  

Pe linia lui En1 nu vedem vreo fereastră — și totuși, En1 are o fereastră („ascunsă”) în ora 3, dat fiind că în orele 1 și 2 are de intrat împreună cu En5 la clasa 11A.
Pe linia lui En2 vedem o fereastră în ora 4 — dar este o fereastră „falsă”, dat fiind că în ora 4 are de intrat în cuplaj cu En4 la clasa 12A.
En3 are o singură fereastră, în ora 2 (cea din ora 3 este acoperită de cuplajul cu En4); En4 și En5 au zero ferestre.

În total, pentru ziua Lu avem 16 ferestre (adică aproape 9.5% din totalul 169 de lecții).
De observat că Gr2 și Gr1 apar numai în cuplaj (nu și singuri, cu ore proprii); altfel spus, aceștia sunt „externi” pentru ziua Lu.

vezi Cărţile mele (de programare)

docerpro | Prev |