momente şi schiţe de informatică şi matematică
anti point—and—click

Explorarea datelor orarului şcolar

R | orar şcolar
2020 dec

Orarul, ca „cearşaf” Excel

Orarul şcolar prevede intrările fiecărui profesor, în anumite ore de pe parcursul zilelor de lucru, la clasele care i-au fost repartizate – pentru a desfăşura câte o lecţie specifică unei anumite discipline şcolare; de regulă, o lecţie angajează un singur profesor şi o singură clasă (întreagă).
De observat că avem cinci termeni, sau părţi: profesori, obiecte (sau discipline), clase, zile şi ore; neglijăm aici, alte condiţionări (asupra sălilor de clasă, asupra zilelor sau orelor de lucru pentru diverşi profesori, etc.) fiindcă vizăm nu elaborarea orarului, ci – ca şi în [1] – însuşi orarul deja constituit (şi aproape de a fi definitivat).

De obicei orarul este produs (sau redat) într-o formă uşor de citit, indicând pe coloane obiectul, profesorul şi clasele repartizate acestuia în ordinea orelor din zi, pentru fiecare zi (în total ar fi 62 de coloane; este „uşor” de citit, nu?):

obj,prof,l1,l2,l3,l4,l5,l6,l7,l8,l9,l10,l11,l12,  # orarul pe Luni: -,-,-,12A,12B,11B,9A,~,~,~,~,~
         m1,m2,m3,m4,m5,m6,m7,m8,m9,m10,m11,m12,  # Marţi:         -,-,-,12B,-,-,9A,10A,9B,~,~,~
         c1,c2,c3,c4,c5,c6,c7,c8,c9,c10,c11,c12,  # Miercuri:      -,-,-,-,-,-,~,~,10B,~,~,~
         j1,j2,j3,j4,j5,j6,j7,j8,j9,j10,j11,j12,  # Joi:           -,-,11A,11B,-,-,~,10B,~,~,~,~
         v1,v2,v3,v4,v5,v6,v7,v8,v9,v10,v11,v12  # Vineri:         -,-,-,11B,-,-,~,9B,10A,~,~,~

De obicei, fiecare zi conţine 12 ore; '-' şi '~' marchează aici orele în care profesorul este liber (în primul schimb de 6 ore, respectiv în al doilea). În exemplificarea de mai sus, în ziua de Luni, profesorul este liber primele 3 ore, apoi are ore la clasele 12A, 12B, 11B şi 9A (şi apoi este liber).

Forma aceasta este moştenită cumva de pe vremea când orarul era produs manual (nu se inventase Excel… ca să faci la fel): se folosea o coală de hârtie A3 liniată vertical pentru cele 62 de coloane şi orizontal pentru profesori – „cearşaf”, în jargonul acelor autori; în celulele formate se înscriau clasele (cu creion, în eventualitatea ştergerii ulterioare) – sau se aşezau jetoane inscripţionate cu numele claselor – evitând mereu situaţia în care doi profesori ar intra simultan la o aceeaşi clasă.

Orarul unui profesor se vede imediat pe linia (cam lungă) corespunzătoare lui; dar orarul unei clase este mai greu de extras (trebuie să cauţi clasa pe fiecare coloană). Pentru unele prelucrări, forma „cearşaf” este foarte convenabilă; de exemplu, putem extrage imediat orarul unei zile (astfel, pentru orarul zilei de joi, selectăm coloanele obj, prof şi j1:j12); iar dacă am vrea orarul pe o anumită disciplină, selectăm liniile care în coloana obj au înscrisă disciplina respectivă.

Ar fi destule alte întrebări asupra datelor respective, pentru care structurarea ca „cearşaf” este mai degrabă neconvenabilă. Cea mai simplă întrebare ar fi aceasta: care sunt profesorii liberi în cutare zi? Desigur, putem răspunde imediat privind pur şi simplu, ”cearşaful”; dar dacă ar fi să prelucrăm printr-un program datele respective, aduse de exemplu într-o foaie Excel – atunci programul ar trebui să verifice pentru fiecare profesor dacă cele 12 câmpuri aferente acelei zile conţin '-' sau '~'.

O altă întrebare firească ar fi: câte ore are de făcut în total, fiecare profesor la fiecare clasă? Orele profesorului sunt repartizate omogen, pe zilele săptămânii? (nu două ore într-o zi şi 7 într-o alta). Care profesori şi în care zile, au câte o singură oră într-unul sau altul dintre cele două schimburi? Cum arată distribuţia ferestrelor profesorilor? Care profesori au cel mult 4 ore la o clasă, iar acestea sunt repartizate în mai puţine zile decât numărul respectiv de ore (de exemplu, 4 ore de "Română" în două zile)?

Asemenea întrebări „colaterale” sunt importante: permit verificarea corectitudinii orarului şi judecarea calităţii acestuia; pe baza răspunsurilor obţinute, poţi vedea cam ce modificări ar mai fi de încercat pentru a îmbunătăţi orarul iniţial.

Pentru a facilita abordarea unor asemenea chestiuni, datele ar trebui să fie mai bine explicitate; în fond, datele orarului ţin de una dintre aceste cinci categorii de valori: obiect, profesor, clasă, zi, oră. Să observăm că în structura tabelară standard redată mai sus, valorile pentru zi şi oră sunt cumva ascunse în denumirile coloanelor (şi nu sunt „date” propriu-zise): de exemplu, j5 reprezintă împreună valoarea "joi" a variabilei zi şi valoarea "5" a variabilei ora; variabila clasa nu este nici ea, explicitată – valorile posibile fiind disipate în cele 60 de coloane (pentru zile şi ore) ale tabelului (încât, dacă ai vrea să vezi „care sunt clasele?”, ar trebui să cercetezi toate coloanele).

Vom „explicita” şi apoi vom investiga datele respective, folosind pachetul (sau „dialectul”) tidyverse din R. Plecăm de la un orar şcolar reprezentat în forma tabelară standard („cearşaf” Excel) în fişierul qar.csv (a vedea eventual şi [1]).

Explicitarea datelor

Mai întâi să vedem repede (într-o sesiune R interactivă) cum arată datele din qar.csv:

vb@Home:~/20dec$ R -q
> library(tidyverse)
> (read_csv("qar.csv"))
# A tibble: 54 x 62
   obj   prof  l1    l2    l3    l4    l5    l6    l7    l8    l9    l10   l11  
   <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr>
 1 bio   P01   -     -     -     12A   12B   11B   9A    ~     ~     ~     ~    
 2 rom   P02   -     11A   12E   -     -     -     ~     ~     ~     9E    10E  
 3 rom   P03   -     -     -     -     11C   11D   9B    10B   ~     ~     ~    
 4 rom   P04   -     -     -     12B   11B   12G   ~     ~     ~     ~     10F  
 5 rom   P05   12C   11E   -     -     -     -     9G    10G   ~     ~     ~    
 6 rom   P06   -     12A   12F   -     -     -     ~     10D   9C    10A   ~    
 7 fra   P07   11E   -     11F   11C   -     -     ~     ~     ~     ~     ~    
 8 fra   P08   -     -     -     12E   12C   -     ~     ~     ~     ~     9A   
 9 fra   P09   -     -     -     -     -     -     ~     ~     ~     9C    10A  
10 eng   P10   12E   12G   -     -     -     -     ~     ~     ~     ~     ~    
# … with 44 more rows, and 49 more variables: l12 <chr>, m1 <chr>, m2 <chr>,
#   m3 <chr>, m4 <chr>, m5 <chr>, m6 <chr>, m7 <chr>, m8 <chr>, m9 <chr>,
 ... 
#   v9 <chr>, v10 <chr>, v11 <chr>, v12 <chr>

Am folosit read_csv() din readr (pachet ataşat implicit, prin tidyverse), obţinând un obiect de tip tibble (care extinde tipul data.frame cu funcţionalităţi care între altele, asigură şi „explicitarea” datelor); fiindcă am inclus comanda între paranteze, rezultatul citirii este afişat pe ecran: anume, sunt afişate primele 10 linii (dintre cele 54) şi primele 13 coloane (cât încap pe ecran, dintre cele 62), specificând şi tipul valorilor (toate sunt de tip "<chr>", adică sunt „şiruri de caractere”).

Bineînţeles că în prealabil, am înlocuit numele reale din coloana $prof cu nişte nume fictive (folosind de exemplu paste0("P", (10:54)), rezultă numele "P10".."P54"); am prescurtat la trei litere, numele obiectelor din coloana $obj. Valorile din coloanele 3:62 ar fi valori ale variabilei clasa, dar… '-' şi '~' nu sunt „clase”, ci semnifică absenţa clasei; în R, absenţa unei valori (indiferent de tipul acesteia) se indică prin constanta logică NA – deci vom avea de înlocuit '-' şi '~' prin NA.

Următorul program R produce explicitarea datelor, cum am vrut mai sus. Citim qar.csv, dar acum avem grijă să folosim NA (în loc de '-' şi '~'); obiectul "tibble" rezultat este transmis apoi (prin operatorul "pipe", %>%) metodei gather() şi rezultatul acesteia este transmis metodei separate(); rezultatul final este depus în variabila qar:

# explan.R
library(tidyverse)
qar <- read_csv("qar.csv", na = c('-', '~')) %>%
       gather("ziOra", "clasa", 3:62) %>%
       separate(ziOra, c("zi", "ora"), sep=1)

gather("ziOra", "clasa", 3:62) ia toate valorile din coloanele 3:62 (în total, 60×54 = 3240 valori) şi le depune în coloana denumită "clasa", corespunzător valorilor din noua coloană "ziOra", pe care se înregistrează numele de coloană ("l1", "l2", ..., "l12", ...,"v1", "v2", ..., "v12") pe care se aflau în tabelul primit, clasele respective.

Apoi, separate(ziOra, c("zi", "ora"), sep=1) separă după primul caracter valorile existente în coloana "ziOra", înfiinţând coloanele "zi" şi "ora".

Încărcăm programul (implicit, va fi şi executat) şi vedem ce obţinem:

vb@Home:~/20dec$ R -q
> source("explan.R")
> qar
# A tibble: 3,240 x 5
   obj   prof  zi    ora   clasa
   <chr> <chr> <chr> <chr> <chr>
 1 bio   P01   l     1     NA   
 2 rom   P02   l     1     NA   
 3 rom   P03   l     1     NA   
 4 rom   P04   l     1     NA   
 5 rom   P05   l     1     12C  # prof "P05" face "rom" în ora "1" din ziua "l", la clasa "12C"
 6 rom   P06   l     1     NA   
 7 fra   P07   l     1     11E  
 8 fra   P08   l     1     NA   
 9 fra   P09   l     1     NA   
10 eng   P10   l     1     12E  
# … with 3,230 more rows

În qar, datele corespund acum celor cinci categorii la care ne gândisem la început. Putem afişa toate cele 3240 de linii de date, prin comanda print(qar, n=Inf); liniile apar ordonate după tripletele de valori (sau semnificaţia acestora) din coloanele $prof, $zi şi $ora – de exemplu, primele 54 de linii acoperă toţi profesorii, pentru ziua "l" (care este prima din săptămână), ora "1"; următoarele 54 de linii acoperă toţi profesorii, pentru ziua de "l", ora "2"; ş.a.m.d.; ultimele 54 de linii acoperă toţi profesorii, pentru ziua "v" (care este ultima), ora "12".

Spre deosebire de cazul tabelului Excel iniţial, acum putem vedea uşor de exemplu, care sunt clasele (valorile variabilei $clasa):

> sort(unique(qar$clasa))
 [1] "10A" "10B" "10C" "10D" "10E" "10F" "10G" "11A" "11B" "11C" "11D" "11E"
[13] "11F" "11G" "12A" "12B" "12C" "12D" "12E" "12F" "12G" "9A"  "9B"  "9C" 
[25] "9D"  "9E"  "9F"  "9G"

Sunt deci 28 de clase, câte 7 pe nivelele 9, 10, 11 şi 12.

Să observăm că în coloana zi avem ca valori câte o literă, iar literele respective nu respectă ordinea zilelor săptămânii – ceea ce este neconvenabil pentru ordonarea datelor, ca şi pentru diverse alte explorări asupra acestora; deasemenea, ar fi mai convenabil să avem în coloana ora nu "<chr>", ci "<int>" (valori întregi). Pentru a converti în final cele două coloane, completăm programul explan.R astfel:

# explan.R
library(tidyverse)
qar <- read_csv("qar.csv", na = c('-', '~')) %>%
       gather("ziOra", "clasa", 3:62) %>%
       separate(ziOra, c("zi", "ora"), sep=1) %>%
       mutate(zi = case_when(zi == "l" ~ 1L,
                             zi == "m" ~ 2L,
                             zi == "c" ~ 3L,
                             zi == "j" ~ 4L,
                             zi == "v" ~ 5L),
              ora = as.integer(ora))

Prin metoda mutate() (împreună cu case_when()) am înlocuit caracterele prin care erau reprezentate zilele ("l", "m", "c", "j", "v") cu numerele de ordine ale acestora (folosind sufixul "L", pentru a avea tipul "<int>" şi nu "<double>") şi am transformat în "<int>" valorile (iniţial, de tip "<chr>") din coloana ora.

De-acum, valorile din coloana qar$zi sunt indicii 1..5 ai zilelor de lucru, iar cele din coloana qar$ora sunt întregii 1..12.

O bucată de timp, vom lucra în contextul creat prin încărcarea programului explan.R (mai completându-l din când în când, cu diverse alte secvenţe de program); dar mai târziu, nu vom mai avea nevoie din acest program decât de obiectul qar şi este nefiresc să reîncărcăm şi să (re)executăm explan.R, pentru a-l obţine din nou. Funcţia saveRDS() serializează obiectul R indicat şi îl salvează într-un fişier:

> saveRDS(qar, file="qar.rds")

Ulterior, vom putea reconstitui qar (într-o sesiune de lucru cu R în care avem şi tidyverse), folosind readRDS("qar.rds").

Profesorii liberi

Care şi câţi sunt, profesorii liberi într-o zi sau alta? „Liber” înseamnă că în toate cele 12 ore din ziua respectivă, acel profesor are în coloana qar$clasa valoarea NA.

O întrebare similară: care şi câţi sunt, profesorii care au câte o singură oră (sau pentru un alt caz, exact două ore), într-o zi sau alta? Să observăm că aceasta înseamnă că în ziua respectivă, profesorul are în coloana clasa exact 11 (respectiv, 10) valori NA (de unde avem şi asemănarea cu întrebarea precedentă).

Prima operaţie de făcut, ar fi aceea de a reţine din qar numai liniile în care pe coloana $clasa apare NA (pentru aceasta, folosim filter() şi is.na()):

> filter(qar, is.na(clasa))
# A tibble: 2,412 x 5
   obj   prof     zi   ora clasa
   <chr> <chr> <int> <int> <chr>
 1 bio   P01       1     1 NA   
 2 rom   P02       2     1 NA   
 3 rom   P03       3     1 NA   
 4 rom   P04       4     1 NA   
 5 rom   P06       6     1 NA   
 6 fra   P08       8     1 NA   
 7 fra   P09       9     1 NA   
 8 eng   P13      13     1 NA   
 9 eng   P14      14     1 NA   
10 mat   P15      15     1 NA  
# … with 2,399 more rows

Deocamdată doar am afişat rezultatul – nu am prevăzut o variabilă în care să şi reţinem, rezultatul filtrării efectuate.

În treacăt, ar fi de observat că având 2412 valori NA, rămân 3240–2412=828 ore desfăşurate efectiv la clase; aceasta înseamnă că sunt clase care au mai puţin de 30 de ore pe săptămână (fiindcă 28 clase × 30 ore = 840 ore > 828) – şi desigur, vom avea de pus noi întrebări (câte ore are fiecare clasă, în fiecare zi? Nu cumva, lipsesc ore?).
Dar… mai înseamnă ceva: poate că era mai convenabil să vizăm nu valorile NA (în număr de 2412), ci pe cele diferite de NA (în număr de 828) – fiindcă „liber” într-o zi, echivalează cu faptul că profesorul respectiv apare de zero ori pe liniile aferente acestei zile dintre cele 828 de linii (care în câmpul clasa au valori diferite de NA). Însă aici, vom continua demersul de mai sus, angajând valorile NA (… evitând dificultăţile constatării de zero apariţii!).

Putem neglija de-acum, coloana clasa (în care avem peste tot NA); ne interesează numai coloanele prof, zi şi ora. Ideea de bază constă în a grupa cele 2412 linii după valorile din prof şi zi, urmând apoi să contorizăm fiecare grup: dacă grupul conţine 12 linii, atunci profesorii corespunzători acestui grup sunt liberi în ziua respectivă; dacă grupul conţine 11 linii, atunci profesorii respectivi au câte o singură oră în ziua respectivă; ş.a.m.d. Să constituim întâi grupurile respective, într-o variabilă gpz (de adăugat eventual, chiar în programul explan.R):

gpz <- qar %>% 
       filter(is.na(clasa)) %>%
       select(prof, zi, ora) %>%
       group_by(prof, zi)

Inspectând în modul obişnuit (tastând numele variabilei, la promptul afişat în consola R) – nu obţinem mai nimic relevant, fiindcă group_by() nu modifică în vreun fel structura de date primită, ci „doar” îi ataşează nişte liste conţinând fiecare, indecşii liniilor dintr-un acelaşi grup; singura informaţie relevantă obţinută astfel este: "# Groups: prof, zi [270]", confirmând în fond formarea grupurilor, în număr de 270.

Desigur, avem de răsfoit manuale şi tutoriale pentru limbajul R şi pentru diverse pachete (ca tidyverse); dar (măcar de la un punct încolo) cea mai la îndemână şi cea mai bună documentare este oferită prin sistemul de "help" de care dispunem în consola R; folosind help(groups), vedem imediat ce sunt şi cum putem folosi aceste grupuri.

Avem 54 de profesori şi 5 zile, deci sunt într-adevăr, 54×5=270 de grupuri; putem vedea structura acestora, folosind funcţia group_data():

> group_data(gpz)
# A tibble: 270 x 3
   prof     zi       .rows
 * <chr> <int> <list<int>>
 1 P01       1         [8]
 2 P01       2         [8]
 3 P01       3        [11]
 4 P01       4         [9]
 5 P01       5         [9]
 6 P02       1         [8]
 7 P02       2        [10]
 8 P02       3         [7]
 9 P02       4         [7]
10 P02       5         [9]
# … with 260 more rows

Primele 5 linii corespund profesorului "P01", următoarele 5 – lui "P02", ş.a.m.d. Lui "P01" i s-a asociat pentru ziua 1, o listă de 8 întregi – indicând numerele de ordine ale liniilor din gpz pe care în coloana $prof apare P01 şi în coloana $zi apare 1; ţinând seama că în gpz am păstrat numai liniile din qar care au NA în coloana $ora – deducem că în ziua 1 profesorul P01 are 8 ore libere, sau spus altfel, are de făcut ore la 4 clase (12–8=4 ore propriu-zise). De pe liniile următoare, deducem că P01 are de făcut 4 ore în ziua 2, o singură oră în ziua 3 şi câte trei ore în zilele 4 şi 5.

Pentru a vedea eventual, care sunt orele din zi în care este liber, putem explicita conţinutul coloanei $.rows folosind funcţia group_rows():

> (lst1 <- group_rows(gpz)[[1]])  # grupul 1 (P01, ziua 1), asociat cu gpz
[1]   1  41  81 282 322 362 402 442
> gpz[lst1, ]
# A tibble: 8 x 3
# Groups:   prof, zi [1]
  prof     zi   ora
  <chr> <int> <int>
1 P01       1     1  # linia 1 din gpz
2 P01       1     2  # linia 41 din gpz
3 P01       1     3  # linia 81 din gpz
4 P01       1     8  # linia 282 din gpz
5 P01       1     9
6 P01       1    10
7 P01       1    11
8 P01       1    12  # linia 442 din gpz

Deci în ziua 1, profesorul "P01" este liber primele 3 ore şi ultimele 5 ore (şi intră la 4 clase, în orele 4..7).

group_size() furnizează lungimile listelor din coloana $.rows, deci numărul de ore libere pentru fiecare profesor în fiecare zi; dacă vrem „invers” (câte ore efective are fiecare profesor, în fiecare zi), atunci diferenţiem faţă de numărul maxim de ore libere:

> 12 - group_size(gpz)
  [1] 4 4 1 3 3 4 2 5 5 3 4 3 5 5 3 5 4 4 2 5 4 3 6 3 4 5 3 6 4 4 3 5 3 6 4 4 3
 [38] 5 4 5 3 3 3 2 3 2 3 2 3 2 5 6 6 5 6 3 3 3 3 0 0 1 1 2 2 2 0 0 0 0 4 6 6 4
 [75] 2 4 6 3 4 7 4 6 7 3 4 4 3 5 5 4 1 1 0 0 0 3 2 6 6 4 6 6 4 4 2 4 5 2 2 6 1
[112] 0 2 0 1 3 4 4 4 3 4 4 6 2 4 3 4 4 5 5 0 6 0 4 0 5 3 5 5 3 5 5 3 6 4 2 3 1
[149] 1 2 3 3 3 2 4 5 2 0 0 3 7 5 2 5 4 4 3 4 5 3 2 6 2 3 4 4 1 3 3 5 5 4 4 3 5
[186] 3 4 5 4 6 5 5 3 6 3 3 6 5 2 6 1 3 3 3 1 0 0 2 0 0 3 0 0 0 0 5 4 3 2 3 2 2
[223] 1 0 0 0 4 0 3 0 0 2 0 0 0 0 0 2 7 0 0 0 0 6 0 1 5 7 6 3 2 0 0 0 4 5 3 4 3
[260] 5 3 2 4 3 5 1 0 1 2 0

Este mai simplu de folosit count(), în loc de group_size(); următoarea funcţie (de adăugat şi aceasta, în programul explan.R) ne permite să investigăm aspecte ale repartiţiei orelor pe zile şi profesori:

# Câţi profesori au exact h ore, într-o zi sau alta?
with_h_per_day <- function(h) {
    gpz %>% count() %>%  # A tibble: 270 x 3 ($prof, $zi, $n)
            filter(n == (12-h)) %>% 
            group_by(zi) %>%
            count() %>%  # A tibble: 5 x 2 ($zi, $n)
            .subset2(2)  # extrage a doua coloană, $n
}

Să vedem de exemplu, câţi sunt liberi pe zi:

> with_h_per_day(0)
[1]  7  8  9  9 12  # 7 în ziua 1, 8 în a doua, 9 în zilele 3 şi 4, 12 în ziua 5

Nu prea este de crezut că se poate ca dintre cei 54 de profesori, un număr de 15+18+12=45 să aibă câte o zi liberă… Cel mai probabil, există mai mulţi profesori care au câte un număr mic de ore pe săptămână (restul orelor din norma didactică – parcă de 18 ore/săptămână – fiind incluse în orarul unei alte şcoli) şi deci, au fiecare, mai multe zile libere în orarul de faţă.
Pentru a lămuri situaţia, putem folosi de exemplu print(n=Inf) în locul ultimelor două linii din funcţia with_h_per_day(), listând astfel cele 45 de cazuri (observând că există şi profesori care apar de mai multe ori). O altă soluţie constă în a grupa după $prof (nu după $zi) şi a aplica apoi count(); iar aplicând după aceasta,

 %>% group_by(n) %>% count() %>% .subset2(2)
> [1] 2 4 5 5

găsim că doi profesori au câte o singură zi liberă, 4 au câte două, 5 au câte 3 şi 5 au câte 5 zile libere în şcoala respectivă (de unde totalul de 45 zile libere).

Să mai vedem câţi au câte o singură oră, într-o zi sau alta:

> with_h_per_day(1)
[1] 5 3 5 1 2

Acest aspect este important: în ziua în care are o singură oră, am putea să-i aducem una sau mai multe ore dintr-o zi în care profesorul respectiv este mai aglomerat (sau invers, am putea muta acea oră într-o altă zi). Desigur, ca şi în cazul precedent, se poate ca unul sau mai mulţi dintre profesorii socotiţi să aibă în mai multe zile, câte o singură oră (şi am putea încerca după caz, să le comasăm).

Orele claselor

Grupând după $clasa şi $zi şi folosind count(), vedem imediat câte ore are fiecare clasă (pe săptămână, sau în fiecare zi); desigur, de data aceasta ţintim numai liniile din qar pe care valorile din $clasa nu sunt NA:

# ore_cls.R
library(tidyverse)
qar <- readRDS("qar.rds")  # reconstituie obiectul qar (un "tibble")
qzc <- qar %>% 
       filter(!is.na(clasa)) %>%  # cele 828 de linii fără NA în coloana $clasa
       select(zi, clasa)  # ţintim numai clasele şi zilele

hW <- qzc %>% group_by(clasa) %>%
              count()  # numărul de ore pe săptămână (pe fiecare clasă)

hD <- qzc %>% group_by(clasa, zi) %>%
              count()  # numărul de ore pe zi, ale fiecărei clase

print(hW, n=Inf) o să ne listeze cele 28 de linii cu două coloane (clasa şi numărul de ore pe săptămână); dar – ştiind că avem câte 7 clase pe fiecare nivel – putem lista mai bine, definind un obiect data.frame cu câte o coloană (în care selectăm câte 7 linii din hW) pentru fiecare dintre cele 4 nivele de clase (a IX-a, a X-a, etc.):

vb@Home:~/20dec$ R -q
> source("ore_cls.R")
> data.frame(IX=hW[22:28, ], X=hW[1:7, ], XI=hW[8:14, ], XII=hW[15:21, ])
  IX.clasa IX.n X.clasa X.n XI.clasa XI.n XII.clasa XII.n
1       9A   31     10A  31      11A   32       12A    31
2       9B   29     10B  30      11B   27       12B    28
3       9C   28     10C  31      11C   27       12C    28
4       9D   30     10D  30      11D   30       12D    29
5       9E   30     10E  31      11E   29       12E    29
6       9F   30     10F  29      11F   30       12F    29
7       9G   30     10G  30      11G   30       12G    29

Valorile observate astfel – clasele 11B şi 11C au numai câte 27 de ore – obligă la o consultare asupra încadrării originale a şcolii: nu cumva, lipsesc nişte ore? (şi încă, nu cumva la vreo clasă – 11A are 32 de ore – sunt mai multe ore decât trebuie?); întrebând, am aflat că într-adevăr, lipsesc nişte ore (iar 11A are în plus).

print(hD, n=Inf) ne va lista pentru fiecare clasă şi fiecare zi (deci, pe 28×5=140 de linii), numărul de ore ale clasei pe ziua respectivă; am folosi şi în acest caz nişte obiecte data.frame, grupând anumite linii din hD, pentru a evidenţia anumite aspecte:

> data.frame(XIA = hD[36:40, ], XIIA = hD[71:75, ])
  XIA.clasa XIA.zi XIA.n XIIA.clasa XIIA.zi XIIA.n
1       11A      1     7        12A       1      5
2       11A      2     8        12A       2      8
3       11A      3     7        12A       3      6
4       11A      4     5        12A       4      6
5       11A      5     5        12A       5      6

Aspectul evidenţiat – există două clase cu câte 8 ore pe zi şi cu 5 ore într-o altă zi – este dintre cele care trebuie neapărat, corectate (consultând încadrarea originală şi apoi, dacă este cazul, încercând să mutăm o oră dintre cele 8 într-una din zilele cu 5 ore).

Încadrarea profesorilor la clase

Procesul de elaborare a orarului pleacă de la „încadrarea profesorilor”, document (sau ce o fi…) întocmit şi furnizat de către conducerea şcolii. Aici însă, avem doar orarul final, nu şi încadrarea iniţială; cum am obţine din qar, o situaţie a numărului de ore (pe săptămână) puse fiecărui profesor, la fiecare clasă? Confruntând apoi cu încadrarea originală, se pot descoperi eventual anumite scăpări (şi poate exista şansa de a face la timp, corecturile cuvenite, sau eventual… şansa de a o lua de la capăt).

Constituim un fişier nou, "framing.R", pe care îl vom lansa din când în când prin source(), pentru a verifica şi a lămuri diverse aspecte:

# framing.R
library(tidyverse)
qar <- readRDS("qar.rds")  # reconstituie obiectul 'qar'
topc <- qar %>% 
        filter(!is.na(clasa)) %>%
        select(obj, prof, clasa)

Am constituit un obiect topc, reţinând numai liniile din qar care în coloana $clasa au valoare diferită de NA şi am selectat numai coloanele care ne interesează.

Obs. De fapt, topc nu este un „obiect”, ci este o variabilă, în care se păstrează adresa de memorie a obiectului (de tip "tibble") constituit prin secvenţa din partea dreaptă a operatorului de atribuire "<-"; zicem simplu, „obiectul topc” în loc de „obiectul referit de topc”.

Să vedem întâi (în consolă, după source("framing.R")) o statistică pe obiecte:

> topc %>% group_by(obj) %>% count(sort=TRUE)
# A tibble: 15 x 2
# Groups:   obj [15]
   obj       n
   <chr> <int>
 1 eco     175  # 175 ore pentru discipline economice
 2 rom     101  # 101 ore pentru „Română”
 3 mat      91  # 101 ore pentru „Matematică”
 4 eng      60
 5 fra      56
 6 sum      53
 7 ist      51
 8 fiz      45
 9 chi      41
10 edf      37
11 inf      33
12 rel      28
13 geo      27
14 art      15
15 bio      15

Avem deci 15 discipline şi acestea au fost redate (implicând argumentul sort) în ordinea descrescătoare a numărului de linii (dintre cele 828 ale lui topc) corespunzătoare fiecăreia (şi implicit, în ordinea descrescătoare a numărului de profesori pe fiecare disciplină). Din totalul de 828 de ore pe săptămână, 175 sunt alocate pe eco (discipline economice), 101 pe rom, 91 pentru mat, ş.a.m.d.

Pentru a vedea câţi profesori sunt pe fiecare obiect, adăugăm în framing.R mai întâi această definiţie (de care putem avea nevoie şi mai târziu):

DSC <- topc %>% group_by(obj) %>% 
       count(sort=TRUE) %>%  # descrescător după numărul de ore alocat
       .$obj  # reţine valorile din coloana disciplinelor

prin care obţinem un vector având ca elemente cele 15 discipline (în ordinea descrescătoare a numărului de ore alocat). Apoi, adăugăm funcţia:

obj_prof <- function() {
    grop <- topc %>% group_by(obj, prof) %>% 
            count(obj)  # liniile profesorilor, pe fiecare disciplină
    npr <- vector()  # va contoriza valorile $obj identice, din 'grop'
    for(i in 1:length(DSC)) {
        npr[i] <- sum(grop$obj == DSC[i])
    }
    rbind(DSC, npr)  # îmbină liniile (obiecte, numărul de profesori)
}

Grupând după $obj şi $prof şi folosind apoi count(obj), rezultă un tibble conţinând câte o linie pentru fiecare profesor (dintre cei 54), împreună cu disciplina acestuia; contorizând liniile cu aceeaşi valoare în câmpul $obj, rezultă numărul de profesori pe o aceeaşi disciplină. Rezultatul este returnat (prin rbind()) ca o matrice cu două linii (mai convenabil pentru afişare, decât ar fi fost un data.frame); fiindcă într-o matrice elementele trebuie să aibă acelaşi tip, numerele sunt convertite automat la tipul „mai slab” (în loc de 10 avem "10"):

> source("framing.R")
> obj_prof()
     [,1]  [,2]  [,3]  [,4]  [,5]  [,6]  [,7]  [,8]  [,9]  [,10] [,11] [,12] [,13] [,14] [,15]
obj  "eco" "rom" "mat" "eng" "fra" "sum" "ist" "fiz" "chi" "edf" "inf" "rel" "geo" "art" "bio"
npr  "10"  "5"   "4"   "5"   "3"   "3"   "3"   "3"   "3"   "2"   "5"   "2"   "3"   "2"   "1"

Deci pe eco sunt 10 profesori (şi împreună au 175 de ore, cum am văzut mai sus), pe rom sunt 5 profesori, ş.a.m.d. De observat că pe inf („Informatică”) sunt 5 profesori şi împreună, au mai puţine ore decât cei 2 de pe edf (”Educaţie fizică”); revin cam 6 ore inf şi respectiv, 18 ore edf de profesor, pe săptămână (dar, că-s mulţi sau puţini, depinde şi de profilul claselor; se ştie că avem aproape 100 de specializări, în învăţământul liceal).

Grupând datele din topc după cele trei coloane şi aplicând count(), putem vedea încadrarea profesorilor (la ce clase şi câte ore pe săptămână) şi deasemenea, încadrarea claselor (ce obiecte au fiecare şi câte ore pe săptămână):

> topc %>% group_by(obj, prof, clasa) %>% count() %>% print(n=Inf)
# A tibble: 376 x 4
# Groups:   obj, prof, clasa [376]
    obj   prof  clasa     n
    <chr> <chr> <chr> <int>
 46 eco   P33   10D       4  # 10D are 4 ore 'eco' cu 'P33'
 65 eco   P37   10D       7
193 geo   P31   10C       2
227 ist   P25   10C       3
234 ist   P25   12C       4
242 ist   P26   11C       6

Avem astfel, 376 de linii (constituind fiecare câte un "group"); pe coloana $n s-au contorizat liniile cu aceleaşi valori în cele trei coloane după care am grupat, altfel spus – numărul de ore pe tripletul (obiect, profesor, clasă).
Pe cele câteva linii redate mai sus avem de observat: doi profesori fac acelaşi obiect, eco, la aceeaşi clasă, 10D (încât pe orarul clasei, eco va apărea de 4+7=11 ori); este clar că eco acoperă mai multe discipline (care trebuiau specificate din start).
ist („Istorie”) are la majoritatea claselor cel mult 3 ore, dar la clasa 11C are 6 ore, cu P26; pe de altă parte, 11C nu apare la obiectul geo („Geografie”). Cel mai probabil, P26 face ambele obiecte, la clasa respectivă – dar pe orar toate cele 6 ore vor fi consemnate ca ist (aceeaşi situaţie apare şi la 12C).
Se vede că (probabil din start, de la construcţia documentului iniţial de „încadrare”) s-a urmărit să „iasă” numărul de ore pe profesor, nu şi ce se face (în particular, ce obiect se face) la orele respective… Acest obicei este în ton cu situaţia binecunoscută: în orar se trece "Educaţie Fizică" şi "Muzică", dar mulţi învăţători fac în loc, "Matematică" sau "Citire".

Introducem în framing.R secvenţa experimentată mai sus şi adăugăm o funcţie prin care să extragem dintre cele 376 de linii numai pe acelea asociate unui aceluiaşi obiect (indicat ca argument):

grOPC <- topc %>% group_by(obj, prof, clasa) %>% count()
fr_prof <- function(ob) {
    grOPC %>% filter(obj == ob)
}

De exemplu, fr_prof("mat") va produce un obiect tibble cu 28 de linii (toate cele 28 de clase fac „Matematică”), indicând profesorul (care apare pe atâtea linii câte clase are în încadrare), clasa şi numărul de ore la acea clasă, pentru obiectul mat.

Să zicem că vrem să obţinem un tabel având ca intrări pe linie profesorii şi ca intrări pe coloane cele 28 de clase, iar valorile acestuia să fie numărul de ore corespunzător liniei şi coloanei (desigur, vom adăuga şi o coloană pentru disciplinele asociate); am avea astfel, ceea ce pe drept s-ar numi „încadrarea” profesorilor (la nivel de săptămână).

Să constituim întâi un vector conţinând valorile din coloana $clasa, ordonate după nivelul şi litera clasei:

CLS <- sort(unique(topc$clasa))
CLS <- c(CLS[22:28], CLS[1:21])

Modelăm tabelul de care ziceam mai sus, prin funcţia schedule(), pe care o elaborăm mai jos „pas cu pas”; mai întâi, introducem un obiect data.frame, referit prin fram, având drept coloane "obj", "prf" şi cele 28 de valori din vectorul CLS:

schedule <- function() {
    fram <- data.frame(obj = vector(mode="character", length=54), 
                       prf = vector(mode="character", length=54))
    for(cl in CLS) { fram[cl] = vector(mode="integer", length=54) }
# }  (n-am terminat definiţia funcţiei)

Primele două coloane sunt de tip character; pe celelalte coloane vor fi valori întregi (numărul de ore la clasă, pentru profesorul respectiv); toate coloanele vor conţine câte 54 de valori (câţi profesori avem, în cazul nostru). „Imaginea” următoare ne asigură că vom putea reda tabelul chiar şi pe o pagină obişnuită (format A4):

> head(fram, 1)
  obj prf 9A 9B 9C 9D 9E 9F 9G 10A 10B 10C 10D 10E 10F 10G 11A 11B 11C 11D 11E 11F 11G 12A 12B 12C 12D 12E 12F 12G
1          0  0  0  0  0  0  0   0   0   0   0   0   0   0   0   0   0   0   0  0   0   0   0   0   0   0   0   0

Valorile întregi au fost iniţializate cu '0'; când va fi să tipărim, vom putea înlocui '0', de exemplu cu '.'.

Prelucrările de făcut mai departe pot decurge în stil „imperativ”, astfel:

    i <- 1  # vizează câte o linie din 'fram' (1..54)
    for(dsc in DSC) {  # DSC este lista disciplinelor
        frp <- fr_prof(dsc)  # liniile profesorilor de pe o aceeaşi disciplină
        k <- nrow(frp)
        fram[i, 1:2] <- frp[1, 1:2]  # înscrie disciplina şi primul profesor
        for(j in (1:k)) {
            if(frp[j, 2] != fram[i, 2]) {  # întâlnind alt profesor,
                i <- i + 1  # avansează în 'fram' la următoarea linie
                fram[i, 2] <- frp[j, 2]  # şi înscrie noul profesor
            }
            cl <- match(frp[j, 3], CLS)  # indexul clasei profesorului curent
            fram[i, (cl+2)] <- frp[j, 4]  # numărul de ore la clasa respectivă
        }
        i <- i + 1  # pregăteşte înscrierea următorului grup
    }
    saveRDS(fram, file="frame.rds")
    fram  # returnează obiectul 'fram' (salvat deja, în fişierul "frame.rds")
}  # încheie funcţia schedule()

Este de luat seama la faptul că programarea imperativă (cu care suntem obişnuiţi din alte limbaje) este de evitat: în R operaţiile sunt deja vectorizate, aplicându-se „deodată” tuturor componentelor (fără să fie necesar să le indexăm şi să ciclăm prin for); modificarea iterativă a unor elemente dintr-un obiect data.frame (cum avem în schedule()) implică (la nivelul intern) o serie de copieri a întregului obiect, încât timpul de execuţie se măreşte în mod artificial (am putut constata că, deşi fram are de fiecare dată, puţine linii – execuţia funcţiei schedule() durează câteva secunde, ceea ce este prea mult).

Să probăm cumva, lucrul:

vb@Home:~/20dec$ R -q
> source("framing.R")
> frame <- schedule()  # de verificat şi că obţinem fişierul 'frame.rds'
> head(frame, 1)
  obj prf 9A 9B 9C 9D 9E 9F 9G 10A 10B 10C 10D 10E 10F 10G 11A 11B 11C 11D 11E
1 eco P33  0  0  0  4  0  0  0   0   0   0   4   0   3   0   0   0   0   6   0
  11F 11G 12A 12B 12C 12D 12E 12F 12G
1   0   0   0   0   0   0   0   0   5

Am afişat numai prima linie din frame, pe care avem încadrarea profesorului P33, pe disciplina eco (are 4 ore la clasa 9D, 4 ore la 10D, 3 la 10F, 6 la 11D şi 5 la 12G); listingul începe cu eco, fiindcă în vectorul DSC (implicat mai sus în schedule()) disciplinele sunt în ordinea descrescătoare a numărului total de ore.

Cele 30 de coloane n-au încăput pe ecran, încât au fost despărţite pe două linii…

Tabelul de încadrare săptămânală

Având datele cuvenite în fişierul frame.rds, ne putem pune acum problema de a formata (pentru scriere) tabelul respectiv, astfel încât să nu fie necesară despărţirea coloanelor şi întregul tabel să poată fi listat ca atare, pe o coală A4.

Avem în vedere o idee foarte simplă (profitând de faptul că valorile înscrise în tabel sunt numere cu câte o singură cifră): este suficient ca în denumirile coloanelor să indicăm nivelul claselor numai la prima clasă (9A, B, C, ..., G, 10A, B, C, ...); spaţiul necesar pentru a reda o linie de date se va reduce astfel cu 8 + 16×3=56 de caractere, sau dacă mutăm nivelele 9, 10 etc. deasupra literelor "A" – cu încă 7 caractere.

Putem realiza această idee şi fără a formula un program „imperativ”, care pentru fiecare linie dintre cele existente să scrie valoare după valoare, cu spaţierea dorită. Spaţiul pe care se afişează o linie dintr-un data.frame (prin metodele standard print(), cat(), etc.) depinde şi de lungimea denumirilor de coloană; dar putem folosi colnames() pentru a anula aceste denumiri, încât valorile de pe fiecare linie vor fi afişate cu câte un singur spaţiu separator.

Prin funcţia care urmează, reconstituim în variabila fram obiectul salvat în frame.rds şi înlocuim valorile '0' cu '.'; prin sync() redirectăm afişarea standard (pe ecran) pe fişierul "incadrare.txt"; scriem antetul tabelului, prin cat(), apoi anulăm numele de coloană din fram şi folosim metoda obişnuită print(fram):

frame_fmt <- function() {
    fram <- readRDS("frame.rds")
    fram[fram == 0] <- "."  # înlocuieşte toate valorile '0' cu '.'
    sink("incadrare.txt")
    h1 <- "         9             10            11            12           \n"
    h2 <- " obj prf A B C D E F G A B C D E F G A B C D E F G A B C D E F G\n"
    cat(h1); cat(h2)
    colnames(fram) <- NULL  # dispărând numele, liniile se scurtează la afişare 
    print(fram, row.names=FALSE)
    cat(h2); cat(h1)
    sink()    
}

Redăm parţial (cu o mică formatare asupra antetului de tabel), fişierul rezultat:

         9             10            11            12           
 obj prf A B C D E F G A B C D E F G A B C D E F G A B C D E F G
 eco P33 . . . 4 . . . . . . 4 . 3 . . . . 6 . . . . . . . . . 5
     P34 . . . . . 3 . . . . . . . . . . . 2 8 2 . . . . 2 2 . .
     P35 . . . . 4 . . . . . . 4 . . . . . . . 6 . . . . . 3 . .
     P36 . . . . . 6 . . . . . . . . . . . . 2 4 . . . . . 4 . .
     P37 . . . . . . . . . . 7 3 . . . . . . . . . . . . 5 2 . 4
     P38 . . . 6 . . . . . . . . 3 . . . . 4 2 . . . . . 4 . 3 .
     P39 . . . . . . 5 . . . . . . 3 . . . . . . 9 . . . . . 2 2
     P40 . . . . . . 3 . . . . . . 8 . . . . . . 4 . . . . . 7 .
     P41 . . . . 6 . . . . . . 3 . . . . . . 2 . . . . . . . . .
     P54 . . . . . . . . . . . . 4 . . . . . . . . . . . . . . .
 rom P02 . . . 3 3 . . . . . . 4 . . 3 . . . . 3 . . . . . 3 . .
     P03 4 4 . . . . . . 3 . . . . . . . 5 4 . . . . . . . . . .
     P04 . 1 . . . . . . . 4 . . 3 . . 4 . . . . . . 4 . . . . 4
     P05 . . . . . . 3 . . . . . . 3 . . . . 3 . 3 . . 5 3 . . .
     P06 . . 5 . . 3 . 3 . 1 3 . . . . . . . . . . 4 . . . . 3 .
# # # #
 art P48 1 1 2 . . . . 1 1 1 . . . . . . 1 . . . . . . 1 . . . .
     P49 1 1 1 . . . . 1 1 1 . . . . . . . . . . . . . . . . . .
 bio P01 2 2 . . . . . 2 2 . . . . . 1 3 . . . . . 1 2 . . . . .
 obj prf A B C D E F G A B C D E F G A B C D E F G A B C D E F G
         9             10            11            12           

Concepută astfel, încadrarea poate fi scrisă pe o pagină obişnuită de hârtie; se vede imediat câte ore şi la ce clase are fiecare profesor; pe verticale, se vede uşor ce obiecte, cu care profesor şi câte ore (pe săptămână) face fiecare clasă; se pot descoperi uşor diversele scăpări posibile, iar fişierul respectiv se poate actualiza şi poate fi utilizat destul de simplu pentru a constitui un obiect data.frame, care apoi să poată fi luat ca bază a unui program de construcţie a orarului.

Notă, asupra construcţiei orarului

Un program de construcţie a orarului ar consta în opinia noastră, în două etape distincte: mai întâi trebuie generată o repartiţie echilibrată pe zilele de lucru, a orelor din tabelul de încadrare (având în vedere şi cerinţe justificate ale profesorilor); „echilibrat” înseamnă că profesorii şi clasele să aibă într-o zi cam acelaşi număr de ore ca şi în altă zi, iar orele clasei la un acelaşi obiect să fie plasate pe cât posibil, în zile diferite. Apoi, în a doua etapă, orele repartizate într-o aceeaşi zi trebuie aşezate în orarul propriu-zis al zilei (încât să nu intre doi profesori la o aceeaşi clasă în acelaşi moment al zilei şi în plus, numărul total de ferestre să fie cât mai mic posibil).

Termenul de „fereastră” trebuie stabilit (sau convenit) de la bun început, mai ales în cazul când se lucrează în două schimburi, ţinând însă cont şi de interesele claselor de elevi; în prima etapă, la repartizarea orelor pe zile, se poate încerca reducerea numărului de profesori care să aibă ore în ambele schimburi ale unei zile – dar trebuie evitate situaţiile care ar compromite interesele fireşti ale unei clase (nu se poate accepta de exemplu, situaţia în care o clasă are cele 3 ore de „Română” într-o singură zi).

Pe un acelaşi tabel de încadrare sunt posibile foarte multe orare, dintre care foarte puţine sunt convenabile (mai ales în cazul când şcoala funcţionează cu două schimburi). Dacă orarul (produs manual, sau cu unul dintre pachetele de programe existente) nu este convenabil, se spune bineînţeles, că de vină este autorul orarului (sau programul); este adevărat… dar trebuie căutate şi motivele mai simple, sau mai obişnuite.

La baza construcţiei orarului stă documentul iniţial de încadrare, întocmit din timp de către „conducere” (de fapt, directorul şcolii); mai precis, la baza construcţiei orarului stă un tabel de încadrare precum cel redat parţial mai sus, tabel întocmit de cel care a fost delegat de către conducerea şcolii ”să facă orarul” şi anume, tabel întocmit pe baza studiului documentului de încadrare furnizat de către director.

Ori, documentul iniţial de încadrare are un scop, nelegat de orar, iar tabelul de încadrare derivat din acesta are alt scop, strict legat de orar! Scopul documentului întocmit de către director este unul anual, vizând în principal repartizarea profesorilor pe „arii curiculare” (precum eco, sau arte) şi constituirea normelor didactice anuale care urmează să fie bugetate; scopul tabelului de încadrare este unul săptămânal şi vizează fiecare disciplină (inclusiv cele care formează eco, fiindcă nu se cuvine ca în orarul înaintat unei clase să figureze "eco" de 10 ori) şi fiecare schimb (fiecare zi şi fiecare oră).

Câtă vreme la distribuţia iniţială a claselor pe profesori, nu se ţine seama în măsură suficientă, de faptul că unele clase sunt într-un schimb, iar altele sunt în celălalt schimb (şi nu prea se ţine seama de discipline, ci de arii curriculare) – este greu de aşteptat vreun orar convenabil; în general, dacă „încadrarea” este lipsită de anumite principii de echilibrare şi acoperă doar aspecte administrative (sau contabiliceşti), atunci şansele de a genera un orar convenabil scad considerabil. „Convenabil” trebuie să fie pentru profesori, dar şi pentru elevi; nu poţi îngrămădi orele unui profesor într-o singură zi, fără să afectezi interesele celorlalţi (profesori, sau clase de elevi).

vezi Cărţile mele (de programare)

docerpro | Prev | Next