library(data.table)
5 Data table: egy továbbfejlesztett adatkeret
Amint volt már róla szó korábban, az adatkeret (data frame) az alapvető struktúra a feldolgozandó adatok, adatbázisok tárolására és kezelésére az R-ben. Noha ennek a célnak megfelel, számos téren kiegészíthető, továbbfejleszthető. Az évek alatt két nagy lehetőség kristályosodott ki és ment át széleskörű használatba, mely ilyen továbbfejlesztést jelent: a dplyr
csomag és a data.table
csomag. Jelen fejezet a data.table
működését és jellemzőit fogja bemutatni, különös tekintettel a hagyományos data frame-mel való összevetésre.
A data.table
csomag első verziója 2008-ban jelent meg, eredeti megalkotója Matt Dowle. Nagyon erőteljes, gyors, erősen optimalizált, némi gyakorlás után logikus, kompakt, konzisztens, könnyen lekódolható és jól olvasható szintaktikájú, jól támogatott csomag. A data.table
-nek semmilyen függősége nincs az R-en kívül (ott is törekednek a nagyon régi változatok támogatására is), így kifejezetten problémamentesen beépíthető R kódokba, csomagokba.
Központi weboldala: https://rdatatable.gitlab.io/data.table/. Github-oldala: https://github.com/Rdatatable/data.table. CRAN-oldala: https://cran.r-project.org/web/packages/data.table/index.html.
A data.table
mint csomag egy azonos nevű új adatstruktúrát definiál; ez lényegében egy „továbbfejlesztett data frame”. Ez az új adatstruktúra, a data table egyrészt olyan lehetőségeket biztosít, amik valamilyen módon megvalósíthatóak lennének szokásos data frame-mel is, de csak lassabban/nehézkesebben/több hibalehetőséggel, másrészt elérhetővé tesz olyan funkciókat is, amik data frame-mel egyáltalán nem megoldhatóak.
A következőkben át fogjuk tekinteni ezek legfontosabb példait.
A gyakorlati szemléltetésekhez töltsük be a könyvtárat (a data.table
nem jön az alap R installációval, így ha korábban nem tettük meg, elsőként telepíteni kell):
Ebben a fejezetben a magyar Nemzeti Rákregiszter adatait fogjuk példa adatbázisnak használni.
Elsőként töltsük be a következő fájlt, ami eleve data.table
formátumban tartalmazza az adatokat:
if(!file.exists("RawDataLongWPop.rds"))
download.file(paste0(
"https://github.com/tamas-ferenci/RakregiszterVizualizator/",
"raw/refs/heads/master/RawDataLongWPop.rds"),
"RawDataLongWPop.rds")
<- readRDS("RawDataLongWPop.rds") RawData
Néhány esetben össze fogjuk vetni a data.frame
-et a data.table
-lel, ehhez „minősítsük vissza” az adatbázist data frame-mé, és ezt mentsük el egy új változóba:
<- data.frame(RawData) RawDataDF
Érdemes ránézni egy data table felépítésére:
str(RawData)
Classes 'data.table' and 'data.frame': 1313280 obs. of 7 variables:
$ County : chr "Baranya megye" "Baranya megye" "Baranya megye" "Baranya megye" ...
$ Sex : chr "Férfi" "Férfi" "Férfi" "Férfi" ...
$ Age : num 0 0 0 0 0 0 0 0 0 0 ...
$ Year : num 2000 2000 2000 2000 2000 2000 2000 2000 2000 2000 ...
$ ICDCode : chr "C00" "C01" "C02" "C03" ...
$ N : int 0 0 0 0 0 0 0 0 0 0 ...
$ Population: num 9876 9876 9876 9876 9876 ...
- attr(*, ".internal.selfref")=<externalptr>
- attr(*, "sorted")= chr [1:4] "County" "Sex" "Age" "Year"
Ami feltűnhet, hogy az objektumnak egyaránt van data.table
és data.frame
osztálya, egyebekben azonban a fenti információk megfelelnek egy data frame által mutatott felépítésnek. A két osztály jelenléte egyfajta visszafele kompatibilitást1 jelent: egy data table olyan számítógépen is betölthető, ahol nincs data.table
csomag, és működni fog (természetesen csak mint hagyományos data frame). Ezen túl az is igaz ennek következtében, hogy olyan függvénynek, ami data.frame
-et vár mindig átadható data.table
is.
Visszatérve a tábla felépítésére, a fentiek alapján már elmondható, hogy a tábla mit tartalmaz: az új rákos esetek előfordulását Magyarországon évenként (2000-től 2018-ig), megyénként, nemenként, életkoronként (ez 5 éves felbontású, tehát a 40 igazából azt jelenti, hogy „40-45 év”), és a rák típusa szerint. Ez utóbbi ún. BNO-kóddal van megadva: a Betegségek Nemzetközi Osztályozása (BNO, angol rövidítéssel ICD) egy nemzetközileg egységes rendszer, mely minden betegséghez egy kódot rendel. A kód első karaktere egy betű, ez a főcsoport; a rákos betegségek a C főcsoportban, illetve a D elején vannak, a második és harmadik karakter egy szám, ami konkrét betegséget vagy betegségcsoport azonosít; például C00 az ajak rosszindulatú daganata, C01 a nyelvgyök rosszindulatú daganata és így tovább2. Az esetek számát az N
nevű változó tartalmazza, a háttérpopuláció lélekszámát3, tehát, hogy hány fő volt adott évben, adott megyében, adott nemben, adott életkorban – pedig a Population
. (Ez tehát azonos lesz azokra a sorokra, amelyek csak a rák típusában térnek el, hiszen ezekre a háttérpopuláció lélekszáma természetesen ugyanaz.)
5.1 Sebesség és nagyméretű adatbázisok kezelése
Ez a probléma a legtöbb szokásos elemzési feladatnál nem jelentkezik, itt sem fogunk rá részletes példát nézni, de röviden érdemes arról megemlékezni, hogy a hagyományos adatkeret (data frame) adatstuktúra nem szerencsés, ha nagyméretű adatbázisokat kell kezelnünk.
Az első probléma kapásból az adatok beolvasásánál fog jelentkezni: a read.csv
(és társai) egész egyszerűen lassúak. Pár százezer sorig ennek semmilyen érzékelhető hatása nincsen, mert még így is elég gyors a beolvasás, így a legtöbb feladatnál ez a probléma nem jelentkezik, de millió soros, több millió soros adatbázisoknál, ha a tábla mérete több gigabájt vagy több tíz gigabájt, akkor a beolvasás a méret növekedtével gyorsan lassul, míg végül teljesen reménytelenné válik. A data.table
definiálja az fread
függvényt mely ezzel szemben villámgyors, és még ilyen méretű adatok beolvasásánál is elfogadható sebességet produkál. (Az fread
-nek ezen kívül van pár további előnye is az R beépített beolvasó függvényeihez képest, olyan, amik kis méretű adatbázisoknál is érdekesek lehetnek, például nagyon okosan detektálja az oszlopelválasztókat és az oszloptípusokat.) Hasonló a helyzet kiírásnál: a write.csv
és társai nagyon nagy adatbázisoknál elfogadhatatlanul lassúak lesznek, de a data.table
könyvtár fwrite
függvénye ilyenkor is jól működik.
A második probléma, hogy még ha valahogy be is olvastuk az adatbázist a memóriába, akkor is bajban leszünk az adattranszformációkkal: a data frame nincs túl jól optimalizálva ilyen szempontból, egy sor művelet nagyon lassú. Ismét csak: ennek kis, közepes és a legtöbb terület mércéje szerinti nagy adatbázisoknál nincs jelentősége, mert még így is gyors, de a nagyon nagy adatbázisoknál bajban leszünk data frame-et használva. A data table ezzel szemben nagyon jól optimalizált, képest többmagú processzoroknál bizonyos műveletek párhuzamos végrehajtására is, így az adattranszformációs műveleteknél4 (aggregáció, táblaegyesítések, de akár új változó létrehozása) sokkal jobb sebességet tud produkálni.
A fentieket többféle benchmark vizsgálat is megerősíti.
5.2 Jobb kiíratás
A data frame kiíratásánál (tehát ha egyszerűen beírjuk, hogy RawDataDF
, ami ekvivalens a print(RawDataDF)
függvény meghívásával) az alapbeállítás az, hogy kiírja a konzolra az első jó sok sorát az adatbázisnak5. Ez nem túl praktikus: az 587. sor ismerete jellemzően nem sokat ad hozzá az első 586-hoz, cserében hosszasan kell görgetnünk a rengeteg sor miatt, hogy elérjünk a kiíratás tetejére, aminek viszont volna jelentősége, mert ott látjuk az oszlopok neveit. (Nem véletlenül gyakori, hogy sokan eleve a head(RawDataDF)
típusú kéréssel íratják ki a data frame-eket!)
A data table alapértelmezett kiíratása okosabb, mert csak az első néhány és az utolsó néhány sort6 írja ki:
RawData
Key: <County, Sex, Age, Year>
County Sex Age Year ICDCode N Population
<char> <char> <num> <num> <char> <int> <num>
1: Baranya megye Férfi 0 2000 C00 0 9876.0
2: Baranya megye Férfi 0 2000 C01 0 9876.0
3: Baranya megye Férfi 0 2000 C02 0 9876.0
4: Baranya megye Férfi 0 2000 C03 0 9876.0
5: Baranya megye Férfi 0 2000 C04 0 9876.0
---
1313276: Zala megye Nő 85 2018 D06 0 4483.5
1313277: Zala megye Nő 85 2018 D07 0 4483.5
1313278: Zala megye Nő 85 2018 D09 0 4483.5
1313279: Zala megye Nő 85 2018 D30 0 4483.5
1313280: Zala megye Nő 85 2018 D33 0 4483.5
Természetesen láthatóak az oszlopfejlécek (változónevek) is, sőt, itt van még egy további apró fejlesztés: a data table kiírja az egyes oszlopok adattípusát is, standard rövidítéssel.
5.3 Kényelmesebb sorindexelés (sor-szűrés és -rendezés)
Data frame indexeléséhez szögletes zárójelet kell írnunk a változó neve után, abba vesszőt tennünk, majd a vessző elé kerül az sor indexelése. Ezt tipikusan szűréshez használjuk. Például, ha ki akarjuk választani csak a 2010-es év adatait:
head(RawDataDF[RawDataDF$Year == 2010,])
County Sex Age Year ICDCode N Population
961 Baranya megye Férfi 0 2010 C00 0 9430
962 Baranya megye Férfi 0 2010 C01 0 9430
963 Baranya megye Férfi 0 2010 C02 0 9430
964 Baranya megye Férfi 0 2010 C03 0 9430
965 Baranya megye Férfi 0 2010 C04 0 9430
966 Baranya megye Férfi 0 2010 C05 0 9430
Ez lényegében a „logikai vektorral indexelés” esete: a RawDataDF$Year == 2010
egy adatbázissal sorainak számával azonos hosszúságú logikai vektor lesz.
Ha ki akarjuk választani 2010 évben a 40 évnél idősebbek adatait, akkor a logikai ÉS operátort (&
) kell használnunk; ez egyúttal azt is szemlélteti, hogy a feltételek természetesen nem csak egyenlőségek lehetnek:
head(RawDataDF[RawDataDF$Year == 2010 & RawDataDF$Age >= 40,])
County Sex Age Year ICDCode N Population
15553 Baranya megye Férfi 40 2010 C00 0 13076
15554 Baranya megye Férfi 40 2010 C01 0 13076
15555 Baranya megye Férfi 40 2010 C02 0 13076
15556 Baranya megye Férfi 40 2010 C03 0 13076
15557 Baranya megye Férfi 40 2010 C04 0 13076
15558 Baranya megye Férfi 40 2010 C05 0 13076
A dolog hasonlóan folytatódik, ha további feltételek vannak. Például 2010 évben a 40 évnél idősebb budapesti vagy Pest megyei férfiak körében előforduló vastagbélrákos (BNO-kód: C18) esetek kiválasztása:
head(RawDataDF[RawDataDF$Year == 2010 & RawDataDF$Age >= 40 &
$County %in% c("Budapest",
RawDataDF"Pest megye") &
$Sex == "Férfi" &
RawDataDF$ICDCode == "C18",]) RawDataDF
County Sex Age Year ICDCode N Population
146899 Budapest Férfi 40 2010 C18 3 57445.5
148723 Budapest Férfi 45 2010 C18 10 42410.0
150547 Budapest Férfi 50 2010 C18 17 45329.0
152371 Budapest Férfi 55 2010 C18 44 55633.5
154195 Budapest Férfi 60 2010 C18 59 45170.0
156019 Budapest Férfi 65 2010 C18 120 39588.0
A dolog tökéletesen működik, ámde nem túl kényelmes: folyton be kell írni a RawDataDF$
-t a feltételek közé. A kód hosszú, lassabb megírni, és az olvashatóság is romlik. Fontos hangsúlyozni, hogy ez nem hagyható el, és teljesen igaza is van az R-nek, hogy nem hagyható el: Year
nevű változó nem létezik, tehát teljes joggal ad hibát, ha előle – vagy bármelyik másik elől – elhagyjuk a data frame nevét.
Mégis: a gyakorlatban az esetek 99,99%-ában, ha egy változó nevére hivatkozunk miközben egy adatkeret sorindexelését végezzük, akkor azt természetesen úgy értjük, hogy annak az adatkeretnek az adott nevű oszlopa (és nem egy külső változó). Éppen emiatt a data table megengedi ezt a szintaktikát: ha pusztán egy változó nevére hivatkozunk, akkor ő megnézi, hogy nincs-e olyan nevű oszlopa az indexelt adattáblának, és ha van, akkor úgy veszi, hogy arra szerettünk volna hivatkozni. Éppen ezért az alábbi kód data.frame
-mel nem, de data.table
-lel működik:
== 2010 & Age >= 40 &
RawData[Year %in% c("Budapest", "Pest megye") &
County == "Férfi" & ICDCode == "C18",] Sex
Key: <County, Sex, Age, Year>
County Sex Age Year ICDCode N Population
<char> <char> <num> <num> <char> <int> <num>
1: Budapest Férfi 40 2010 C18 3 57445.5
2: Budapest Férfi 45 2010 C18 10 42410.0
3: Budapest Férfi 50 2010 C18 17 45329.0
4: Budapest Férfi 55 2010 C18 44 55633.5
5: Budapest Férfi 60 2010 C18 59 45170.0
6: Budapest Férfi 65 2010 C18 120 39588.0
7: Budapest Férfi 70 2010 C18 80 27335.5
8: Budapest Férfi 75 2010 C18 75 22253.5
9: Budapest Férfi 80 2010 C18 72 15775.5
10: Budapest Férfi 85 2010 C18 35 10922.0
11: Pest megye Férfi 40 2010 C18 8 48086.0
12: Pest megye Férfi 45 2010 C18 6 36113.5
13: Pest megye Férfi 50 2010 C18 14 37167.0
14: Pest megye Férfi 55 2010 C18 32 41154.0
15: Pest megye Férfi 60 2010 C18 43 32527.0
16: Pest megye Férfi 65 2010 C18 56 26071.0
17: Pest megye Férfi 70 2010 C18 62 16926.5
18: Pest megye Férfi 75 2010 C18 45 11964.5
19: Pest megye Férfi 80 2010 C18 23 7015.0
20: Pest megye Férfi 85 2010 C18 11 4250.5
County Sex Age Year ICDCode N Population
A kapott kód világosabb, gyorsabban beírható és jobban olvasható!
A data table azt is megengedi, hogy a vesszőt elhagyjuk (a data frame nem, ott hibát adna ha nem írnánk vesszőt!):
== 2010 & Age >= 40 &
RawData[Year %in% c("Budapest", "Pest megye") &
County == "Férfi" & ICDCode == "C18"] Sex
Key: <County, Sex, Age, Year>
County Sex Age Year ICDCode N Population
<char> <char> <num> <num> <char> <int> <num>
1: Budapest Férfi 40 2010 C18 3 57445.5
2: Budapest Férfi 45 2010 C18 10 42410.0
3: Budapest Férfi 50 2010 C18 17 45329.0
4: Budapest Férfi 55 2010 C18 44 55633.5
5: Budapest Férfi 60 2010 C18 59 45170.0
6: Budapest Férfi 65 2010 C18 120 39588.0
7: Budapest Férfi 70 2010 C18 80 27335.5
8: Budapest Férfi 75 2010 C18 75 22253.5
9: Budapest Férfi 80 2010 C18 72 15775.5
10: Budapest Férfi 85 2010 C18 35 10922.0
11: Pest megye Férfi 40 2010 C18 8 48086.0
12: Pest megye Férfi 45 2010 C18 6 36113.5
13: Pest megye Férfi 50 2010 C18 14 37167.0
14: Pest megye Férfi 55 2010 C18 32 41154.0
15: Pest megye Férfi 60 2010 C18 43 32527.0
16: Pest megye Férfi 65 2010 C18 56 26071.0
17: Pest megye Férfi 70 2010 C18 62 16926.5
18: Pest megye Férfi 75 2010 C18 45 11964.5
19: Pest megye Férfi 80 2010 C18 23 7015.0
20: Pest megye Férfi 85 2010 C18 11 4250.5
County Sex Age Year ICDCode N Population
Fontos azonban, hogy ez csak ebben az esetben, tehát sorindexelésnél használható: ha nincs vessző, akkor automatikusan úgy veszi, hogy amit beírtunk, az sorindex (e megállapodás nélkül nem tudhatná, hogy mit akartunk indexelni).
A tény, hogy nem kell hivatkozni az adatkeret nevére, nem csak szűrésnél igaz, hanem rendezésnél is. Ezt ugyanis az order
függvény valósítja meg, ami elérhető volt a data frame-hez is, csak ott ilyen módon kellett használnunk:
head(RawDataDF[order(RawDataDF$N),])
County Sex Age Year ICDCode N Population
1 Baranya megye Férfi 0 2000 C00 0 9876
2 Baranya megye Férfi 0 2000 C01 0 9876
3 Baranya megye Férfi 0 2000 C02 0 9876
4 Baranya megye Férfi 0 2000 C03 0 9876
5 Baranya megye Férfi 0 2000 C04 0 9876
6 Baranya megye Férfi 0 2000 C05 0 9876
A data table azonban itt is megengedi7 a fenti – nagyon logikus – egyszerűsítést:
order(N),] RawData[
County Sex Age Year ICDCode N Population
<char> <char> <num> <num> <char> <int> <num>
1: Baranya megye Férfi 0 2000 C00 0 9876.0
2: Baranya megye Férfi 0 2000 C01 0 9876.0
3: Baranya megye Férfi 0 2000 C02 0 9876.0
4: Baranya megye Férfi 0 2000 C03 0 9876.0
5: Baranya megye Férfi 0 2000 C04 0 9876.0
---
1313276: Budapest Nő 60 2008 C50 307 64674.0
1313277: Budapest Nő 55 2000 C50 308 67218.5
1313278: Budapest Nő 55 2002 C50 308 69118.0
1313279: Budapest Nő 70 2018 C44 331 56508.0
1313280: Budapest Nő 55 2003 C50 350 69396.5
Természetesen a „szűrés” és „rendezés” csak felhasználói szempontból két külön művelet. Az R számára a kettő ugyanaz: sorindexelés, csak annyi eltéréssel, hogy az előbbi esetben logikai vektort kap, az utóbbiban pedig számvektort (hiszen az order
egyszerűen megadja sorban minden elemre, hogy az adott elem hányadik a nagyság szerinti sorrendben).
5.4 Kibővített oszlopindexelés: oszlop-kiválasztás és oszlop-létrehozás műveletekkel
Hagyományos data frame esetén a vessző után jön az oszlopindexelés, ami egy dolgot jelenthet: oszlopok kiválasztását. Tehát, dönthetünk, hogy mely oszlopokat kérjük (és melyeket nem), de más lehetőségünk nincs. Oszlopok kiválasztását célszerű mindig névvel és nem számmal végeznünk (hogy a kód az adatbázis esetleges későbbi módosításaira robusztusabb legyen, ne romoljon el új oszlop beszúrásától vagy törlésétől, valamint, hogy önállóan is jobban olvasható legyen a kód). Ekkor lényegében egy sztring-vektort kell átadnunk. A példa kedvéért itt – az előzőekkel szemben – a 40-45 éves budapesti férfiak vastagbélrákos eseteire szorítsuk meg magunkat, viszont tartsuk meg az összes évet. Ez esetben logikus csak az évet – és persze az N
-et és a Population
-t – kiíratni, hiszen a többi konstans:
head(RawDataDF[RawDataDF$Age == 40 &
$County == "Budapest" &
RawDataDF$Sex == "Férfi" &
RawDataDF$ICDCode == "C18",
RawDataDFc("Year", "N", "Population")])
Year N Population
145939 2000 8 51602.0
146035 2001 4 47836.0
146131 2002 4 45296.5
146227 2003 4 43632.5
146323 2004 4 43085.0
146419 2005 5 43442.5
Ez a szintaktika a data.table
-lel is működik8:
== 40 & County == "Budapest" & Sex == "Férfi" &
RawData[Age == "C18",
ICDCode c("Year", "N", "Population")]
Year N Population
<num> <int> <num>
1: 2000 8 51602.0
2: 2001 4 47836.0
3: 2002 4 45296.5
4: 2003 4 43632.5
5: 2004 4 43085.0
6: 2005 5 43442.5
7: 2006 5 44511.5
8: 2007 6 46903.5
9: 2008 4 50505.5
10: 2009 6 54015.0
11: 2010 3 57445.5
12: 2011 8 60721.0
13: 2012 7 62471.5
14: 2013 5 63746.5
15: 2014 8 66250.5
16: 2015 13 70511.5
17: 2016 11 74622.0
18: 2017 6 77902.0
19: 2018 10 80555.0
A data.table
-nek van azonban egy saját, külön szintaktikája erre, és célszerű is azt megszokni és használni mindig, mert a későbbi funkciókat az teszi elérhetővé:
== 40 & County == "Budapest" & Sex == "Férfi" &
RawData[Age == "C18",
ICDCode .(Year, N, Population)]
Year N Population
<num> <int> <num>
1: 2000 8 51602.0
2: 2001 4 47836.0
3: 2002 4 45296.5
4: 2003 4 43632.5
5: 2004 4 43085.0
6: 2005 5 43442.5
7: 2006 5 44511.5
8: 2007 6 46903.5
9: 2008 4 50505.5
10: 2009 6 54015.0
11: 2010 3 57445.5
12: 2011 8 60721.0
13: 2012 7 62471.5
14: 2013 5 63746.5
15: 2014 8 66250.5
16: 2015 13 70511.5
17: 2016 11 74622.0
18: 2017 6 77902.0
19: 2018 10 80555.0
Megjegyzendő, hogy a .
egyszerűen egy rövidítés, amit a data.table
csomag bevezet arra, hogy list
, magyarán itt az történik, hogy egy listát kell átadnunk9, benne az – idézőjelek nélküli – oszlopnevekkel. A listás megoldás előnye, hogy valójában nem kötelező explicite kiírni, hogy .
majd felsorolni a változóneveket zárójelben, bármilyen függvényt is használhatunk a vessző után ami listát ad eredményül. Később látunk majd erre példát.
Az is érthető a listás megoldás fényében, hogy data table-lel átnevezhetünk változót úgymond „menet közben” (data frame-mel már ezt sem lehetett!):
== 40 & County == "Budapest" & Sex == "Férfi" &
RawData[Age == "C18",
ICDCode Esetszam = N, Lelekszam = Population)] .(Year,
Year Esetszam Lelekszam
<num> <int> <num>
1: 2000 8 51602.0
2: 2001 4 47836.0
3: 2002 4 45296.5
4: 2003 4 43632.5
5: 2004 4 43085.0
6: 2005 5 43442.5
7: 2006 5 44511.5
8: 2007 6 46903.5
9: 2008 4 50505.5
10: 2009 6 54015.0
11: 2010 3 57445.5
12: 2011 8 60721.0
13: 2012 7 62471.5
14: 2013 5 63746.5
15: 2014 8 66250.5
16: 2015 13 70511.5
17: 2016 11 74622.0
18: 2017 6 77902.0
19: 2018 10 80555.0
Ez már utat mutat a következő, igazi újdonsághoz.
Előtte még említsük meg, hogy a data table egyik jellegzetessége, hogy a RawData[, .(Year)]
típusú hívások mindig data table-t adnak vissza10. Ha egyetlen változót választunk ki, de azt vektorként szeretnénk visszakapni (ez a kérdés nyilván csak egyetlen változó kiválasztásakor merül fel), akkor használjuk a RawData$Year
vagy a RawData[["Year"]]
formát11.
Ez eddig nem nagy változás, még csak azt sem igazán lehet mondani, hogy az előzőhöz hasonló kényelmi továbbfejlesztés, hiszen ez a szintaktika nem sokkal tér el a korábbitól. Az igazán érdekes rész azonban most jön, a data.table
ugyanis lehetővé tesz valamit, ami a data.frame
-nél fel sem merült: nem csak passzívan kiválaszthatunk oszlopokat, hanem műveleteket is végezhetünk velük, így új oszlopokat hozva létre! Lényegében „on the fly”, azaz menet közben végezhetünk műveleteket és hozhatunk létre új oszlopokat, anélkül, hogy azokat fizikailag le kellene tárolnunk az adatbázisba. A data table vessző utáni pozíciójában tehát
Például a rákos megbetegedéseknél fontos az incidencia, tehát a lélekszámhoz viszonyított előfordulás. (Értelemszerűen nem mindegy, hogy 10 vagy 10 ezer ember közül került ki 1 rákos adott évben.) Ezt tipikusan 100 ezer lakosra vonatkoztatva szokták megadni. Nézzük meg a következő data table-t használó megoldást:
== 40 & County == "Budapest" & Sex == "Férfi" &
RawData[Age == "C18",
ICDCode Inc = N / Population * 1e5)] .(Year, N, Population,
Year N Population Inc
<num> <int> <num> <num>
1: 2000 8 51602.0 15.503275
2: 2001 4 47836.0 8.361903
3: 2002 4 45296.5 8.830704
4: 2003 4 43632.5 9.167478
5: 2004 4 43085.0 9.283974
6: 2005 5 43442.5 11.509467
7: 2006 5 44511.5 11.233052
8: 2007 6 46903.5 12.792222
9: 2008 4 50505.5 7.919930
10: 2009 6 54015.0 11.108026
11: 2010 3 57445.5 5.222341
12: 2011 8 60721.0 13.175014
13: 2012 7 62471.5 11.205110
14: 2013 5 63746.5 7.843568
15: 2014 8 66250.5 12.075381
16: 2015 13 70511.5 18.436709
17: 2016 11 74622.0 14.740961
18: 2017 6 77902.0 7.701985
19: 2018 10 80555.0 12.413879
Azaz az Inc
oszlopot létrehoztuk a nélkül, hogy előzetesen azt le kellett volna tárolnunk magába az adatbázisba! Menet közben számoltuk ki, és még nevet is adtunk neki. Az oszlopok tehát itt, a vessző utáni pozícióban úgy viselkednek egy data table-nél mintha szokásos változók lennének!
Ebből is adódik, hogy a lehetőségeink még ennél is bővebbek: nem csak egyszerű aritmetikai műveleteket végezhetünk egy oszloppal (vagy épp több oszloppal! – mint arra ez előbbi kód is példát mutat), hanem bármilyen R függvényt rájuk ereszthetünk! Tekintsünk példának a következő kódot, mely megadja, hogy a 40-45 éves budapesti férfiak körében összesen hány vastagbélrákos eset volt az adatbázis által lefedett 19 év alatt:
== 40 & County == "Budapest" & Sex == "Férfi" &
RawData[Age == "C18",
ICDCode N = sum(N))] .(
N
<int>
1: 121
Az N
oszlop egy vektor, tehát azon túl, hogy oszthatjuk – elemenként – egy másik vektorral, mint ahogy az előbbi esetben tettük, nyugodtan összegezhetjük is példának okáért. Ebből mellesleg az is látszik, hogy még az sem jelent problémát, ha a művelet által visszaadott eredménynek a hossza is eltér a bemenő változóétól! Hiszen a sum(N)
1 hosszú, míg a Year
19. (Az azonban fontos, hogy itt már a Year
nem szerepel a kiválasztott oszlopok között: megtarthattuk volna a Year
-t is, de mivel az 19 hosszú, így a mellette lévő oszlopban ugyanaz az összeg 19-szer meg lett volna ismételve.)
A fenti példákban egyszerre szűrtünk sorokat és számoltunk oszlopokat. (Ez természetesen nem kötelező, lehet csak az egyiket csinálni a másik nélkül.) Egyetlen példa a data.table
optimalizálására: ilyenkor nem azt csinálja, hogy leszűri az egész adatbázist, és aztán végzi az oszlopműveleteket, hanem először megnézi, hogy mely oszlopokra van egyáltalán szükség – például csak a Year
-re, N
-re és Population
-re – és ilyenkor csak azokat szűri le, így kerülve el, hogy olyan oszlopok szűrését is el kelljen végeznie, amik később nem is jelennek meg az eredményben. Ez azért lehetséges, mert a data.table
„egyben látja” az egész feladatot, és így tud ilyen optimalizálásokat tenni.
Visszatérve, a dolog még jobban kombinálható: legyen a példa kedvéért a feladatunk az, hogy számoljuk ki az egész 19 éves periódusra az incidenciát. (Egy pillanatra érdemes itt megállni, és végiggondolni, hogy mi egyáltalán az ehhez szükséges művelet!) Íme a megvalósítás data.table
használatával:
== 40 & County == "Budapest" & Sex == "Férfi" &
RawData[Age == "C18",
ICDCode Inc = sum(N) / sum(Population) * 1e5)] .(
Inc
<num>
1: 11.1515
Amint láthatjuk, tetszőleges komplexitású műveletet, számítást elvégezhetünk a vessző után! És ezt szó szerint kell érteni: bármilyen R függvényt használhatunk az oszlopindexelés pozíciójában, a vessző után, bármilyen műveletet vagy számítást végezhetünk (tehát még csak olyan megkötés sincs, hogy csak bizonyos függvényeket, műveleteket tesz csak elérhetővé a data.table
). Íme egy példa; lognormális eloszlást illesztünk az esetszámok különböző években mért értékeiből kapott eloszlásra:
== 40 & County == "Budapest" & Sex == "Férfi" &
RawData[Age == "C18",
ICDCode ::fitdist(N, "lnorm")$estimate["meanlog"])] .(fitdistrplus
V1
<num>
1: 1.772807
Szépen látszik itt is, hogy nyugodtan használhatjuk az N
-et csak így, minden további nélkül – ugyanúgy viselkedik, mint egy szokásos változó, ugyanúgy használhatjuk egy számítás során.
Ráadásul, ha visszaemlékszünk, akkor szerepelt, hogy a vessző utáni pozícióban egy listának kell szerepelnie – de ezt előállíthatja egy függvény is! Például a fitdistrplus::fitdist
eredményének estimate
nevű komponense egy vektor. De ha ez as.list
-tel átalakítjuk, akkor egy listát kapunk, így közvetlenül átadható a vessző utáni pozícióban (természetesen ilyenkor .
nem kell, hiszen az as.list
eleve egy listát ad vissza!):
== 40 & County == "Budapest" & Sex == "Férfi" &
RawData[Age == "C18",
ICDCode as.list(fitdistrplus::fitdist(N, "lnorm")$estimate)]
meanlog sdlog
<num> <num>
1: 1.772807 0.3902478
Ez tehát már messze-messze nem csak egyszerűen oszlopkiválasztás, amire itt módunk van, ha data.table
-t használunk.
Egyetlen megjegyzés a végére: mi van akkor, ha kíváncsiak vagyunk arra, hogy hány sor van egy adattáblában (esetleg szűkítés után)? A RawData[Age == 40 & County == "Budapest" & Sex == "Férfi" & ICDCode == "C18", length(N)]
kézenfekvő megoldás, de nem túl elegáns (miért pont az N
hosszát néztük meg? bármi más is ugyanezt az eredményt adná!). Erre a célra a data.table
bevezet egy speciális szimbólumot, a .N
-et, ami egyszerűen visszaadja12 a sorok számát:
== 40 & County == "Budapest" & Sex == "Férfi" &
RawData[Age == "C18", .N] ICDCode
[1] 19
5.5 Csoportosítás (aggregáció)
A data.table
második, rendkívül erőteljes bővítése a hagyományos data frame funkcionalitásához képest a csoportosítás (aggregáció) lehetősége. A data.table
bevezet egy harmadik pozíciót a szögletes zárójelen belül: megtehetjük, hogy két vesszőt teszünk ki a szögletes zárójelen belül, ez esetben az első vessző előtt van a sorindexelés (ahogy eddig is), az első és a második vessző között az oszlopkiválasztás és -számítás (ahogy eddig is), viszont a második vessző után megadhatunk egy listát egy vagy több változóból. (A .
ugyanúgy használható a list
helyett. Megadhatunk sztring-vektort is, benne a változók neveivel; ez különösen jól jön akkor, ha gépi úton állítjuk elő, hogy mik ezek a változók.) Mi fog ilyenkor történni? A data.table
elsőként végrehajtja a sorok szűrését, ha kértünk ilyet, ezután pedig az új, harmadik pozícióban megadott változó vagy változók szerint csoportokat képez. Mit jelent az, hogy „csoport”? Azok a sorai a táblának, amelyekben a csoportosító változó egy adott értéket vesz fel: ahány lehetséges értéke van a csoportosító változónak a táblában, annyi csoport képződik, úgy, hogy csoporton belül a csoportosító változó homogén lesz. Ezt követően a data.table
végrehajtja a megadott oszlopkiválasztásokat és/vagy oszlopműveleteket csoportonként külön-külön, végül pedig a kapott eredményeket újra összerakja egy táblába, úgy, hogy mindegyik csoport eredménye mellé beteszi oszlopként azt, hogy ott mi volt a csoportosító változó értéke. Az egyes csoportok abban a sorrendben fognak szerepelni az eredményben, ahogy egymás után jöttek a kiinduló táblában.
A jobb megértés kedvéért nézzünk egy gyakorlati példát! Kíváncsiak vagyunk az egész időintervallumra vonatkozó incidenciára, de az összes rák-típus esetén külön-külön megadva. Mit tudunk tenni? Fent láttuk a kódot, mely egy adott típusra ezt kiszámolja. Az remélhetőleg senkinek nem jut az eszébe, hogy kézzel lefuttassa először C00
-val, aztán C01
-gyel, aztán C02
-vel… Működőképesebb megoldás ennek valamilyen R paranccsal történő automatizálása. Rosszabb esetben a for
jut az eszünkbe, jobb esetben az apply
család valamely tagja. (A for
-ciklus rosszabb eset, mert az R-ben a legtöbb esetben illendő kerülni, és jelen esetben tényleg meg is oldható a probléma megfelelő apply
használatával, így ez is a célszerű választás.) Ha azonban a data.table
-t használjuk, akkor még csak erre sincs szükség!
Nézzük ugyanis meg a következő hívást:
== 40 & County == "Budapest" & Sex == "Férfi",
RawData[Age Inc = sum(N) / sum(Population) * 1e5),
.( .(ICDCode)]
ICDCode Inc
<char> <num>
1: C00 0.36864474
2: C01 2.21186843
3: C02 2.58051316
4: C03 1.01377303
5: C04 2.48835198
6: C05 0.73728948
7: C06 0.55296711
8: C07 0.92161184
9: C08 0.73728948
10: C09 1.65890132
11: C10 3.13348027
12: C11 1.19809540
13: C12 0.46080592
14: C13 4.42373685
15: C14 0.82945066
16: C15 4.14725330
17: C16 6.72776646
18: C17 1.75106250
19: C18 11.15150331
20: C19 2.58051316
21: C20 7.09641120
22: C21 0.55296711
23: C22 3.87076974
24: C23 1.01377303
25: C24 1.47457895
26: C25 7.37289475
27: C26 0.46080592
28: C30 0.64512829
29: C31 1.10593421
30: C32 7.46505593
31: C33 0.09216118
32: C34 30.78183558
33: C37 0.46080592
34: C38 1.10593421
35: C39 0.36864474
36: C40 1.01377303
37: C41 2.94915790
38: C43 17.41846385
39: C44 43.13143429
40: C45 0.64512829
41: C46 0.46080592
42: C47 0.00000000
43: C48 2.39619079
44: C49 10.78285857
45: C50 2.76483553
46: C51 0.00000000
47: C52 0.00000000
48: C53 0.00000000
49: C54 0.00000000
50: C55 0.00000000
51: C56 0.00000000
52: C57 0.00000000
53: C58 0.00000000
54: C60 0.92161184
55: C61 2.94915790
56: C62 22.11868425
57: C63 0.64512829
58: C64 12.25743752
59: C65 0.27648355
60: C66 0.00000000
61: C67 8.38666778
62: C68 0.18432237
63: C69 1.10593421
64: C70 0.55296711
65: C71 8.75531252
66: C72 1.10593421
67: C73 5.06886514
68: C74 0.73728948
69: C75 0.27648355
70: C76 2.67267435
71: C80 2.39619079
72: C81 3.40996382
73: C82 2.58051316
74: C83 3.96293093
75: C84 0.92161184
76: C85 4.42373685
77: C88 0.00000000
78: C90 2.21186843
79: C91 3.31780264
80: C92 4.51589803
81: C93 0.00000000
82: C94 0.18432237
83: C95 0.09216118
84: C96 1.56674013
85: C97 0.00000000
86: D00 0.46080592
87: D01 0.18432237
88: D02 0.09216118
89: D03 4.60805922
90: D04 1.75106250
91: D05 0.00000000
92: D06 0.00000000
93: D07 0.36864474
94: D09 0.27648355
95: D30 1.38241777
96: D33 6.26696054
ICDCode Inc
Mi történt itt? Először is, a sor-szűrések közül kivettük a konkrét rák-típust – ez értelemszerű, hiszen az összes ráktípusra vonatkozó adatot szeretnénk kapni, épp ez volt a feladat, tehát ebben nyilván nem szűrhetjük le előzetesen az adatbázist. Másodszor, bekerült a harmadik pozíciója, csoportosító változóként a rák típusa. Mit jelent ez? Azt, hogy a szűrés után a data.table
a leszűrt adatbázisból rák-típus szerint csoportokat képez, tehát szétszedi az adatbázist kis táblákra úgy, hogy mindegyikben egy adott rák-típus adatai legyenek, mindegyikre elvégzi a második pozícióban, az oszlopindexelésnél megadott műveleteket (jelen esetben: kiszámítja az incidenciákat), majd ezeket az eredményeket, ami itt most egyetlen sor lesz, újra összerakja egy nagy táblába, jelezve, hogy az adott eredmény melyik kódhoz tartozik.
Nagyon szájbarágós, de talán egyszer érdemes a dolgot megnézni lépésről-lépésre. A data.table
elsőként leszűri a táblát a sorindex szerint:
== 40 & County == "Budapest" & Sex == "Férfi"] RawData[Age
Key: <County, Sex, Age, Year>
County Sex Age Year ICDCode N Population
<char> <char> <num> <num> <char> <int> <num>
1: Budapest Férfi 40 2000 C00 1 51602
2: Budapest Férfi 40 2000 C01 6 51602
3: Budapest Férfi 40 2000 C02 2 51602
4: Budapest Férfi 40 2000 C03 1 51602
5: Budapest Férfi 40 2000 C04 2 51602
---
1820: Budapest Férfi 40 2018 D06 0 80555
1821: Budapest Férfi 40 2018 D07 0 80555
1822: Budapest Férfi 40 2018 D09 2 80555
1823: Budapest Férfi 40 2018 D30 0 80555
1824: Budapest Férfi 40 2018 D33 4 80555
Ezt követően megnézi, hogy a csoportosító változó milyen értékeket vesz fel:
unique(RawData[Age == 40 & County == "Budapest" &
== "Férfi"]$ICDCode) Sex
[1] "C00" "C01" "C02" "C03" "C04" "C05" "C06" "C07" "C08" "C09" "C10" "C11"
[13] "C12" "C13" "C14" "C15" "C16" "C17" "C18" "C19" "C20" "C21" "C22" "C23"
[25] "C24" "C25" "C26" "C30" "C31" "C32" "C33" "C34" "C37" "C38" "C39" "C40"
[37] "C41" "C43" "C44" "C45" "C46" "C47" "C48" "C49" "C50" "C51" "C52" "C53"
[49] "C54" "C55" "C56" "C57" "C58" "C60" "C61" "C62" "C63" "C64" "C65" "C66"
[61] "C67" "C68" "C69" "C70" "C71" "C72" "C73" "C74" "C75" "C76" "C80" "C81"
[73] "C82" "C83" "C84" "C85" "C88" "C90" "C91" "C92" "C93" "C94" "C95" "C96"
[85] "C97" "D00" "D01" "D02" "D03" "D04" "D05" "D06" "D07" "D09" "D30" "D33"
Majd ezek mindegyikére leszűkíti a (szűrt) táblát, lényegében kis táblákat készítve. Így néz ki a C00
-hoz tartozó:
== 40 & County == "Budapest" & Sex == "Férfi" &
RawData[Age == "C00"] ICDCode
Key: <County, Sex, Age, Year>
County Sex Age Year ICDCode N Population
<char> <char> <num> <num> <char> <int> <num>
1: Budapest Férfi 40 2000 C00 1 51602.0
2: Budapest Férfi 40 2001 C00 1 47836.0
3: Budapest Férfi 40 2002 C00 0 45296.5
4: Budapest Férfi 40 2003 C00 0 43632.5
5: Budapest Férfi 40 2004 C00 0 43085.0
6: Budapest Férfi 40 2005 C00 0 43442.5
7: Budapest Férfi 40 2006 C00 0 44511.5
8: Budapest Férfi 40 2007 C00 0 46903.5
9: Budapest Férfi 40 2008 C00 0 50505.5
10: Budapest Férfi 40 2009 C00 0 54015.0
11: Budapest Férfi 40 2010 C00 0 57445.5
12: Budapest Férfi 40 2011 C00 1 60721.0
13: Budapest Férfi 40 2012 C00 1 62471.5
14: Budapest Férfi 40 2013 C00 0 63746.5
15: Budapest Férfi 40 2014 C00 0 66250.5
16: Budapest Férfi 40 2015 C00 0 70511.5
17: Budapest Férfi 40 2016 C00 0 74622.0
18: Budapest Férfi 40 2017 C00 0 77902.0
19: Budapest Férfi 40 2018 C00 0 80555.0
Így a C01
-hez:
== 40 & County == "Budapest" & Sex == "Férfi" &
RawData[Age == "C01"] ICDCode
Key: <County, Sex, Age, Year>
County Sex Age Year ICDCode N Population
<char> <char> <num> <num> <char> <int> <num>
1: Budapest Férfi 40 2000 C01 6 51602.0
2: Budapest Férfi 40 2001 C01 1 47836.0
3: Budapest Férfi 40 2002 C01 0 45296.5
4: Budapest Férfi 40 2003 C01 3 43632.5
5: Budapest Férfi 40 2004 C01 2 43085.0
6: Budapest Férfi 40 2005 C01 0 43442.5
7: Budapest Férfi 40 2006 C01 3 44511.5
8: Budapest Férfi 40 2007 C01 2 46903.5
9: Budapest Férfi 40 2008 C01 0 50505.5
10: Budapest Férfi 40 2009 C01 2 54015.0
11: Budapest Férfi 40 2010 C01 0 57445.5
12: Budapest Férfi 40 2011 C01 3 60721.0
13: Budapest Férfi 40 2012 C01 1 62471.5
14: Budapest Férfi 40 2013 C01 1 63746.5
15: Budapest Férfi 40 2014 C01 0 66250.5
16: Budapest Férfi 40 2015 C01 0 70511.5
17: Budapest Férfi 40 2016 C01 0 74622.0
18: Budapest Férfi 40 2017 C01 0 77902.0
19: Budapest Férfi 40 2018 C01 0 80555.0
És így tovább.
Ezt követően minden kis táblára elvégzi az oszlopindexelésnél kijelölt műveletet. Így fog kinézni az eredmény a C00
-s kis táblára:
== 40 & County == "Budapest" & Sex == "Férfi" &
RawData[Age == "C00",
ICDCode Inc = sum(N) / sum(Population) * 1e5)] .(
Inc
<num>
1: 0.3686447
Így a C01
-es kis táblára:
== 40 & County == "Budapest" & Sex == "Férfi" &
RawData[Age == "C01",
ICDCode Inc = sum(N) / sum(Population) * 1e5)] .(
Inc
<num>
1: 2.211868
Majd ezeket a kis táblákat egymás alá rendezi, abban a sorrendben, ahogy az eredeti táblában előfordultak a csoportosító változó értékei, és úgy, hogy mindegyikhez melléírja, hogy az adottnál mi volt a csoportosító változó értéke, tehát jelen esetben, hogy melyik ráktípushoz tartozik.
Így kaptuk a fent látható táblát (menjünk vissza és ellenőrizzük)…!
Nézzünk meg – most már nagyon részletes levezetés nélkül – még egy példát csoportosításra. Kíváncsiak vagyunk egy adott ráktípus korspecifikus incidenciájára, tehát, hogy mennyi az incidencia adott életkorban. Ha mindezt rögzített évre, nemre és megyére kérdezzük, akkor célt érhetünk így:
== 2010 & County == "Budapest" &
RawData[Year == "Férfi" & ICDCode == "C18",
Sex Inc = N / Population * 1e5)] .(Age,
Age Inc
<num> <num>
1: 0 0.000000
2: 5 0.000000
3: 10 0.000000
4: 15 0.000000
5: 20 1.960054
6: 25 3.072716
7: 30 2.321088
8: 35 11.088472
9: 40 5.222341
10: 45 23.579344
11: 50 37.503585
12: 55 79.089038
13: 60 130.617667
14: 65 303.122158
15: 70 292.659728
16: 75 337.025636
17: 80 456.403917
18: 85 320.454129
A dolog azonban nagyon nem szerencsés: kizárólag azért fog működni, mert a leszűkítés után egy adott életkorhoz már csak egyetlen sor tartozik. De ha ez nem így lenne, például kitörlünk valamit a feltételek közül, akkor teljesen rossz eredményt fog adni, hiszen ilyenkor ugyanaz az életkor többször fog megjelenni az eredményben, míg nekünk össze kellene adnunk az adott életkorhoz tartozó különböző megfigyeléseket.
A megoldás a csoportosítás az életkor szerint, és az összeadás adott életkoron belül:
== 2010 & County == "Budapest" &
RawData[Year == "Férfi" & ICDCode == "C18",
Sex Inc = sum(N) / sum(Population) * 1e5), .(Age)] .(
Age Inc
<num> <num>
1: 0 0.000000
2: 5 0.000000
3: 10 0.000000
4: 15 0.000000
5: 20 1.960054
6: 25 3.072716
7: 30 2.321088
8: 35 11.088472
9: 40 5.222341
10: 45 23.579344
11: 50 37.503585
12: 55 79.089038
13: 60 130.617667
14: 65 303.122158
15: 70 292.659728
16: 75 337.025636
17: 80 456.403917
18: 85 320.454129
Ez immár működik másféle szűréssel is, például ha Budapest helyett az egész országra vagyunk kíváncsiak:
== 2010 & Sex == "Férfi" & ICDCode == "C18",
RawData[Year Inc = sum(N) / sum(Population) * 1e5), .(Age)] .(
Age Inc
<num> <num>
1: 0 0.4013437
2: 5 0.0000000
3: 10 0.3909763
4: 15 0.3278087
5: 20 1.2121488
6: 25 2.2652654
7: 30 3.2732344
8: 35 7.1454000
9: 40 11.2810011
10: 45 21.7613797
11: 50 46.2794518
12: 55 94.8719665
13: 60 130.8683356
14: 65 246.5976935
15: 70 293.1432072
16: 75 367.2073711
17: 80 347.1158754
18: 85 323.4103513
Érdemes végiggondolni (ez általában is hasznos): ilyenkor az életkor szerinti kis táblákban 20 sor lesz – az egyes megyékkel – és ezek fölött fogunk összegezni.
Megjegyzendő, hogy a csoportosító változónak nevet is adhatunk:
== 2010 & Sex == "Férfi" & ICDCode == "C18",
RawData[Year Inc = sum(N) / sum(Population) * 1e5),
.(Eletkor = Age)] .(
Eletkor Inc
<num> <num>
1: 0 0.4013437
2: 5 0.0000000
3: 10 0.3909763
4: 15 0.3278087
5: 20 1.2121488
6: 25 2.2652654
7: 30 3.2732344
8: 35 7.1454000
9: 40 11.2810011
10: 45 21.7613797
11: 50 46.2794518
12: 55 94.8719665
13: 60 130.8683356
14: 65 246.5976935
15: 70 293.1432072
16: 75 367.2073711
17: 80 347.1158754
18: 85 323.4103513
Ami azonban sokkal izgalmasabb, hogy műveletet is végezhetünk! Itt is igaz, hogy nem kell a változót külön letárolni, hanem menet közben kiszámolhatjuk, majd építhetünk is rá rögtön (jelen esetben egy csoportosítást). Például, ha ki akarjuk számolni az incidenciát külön a 70 év alattiak és felettiek körében:
== 2010 & Sex == "Férfi" & ICDCode == "C18",
RawData[Year Inc = sum(N) / sum(Population) * 1e5),
.(Idos = Age > 70)] .(
Idos Inc
<lgcl> <num>
1: FALSE 43.83737
2: TRUE 352.54419
5.6 Indexelések láncolása egymás után
A data.table
következő újítása, hogy megengedi egy már indexelt tábla (RawData[...]
) újabb indexelését. Tehát használhatjuk a RawData[...][...]
alakot, ahol a második indexelés pontosan ugyanúgy fog viselkedni, mint az első (ugyanúgy használhatunk sorindexelést, szűrést és rendezést, oszlopkiválasztást és -transzformációt, csoportosítást), de úgy, hogy az az első, már indexelt táblára vonatkozik! Lényegében mintha elmentettük volna a RawData[...]
-t egy változóba, és utána azt a változót indexelnénk szokásos módon – csak itt nem kell semmit külön elmenteni. Az, hogy a második indexelés már az első indexelésben átalakított táblára vonatkozik, egy kritikusan fontos előny, amint az rögtön világossá is fog válni.
Ha pontosak akarunk lenni, akkor ezt az egymás utáni többszöri indexelést igazából a hagyományos data frame is megengedi, tehát például a RawDataDF[101:200,][5:15,]
egy teljesen szabályos hívás (és természetesen egyenértékű lesz azzal, hogy RawDataDF[105:115,]
). A probléma az, hogy a használhatósága nagyon korlátozott, mert a második indexben, ha változóra hivatkozunk, az az eredeti adatkeret változója tud csak lenni, nem az első indexelésben már áttranszformálté! (Értelemszerűen, hiszen az nincs is elmentve, nincs is semmilyen külön neve, ahogy hivatkozhatnánk rá.) Ha csak a legegyszerűbb transzformációt, a sorok szűrését vesszük: a RawDataDF[RawDataDF$Sex == "Férfi",][RawDataDF$Year == 2010,]
nem fog működni, ez onnan is kapásból látszik, hogy a RawDataDF$Year == 2010
ugyanolyan hosszú, mint a a RawDataDF
, viszont a RawDataDF[RawDataDF$Sex == "Férfi",]
már rövidebb, tehát ez így biztosan nem lehet jó, mert az adattáblát hosszabb vektorral próbáljuk indexelni, mint ahány sora van. Data frame használatával erre a problémára nincs megoldás, hiszen a RawDataDF[RawDataDF$Sex == "Férfi",]
táblázat Year
változójára nem tudunk sehogy sem hivatkozni a második indexelésben, hiszen az nincs elmentve, nincs is külön neve, amivel hivatkozhatnánk.
A data table esetében azonban, kihasználva, hogy a változóra hivatkozhatunk csak a nevével, a táblázat neve nélkül, erre nagyon egyszerű a megoldás: annyi a feladat, hogy a második indexben szereplő Year
alatt a data.table
azt értse, hogy az első indexelés után kapott táblázat Year
nevű változója (ne azt, hogy az eredetié). És így is van megírva a data.table
, ezért szerepelt korábban az a megfogalmazás, hogy a második index az első indexeléssel már transzformált táblára vonatkozik. Így aztán a következő hívás tökéletesen működik data table-lel:
== "Férfi"][Year == 2010] RawData[Sex
Key: <County, Sex, Age, Year>
County Sex Age Year ICDCode N Population
<char> <char> <num> <num> <char> <int> <num>
1: Baranya megye Férfi 0 2010 C00 0 9430.0
2: Baranya megye Férfi 0 2010 C01 0 9430.0
3: Baranya megye Férfi 0 2010 C02 0 9430.0
4: Baranya megye Férfi 0 2010 C03 0 9430.0
5: Baranya megye Férfi 0 2010 C04 0 9430.0
---
34556: Zala megye Férfi 85 2010 D06 0 1447.5
34557: Zala megye Férfi 85 2010 D07 0 1447.5
34558: Zala megye Férfi 85 2010 D09 0 1447.5
34559: Zala megye Férfi 85 2010 D30 0 1447.5
34560: Zala megye Férfi 85 2010 D33 4 1447.5
Ez még nem a legátütőbb példa – bár sokszor az ilyenek is nagyon jól jönnek – hiszen használhattunk volna egyszerűen &
jelet és egyetlen indexelést. A dolog igazi erejét az adja, hogy – ismét csak abból fakadóan, hogy a második index már az elsőnek indexelt táblát látja, neki nem is számít, hogy az nem egy lementett tábla, hanem egy már átalakított – módunk van menet közben létrehozott változókra is hivatkozni! Például miután kiszámoltuk rák-típusonként az incidenciát, szeretnénk a táblázatot az incidenciák szerint növekvő sorba rakni. Íme a megoldás:
== 40 & County == "Budapest" & Sex == "Férfi",
RawData[Age Inc = sum(N) / sum(Population) * 1e5),
.(order(Inc)] .(ICDCode)][
ICDCode Inc
<char> <num>
1: C47 0.00000000
2: C51 0.00000000
3: C52 0.00000000
4: C53 0.00000000
5: C54 0.00000000
6: C55 0.00000000
7: C56 0.00000000
8: C57 0.00000000
9: C58 0.00000000
10: C66 0.00000000
11: C88 0.00000000
12: C93 0.00000000
13: C97 0.00000000
14: D05 0.00000000
15: D06 0.00000000
16: C33 0.09216118
17: C95 0.09216118
18: D02 0.09216118
19: C68 0.18432237
20: C94 0.18432237
21: D01 0.18432237
22: C65 0.27648355
23: C75 0.27648355
24: D09 0.27648355
25: C00 0.36864474
26: C39 0.36864474
27: D07 0.36864474
28: C12 0.46080592
29: C26 0.46080592
30: C37 0.46080592
31: C46 0.46080592
32: D00 0.46080592
33: C06 0.55296711
34: C21 0.55296711
35: C70 0.55296711
36: C30 0.64512829
37: C45 0.64512829
38: C63 0.64512829
39: C05 0.73728948
40: C08 0.73728948
41: C74 0.73728948
42: C14 0.82945066
43: C07 0.92161184
44: C60 0.92161184
45: C84 0.92161184
46: C03 1.01377303
47: C23 1.01377303
48: C40 1.01377303
49: C31 1.10593421
50: C38 1.10593421
51: C69 1.10593421
52: C72 1.10593421
53: C11 1.19809540
54: D30 1.38241777
55: C24 1.47457895
56: C96 1.56674013
57: C09 1.65890132
58: C17 1.75106250
59: D04 1.75106250
60: C01 2.21186843
61: C90 2.21186843
62: C48 2.39619079
63: C80 2.39619079
64: C04 2.48835198
65: C02 2.58051316
66: C19 2.58051316
67: C82 2.58051316
68: C76 2.67267435
69: C50 2.76483553
70: C41 2.94915790
71: C61 2.94915790
72: C10 3.13348027
73: C91 3.31780264
74: C81 3.40996382
75: C22 3.87076974
76: C83 3.96293093
77: C15 4.14725330
78: C13 4.42373685
79: C85 4.42373685
80: C92 4.51589803
81: D03 4.60805922
82: C73 5.06886514
83: D33 6.26696054
84: C16 6.72776646
85: C20 7.09641120
86: C25 7.37289475
87: C32 7.46505593
88: C67 8.38666778
89: C71 8.75531252
90: C49 10.78285857
91: C18 11.15150331
92: C64 12.25743752
93: C43 17.41846385
94: C62 22.11868425
95: C34 30.78183558
96: C44 43.13143429
ICDCode Inc
Hiába nem is létezik Inc
nevű változó az eredeti adattáblában, ez a hívás mégis tökéletesen fog működni! Megint csak: azért, mert a második index már az első indexeléssel átalakított táblát kapja meg, és azt látja, pontosan ugyanúgy, mintha az egy lementett tábla lenne.
5.7 Referencia szemantika
A data.table
bevezet egy új megközelítést arra, hogy új változót definiáljunk egy táblában – ám hamar ki fog derülni, hogy itt jóval többről van szó, mint egyszerűen egy alternatív jelölésről.
Például számoljuk ki, és ezúttal a táblázatban is tároljuk el az incidenciákat13:
$Inc <- RawData$N / RawData$Population * 1e5 RawData
A data.table
által bevezett új megoldás esetén az értékadás jele a :=
, de ami talán még fontosabb, hogy ezt, elsőre elég meglepő módon, úgy kell megadni, mintha indexelnénk, tehát szögleges zárójelek között! A második pozícióba, az oszlopindex helyébe kell kerüljön:
:= N / Population * 1e5] RawData[, Inc2
A kettő valóban ugyanazt eredményezi:
identical(RawData$Inc, RawData$Inc2)
[1] TRUE
Ebben van egy újdonság: az összes eddigi példában új táblát hoztunk létre (még ha csak ki is írattuk, és nem mentettük el változóba), ez az első eset, ahol meglevő táblát módosítunk. Ez nagyon fontos: mint láthatjuk is, nem kell az eredményt belementenünk egy változóba, azért nem, mert az utasítás lefuttatásakor maga az eredeti tábla módosult! Ezt szokták az informatikában referencia szerinti módosításnak14 hívni. (És igen, ezt az indexelés szintaktikájával éri el a data.table
, még ha elég meglepő is első látásra.)
Kiíratás ilyenkor ugyanúgy nincs, mint általában az értékadásos utasításoknál R-ben. Ha szeretnénk az értékadás után rögtön ki is íratni a táblát akkor egy []
jelet kell tennünk a parancs után, pl. RawData[, Inc2 := N / Population * 1e5][]
.
Használhatjuk ezt a megoldást meglevő változó felülírására, nem csak új létrehozására. Például, ha meggondoljuk magunkat, és az incidenciát per millió fő mértékegységben szeretnénk megadni:
:= Inc2 * 10] RawData[, Inc2
Egyszerre több változót is definiálhatunk (lehet vegyesen új definiálása és régi felülírása, ennek nincs jelentősége), ennek módszere:
c("logPop", "sqrtPop") := list(log(Population),
RawData[, sqrt(Population))]
Mivel az értékadás bal oldalán sztring-vektor áll, így könnyen előállítható gépi úton is. A jobb oldalon pedig lista szerepel, így itt is igaz, hogy nem muszáj kézzel felsorolni, bármilyen olyan függvény szerepelhet ott, ami listát ad vissza.
Változó törölhető is ilyen módon:
:= NULL] RawData[, Inc2
Ha több változót törölnénk:
c("logPop", "sqrtPop") := NULL] RawData[,
Mi értelme van mindennek? Az első válasz az, hogy bizonyos esetekben gyorsabb15. A második, hogy mindez kombinálható a data.table
többi elemével, tehát a sorindexeléssel és a csoportosítással.
Például szeretnénk a „Budapest” kifejezést lecserélni arra, hogy „Főváros” a megye változóban. Ezt megoldhatjuk így:
== "Budapest", County := "Főváros"] RawData[County
Tehát: ha az értékadást szűréssel kombináljuk, akkor a nem kiválasztott soroknál nem változik az érték. (Ha pedig nem meglevő változót módosítunk, hanem újat hozunk létre, akkor a nem kiválasztott soroknál NA
kerül az új változóba.)
Ezt könnyen megoldhattuk volna másképp is, de nézzük egy izgalmasabb példát. Szeretnénk minden nemre, életkorra, megyére és ráktípusra eltárolni, hogy az adott nemből, életkorból, megyéből és ráktípusból mi volt a legkisebb feljegyzett incidencia (a különböző évek közül, tehát). Ezt data.table
nélkül csak macerásabban tudnánk megtenni, de a data.table
használatával nagyon egyszerű (és nagyon logikus) a megoldás:
:= min(Inc), .(County, Sex, Age, ICDCode)] RawData[, MinInc
A csoportosító változót kell használnunk, ami teljesen logikus is: képezi a csoportokat nem, életkor, megye és ráktípus szerint (tehát az egyes csoportokban a különböz évek fognak szerepelni), veszi azok körében az Inc
minimumát, és azt menti el MinInc
néven – az adott csoport különböző soraihoz mindig ugyanazt az értéket. Íme:
== "C18" & Age == 70 & County == "Főváros"] RawData[ICDCode
County Sex Age Year ICDCode N Population Inc MinInc
<char> <char> <num> <num> <char> <int> <num> <num> <num>
1: Főváros Férfi 70 2000 C18 97 30697.5 315.9866 217.0223
2: Főváros Férfi 70 2001 C18 111 32326.5 343.3715 217.0223
3: Főváros Férfi 70 2002 C18 108 31711.5 340.5705 217.0223
4: Főváros Férfi 70 2003 C18 99 30984.0 319.5198 217.0223
5: Főváros Férfi 70 2004 C18 100 30205.5 331.0655 217.0223
6: Főváros Férfi 70 2005 C18 97 29194.5 332.2544 217.0223
7: Főváros Férfi 70 2006 C18 90 28123.5 320.0171 217.0223
8: Főváros Férfi 70 2007 C18 96 27422.5 350.0775 217.0223
9: Főváros Férfi 70 2008 C18 95 27080.5 350.8059 217.0223
10: Főváros Férfi 70 2009 C18 102 26957.0 378.3804 217.0223
11: Főváros Férfi 70 2010 C18 80 27335.5 292.6597 217.0223
12: Főváros Férfi 70 2011 C18 97 28288.0 342.9016 217.0223
13: Főváros Férfi 70 2012 C18 89 30601.5 290.8354 217.0223
14: Főváros Férfi 70 2013 C18 118 32451.0 363.6252 217.0223
15: Főváros Férfi 70 2014 C18 108 34269.5 315.1490 217.0223
16: Főváros Férfi 70 2015 C18 103 35428.0 290.7305 217.0223
17: Főváros Férfi 70 2016 C18 107 35831.5 298.6199 217.0223
18: Főváros Férfi 70 2017 C18 101 36012.0 280.4621 217.0223
19: Főváros Férfi 70 2018 C18 78 35941.0 217.0223 217.0223
20: Főváros Nő 70 2000 C18 115 52434.0 219.3233 161.1007
21: Főváros Nő 70 2001 C18 99 53138.5 186.3056 161.1007
22: Főváros Nő 70 2002 C18 95 51751.0 183.5713 161.1007
23: Főváros Nő 70 2003 C18 110 50498.5 217.8283 161.1007
24: Főváros Nő 70 2004 C18 92 49073.0 187.4758 161.1007
25: Főváros Nő 70 2005 C18 102 47308.5 215.6061 161.1007
26: Főváros Nő 70 2006 C18 74 45934.0 161.1007 161.1007
27: Főváros Nő 70 2007 C18 95 45165.0 210.3399 161.1007
28: Főváros Nő 70 2008 C18 94 44594.5 210.7883 161.1007
29: Főváros Nő 70 2009 C18 84 44558.5 188.5162 161.1007
30: Főváros Nő 70 2010 C18 81 45258.5 178.9719 161.1007
31: Főváros Nő 70 2011 C18 101 46479.0 217.3024 161.1007
32: Főváros Nő 70 2012 C18 104 48132.0 216.0725 161.1007
33: Főváros Nő 70 2013 C18 134 50451.0 265.6042 161.1007
34: Főváros Nő 70 2014 C18 124 52854.0 234.6085 161.1007
35: Főváros Nő 70 2015 C18 94 54534.5 172.3680 161.1007
36: Főváros Nő 70 2016 C18 120 55317.5 216.9295 161.1007
37: Főváros Nő 70 2017 C18 95 55986.5 169.6838 161.1007
38: Főváros Nő 70 2018 C18 113 56508.0 199.9717 161.1007
County Sex Age Year ICDCode N Population Inc MinInc
Arra azért vigyázni kell, hogy van példa arra, hogy pontosan ugyanaz a hívás mást ad vissza a data frame-nél és data table-nél. Egyébként ez a válasz arra a gyakran felmerülő kérdésre, hogy ha olyan jó a
data.table
, akkor miért nem győzik meg egyszerűen a fejlesztői az R fejlesztőit, hogy építsék be a tulajdonságait az R-es alap data frame-be is. Egyébként volt példa ilyenre is, de az előbbi ok miatt ez nem lehet általános, hiszen ez azt jelentené, hogy meglevő kódok működése is megváltozna, ami végeláthatlan sok R kód működését ronthatná el. Ilyen módosítást ma már nem igen lehet megtenni adata.frame
-mel.↩︎A kód folytatható, ami finomabb felbontást ad, például a C00.0 a felső ajak külső felszínének daganata, a C00.1 az alsó ajak külső felszínének daganata stb., de a táblázatunk a háromjegyű besorolást tartalmazza.↩︎
Ez ún. évközepi lélekszám, tehát az év alatti – folyamatosan változó – lélekszámok átlaga. Ezért lehet az értéke törtszám is.↩︎
Ezek egy részénél nem kell külön függvényt hívni, csak „maga a data table” gyorsabb lesz mint a data frame. Más részénél szükség van egy külön függvényre, például a táblaegyesítésnél a
merge
-re. De ez is gyorsabb lesz, aminek a hátterében az van, hogy adata.table
-nek van saját, ugyanilyen nevű függvénye (data.table::merge
), és ez fog a data frame-hez tartozó alapváltozat, tehát abase::merge
helyett futni.↩︎Egész pontosan annyit, amennyi a
max.print
opció értéke; ez agetOption("max.print")
paranccsal kérdezhető le. Az alapbeállítása tipikusan 1000.↩︎A precizitás kedvéért: ezt csak akkor teszi, ha a sorok száma nagyobb mint a
datatable.print.nrows
opció értéke, ami alapbeállítás szerint 100. De ez is logikus: kis adatbázisnál érdemes az egészet kiíratni, hiszen úgy is áttekinthető, nagyoknál lesz fontos csak az első néhány és az utolsó néhány sor kiíratása.↩︎Egyébként ez utóbbi esetben nem ugyanaz az
order
fut le: adata.table
definiál egy sajátorder
-t, tehát az előbbi esetben abase::order
, az utóbbinál adata.table::order
fut. Adata.table
csomagorder
-je egyébként is okosabb, például sokkal kényelmesebb ha több változó szerint és változó irányban kell rendeznünk: egyszerűen fel kell sorolnunk azorder
-en belül a változókat, és amelyik szerint csökkenő sorrendben akarunk rendezni, ott ki kell tennünk a változó neve elé egy-
jelet.↩︎Valójában van egy különbség, ami akkor jelentkezik, ha a kiválasztandó oszlopok neveit eltároljuk egy változóban, és az indexelésnél ezt a változót szeretnénk felhasználni ahelyett, hogy kézzel beírjuk a neveket. Legyen például
colsel <- c("Year", "N", "Population")
. Ekkor a data frame-nél mindegy, hogy aRawDataDF[, c("Year", "N", "Population")]
vagy aRawDataDF[, colsel]
formát használjuk, az eredmény ugyanaz lesz. Ami logikus is, hiszen látszólag ugyanazt írtuk be kétszer. Nagyon meglepő módon azonban a data table-nél nem mindegy: aRawData[, c("Year", "N", "Population")]
működni fog, de aRawData[, colsel]
nem! Ennek az az oka, hogyRawData[, colsel]
összeakad egy szintaktikával, amit később fogunk látni, és amelyben ez azt jelentené, hogy „válaszd ki acolsel
nevű oszlopot és add vissza vektorként”. Ami természetesen nem fog sikerülni, hiszen ilyen nevű oszlop nincs. Van azonban megoldás: ha erre volna szükségünk akkor vagy aRawData[, ..colsel]
vagy aRawData[, colsel, with = FALSE]
alakot kell használnunk.↩︎Ez elsőre meglepő lehet, de valójában teljesen logikus: ha visszaemlékszünk, akkor már a
data.frame
-nél is láttuk, hogy az igazából az oszlopokból, mint vektorokból alkotott lista. Innen nézve teljesen érthető, hogy az oszlopokat egy lista elemeiként kell felsorolni!↩︎Ez nem nyilvánvaló: a
RawDataDF[, "Year"]
egy vektor lesz! Természetesen aRawDataDF[, c("Year", "County")]
megint csak data frame; vagyis lényegében az történik, hogy a data frame automatikusan egyszerűsít: ha lehet – azaz egyetlen változót (oszlopot) választottunk ki – akkor egyszerűsíti vektorrá, ha nem, mert többet, akkor marad a data frame. Ez kényelmes is lehet, de közben mégis csak egy inkonzisztencia, hogy ugyanolyan típusú hívások eredménye teljesen eltérő adatstruktúra is lehet. Ezzel szemben adata.table
-nél aRawData[, .(...)]
típusú hívások mindig data table-t adnak vissza.↩︎Elvileg a
RawData[, Year]
is használható, de ezt talán jobb kerülni, ritkán fordul elő.↩︎Észrevehető, hogy az eredmény egy szám lesz, nem egy data table. Ennek az oka, hogy a
.N
– hiába van a nevében egy.
– nem egy lista. Ha data frame-et szeretnénk visszakapni, akkor a korábbiakkal összhangban azt kell írnunk, hogy.(.N)
.↩︎Az összes fenti esetben ezt el tudtuk kerülni, és jobb is elkerülni: gondoljunk arra, hogy ha csoportosítást is csinálunk, akkor ezekkel az előre kiszámolt rétegenkénti incidenciákkal nem megyünk semmire. (Általában is igaz, hogy a kiszámítható dolgok közül csak azokat érdemes fizikailag letárolni az adatbázisban, amik kiszámítása sok időt venne igénybe.) Tehát ez most szigorúan csak illusztratív példa új változó létrehozására.↩︎
Ez problémát jelenthet akkor, ha egy függvényen belül csinálunk ilyet, hiszen ez azt fogja maga után vonni, hogy a bemenetként átadott adattábla át fog alakulni. Ez esetben a
copy
függvény segíthet: ezzel készíthetünk első lépésben egy másolatot a tábláról, és ha utána azon dolgozunk, akkor az eredeti, bemenetként megkapott tábla nem fog átalakulni.↩︎Az R a 3.1.0-s verzió előtt minden ilyen változó-értékadási műveletnél deep copy-t csinált az adatbázisról, ami azt jelenti, hogy nem csak a memóriamutatókat frissítette (ez lenne a shallow copy), hanem az egész adatbázist fizikailag átmásolta egy másik memóriaterületre. Ez nagyon gazdaságtalan, pláne, mert értelmetlen is, hiszen egy új változó definiálásától a meglevő tartalom maradhatna ugyanott. Ezt a 3.1.0-s verzióban orvosolták, de az továbbra is megmaradt, hogy nem az egész oszlop kap értéket, csak egy része, akkor deep copy készül. Ezzel szemben a
data.table
minden esetben és minden verzióban shallow copy-t csinál értékadásnál.↩︎