Bevezetés az R programozási nyelvbe 2. Adatkeretek

Ebben a blogposztban a táblázatos formában tárolt adatok alapszintű kezeléséről lesz szó. Ezeket az R nyelvben egy kétdimenziós adatstruktúra, az adatkeret (data frame) reprezentálja.

Az alábbiakban R nyelven (v4.0.3) írt kódot használok a feladat végrehajtásához. A magyarázó szövegek közé ékelt fekete kódblokkok tartalmát az RStudio-ban egymás alá illesztve elvileg bárki által reprodukálható az itt bemutatott műveletsor. A kódblokkok # kezdetű sorai pusztán magyarázó funkcióval bírnak, ezekre a program futtatásakor nincs szükség.

Az adatkeret fogalma és létrehozása

Az adatkeret tulajdonképpen nem más, mint egy olyan táblázat, amelynek oszlopait (mezőit) egyenlő hosszúságú vektorok alkotják. Vagyis minden, amit az előző posztomban a vektorokkal kapcsolatban leírtam, értelemszerűen alkalmazható az adatkeret oszlopaira is. (A Bevezetés az R programozási nyelvbe sorozat írásai egymásra épülnek. Aki most ismerkedik az R-rel, annak erősen ajánlom, hogy az imént belinkelt blogposzttal kezdje.) Az adatkeret tehát – nomen est omen – keretbe foglalja az általunk összetartozónak ítélet vektorokat.

Elöljáróban hangsúlyozni szeretném, hogy ebben a posztban kizárólag az R alapfunkcióira, vagyis a base és az utils csomagra támaszkodom. Az egyéb kiegészítő csomagok, mint a – méltán népszerű és sokak által használt – dplyr nyújtotta további lehetőségekről most nem lesz szó. Fontosnak érzem ugyanis, hogy ahol lehetséges, ott a legegyszerűbb megoldásokra törekedjünk. Vagy legalábbis ismertjük ezek használatát.

Adatkeretet a data.frame() függvénnyel tudunk létrehozni. Megtehetjük például, hogy külső változókban tárolt vektorokat fűzünk vele egybe. Ekkor az adatkeret oszlopai a paraméterként megadott változók nevét kapják meg.

vektA <- seq(1, 10, by = 2)
vektB <- LETTERS[1:5]
vektC <- c("egy", "kettő", "három", "négy", "öt")
data.frame(vektA, vektB, vektC)
##   vektA vektB vektC
## 1     1     A   egy
## 2     3     B kettő
## 3     5     C három
## 4     7     D  négy
## 5     9     E    öt

Ugyanilyen eredményt kapunk akkor is, ha közvetlenül a data.frame() függvényben definiáljuk az adatkeret mezőit. Itt a létrehozandó oszlopok elnevezése idézőjelben szerepel. Ezekhez egy értékadó operátor (=) segítségével rendeljük hozzá a mezők tartalmát egy-egy vektorban. Megjegyzendő, hogy idézőjel nélkül is működne a dolog, de az adatkeret tartalmához való hozzáférésnél – mint látni fogjuk majd – mindenképpen idézőjelben kell megadni az oszlopok nevét, ezért jobb, ha erre kondicionáljuk magunkat.

Az R-ben a kód jobb olvashatósága érdekében a függvények több sorba tördelhetők a paramétereiket elválasztó vesszők mentén.

data.frame("vektA" = seq(1, 10, by = 2),
           "vektB" = LETTERS[1:5],
           "vektC" = c("egy", "kettő", "három", "négy", "öt"))
##   vektA vektB vektC
## 1     1     A   egy
## 2     3     B kettő
## 3     5     C három
## 4     7     D  négy
## 5     9     E    öt

Ha nem adunk neveket az adatkeret mezőinek, a data.frame() függvény akkor is elfogadja a megadott vektorokat. Ebben az esetben a rendszer generálja le az oszlopok nevét azok tartalmából, amiben nem sok köszönet van…

data.frame(seq(1, 10, by = 2),
           LETTERS[1:5],
           c("egy", "kettő", "három", "négy", "öt"))
##   seq.1..10..by...2. LETTERS.1.5. c..egy....kettő....három....négy....öt..
## 1                  1            A                                      egy
## 2                  3            B                                    kettő
## 3                  5            C                                    három
## 4                  7            D                                     négy
## 5                  9            E                                       öt

A mezőknek tehát mindenféleképpen van valamilyen neve: ezeket vagy mi adjuk meg, vagy a rendszer hozza automatikusan létre. Mivel ezekre az adatkeret tartalmának elérésekor szükségünk lehet, ezért jobban járunk, ha a kódolás során rendesen megadjuk az oszlopok neveit.

A fentiek mellett előfordulhat továbbá, hogy csak egy üres táblázatra van szükségünk. Ebben az esetben mindössze a mezők nevét és az azokban tárolandó adatok típusát kell definiálnunk.

data.frame("vektA" = as.numeric(),
           "vektB" = as.character(),
           "vektC" = as.character())
## [1] vektA vektB vektC
## <0 rows> (or 0-length row.names)

A bevezető szövegben említettek szerint egy adatkeret azonos hosszúságú vektorokból áll. Ez jelen példák esetében öt értéket jelent. Amennyiben figyelmen kívül hagynánk ezt a szempontot, akkor egy hibaüzenetet kapunk és nem jön létre az adatkeret. Ellenben ha mindössze egyetlen értéket adunk meg, akkor az nem okoz hibát és feltöltődik azzal az egész mező.

data.frame("vektA" = 1:2,
           "vektB" = LETTERS[1:5],
           "vektC" = c("egy", "kettő", "három", "négy", "öt"))

## Error in data.frame(vektA = 1:2, vektB = LETTERS[1:5], vektC = c("egy",  : arguments imply differing number of rows: 2, 5

data.frame("vektA" = 1,
           "vektB" = LETTERS[1:5],
           "vektC" = c("egy", "kettő", "három", "négy", "öt"))
##   vektA vektB vektC
## 1     1     A   egy
## 2     1     B kettő
## 3     1     C három
## 4     1     D  négy
## 5     1     E    öt

Gyakran előfordul az is, hogy nem új adatkeretet akarunk létrehozni, hanem egy meglévő táblázatot szeretnénk betölteni és adatkeretté alakítani. Természetesen erre is megvan a lehetőségünk. Az Access által alapértelmezettnek tekintett ACCDB, az Excel által preferált XLSX és a platformfüggetlen CSV formátumú adatállományok betöltésének korábban egy külön posztot szenteltem, amelyben részletesen leírtam az ezzel kapcsolatos tudnivalókat.

Információk az adatkeretről

Az adatkeret felépítéséről az str() függvénnyel kérhetünk rövid áttekintést. Ekkor információt kapunk egyrészt a táblázat sorainak (obs.) és oszlopainak (variables) számáról. Emellett az utóbbiaknak megadja a nevét, a típusát és felsorolja az első néhány adatot is. A summary() függvény elsősorban a numerikus adatokat tartalmazó táblázatoknál lehet hasznos, ahol mezőnkénti bontásban az átlaggal, mediánnal, kvartilisekkel, a legkisebb és a legnagyobb értékkel kapcsolatos mutatókat kapunk. A character típusú oszlopoknál ezek a mutatók magától értetődően nem értelmezhetők.

Ezzel a két függvénnyel tehát egy gyors átekintést kaphatunk az adatkeretünk tartalmáról.

adatkeret <- data.frame("vektA" = seq(1, 10, by = 2),
                        "vektB" = LETTERS[1:5],
                        "vektC" = c("egy", "kettő", "három", "négy", "öt"))
adatkeret
##   vektA vektB vektC
## 1     1     A   egy
## 2     3     B kettő
## 3     5     C három
## 4     7     D  négy
## 5     9     E    öt
str(adatkeret)
## 'data.frame':    5 obs. of  3 variables:
##  $ vektA: num  1 3 5 7 9
##  $ vektB: chr  "A" "B" "C" "D" ...
##  $ vektC: chr  "egy" "kettő" "három" "négy" ...
summary(adatkeret)
##      vektA      vektB              vektC          
##  Min.   :1   Length:5           Length:5          
##  1st Qu.:3   Class :character   Class :character  
##  Median :5   Mode  :character   Mode  :character  
##  Mean   :5                                        
##  3rd Qu.:7                                        
##  Max.   :9

Az nrow() függvény az adatkeret sorainak, míg az ncol() az oszlopainak számát adja vissza. A dim() függvénnyel pedig egy kételemű vektorban kapjuk meg ugyanezeket az értékeket. Ezek a mutatók már nem pusztán az adatkeretről való tájékozódás végett lehetnek fontosak a számunkra. Az algoritmusokban ugyanis gyakran úgy kell hivatkoznunk az adatkeret méretére, hogy előre nem tudjuk pontosan megmondani azt.

nrow(adatkeret)
## [1] 5
ncol(adatkeret)
## [1] 3
dim(adatkeret)
## [1] 5 3

Az oszlopok és a sorok elnevezése

Ahogy fentebb láthattuk, az adatkeret mezőinek mindig van valamilyen elnevezése. Erre a colnames() függvénnyel tudunk rákérdezni, amely vektoros formában adja vissza a neveket. Ugyanezt azonban arra is felhasználhatjuk, hogy új nevet adjunk az adatkeretünk oszlopainak. Ebben az esetben egy értékadó operátor segítségével kell hozzárendelnünk egy character típusú vektorban az új elnevezéseket.

colnames(adatkeret)
## [1] "vektA" "vektB" "vektC"
colnames(adatkeret) <- c("oszlopA", "oszlopB", "oszlopC")
colnames(adatkeret)
## [1] "oszlopA" "oszlopB" "oszlopC"

Az adatkeret sorai alapesetben nem kapnak nevet, illetve a nevük megegyezik a sorok számozásával, mégpedig karakteres formátumban. Ugyanakkor a fentiek mintájára ezeknek is adhatunk egyedi nevet. Ekkor a rownames() függvényt kell használnunk. Erre egyébként az oszlopok elnevezéséhez képest jóval ritkábban szokott szükség lenni.

rownames(adatkeret)
## [1] "1" "2" "3" "4" "5"
rownames(adatkeret) <- c("sorA", "sorB", "sorC", "sorD", "sorE")
rownames(adatkeret)
## [1] "sorA" "sorB" "sorC" "sorD" "sorE"

Hozzáférés az adatkeret elemeihez

Amennyiben nem az adatkeret egészére, hanem annak egy tetszőleges tartományára van szükségünk, akkor le kell szűkítenünk a tárolt vagy ábrázolt adatok körét. Ezt legegyszerűbben az adatkerethez illesztett szögletes zárójellel tudjuk megtenni, például: adatkeretNeve[s, o]. Ebben vesszővel elválasztva kell megadnunk előbb a sorok (s), majd az oszlopok (o) indexét. Ha ezek közül valamelyik helyét üresen hagyjuk, akkor ezt úgy értelmezi a rendszer, hogy az adott dimenzióban nem akarunk szűkíteni, azaz minden sorra vagy oszlopra szükségünk van. (A vesszőt ekkor is ki kell tenni!)

Az adatkeretek indexelése többféle módszer létezik. Ezeket egymással vegyítve is alkalmazhatjuk a szögletes zárójelben.

Az egyik megoldás az, ha egy vektorban felsoroljuk az adatkeret releváns sorainak, illetve oszlopainak sorszámát. (Természetesen egyetlen szám is megadható. Ebben az esetben értelemszerűen nincsen szükség vektorra.) Az adatkeret indexei a bal felső sarokból kiindulva fentről lefelé és balról jobbra növekszenek. Ha pozitív számokat adunk meg, akkor a művelet nyomán a megadott indexű sorok és oszlopok megmaradnak, a többi pedig – jelen példákban csak ideiglenesen – elveszik. Ellenben a negatív számokkal kizárhatunk bizonyos sorokat vagy oszlopokat.

adatkeret
##      oszlopA oszlopB oszlopC
## sorA       1       A     egy
## sorB       3       B   kettő
## sorC       5       C   három
## sorD       7       D    négy
## sorE       9       E      öt
adatkeret[1, 1]
## [1] 1
adatkeret[-1, 1]
## [1] 3 5 7 9
adatkeret[1, -1]
##      oszlopB oszlopC
## sorA       A     egy
adatkeret[c(1, 3, 5), 1:2]
##      oszlopA oszlopB
## sorA       1       A
## sorC       5       C
## sorE       9       E
adatkeret[, 1:2]
##      oszlopA oszlopB
## sorA       1       A
## sorB       3       B
## sorC       5       C
## sorD       7       D
## sorE       9       E
adatkeret[1:2,]
##      oszlopA oszlopB oszlopC
## sorA       1       A     egy
## sorB       3       B   kettő

Az adatkeretek tartalmához a sorok és oszlopok nevei alapján is hozzáférhetünk. Ekkor az eljárás lényegében megegyezik a fentiekkel, csak nem számokat, hanem karakterláncokat kell megadnunk. Oszlopok vagy sorok explicit kizárására ezen a módon nincs lehetőségünk.

adatkeret["sorA", "oszlopA"]
## [1] 1
adatkeret[-1, "oszlopA"]
## [1] 3 5 7 9
adatkeret["sorA", -1]
##      oszlopB oszlopC
## sorA       A     egy
adatkeret[c("sorA", "sorC", "sorE"), 1:2]
##      oszlopA oszlopB
## sorA       1       A
## sorC       5       C
## sorE       9       E
adatkeret[, c("oszlopA", "oszlopB")]
##      oszlopA oszlopB
## sorA       1       A
## sorB       3       B
## sorC       5       C
## sorD       7       D
## sorE       9       E
adatkeret[c("sorA", "sorB"),]
##      oszlopA oszlopB oszlopC
## sorA       1       A     egy
## sorB       3       B   kettő

Egy-egy oszlop tartalmához az adatkeretNeve$oszlopNeve formában is hozzá tudunk férni. Ez sokszor praktikusabbnak bizonyuló alternatívája a szögletes zárójeles – adatkeretNeve[, "oszlopNeve"] – módszernek. Az alábbiakban a logikai kifejezésekkel való indexelésben is ezt a megközelítést fogjuk használni.

adatkeret$oszlopA
## [1] 1 3 5 7 9
adatkeret$oszlopA == adatkeret[, "oszlopA"]
## [1] TRUE TRUE TRUE TRUE TRUE

Az adatkeret tartalmát leszűkíthetjük úgy is, ha logikai értékeket használunk. Ebben az esetben arról van szó, hogy a táblázat minden sorához, illetve oszlopához hozzárendelünk egy-egy TRUE vagy FALSE értéket. Amelyiknél TRUE szerepel, az megjelenik, amelyiknél FALSE, az nem. Ezeket direkt módon is megadhatjuk, de nagyobb táblázatok esetében az egyenkénti felsorolás értelemszerűen nem járható út. Így sokszor az adatkeret tartalmát alapul véve, logikai kifejezésekkel állítjuk elő ezeket. Mivel a logikai értékekről és a logikai kifejezésekről már részletesen is szó volt a vektorokkal kapcsolatban, ezért most különösebb magyarázat nélkül nézzünk meg néhány gyakorlati példát ezekre!

adatkeret
##      oszlopA oszlopB oszlopC
## sorA       1       A     egy
## sorB       3       B   kettő
## sorC       5       C   három
## sorD       7       D    négy
## sorE       9       E      öt
adatkeret[adatkeret$oszlopA > 1 & adatkeret$oszlopB != "E", c(TRUE, FALSE, FALSE)]
## [1] 3 5 7
adatkeret[adatkeret$oszlopC == "három",]
##      oszlopA oszlopB oszlopC
## sorC       5       C   három
adatkeret[, !(colnames(adatkeret) %in% c("oszlopB", "oszlopC"))]
## [1] 1 3 5 7 9
adatkeret[adatkeret$oszlopA <= nrow(adatkeret),]
##      oszlopA oszlopB oszlopC
## sorA       1       A     egy
## sorB       3       B   kettő
## sorC       5       C   három

Végezetül a head(x, n) függvénnyel az adatkeret első n sorát, a tail(x, n) függvénnyel pedig az utolsó n sorát kapjuk meg. Alapértelmezés szerint az n paraméter értéke 6. Ha nem adjuk meg, akkor ennyi sort kapunk vissza. (Jelen esetben csak 5 sora van a táblázatunknak.)

head(adatkeret, 3)
##      oszlopA oszlopB oszlopC
## sorA       1       A     egy
## sorB       3       B   kettő
## sorC       5       C   három
tail(adatkeret, 3)
##      oszlopA oszlopB oszlopC
## sorC       5       C   három
## sorD       7       D    négy
## sorE       9       E      öt

Az adatkeret tartalmának felülírása

A fentiekben számos módszert megnéztünk az adatkeret tartalmának szűkítésére. Ezek azonban csak virtuálisan végezték el a feladatot. Maga az eredeti adatkeret, ahogy alább látható, változatlan maradt.

adatkeret
##      oszlopA oszlopB oszlopC
## sorA       1       A     egy
## sorB       3       B   kettő
## sorC       5       C   három
## sorD       7       D    négy
## sorE       9       E      öt

Ahhoz, hogy a változtatások elmentődjenek, egy értékadó operátor segítségével felül kell írni az eredeti adatkeretet, vagy annak egy kiválasztott részét. Most ez utóbbira nézünk egy példát. Itt magától értetődő, hogy az új adatoknak “bele kell férni” a régiek helyére. Azaz jelen esetben, amikor a táblázatunknak 5 sora van, nem tudnánk felülírni az egyik oszlopot, ha az új adatokat tartalmazó vektornak nem ugyanúgy 5 eleme lenne.

A felülírandó oszloptól eltérő típusú adatok nem feltétlenül okoznak hibát, de az automatikus típuskonverzió miatt megváltoztatják az adott mező típusát. Az eredendően számokat tartalmazó első oszlop az alábbi példában character típusúvá változik majd azáltal, hogy az [1, 1] cellában a 0-t kicserélem a “0”-ra. Erre érdemes nagyon figyelni, mert bár elsőre nem feltétlenül szembetűnő a különbség, de ez alkalom adtán hibát okozhat a programunk futása közben.

adatkeret[, "oszlopA"]
## [1] 1 3 5 7 9
adatkeret[, "oszlopA"] <- seq(0, 16, by = 4)
adatkeret[1, 1] <- "0"
adatkeret
##      oszlopA oszlopB oszlopC
## sorA       0       A     egy
## sorB       4       B   kettő
## sorC       8       C   három
## sorD      12       D    négy
## sorE      16       E      öt
str(adatkeret)
## 'data.frame':    5 obs. of  3 variables:
##  $ oszlopA: chr  "0" "4" "8" "12" ...
##  $ oszlopB: chr  "A" "B" "C" "D" ...
##  $ oszlopC: chr  "egy" "kettő" "három" "négy" ...
adatkeret[, "oszlopA"] <- as.numeric(adatkeret[, "oszlopA"])
str(adatkeret)
## 'data.frame':    5 obs. of  3 variables:
##  $ oszlopA: num  0 4 8 12 16
##  $ oszlopB: chr  "A" "B" "C" "D" ...
##  $ oszlopC: chr  "egy" "kettő" "három" "négy" ...

Az adatkeret kibővítése

Egy adatkeret tartalmát nemcsak szűkíteni tudjuk, hanem bővíteni is. Vagyis szükség esetén új oszlopokat vagy sorokat tudunk hozzáfűzni.

Új oszlopokat a cbind() függvénnyel adhatunk az adatkerethez. Itt az első paraméter a kibővítendő adatkeret, a többiben pedig az új oszlopokat kell definiálnunk. Az eljárást tekintve lényegében arról van szó, amit fentebb a data.frame() függvény esetében már láttunk. Tehát megadhatunk külső változókat, de a függvényen belül is létrehozhatjuk az új oszlop tartalmát leíró vektort. Ezzel a módszerrel bármennyi új oszloppal bővíthetjük az adatkeretünket. A legfontosabb szabály az, hogy a megadott vektorok hosszának meg kell egyeznie az adatkeretünk sorainak számával, mert egyébként hibaüzenetet kapunk és sikertelen lesz a művelet.

adatkeret
##      oszlopA oszlopB oszlopC
## sorA       0       A     egy
## sorB       4       B   kettő
## sorC       8       C   három
## sorD      12       D    négy
## sorE      16       E      öt
oszlopD <- 1:5
length(oszlopD) == nrow(adatkeret)
## [1] TRUE
adatkeret <- cbind(adatkeret, 
                   oszlopD,
                   "oszlopE" = c("ez", "egy", "új", "oszlop", "lesz"))
adatkeret
##      oszlopA oszlopB oszlopC oszlopD oszlopE
## sorA       0       A     egy       1      ez
## sorB       4       B   kettő       2     egy
## sorC       8       C   három       3      új
## sorD      12       D    négy       4  oszlop
## sorE      16       E      öt       5    lesz

Alternatív megoldásként egy-egy új oszlopot az adatkeretNeve$ujOszlopNeve <- vektor utasítással is hozzá tudunk adni az adatkeretünkhöz. Ha ugyanezen a módon a NULL értéket rendeljük hozzá, akkor az törli a táblázatból a kérdéses oszlopot.

adatkeret$oszlopF <- letters[22:26]
adatkeret
##      oszlopA oszlopB oszlopC oszlopD oszlopE oszlopF
## sorA       0       A     egy       1      ez       v
## sorB       4       B   kettő       2     egy       w
## sorC       8       C   három       3      új       x
## sorD      12       D    négy       4  oszlop       y
## sorE      16       E      öt       5    lesz       z
adatkeret$oszlopF <- NULL
adatkeret
##      oszlopA oszlopB oszlopC oszlopD oszlopE
## sorA       0       A     egy       1      ez
## sorB       4       B   kettő       2     egy
## sorC       8       C   három       3      új
## sorD      12       D    négy       4  oszlop
## sorE      16       E      öt       5    lesz

Új sorok hozzáadása az rbind() függvénnyel történik. Ennek használata azonban egy kicsit több magyarázatot igényel. Ha ugyanis nem homogén a táblázatunk, azaz numerikus és karakteres típusú oszlopok vegyesen szerepelnek benne, akkor nem adhatjuk meg egy vektorban az új sort, hiszen a vektor csakis azonos típusú adatok tárolására képes. A bővítéshez itt egy másik adatkeretre lesz szükség, amelyben az oszlopok száma és típusa értelemszerűen megegyezik az eredeti adatkeretével. Mindemellett arra is figyelnünk kell, hogy az eredeti és az új adatkeret oszlopainak neve is azonos legyen, mert egyébként nem megye végbe az egyesítés.

Az alábbi példában két új sort adok a már ismert táblázathoz. A második adatkeret definiálásakor szándékosan nem gépelem be az oszlopok nevét, hanem ezeket az eredeti táblából veszem át a már megismert colnames() függvény segítségével.

adatkeret2 <- data.frame(c(20, 24), 
                         c("F", "G"), 
                         c("hat", "hét"), 
                         c(6, 7),
                         c("a", "táblázatban"))
adatkeret2
##   c.20..24. c..F....G.. c..hat....hét.. c.6..7. c..a....táblázatban..
## 1        20           F             hat       6                     a
## 2        24           G             hét       7           táblázatban
colnames(adatkeret2) <- colnames(adatkeret)
colnames(adatkeret2)
## [1] "oszlopA" "oszlopB" "oszlopC" "oszlopD" "oszlopE"
rownames(adatkeret2) <- c("sorF", "sorG")
rbind(adatkeret, adatkeret2)
##      oszlopA oszlopB oszlopC oszlopD     oszlopE
## sorA       0       A     egy       1          ez
## sorB       4       B   kettő       2         egy
## sorC       8       C   három       3          új
## sorD      12       D    négy       4      oszlop
## sorE      16       E      öt       5        lesz
## sorF      20       F     hat       6           a
## sorG      24       G     hét       7 táblázatban

Ez e blogposztban elmondottak reményeim szerint elegendők arra, hogy az olvasó elkezdje az adatkeretekkel való munkát. A későbbiekben azonban még bizonyára vissza fogok térni erre a témára, hiszen számos olyan csomag létezik, amely hasznos funkciókat tartalmaz az adatkeretek manipulálásával kapcsolatban. (Most csak mellékesen említem meg, hogy az adatkeretek relációs kapcsolatba hozásáról a korábbiakban már szó volt a blogon.)

comments powered by Disqus