3 Adatkezelés R-ben

3.1 Az adatok importálása

Az adatok importálására az R alapfüggvénye mellett több csomag is megoldást kínál. Ezek közül a könyv írásakor a legnépszerűbbek a readr és a rio csomagok. A szövegek különböző karakterkódolásának problémáját tapasztalataink szerint a legjobban a readr csomag read_csv() függvénye kezeli, ezért legtöbbször ezt fogjuk használni a .csv állományok beolvasására. Amennyiben kihasználjuk az RStudio projekt opcióját (lásd a Függelékben) akkor elegendő csak az elérni kívánt adatok relatív elérési útját megadni (relative path). Ideális esetben az adataink egy csv fájlban vannak, ahol az egyes értékeket vesszők (vagy egyéb speciális karakterek) választják el. Ez esetben a read_delim() függvényt is használhatjuk. A beolvasásnál egyből el is tároljuk az adatokat egy objektumban. A sep = opcióval tudjuk a szeparátor karaktert beállítani, mert előfordulhat, hogy vessző helyett pontosvessző tagolja az adatainkat.

library(readr)
library(dplyr)
library(gapminder)
library(stringr)
library(readtext)
df <- readr::read_csv("data/adatfile.csv")

Az R képes linkről letölteni fájlokat, elég megadnunk egy működő elérési útvonalat (lenti kódrészlet nem egy valódi linkre mutat, csupán egy példa).

df_online <- read_csv("https://www.pelda_link.hu/adatok/pelda_file.csv")

Az R csomag ökoszisztémája kellően változatos ahhoz, hogy gyakorlatilag bármilyen inputtal meg tudjon birkózni. Az Excel fájlokat a readxl csomagot használva tudjuk betölteni a read_excel() függvény használatával.lásd ehhez a Függeléket A leggyakoribb statisztikai programok formátumait pedig a haven csomag tudja kezelni (például Stata, Spss, SAS). A szintaxis itt is hasonló: read_stata(), read_spss(), read_sas().

A nagy mennyiségű szöveges dokumentum (a legyakrabban előforduló kiterjesztések: .txt, .doc, .pdf, .json, .csv, .xml, .rtf, .odt) betöltésére a legalkalmasabb a readtext csomag. Az alábbi példa azt mutatja be, hogyan tudjuk beolvasni egy adott mappából az összes .txt kiterjesztésű fájlt anélkül, hogy egyenként kellene megadnunk a fájlok neveit. A kódsorban szereplő * karakter ebben a környezetben azt jelenti, hogy bármilyen fájl az adott mappában, ami .txt-re végződik. Amennyiben a fájlok nevei tartalmaznak valamilyen metaadatot, akkor ezt is be tudjuk olvasni a betöltés során. Ilyen metaadat lehet például egy parlamenti felszólalásnál a felszólaló neve, a beszéd ideje, a felszólaló párttagsága (például: kovacsjanos_1994_fkgp.txt).

df_text <- readtext::readtext(
  "data/*.txt",
  docvarsfrom = "filenames",
  dvsep = "_",
  docvarnames = c("nev", "ev", "part")
)

3.2 Az adatok exportálása

Az adatainkat R-ből a write.csv()-vel exportálhatjuk a kívánt helyre, .csv formátumba. Az openxlsx csomaggal .xls és .xlsx Excel formátumokba is tudunk exportálni. Az R rendelkezik saját, .Rds és .Rda kiterjesztésű, tömörített fájlformátummal. Mivel ezeket csak az R-ben nyithatjuk meg, érdemes a köztes, hosszadalmas számítást igénylő lépések elmentésére használni, a saveRDS() és a save() parancsokkal.

3.3 A pipe operátor

Az úgynevezett pipe operátor alapjaiban határozta meg a modern R fejlődését és a népszerű csomag ökoszisztéma, a tidyverse, egyik alapköve. Úgy gondoljuk, hogy a tidyverse és a pipe egyszerűbbé teszi az R használatának elsajátítását, ezért mi is erre helyezzük a hangsúlyt.3 Vizuálisan a pipe operátor így néz ki: %>% (a pipe operátor a Ctrl + Shift + M billentyúk kombinációjával könnyedén kiírható) és arra szolgál, hogy a kódban több egymáshoz kapcsolódó műveletet egybefűzzünk.4 Technikailag a pipe a bal oldali elemet adja meg a jobb oldali függvény első argumentumának. A lenti példa, amely nem tartalmaz igazi kódot, csupán a logikát kívánja szemlélteni ugyanazt a folyamatot írja le az alap R (base R), illetve a pipe használatával. 5 Miközben a kódot olvassuk, érdemes a pipe-ot „és aztán”-nak fordítani.

reggeli(oltozkodes(felkeles(ebredes(en, idopont = "8:00"), oldal = "jobb"), nadrag = TRUE, ing = TRUE))

en %>%
  ebredes(idopont = "8:00") %>%
  felkeles(oldal = "jobb") %>%
  oltozkodes(nadrag = TRUE, ing = TRUE) %>%
  reggeli()

A fenti példa is jól mutatja, hogy a pipe a bal oldali elemet fogja a jobb oldali függvény első elemének berakni. A pipe operátor működését még egy soron demonstrálhatjuk ez alkalommal valódi kódsorokkal és funkciókkal. Láthatjuk, hogy a pipe baloldalán elhelyezkedő kiindulópont egy számsor, ezt először a sum() funkcióba helyezzük, az így kapott eredmény 16 lesz, amelynek a gyökét vesszük az sqrt() az operátor alkalmazásával ezt a két műveletet egyetlen kódsorban könnyen olvasható módon végezzük el és megkapjuk a teljes folyamat eredeményét vagyis négyet.

c(3, 5, 8) %>% sum() %>% sqrt()
#> [1] 4

A fejezet további részeiben még bőven fogunk gyakorlati példát találni a pipe használatára. Mivel az itt bemutatott példák az alkalmazásoknak csak egy relatíve szűk körét mutatják be, érdemes átolvasni a csomagokhoz tartozó dokumentációt, illetve ha van, akkor tanulmányozni a működést demonstráló bemutató oldalakat is.

3.4 Műveletek adattáblákkal

Az adattábla (data frame) az egyik leghasznosabb és leggyakrabban használt adattárolási mód az R-ben (a részletesebb leírás a Függelékben található). Ebben az alfejezetben azt mutatjuk be a dplyr és gapminder csomagok segítségével, hogyan lehet vele hatékonyan dolgozni. A dplyr az egyik legnépszerűbb R csomag, a tidyverse része. A gapminder csomag pedig a példa adatbázisunkat tartalmazza, amiben a világ országainak különböző gazdasági és társadalmi mutatói találhatók.

A sorok (megfigyelések) szűréséhez a dplyr csomag filter() parancsát használva lehetőségünk van arra, hogy egy vagy több kritérium alapján szűkítsük az adatbázisunkat. A lenti példában azokat a megfigyeléseket tartjuk meg, ahol az év 1962 és a várható élettartam több mint 72 év.

gapminder %>%
  dplyr::filter(year == 1962, lifeExp > 72)
#> # A tibble: 5 × 6
#>   country     continent  year lifeExp      pop gdpPercap
#>   <fct>       <fct>     <int>   <dbl>    <int>     <dbl>
#> 1 Denmark     Europe     1962    72.4  4646899    13583.
#> 2 Iceland     Europe     1962    73.7   182053    10350.
#> 3 Netherlands Europe     1962    73.2 11805689    12791.
#> 4 Norway      Europe     1962    73.5  3638919    13450.
#> 5 Sweden      Europe     1962    73.4  7561588    12329.

Ugyanígy leválogathatjuk az adattáblából az adatokat akkor is, ha egy karakter változó alapján szeretnénk szűrni.

gapminder %>%
  filter(country == "Sweden", year > 1990)
#> # A tibble: 4 × 6
#>   country continent  year lifeExp     pop gdpPercap
#>   <fct>   <fct>     <int>   <dbl>   <int>     <dbl>
#> 1 Sweden  Europe     1992    78.2 8718867    23880.
#> 2 Sweden  Europe     1997    79.4 8897619    25267.
#> 3 Sweden  Europe     2002    80.0 8954175    29342.
#> 4 Sweden  Europe     2007    80.9 9031088    33860.

Itt tehát az adattábla azon sorait szeretnénk látni, ahol az ország megegyezik a „Sweden” karakterlánccal, az év pedig 1990 utáni.

A select() függvény segítségével válogathatunk oszlopokat a data frame-ből. A változók kiválasztására több megoldás is van. A dplyr csomag tartalmaz apróbb kisegítő függvényeket, amik megkönnyítik a nagy adatbázisok esetén a változók kiválogatását a nevük alapján. Ezek a függvények a contains(), starts_with(), ends_with(), matches(), és beszédesen arra szolgálnak, hogy bizonyos nevű változókat ne kelljen egyenként felsorolni. A select()-en belüli változó sorrend egyben az eredmény data frame változójának sorrendjét is megadja. A negatív kiválasztás is lehetséges, ebben az esetben egy - jelet kell tennünk a nem kívánt változó(k) elé (pl.: select(df, year, country, -continent).

gapminder %>%
  dplyr::select(dplyr::contains("ea"), dplyr::starts_with("co"), pop)
#> # A tibble: 1,704 × 4
#>    year country     continent      pop
#>   <int> <fct>       <fct>        <int>
#> 1  1952 Afghanistan Asia       8425333
#> 2  1957 Afghanistan Asia       9240934
#> 3  1962 Afghanistan Asia      10267083
#> 4  1967 Afghanistan Asia      11537966
#> 5  1972 Afghanistan Asia      13079460
#> 6  1977 Afghanistan Asia      14880372
#> # ℹ 1,698 more rows

Az így kiválogatott változókból létrehozhatunk és objektumként eltárolhatunk egy új adattáblát az objektumok részletesebb leírása a függelékben található Függelékben, amivel azután tovább dolgozhatunk, vagy kiírathatjuk például .csv fájlba, vagy elmenthetjük a saveRDS segítségével.

gapminder_select <- gapminder %>%
  select(contains("ea"), starts_with("co"), pop)
readr::write_csv(gapminder_select, "gapminder_select.csv") 
saveRDS(gapminder_select, "gapminder_select.Rds") 

A saveRDS segítségével elmentett fájlt később a readRDS() függvénnyel olvashatjuk be, majd onnan folytathatjuk a munkát, ahol korábban abbahagytuk.

readRDS("gapminder_select.Rds") 

Az elemzési munkafolyamat elkerülhetetlen része, hogy új változókat hozzunk létre, vagy a meglévőket módosítsuk. Ezt a mutate()-el tehetjük meg, ahol a szintaxis a következő: mutate(data frame, uj valtozo = ertekek). Példaként kiszámoljuk a svéd GDP-t (milliárd dollárban) 1992-től kezdve. A mutate() alkalmazását részletesebben is bemutatjuk a szövegek előkészítésével foglalkozó fejezetben.

gapminder %>%
  filter(country == "Sweden", year >= 1992) %>%
  dplyr::mutate(gdp = (gdpPercap * pop) / 10^9)
#> # A tibble: 4 × 7
#>   country continent  year lifeExp     pop gdpPercap   gdp
#>   <fct>   <fct>     <int>   <dbl>   <int>     <dbl> <dbl>
#> 1 Sweden  Europe     1992    78.2 8718867    23880.  208.
#> 2 Sweden  Europe     1997    79.4 8897619    25267.  225.
#> 3 Sweden  Europe     2002    80.0 8954175    29342.  263.
#> 4 Sweden  Europe     2007    80.9 9031088    33860.  306.

Az adataink részletesebb és alaposabb megismerésében segítenek a különböző szintű leíró statisztikai adatok. A szintek megadására a group_by() használható, a csoportokon belüli számításokhoz pedig a summarize(). A lenti példa azt illusztrálja, hogy ha kontinensenként csoportosítjuk a gapminder adattáblát, akkor a summarise() használatával megkaphatjuk a megfigyelések számát, illetve az átlagos per capita GDP-t. A summarise() a mutate() közeli rokona, hasonló szintaxissal és logikával használható. Ezt a függvénypárost fogjuk majd használni a szöveges adataink leíró statisztikáinál is az 5. fejezetben.

gapminder %>%
  dplyr::group_by(continent) %>%
  dplyr::summarise(megfigyelesek = n(), atlag_gdp = mean(gdpPercap))
#> # A tibble: 5 × 3
#>   continent megfigyelesek atlag_gdp
#>   <fct>             <int>     <dbl>
#> 1 Africa              624     2194.
#> 2 Americas            300     7136.
#> 3 Asia                396     7902.
#> 4 Europe              360    14469.
#> 5 Oceania              24    18622.

3.5 Munka karakter vektorokkal6

A szöveges adatokkal (karakter stringekkel) való munka elkerülhetetlen velejárója, a vektorokról, köztük a karakter vektorokról részletesebben a Függelékben írunk. A felesleges szövegelemeket, karaktereket el kell távolítanunk, hogy javuljon az elemzésünk hatásfoka. Erre a célra a stringr csomagot fogjuk használni, kombinálva a korábban bemutatott mutate()-el. A stringr függvények az str_ előtaggal kezdődnek és eléggé beszédes nevekkel rendelkeznek. Egy gyakran előforduló probléma, hogy extra szóközök maradnak a szövegben, vagy bizonyos szavakról, karakterkombinációkról tudjuk, hogy nem kellenek az elemzésünkhöz. Ebben az esetben egy vagy több reguláris kifejezés (regular expression, regex) használatával tudjuk pontosan kijelölni, hogy a karakter sornak melyik részét akarjuk módosítani.7 A legegyszerűbb formája a regexeknek, ha pontosan tudjuk milyen szöveget akarunk megtalálni. A kísérletezésre az str_view()-t használjuk, ami megjeleníti, hogy a megadott regex mintánk pontosan mit jelöl. A függvény match = TRUE paramétere lehetővé teszi, hogy csak a releváns találatokat kapjuk vissza.

szoveg <- c("gitar", "ukulele", "nagybogo")

stringr::str_view(szoveg, pattern = "ar")
#> [1] │ git<ar>

Reguláris kifejezésekkel rákereshetünk nem csak egyes elemekre (pl.: szavak, szótagok, betűk) hanem olyan konkrét esetekre is, amikor ezek egymás után fordulnak elő. Ilyenkor úgynevezett ngrammokat használunk, amelyek egy karakterláncban szereplő n-számú elem szekvenciája. A lenti példban ezeke működését egy úgynevezett bigrammal tehát egy n=2 értékű ngrammal mutatjuk be. Az ngrammok segítenek kezelni olyan eseteket, amikor két egymást követő elem eltérő jelentéssel bír, egymás mellett, mint külön-külön.

szoveg <- c("a fehér ház fehérebb mint bármely más ház")

stringr::str_view(szoveg, pattern = "fehér ház")
#> [1] │ a <fehér ház> fehérebb mint bármely más ház

Az ún. „horgonyokkal“, (anchor) azt lehet megadni, hogy a karakter string elején vagy végén szeretnénk-e egyezést találni. A string eleji anchor a ^, a string végi pedig a $.

str_view("Dr. Doktor Dr.", pattern = "^Dr.")
#> [1] │ <Dr.> Doktor Dr.
str_view("Dr. Doktor Dr.", pattern = "Dr.$")
#> [1] │ Dr. Doktor <Dr.>

Továbbá azt is meghatározhatjuk, hogy egy adott karakter, vagy karakter kombinációt, valamint a mellett elhelyezkedő karaktereket is szeretnénk kijelölni.

str_view("Dr. Doktor Dr.", pattern = ".k.")
#> [1] │ Dr. D<okt>or Dr.

Egy másik jellemző probléma, hogy olyan speciális karaktert akarunk leírni a regex kifejezésünkkel, ami amúgy a regex szintaxisban használt. Ilyen eset például a ., ami mint írásjel sokszor csak zaj, ám a regex kontextusban a „bármilyen karakter„ megfelelője. Ahhoz, hogy magát az írásjelet jelöljük, a \\ -t kell elé rakni, ennek egy alternatívája, hogy a keresett szóláncot a fixed() funkcióba helyezzük, így pedig a szóláncban lévő karakterek különleges jelentéseit figyelmen kívül hagyja és csak a magára az egzakt szóláncra keres rá. Ha rákeresünk a . karakterre, akkor látható, hogy a szólánc mindegyik karakterét kijelöltük, hiszen a . jelentése “bármilyen karakter”.

str_view("Dr. Doktor Dr.", pattern = ".")
#> [1] │ <D><r><.>< ><D><o><k><t><o><r>< ><D><r><.>

A \\ jel segítségével már csak a szövegben lévő tényleges pontokra keresünk rá.

str_view("Dr. Doktor Dr.", pattern = "\\.")
#> [1] │ Dr<.> Doktor Dr<.>

Ugyanúgy a fixed() funkció segítségével szintén csak a tényleges pontokra keresünk rá.

str_view("Dr. Doktor Dr.", pattern = fixed("."))
#> [1] │ Dr<.> Doktor Dr<.>

Néhány hasznos regex kifejezés:

  • [:digit:] - számok (123)
  • [:alpha:] - betűk (abc ABC)
  • [:lower:] - kisbetűk (abc)
  • [:upper:] - nagybetűk (ABC)
  • [:alnum:] - betűk és számok (123 abc ABC)
  • [:punct:] - központozás (.!?\(){})
  • [:graph:] - betűk, számok és központozás (123 abc ABC .!?\(){})
  • [:space:] - szóköz ( )
  • [:blank:] - szóköz és tabulálás
  • [:cntrl:] - kontrol karakterek (\n, \r, stb.)
  • * - bármi

  1. A tidyverse megközelítés miatt a kötetben szereplő R kód követi a “The tidyverse style guide” dokumentációt (https://style.tidyverse.org/)↩︎

  2. Az RStudio-ban a pipe operátor billentyű kombinációja a Ctrl + Shift + M↩︎

  3. Köszönjük Andrew Heissnek a kitűnő példát.↩︎

  4. A könyv terjedelme miatt ezt a témát itt csak bemutatni tudjuk, de minden részletre kiterjedően nem tudunk elmélyülni benne. A témában nagyon jól használható online anyagok találhatóak az RStudio GitHub tárhelyén (https://github.com/rstudio/cheatsheets/raw/master/strings.pdf), illetve Wickham and Grolemund (2016) 14. fejezetében.↩︎

  5. A reguláris kifejezés egy olyan, meghatározott szintaktikai szabályok szerint leírt karakterlánc (string), amivel meghatározható stringek egy adott halmaza. Az ilyen kifejezés valamilyen minta szerinti szöveg keresésére, cseréjére, illetve a szöveges adatok ellenőrzésére használható. További információ: http://www.regular-expressions.info/↩︎