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

Explorarea şi analiza datelor (partea a II-a)

Evaluare naţională | limbajul R
2016 mar

O linie din fişierul "evna.csv" - ajustat în [1] după EVNAT-2015.csv - arată astfel:

11096387,F,2001-03-11,URBAN,4061103713,Prezent,,Prezent,1.75,,4,NU,,NU,,NU,,1.75,,4,2.87

Al cincilea câmp reprezintă codul SIIIR, format din zece caractere de şablon "\d" (cifră în baza 10), dintre care ne interesează numai primele două (codul judeţului); pentru a identifica acest câmp în cadrul liniei putem folosi şablonul de expresie regulată ",\d{10}" - în care virgula iniţială serveşte pentru a exclude primul câmp (care ar putea să aibă tot 10 cifre, dar nu este precedat de ','). Următoarea comandă Perl suprimă ultimele 8 cifre din codul SIIIR, pe fiecare linie a fişierului:

vb@Home:~/Evna$ perl -pi -e 's/,(\d\d)\d{8}/,$1/g' evna.csv

În [1] am folosit direct read.csv("evna.csv") şi am văzut că în structura de date constituită de R (un "data.frame") unele câmpuri (coloane, sau variabile) au un alt tip decât cel care ne-ar conveni. De fapt, revăzând rezultatul produs de funcţia str() constatăm că ar fi de forţat tipul numai pentru două sau trei variabile, încât putem proceda astfel:

evna <- read.csv("evna.csv")  # evna$Cod_siiir este de tip 'numeric' (şi vrem 'factor')
                              # evna$Data_nastere este 'factor', vrem 'Date'
# a inspecta cu 'str(evna)' şi după următoarea comandă, cu 'str(classes)'
classes <- sapply(evna, class)  # tipul fiecărei variabile, indexând prin numele câmpului
classes[names(classes) %in% c("Cod_siiir")] <- "factor"
classes[names(classes) %in% c("Data_nastere")] <- "Date"
evna <- read.csv("evna.csv", colClasses=classes)  # acum evna$Cod_siiir este 'factor'
                                                  # iar evna$Data_nastere este 'Date'

Câmpul final "Media" rămâne de tip "factor" (deşi am fi vrut iniţial, să forţăm "numeric"):

> str(evna$Media)
 Factor w/ 705 levels "","1","10","1.02",..: 135 313 261 467 419 367 303 513 ...

Nu am putut forţa variabila "Media" pe valori de tip "numeric", fiindcă există linii pe care "Media" are valoarea "Absent" (şi nu puţine, încât nu se cuvine să le ignorăm); acest termen ("Absent") apare ca valoare şi în alte coloane, încât deocamdată preferăm să nu-l modificăm.

Prezenţa la examen, pe judeţe

Cele trei coloane "Status_" vor fi servit pe parcursul desfăşurării examenului (dacă tot numărând zilnic tezele, comisia de examen găseşte mai puţin decât numărul de "Prezent", atunci se produce panica gravă de rătăcire a unor teze); poate că aceste coloane de date vor fi servit şi pentru a înscrie "Absent" în loc de medie, acelor elevi care nu s-au prezentat la vreuna dintre probe:

> table(evna$Status_romana)
            Absent Eliminat cu nota 1            Prezent 
              4390                  6             159022 
> table(evna$Status_limba_materna)
         Absent Prezent 
 153845     216    9357 
> table(evna$Status_matematica)
            Absent Eliminat cu nota 1            Prezent 
              4828                  6             158584 

Dar după definitivarea examenului, coloanele "Status_" sunt aproape inutile şi sunt oricum, prea puţin interesante din punct de vedere statistic. De exemplu, "Status_limba_materna" redat mai sus ne arată că au fost cam 9500 de candidaţi din rândul naţionalităţilor conlocuitoare - dar acest lucru poate fi dedus în finalul examenului pe baza datelor din coloana "Nota_finala_lb_materna".

Putem obţine o situaţie statistică suficient de relevantă privind prezenţa la examen, pe baza datelor din coloanele "Media" şi "Cod_siiir" - procedând de exemplu astfel:

jud.all <- subset(evna, select=c('Cod_siiir', 'Media'))
jud.abs <- subset(jud.all, Media=='Absent' | Media=='')
package(plyr)
jud.all <- count(jud.all, c('Cod_siiir'))  # număr candidaţi înscrişi, pe judeţe
jud.abs <- count(jud.abs, c('Cod_siiir'))  # absenţi (una sau mai multe probe) pe judeţe

Am extras cele două coloane din "evna", într-o nouă structură de date "jud.all" şi apoi, am extras liniile din "jud.all" corespunzătoare absenţilor, într-o nouă structură de date "jud.abs"; apoi, prin funcţia plyr::count() am contorizat liniile cu aceeaşi valoare "Cod_siiir" - obţinând pe fiecare judeţ, numărul total de candidaţi şi respectiv, numărul candidaţilor care au absentat la cel puţin o probă.

În [1] am obţinut un fişier "42jud.csv", conţinând numele corect al judeţului, codul acestuia, numărul de şcoli şi numărul de locuitori (pentru fiecare dintre cele 42 de judeţe, în ordinea alfabetică); încărcăm datele respective în "jud.csp" şi ordonăm liniile respective după codul judeţului - adică în aceeaşi ordine în care avem liniile în "jud.all" şi în "jud.abs". Apoi, adăugăm coloana "pabs" pe care înscriem procentul de absenţi - calculat pe baza câmpurilor "freq" din "tabelele" obţinute mai sus; în final, reordonăm alfabetic şi rescriem fişierul:

jud.csp <- read.csv("42jud.csv", 
                    colClass=c("character", "character", "integer", "integer"))
jud.csp <- jud.csp[order(jud.csp$cod), ]  # în ordinea codurilor judeţelor

jud.csp$pabs <- round(jud.abs$freq / jud.all$freq * 100, 3)  # procentul de absenţi

jud.csp <- jud.csp[order(jud.csp$judeţ), ]  # în ordinea alfabetică a judeţelor
write.csv(jud.csp, file="42jud.csv", row.names=FALSE)

Fie prin inspectare directă a datelor obţinute astfel, fie "citind" histograma produsă de exemplu prin comanda hist(jud.csp$pabs) - putem face câteva constatări interesante: judeţul Olt are 11.278% absenţi, însemnând de aproape patru ori media pe celelalte judeţe; următorul cel mai mare procent de absenţi este la distanţă de 6 procente (Sălaj cu 5.298% şi Maramureş cu 5.275%). Un număr de 9 judeţe au procentul de absenţi între 4.09% şi 5.3%, alte 10 judeţe au între 0.5% şi 1.88%, iar celelalte 22 de judeţe au între 2.01% şi 3.88% absenţi - adică dacă excludem Olt, repartiţia absenţilor este apropiată de o repartiţie normală:

> summary(jud.csp$pabs[-28])  # exclude judeţul Olt
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  0.514   2.012   2.860   2.911   3.869   5.298 

În [1] am văzut că este foarte plauzibilă o relaţie liniară între numărul de şcoli şi numărul de locuitori şi că în raport cu dreapta de regresie respectivă, Olt prezenta surplusul cel mai mare (138 de şcoli "mai mult decât s-ar cuveni" conform regresiei - cu mult peste media surplusurilor altor judeţe) în timp ce M.Bucureşti prezenta "lipsa" de şcoli cea mai mare (mai mult de 400 de şcoli în minus, faţă de valoarea liniei de regresie); iar acum vedem că Olt are şi procentul cel mai mare de absenţi la examen (de 3.8 ori media), în timp ce M.Bucureşti are procentul cel mai scăzut de absenţi, 0.514%.

Aceste observaţii sugerează eventual existenţa unei anumite relaţii între procentul de absenţi şi valoarea reziduală a regresiei întreprinse în [1], valoare exprimată pe coloana "resid" din tabelul "jud41" redat la sfârşitul lui [1]…

Vârsta candidaţilor

Următorul mic experiment arată că evna$Data_nastere are valori de tip "Date" (dată calendaristică), care apar în formatul "an-lună-zi" dar sunt reprezentate intern ca diferenţa în zile faţă de data (luată ca origine) 1970-01-01:

> str(evna$Data_nastere)
## Date[1:163418], format: "2001-03-11" "2000-11-15" "2000-05-29" "2000-08-20" ...
> unclass(evna$Data_nastere[1])
## [1] 11392  # (numărul de zile de la originea 1970-01-01 până la 2001-03-11)
> evna$Data_nastere[1] - as.Date("1970-01-01")
## Time difference of 11392 days

Am putea determina vârsta candidaţilor împărţind la 365 (sau mai bine, la 365.25) diferenţa dintre data susţinerii examenului şi vectorul evna$Data_nastere (de exemplu, 11392 / 365.25 = 31.1896 - însemnând că între datele menţionate mai sus au trecut 31 de ani); dar convertind datele la clasa R POSIXlt dispunem de "accesorii" pentru an, lună şi zi, încât putem formula următoarea funcţie (dar deloc nouă) pentru a determina numărul de ani cuprins între două date calendaristice:

age = function(fromDate, toDate) {
    from = as.POSIXlt(fromDate)
    to = as.POSIXlt(toDate)
    age = to$year - from$year  # Vârsta măsurată în ani, dar se va
    # scădea 1 an dacă 'toDate' este înaintea aniversării zilei de naştere
    ifelse(to$mon < from$mon | (to$mon == from$mon & to$mday < from$mday),
           age - 1, age)
}

Extragem coloanele "Data_nastere", "Cod_siiir" şi "Media", calculăm vârsta în ani folosind funcţia age() introdusă mai înainte şi contorizăm după vârstă:

> varsta <- subset(evna, select=c('Data_nastere', 'Cod_siiir', 'Media'))
> varsta$Data_nastere <- age(varsta$Data_nastere, as.Date('2015-06-01'))
> varsta.count <- count(varsta, c('Data_nastere'))

Schimbăm totuşi numele primei coloane şi apoi filăm (şi rearanjăm aici) rezultatul:

> colnames(varsta.count)[1] <- "vârsta"
> varsta.count
   vârsta   freq                vârsta   freq
       10      1                    25     14
       12      2                    ...
       13   4783                    33     10
       14 101401                    34      9
       15  54289                    35     12
       16   2279                    ...
       17    432                    55      1
       18     74                   108      1
       19     20                   114      1
       ...                         115      1

Constatăm că marea majoritate a candidaţilor au vârsta standard (14 sau 15 ani), sau apropiată de aceasta (13 ani, sau 16 ani); există şi excepţii (sub 13 ani, sau 17 ani, sau 18..35 de ani) pe care probabil că numai Ministerul le-ar putea eventual explica. Vedem însă că există şi candidaţi cu vârsta de peste 100 de ani - însemnând că s-au strecurat anumite erori la înregistrarea datelor.

Fiindcă am reţinut şi codul judeţului, putem găsi eventual "vinovaţii":

> subset(varsta, vârsta < 13 | vârsta > 100)
       vârsta Cod_siiir Media
1822       12        40   9.5  # 40 este codul pentru 'M.Bucureşti'
18084      12        40  6.97
77868     114        16  7.22  # 16 este codul pentru 'Dolj'
104217    108        17  4.37
142867    115        37   5.1  # 37 este codul pentru 'Vaslui'
150656     10        33  5.65

În structurile de tip "data.frame" şi liniile au "nume" (nu numai coloanele), iar acestea se "moştenesc"; de exemplu "numele" 1822 al primeia dintre liniile de date redate mai sus reprezintă (de vreme ce nu am prevăzut alte denumiri) numărul de ordine al acelei linii din structura "varsta" - moştenit de fapt de la "evna", din care aceasta a provenit - care conţine datele 12, 40, 9.5 în coloanele vizate mai sus:

# Linia 1823 din fişierul "evna.csv"
10659537,F,2002-07-21,URBAN,40,Prezent,,Prezent,9.5,,9.5,NU,,NU,,NU,,9.5,,9.5,9.5

Desigur, am căutat linia 1823 în loc de 1822, fiindcă prima linie din fişier este linia de antet, iar liniile de date încep de la linia 2. Vedem că este vorba de o elevă (din Bucureşti) care va împlini 13 ani puţin după încheierea examenului şi care a fost notată cu 9.50 la ambele probe.

vezi Cărţile mele (de programare)

docerpro | Prev | Next