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

Grafică statistică şi grafică "artistică", pentru două variabile

limbajul R
2016 nov

În These Delicate Drawings Are The Handiwork Of A Very Smart Computer sunt reproduse câteva lucrări spectaculoase de "computer art"; din explicaţiile strecurate la mathimagery, deducem că principiul programului care le-a executat constă în plotarea câtorva mii de segmente care unesc puncte ale căror coordonate sunt anumite expresii bazate pe funcţiile circulare sin() şi cos().

Imaginăm un astfel de program în R; funcţiile pentru estimarea densităţii punctelor (existente şi în module statistice specificate în alte limbaje) pot genera "completări" interesante chiar şi în cazul graficii care nu are de-a face cu statistica.

Generăm un anumit număr de puncte având drept coordonate expresii simple de sin() şi cos(); cu funcţia smoothScatter() obţinem o estimare de densitate a punctelor, reliefată printr-o anumită gradare a culorilor de acoperire a unor zone (obţinută prin funcţia colorRampPalette()); în final - plotăm segmentele corespunzătoare perechilor de puncte:

ang <- seq(0, 2*pi, pi/2000)  # o progresie aritmetică de unghiuri 0..2*PI
x0 <- sin(12*ang) # ^5 sau ^3 (cu eventuale exponenţieri: sin(12*ang)^5)
y0 <- cos(10*ang)
x1 <- sin(8*ang)
y1 <- cos(6*ang) # ^3  sau ^5
opar <- par(mar = c(0, 0, 0, 0))  # anulează marginile ferestrei grafice
kol <- sample(colors(), 1)  # selectează aleatoriu o culoare, din setul indicat
grd <- colorRampPalette(  # constituie un gradient de culori transparente
           c("white", "lemonchiffon3", "lightyellow1", "white"), alpha=TRUE)
smoothScatter(x0, x1, col = NA, colramp = grd)  # colorează cu gradient, după densitate
segments(x0, y0, x1, y1, col = kol, lwd = 0.6)  # linii de grosime 'lwd' şi culoare 'kol' 
par(opar)  # reconstituie parametrii grafici standard

Pentru imaginea din dreapta am folosit x0^5 şi y1^3. Putem avea o anumită clarificare vizuală a lucrurilor mărind raţia progresiei din vectorul ang (de exemplu, pi/500) şi rulând programul întâi după comentarea liniei care trasează segmentele şi apoi după decomentarea acesteia:

Imaginea mai mică reprezintă în fiecare caz rezultatul aplicării gradientului de culori grd pe vectorul de densităţi constituit de apelul smoothScatter(), iar imaginea cealaltă (uşor mărită) corespunde adăugării celor 500 de segmente. Revenind la pasul pi/2000 (sau şi mai mic) şi rulând din nou programul, compunem respectiv cele două imagini redate iniţial.

Bineînţeles că avem o infinitate de posibilităţi: putem alege diverşi factori de scalare pentru argumentele funcţiilor sin() şi cos() - în programul redat mai sus avem 12, 10, 8 şi 6 - şi putem imagina un alt gradient de culori (poate fi interesant de implicat şi culoarea generată în kol, de exemplu gradând de la "white" - la kol - la "white"); putem viza în smoothScater() nu punctele date de x0 şi x1 ci de exemplu, pe cele date de x0 şi y0 (şi de fapt, putem încerca cu diverşi alţi doi vectori). Mai mult, putem încerca să folosim pentru coordonatele punctelor expresii care combină funcţii circulare, sau chiar alte funcţii decât cele circulare.

Ar fi de formulat o funcţie generică pentru a reflecta unitar aceste posibilităţi; dar aici alegem să încheiem cu un exemplu de folosire în mod standard (în statistică) a unor funcţii de estimare a densităţii bidimensionale a unor date reale.

Preluăm setul de date din Studiul datelor evaluării naţionale din 2016 (folosind R), din care extragem coloanele 4 şi 6, cu note ale elevilor prezenţi la cele două probe de bază:

load("../16_evn/evn.RData")
romat <- subset(evn, !is.na(Media), select = c(4, 6))
str(romat)  # inspectăm structura datelor (notele pe cele două probe)
'data.frame':	148648 obs. of  2 variables:
 $ Română1    : num  3.2 5.1 6.05 6.4 7.4 9.5 4.1 9.9 8.9 8.65 ...
 $ Matematică1: num  2.2 6.5 4.15 6.8 3.85 7.95 4.1 8 9.85 7.6 ...

Reprezentăm fiecare elev prin câte un punct având drept coordonate cele două note ale sale (desigur, în scatterplot-ul rezultat punctele respective nu se pot distinge totdeauna, unul de celălalt); apoi, reprezentăm printr-un gradient de culori densitatea punctelor respective:

plot(romat, cex=0.6, pch=20, col="sienna", asp=1)  # scatterplot
grid()
smoothScatter(romat, col="red", cex=2, asp=1)  # smoothed color density representation
grid()

Avem un număr de puncte izolate (în vecinătatea lor nu există alte puncte), reprezentate cu roşu pe imaginea din dreapta; de exemplu, la abscisa 2 se vede un punct cu înălţimea puţin mai mare ca 8 - însemnând că există un elev cu nota 2 la "Română" şi puţin peste 8 la "Matematică":

subset(romat, Română1 == 2 & Matematică1 > 8)
     Română1 Matematică1
9718       2         8.3

Avem o pată de cea mai închisă culoare în cadranul aproximativ (8, 10)×(8, 10), însemnând că dintre toate combinaţiile care împerechează o notă la o probă cu o notă la cealaltă probă, cea mai densă (acoperind o proporţie mai mare) este aceea formată de note între 8 şi aproape 10 la "Română" şi respectiv, între aproape 8 şi aproape 10 la "Matematică"; iar dedesubtul diagonalei "stânga-jos - dreapta-sus" avem mult mai multă culoare decât deasupra, însemnând că pentru majoritatea elevilor notele la "Română" sunt mai mari decât cele de la "Matematică".

O altă reprezentare a densităţii obţinem folosind pachetul ash ("univariate/bivariate Averaged Shifted Histogram"):

bin2d <- ash::bin2(as.matrix(romat))  # implicit, 20 bare x 20 bare = 400 careuri
sh <- ash::ash2(bin2d)  # implicit, câte 5 glisări ale barelor ("netezind" histogramele)
image(sh$x, sh$y, sh$z, col=topo.colors(10), asp=1, xlab="Română", ylab="Matematică")
contour(sh$x, sh$y, sh$z, add=TRUE, labcex=0.9)
grid()

Ideea este de a corela o histogramă verticală (pentru notele de la "Română") cu una orizontală (pentru notele de la "Matematică"). ash::bin2() consideră o anumită extindere a domeniilor (în mod implicit, cu 5% - de la 0.55 la 10.45), un anumit număr de bare de o aceeaşi lăţime - în mod implicit câte 20 (rezultând 400 de pătrăţele) - şi determină pentru fiecare careu numărul de puncte conţinute de acesta, returnând matricea corespunzătoare.

ash::ash2() preia această matrice şi "glisează" de un anumit număr de ori (implicit, 5) barele aferente, însumând (cu o anumită ponderare) "înălţimile" porţiunilor care (datorită glisării curente) se suprapun - asociind apoi careului respectiv valoarea medie a acestor înălţimi; lista returnată - conţinând densităţile astfel estimate şi coordonatele centrelor careurilor - este folosită apoi în image(), pentru a colora după densitate careurile respective. Prin funcţia contour() s-au adăugat câteva "curbe de nivel", fiecare trecând prin careuri la fel colorate, înscriind pe curbă densitatea (sau "cota") comună.

În final, evocăm o constatare metodică proprie: este mai greu să descrii "în cuvinte" - cât mai sugestiv, dar concis şi cu o marjă de "eroare" acceptabilă - ce face o funcţie sau alta, decât să reproduci diversele formule pe care se bazează aceasta şi eventual, pseudocodul aferent (care oricum - dacă interesează - se pot găsi în diverse locuri).

vezi Cărţile mele (de programare)

docerpro | Prev | Next