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):

library(data.table)

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")

RawData <- readRDS("RawDataLongWPop.rds")

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:

RawDataDF <- data.frame(RawData)

É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 &
                 RawDataDF$County %in% c("Budapest",
                                         "Pest megye") &
                 RawDataDF$Sex == "Férfi" &
                 RawDataDF$ICDCode == "C18",])
         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:

RawData[Year == 2010 & Age >= 40 &
          County %in% c("Budapest", "Pest megye") &
          Sex == "Férfi" & ICDCode == "C18",]
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!):

RawData[Year == 2010 & Age >= 40 &
          County %in% c("Budapest", "Pest megye") &
          Sex == "Férfi" & ICDCode == "C18"]
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:

RawData[order(N),]
                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 &
                 RawDataDF$County == "Budapest" &
                 RawDataDF$Sex == "Férfi" &
                 RawDataDF$ICDCode == "C18",
               c("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:

RawData[Age == 40 & County == "Budapest" & Sex == "Férfi" &
          ICDCode == "C18",
        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é:

RawData[Age == 40 & County == "Budapest" & Sex == "Férfi" &
          ICDCode == "C18",
        .(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!):

RawData[Age == 40 & County == "Budapest" & Sex == "Férfi" &
          ICDCode == "C18",
        .(Year, Esetszam = N, Lelekszam = Population)]
     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:

RawData[Age == 40 & County == "Budapest" & Sex == "Férfi" &
          ICDCode == "C18",
        .(Year, N, Population, Inc = N / Population * 1e5)]
     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:

RawData[Age == 40 & County == "Budapest" & Sex == "Férfi" &
          ICDCode == "C18",
        .(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:

RawData[Age == 40 & County == "Budapest" & Sex == "Férfi" &
          ICDCode == "C18",
        .(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:

RawData[Age == 40 & County == "Budapest" & Sex == "Férfi" &
          ICDCode == "C18",
        .(fitdistrplus::fitdist(N, "lnorm")$estimate["meanlog"])]
         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!):

RawData[Age == 40 & County == "Budapest" & Sex == "Férfi" &
          ICDCode == "C18",
        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:

RawData[Age == 40 & County == "Budapest" & Sex == "Férfi" &
          ICDCode == "C18", .N]
[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:

RawData[Age == 40 & County == "Budapest" & Sex == "Férfi",
        .(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:

RawData[Age == 40 & County == "Budapest" & Sex == "Férfi"]
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" &
                 Sex == "Férfi"]$ICDCode)
 [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ó:

RawData[Age == 40 & County == "Budapest" & Sex == "Férfi" &
          ICDCode == "C00"]
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:

RawData[Age == 40 & County == "Budapest" & Sex == "Férfi" &
          ICDCode == "C01"]
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:

RawData[Age == 40 & County == "Budapest" & Sex == "Férfi" &
          ICDCode == "C00",
        .(Inc = sum(N) / sum(Population) * 1e5)]
         Inc
       <num>
1: 0.3686447

Így a C01-es kis táblára:

RawData[Age == 40 & County == "Budapest" & Sex == "Férfi" &
          ICDCode == "C01",
        .(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:

RawData[Year == 2010 & County == "Budapest" &
          Sex == "Férfi" & ICDCode == "C18",
        .(Age, Inc = N / Population * 1e5)]
      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:

RawData[Year == 2010 & County == "Budapest" &
          Sex == "Férfi" & ICDCode == "C18",
        .(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:

RawData[Year == 2010 & Sex == "Férfi" & ICDCode == "C18",
        .(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:

RawData[Year == 2010 & Sex == "Férfi" & ICDCode == "C18",
        .(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:

RawData[Year == 2010 & Sex == "Férfi" & ICDCode == "C18",
        .(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:

RawData[Sex == "Férfi"][Year == 2010]
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:

RawData[Age == 40 & County == "Budapest" & Sex == "Férfi",
        .(Inc = sum(N) / sum(Population) * 1e5),
        .(ICDCode)][order(Inc)]
    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:

RawData$Inc <- RawData$N / RawData$Population * 1e5

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:

RawData[, Inc2 := N / Population * 1e5]

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:

RawData[, Inc2 := Inc2 * 10]

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:

RawData[, c("logPop", "sqrtPop") := list(log(Population),
                                         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:

RawData[, Inc2 := NULL]

Ha több változót törölnénk:

RawData[, c("logPop", "sqrtPop") := NULL]

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:

RawData[County == "Budapest", County := "Főváros"]

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:

RawData[, MinInc := min(Inc), .(County, Sex, Age, ICDCode)]

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:

RawData[ICDCode == "C18" & Age == 70 & County == "Főváros"]
     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

  1. 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 a data.frame-mel.↩︎

  2. 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.↩︎

  3. 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.↩︎

  4. 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 a data.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 a base::merge helyett futni.↩︎

  5. Egész pontosan annyit, amennyi a max.print opció értéke; ez a getOption("max.print") paranccsal kérdezhető le. Az alapbeállítása tipikusan 1000.↩︎

  6. 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.↩︎

  7. Egyébként ez utóbbi esetben nem ugyanaz az order fut le: a data.table definiál egy saját order-t, tehát az előbbi esetben a base::order, az utóbbinál a data.table::order fut. A data.table csomag order-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 az order-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.↩︎

  8. 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 a RawDataDF[, c("Year", "N", "Population")] vagy a RawDataDF[, 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: a RawData[, c("Year", "N", "Population")] működni fog, de a RawData[, colsel] nem! Ennek az az oka, hogy RawData[, colsel] összeakad egy szintaktikával, amit később fogunk látni, és amelyben ez azt jelentené, hogy „válaszd ki a colsel 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 a RawData[, ..colsel] vagy a RawData[, colsel, with = FALSE] alakot kell használnunk.↩︎

  9. 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!↩︎

  10. Ez nem nyilvánvaló: a RawDataDF[, "Year"] egy vektor lesz! Természetesen a RawDataDF[, 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 a data.table-nél a RawData[, .(...)] típusú hívások mindig data table-t adnak vissza.↩︎

  11. Elvileg a RawData[, Year] is használható, de ezt talán jobb kerülni, ritkán fordul elő.↩︎

  12. É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).↩︎

  13. 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.↩︎

  14. 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.↩︎

  15. 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.↩︎