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

Problema orarului școlar echilibrat și limbajul R (III)

limbajul R | orar şcolar
2023 may

[1] Vlad Bazon - Problema orarului școlar, pe tărâmul limbajului R (Google Play/Books, 2022)

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

Funcția move_cls() din [1] asigură mutarea unei clase pe orarul unei zile fixate (urmărind lanțul Kempe care conține clasa, din graful asociat lecțiilor din cele două coloane orare între care se face schimbarea) – scopul fiind dat de căutarea unei reduceri a numărului de ferestre din orarul zilei respective; în [2] avansasem ideea unei variante de move_cls(), prin care să schimbăm (între doi profesori) o clasă din orarul unei zile pe orarul unei alte zile – de fapt o idee cam simplistă, de folosit ca atare cel mult într-o ultimă instanță…

Schimbarea de clase între zile ține de „repartizarea pe zile” a lecțiilor săptămânii, care la noi (în [1]) precede stabilirea orarului pe fiecare zi în parte. Iar reduce_gaps.R produce la fiecare execuție un alt orar (cu altă distribuție a ferestrelor) și n-ar putea fi vorba în program, de inspirația necesară pentru indicarea unor lecții prof|cls|ora a căror mutare dintr-o zi în alta ar conduce la micșorarea numărului de ferestre pe orarele celor două zile.
Deci nu prea are sens, să ne gândim la „variante” de move_cls().

Mai bine să ne gândim la motivele pentru care nu mai putem reduce numărul de ferestre din orarul obținut pe o zi sau alta… Dacă aceste motive ar ține numai de modul în care procedăm (urmărind consecvent o repartizare echilibrată a lecțiilor), atunci repetând „la nesfârșit” reduce_gaps.R, am nimeri la un moment dat și un orar cu număr micșorat de ferestre. Motivele reale pornesc totuși, în cea mai mare măsură, de la specificul încadrării inițiale a profesorilor și cuplajelor; dacă s-au prevăzut cuplaje întortocheate, acoperind în total prea multe ore pe zi – atunci ferestrele vor fi greu de evitat (la fel, dacă școala funcționează în două schimburi și sunt mulți profesori încadrați pe clase din ambele schimburi).

Ei, dar tot vizualizând (ca în [2]) datele din orar_mai.RDS, am observat o „greșală” de repartizare: la doi profesori, câte o clasă apare într-o aceeași zi de două ori – fără ca aceasta să fi fost necesar (de exemplu – v. [2] – clasa 12A apare de două ori în ziua Ma la profesorul IP1, deși acesta are doar 4 ore pe săptămână la 12A și deci, i s-ar fi cuvenit câte o singură intrare pe zi, în 4 zile). Ca urmare, fără să ne mai gândim la ferestre – am reluat repartizarea pe zile a lecțiilor, corectând interactiv defectul respectiv și chiar, mai schimbând pentru echilibrare, alte câteva repartiții individuale.

Am actualizat apoi, corespunzător noii repartiții pe zile, listele de coeficienți "betweenness" ai profesorilor și claselor pe fiecare zi, precum și listele de dependențe între profesori și cuplaje, pe fiecare zi; deasemenea, am determinat pentru fiecare zi, profesorii „externi” – aceia care intră în măcar două cuplaje, dar în ziua respectivă nu au și ore proprii (și atunci, orice fereastră a cuplajelor în care este implicat devine fereastră „ascunsă” a profesorului extern respectiv; în funcția emph_gaps() din [2] ne-a scăpat acest aspect!).

Folosind apoi mount_hours(), am obținut noile orare zilnice (iarăși, foarte repede – dat fiind că nu ne-a păsat deocamdată, de ferestre); cam ca în [2] (dar acum trebuie să folosim și prof_matrix() din [1]) – afișăm numărul curent de ferestre (inițial, foarte mare):

Orr <- readRDS("orarMai.RDS")
    #> str(Orr[["Lu"]])  ## structura orarului unei zile
    #tibble [204 × 3] (S3: tbl_df/tbl/data.frame)
    # $ prof: Ord.factor w/ 69 levels "Is3"<"En6"<"Ch3Bi1"<..: 30 38 48 57 ...
    # $ cls : chr [1:204] "10A" "10A" "10A" "10A" ...
    # $ ora : int [1:204] 1 2 3 5 4 6 1 2 4 5 ...
Mai <- lapply(Zile, function(zi) 
              prof_matrix(Orr[[zi]])  # v. [1]
       ) %>% setNames(Zile)
sapply(Zile, function(zi) {
    set_globals(zi)
    count_gaps(Mai[[zi]])
}) |> print()  # numărul curent de ferestre, pe fiecare zi
#    Lu Ma Mi Jo Vi 
#    38 35 43 38 37 

Mai departe, prin programul reduce_gaps.R am obținut ca și în [2], orare cu mai puține ferestre – pe care apoi, nu le-am mai putut reduce:

Mai <- readRDS("orar_mai_12.RDS")  # după reducerea de ferestre
sapply(Zile, function(zi) {
    set_globals(zi)
    count_gaps(Mai[[zi]])
}) |> print()
    Lu Ma Mi Jo Vi 
     6 12  9  5  4   # a compara cu [2] (5 10 9 6 5)

Ne-au rămas aceleași două zile ca și în [2], Ma și Mi, pe care numărul de ferestre este sensibil mai mare decât în celelalte zile (și n-a mai putut fi redus)…

Dacă vrem să evidențiem (corect) repartiția ferestrelor pe zile și profesori – trebuie să completăm emph_gaps() din [2], cu o funcție prin care să depistăm profesorii „externi”:

emph_gaps <- function(ORR) {
    Oz <- ORR %>% as.data.frame() %>% 
          mutate(prof = rownames(.), .before=1) %>%
          mutate(SB = "")
    rownames(Oz) <- NULL
    gaps_ext <- function() {
        l36 <- Oz %>% split(nchar(.$prof))
        k3 <- l36[[1]] %>% pull(prof)
        k6 <- l36[[2]] %>% pull(prof)
        exz <- map_chr(k6, function(K) { 
                  P1 <- substr(K, 1, 3)
                  P2 <- substr(K, 4, 6)
                  if_else(!P1 %in% k3, P1, if_else(!P2 %in% k3, P2, ""))})
        unique(exz[exz != ""])
    }  # profesorii "externi" pe ziua respectivă
    for(P in Oz$prof) {
        if(nchar(P) > 3) next
        MP <- Oz %>% filter(grepl(P, prof))
        if(nrow(MP) == 1) {
            patt <- sum(h2bin[which(! MP[1, 2:8] %in% "-")]) %>%
                    byte_as_line()
            if(grepl(".*\\*-{1,2}\\*.*", patt))
                Oz[Oz$prof == P, "SB"] <- patt
        } else {
            patt <- 0L
            for(i in 1:nrow(MP))
                patt <- patt + sum(h2bin[which(! MP[i, 2:8] %in% "-")])
            Oz[Oz$prof == P, "SB"] <- byte_as_line(patt)
        }
    }
    Oz <- Oz %>% arrange(prof)
    for(P in gaps_ext()) {
        MP <- Oz %>% filter(grepl(P, prof))
        patt <- 0L
        for(i in 1:nrow(MP))
            patt <- patt + sum(h2bin[which(! MP[i, 2:8] %in% "-")])
        Oz <- Oz %>% add_row(prof = P, SB = byte_as_line(patt))
    }
    Oz[is.na(Oz)] <- "="  # extern (numai în cuplaj, fără ore proprii)
    Oz
}

De exemplu, cele 9 ferestre din ziua Mi sunt repartizate astfel:

> Mai[["Mi"]] |> emph_gaps() |> per_gaps()  # v. [2]
          prof   1   2   3   4   5   6 7      SB
        1  En1 12D  9E   - 10C  9B 10B - **-***-
        2  En2  6D 11B  9D   -  8C 10D - ***-**-
        3  ES1  6C  7B   -   -  6D   - - **-**--
        4  Fi1  6B 12C 11A   - 10B   - - ***-*--
        5  Is1  8B  8A   -  6C 10D 10E - **-***-
        6  Re1  7A  9A   -   - 11C  6A - **--**-
        7  TI1  9B  9D   - 10D 12B   - - **-**--
        8  GF3   =   =   =   =   =   = = -**-**-

GF3 (adăugat la sfârșit, prin add_row()) are 4 ore, toate în cuplaj cu alți profesori – și i-a rezultat o fereastră „ascunsă”, anume în ora a 4-a a zilei.

Găsim ca explicație principală a „imposibilității” de a mai reduce numărul de ferestre, faptul că pe ziua respectivă avem multe ore cuplate (și întortocheat), la un număr mare de clase.
Folosindu-ne de listele lTz1 și lTz2 (care indică pentru fiecare zi, profesorii angajați în cuplaje și cuplajele respective – v. [1]), afișăm situația orelor cuplate pentru ziua Mi (adăugând și coloana șabloanelor orare ale celor implicați în cuplaje):

> Mai[["Mi"]][rownames(Mai[["Mi"]]) %in% union(names(lTz2[["Mi"]]), 
                                               names(lTz1[["Mi"]])), ])
           1  2   3   4   5   6   7
    GF1    5C -   -   -   -   -   -  ******-
    GF4    5A -   -   -   -   -   -  *****--
    La1ES1 -  -   -   12E -   -   -
    Fr1    -  -   -   -   8B  -   -  ---***-
    GF2Fr1 -  -   -   -   -   12C -
    Mu1De1 -  -   10D 9B  -   -   -
    GF2GF3 -  10B -   -   12D -   -
    La1    -  -   -   -   7C  11E -  ---***-
    De1    7B 5D  -   -   6A  -   -  *****--
    GF1Fr1 -  -   -   9A  -   -   -
    GF1GF3 -  -   12B -   -   10A -
    ES1    6C 7B  -   -   6D  -   -  **-**--
    GF1GF4 -  12A -   -   9D  -   -
    GF2GF4 -  -   9B  11E -   -   -
    Mu1    -  8C  -   -   5B  6C  -  -*****-
    GF3    =  =   =   =   =   =   =  -**-**-  # extern  

Numai doi dintre profesorii angajați în cuplaje (ES1 și GF3) au câte o (singură) fereastră (iar celelalte 7 ferestre sunt repartizate la profesori neimplicați în cuplaje).

Transformând matricea redată în data.frame, afișând valorile diferite de '-' și folosind unique() – constatăm că pe Mi avem 27 de ore „pe grupe”, la 23 de clase distincte (dintre cele 34 de clase existente).

Numărul de clase angajate în aceste cuplaje este chiar mare (23 din 34), iar pe de altă parte, avem și cuplaje „întortocheate” – de exemplu, GF1 are o oră în care intră singur și pe încă 5 ore, intră în cuplaj cu Fr1, GF3 și GF4 (ocupând 6 ore dintre cele 7 ale zilei, în condițiile în care numai două-trei clase au și ora 7); cam la fel stau lucrurile și pentru GF2 și GF4.

Nu prea se poate să asigurăm (păstrând echilibrele) că lecțiile astfel cuplate – și întortocheat și pe așa de multe clase – decurg fără suprapuneri și fără prea multe ferestre și pe de altă parte, că nu apar prea multe ferestre (7 în cazul zilei Mi) la „ceilalți” profesori!

vezi Cărţile mele (de programare)

docerpro | Prev | Next