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]).
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")
.
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).
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).
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…
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.
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)