Kapitola 4 Transformácia údajov a súhrny pomocou dplyr

V tejto kapitole preberieme moderné nástroje na manipuláciu so súbormi údajov, ktoré sú už v čistej tabuľkovej forme, čiže s dátovými rámcami. (O tom, ako dáta dostať do čistej formy, pojednáva kapitola 6.) Informácie pochádzajú najmä zo zdrojov (Wickham a Grolemund 2016, kap. 5; Ismay a Kim 2019, kap. 3; R. D. Peng 2016, kap. 13).

4.1 Všeobecne

Základnou dátovou štruktúrou (kontajnerom na údaje) v R pre ďalšie štatistické spracovanie je dátový rámec (data.frame). V ňom každý riadok predstavuje jedno pozorovanie (meranie, záznam…) a každý stĺpec jednu premennú (veličinu, mieru, vlastnosť, charakteristiku, štatistický znak…). Už sme si ukázali základné nástroje na vytváranie a manipuláciu s dátovými rámcami ako napr. výber prvkov ($, [ ], subset), avšak zložitejšie operácie ako napr. filtrovanie (výber pozorovaní podľa zadaných kritérií), zoraďovanie riadkov či tvorenie súhrnov (agregácia hodnôt premenných) môžu byť trochu únavné a neprehľadné, syntax jazyka R totiž nie je veľmi intuitívna. Toto sa snaží odstrániť balík dplyr (Wickham et al. 2021, 2020), ktorý implementuje konzistentnú „gramatiku” (v súlade s názvoslovím databázového jazyka SQL - Structured Query Language) a je veľmi rýchly. Kľúčovými funkciami sú:

  • select – výber stĺpcov,

  • filter – výber riadkov na základe logických podmienok,

  • arrange – zoradenie riadkov,

  • rename – premenovanie stĺpcov (premenných),

  • mutate – pridanie nových stĺpcov napr. transformáciou iných,

  • summarise, summarize – generovanie podmienených súhrnov pre daný stĺpec,

  • %>% - pipe operátor pre reťazenie príkazov (analógia skladania funkcií v matematike).

Balík dplyr je súčasťou ekosystému tidyverse (Wickham et al. 2019), čo je kolekcia balíkov navrhnutých pre data science a vychádzajúcich zo spoločnej filozofie, gramatiky a dátových štruktúr. Funkcie v tejto kolekcii zdieľajú niekoľko spoločných vlastností:

  1. prvý argument je data frame,
  2. ďalšie argumenty špecifikujú, čo sa má s dátami z prvého argumentu urobiť, pričom na stĺpce stačí odkázať menom (t. j. názvom elementu, bez príslušnosti ku dátovému objektu),
  3. výstupom je opäť data frame,
  4. dátové rámce musia byť riadne formátované, „čisté” (angl. tidy).

4.2 Prakticky

Balík pri načítaní predefinuje niektoré známe funkcie, preto je dobrým zvykom písať funkcie celým menom (aj s príslušnosťou ku balíku), napr. stats::filter() zavolá funkciu filter z predinštalovaného balíka stats a nie rovnomennú funkciu z balíka dplyr.

library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union

Prvou kľúčovou funkciou je funkcia select, ktorou sa vyberá podmnožina stĺpcov. V nasledujúcom príklade z datasetu mtcars pre ilustráciu vyberme postupne stĺpce – najprv jednotlivo po mene dojazd a hmotnosť, potom všetky stĺpce začínajúce písmenom „c” okrem carb, ďalej ôsmy stĺpec a nakoniec všetky stĺpce v poradí od zdvihového objemu až po drat:

data(mtcars)
tmp <- select(mtcars, c(mpg, wt), starts_with("c"), - carb, 8, disp:drat)
head(tmp)
##                    mpg    wt cyl vs disp  hp drat
## Mazda RX4         21.0 2.620   6  0  160 110 3.90
## Mazda RX4 Wag     21.0 2.875   6  0  160 110 3.90
## Datsun 710        22.8 2.320   4  1  108  93 3.85
## Hornet 4 Drive    21.4 3.215   6  1  258 110 3.08
## Hornet Sportabout 18.7 3.440   8  0  360 175 3.15
## Valiant           18.1 3.460   6  1  225 105 2.76

Týchto pomocných funkcií na výber stĺpcov je ešte oveľa viac: ends_with, contains, matches, num_range, all_of, any_of, everything. Spolu s nimi môžme používať známe operátory sekvencie :, negácie !, logického súčtu | a súčinu &, a kombinačnú funkciu c().

Pre zmenu, výber riadkov zabezpečuje funkcia filter. Obmedzme výber na všetky autá s hmotnosťou pod 3000 libier a výkonom nad 150 koní:

filter(tmp, wt < 3.0 & hp > 150)
##               mpg   wt cyl vs disp  hp drat
## Ferrari Dino 19.7 2.77   6  0  145 175 3.62

Na formulovanie podmienok sa dajú použiť aj funkcie ako is.na, between, near, a tiež porovnávacie a logické operátory.

Zoradenie riadkov podľa jedného alebo viacerých stĺpcov zabezpečuje funkcia arrange. Prednastavené je vzostupné radenie (angl. ascending). Tu napríklad zoradíme autá primárne podľa počtu valcov zostupne (angl. descending) a sekundárne podľa uloženia valcov vzostupne.

arrange(tmp, desc(cyl), vs)
##                      mpg    wt cyl vs  disp  hp drat
## Hornet Sportabout   18.7 3.440   8  0 360.0 175 3.15
## Duster 360          14.3 3.570   8  0 360.0 245 3.21
## Merc 450SE          16.4 4.070   8  0 275.8 180 3.07
## Merc 450SL          17.3 3.730   8  0 275.8 180 3.07
## Merc 450SLC         15.2 3.780   8  0 275.8 180 3.07
## Cadillac Fleetwood  10.4 5.250   8  0 472.0 205 2.93
## Lincoln Continental 10.4 5.424   8  0 460.0 215 3.00
## Chrysler Imperial   14.7 5.345   8  0 440.0 230 3.23
## Dodge Challenger    15.5 3.520   8  0 318.0 150 2.76
## AMC Javelin         15.2 3.435   8  0 304.0 150 3.15
## Camaro Z28          13.3 3.840   8  0 350.0 245 3.73
## Pontiac Firebird    19.2 3.845   8  0 400.0 175 3.08
## Ford Pantera L      15.8 3.170   8  0 351.0 264 4.22
## Maserati Bora       15.0 3.570   8  0 301.0 335 3.54
## Mazda RX4           21.0 2.620   6  0 160.0 110 3.90
## Mazda RX4 Wag       21.0 2.875   6  0 160.0 110 3.90
## Ferrari Dino        19.7 2.770   6  0 145.0 175 3.62
## Hornet 4 Drive      21.4 3.215   6  1 258.0 110 3.08
## Valiant             18.1 3.460   6  1 225.0 105 2.76
## Merc 280            19.2 3.440   6  1 167.6 123 3.92
## Merc 280C           17.8 3.440   6  1 167.6 123 3.92
## Porsche 914-2       26.0 2.140   4  0 120.3  91 4.43
## Datsun 710          22.8 2.320   4  1 108.0  93 3.85
## Merc 240D           24.4 3.190   4  1 146.7  62 3.69
## Merc 230            22.8 3.150   4  1 140.8  95 3.92
## Fiat 128            32.4 2.200   4  1  78.7  66 4.08
## Honda Civic         30.4 1.615   4  1  75.7  52 4.93
## Toyota Corolla      33.9 1.835   4  1  71.1  65 4.22
## Toyota Corona       21.5 2.465   4  1 120.1  97 3.70
## Fiat X1-9           27.3 1.935   4  1  79.0  66 4.08
## Lotus Europa        30.4 1.513   4  1  95.1 113 3.77
## Volvo 142E          21.4 2.780   4  1 121.0 109 4.11

Premenovanie stĺpcov pomocou funkcie rename má syntax nové = staré:

tmp <- rename(tmp, wt_lbs = wt, disp_in3 = disp)
head(tmp)
##                    mpg wt_lbs cyl vs disp_in3  hp drat
## Mazda RX4         21.0  2.620   6  0      160 110 3.90
## Mazda RX4 Wag     21.0  2.875   6  0      160 110 3.90
## Datsun 710        22.8  2.320   4  1      108  93 3.85
## Hornet 4 Drive    21.4  3.215   6  1      258 110 3.08
## Hornet Sportabout 18.7  3.440   8  0      360 175 3.15
## Valiant           18.1  3.460   6  1      225 105 2.76

Vytvorenie nových stĺpcov cez funkciu mutate, v ktorej opäť možno použiť množstvo pomocných funkcií, napr. recode, if_else … (pozri nápovedu). V nasledujúcom príklade vytvoríme stĺpec s objemom valcov v metrickej sústave a hneď ho použijeme na vyjadrenie objemu jedného valca:

tmp <- mutate(tmp,     # objekt, z ktorého sa vychádza
              disp_dm3 = disp_in3 * 16e-3,   
              disp1cyl_dm3 = disp_dm3 / cyl    # stĺpec disp_dm3 už existuje
              )  
head(tmp)
##                    mpg wt_lbs cyl vs disp_in3  hp drat disp_dm3 disp1cyl_dm3
## Mazda RX4         21.0  2.620   6  0      160 110 3.90    2.560    0.4266667
## Mazda RX4 Wag     21.0  2.875   6  0      160 110 3.90    2.560    0.4266667
## Datsun 710        22.8  2.320   4  1      108  93 3.85    1.728    0.4320000
## Hornet 4 Drive    21.4  3.215   6  1      258 110 3.08    4.128    0.6880000
## Hornet Sportabout 18.7  3.440   8  0      360 175 3.15    5.760    0.7200000
## Valiant           18.1  3.460   6  1      225 105 2.76    3.600    0.6000000

Výpočet štatistických súhrnov je možný pomocou funkcie summarize. Nasledujúci príkaz vypočíta priemernú hodnotu dojazdu a výkonu.

summarize(tmp, mpg_mean = mean(mpg), hp_mean = mean(hp))
##   mpg_mean  hp_mean
## 1 20.09062 146.6875

Ak chceme aplikovať jednu alebo viac agregačných funkcií postupne na viacero stĺpcov, pomôžeme si funkciou across:

summarize(mtcars, 
          across(.cols = c(mpg:hp, -cyl),    # výber je podobný ako pri select()
                 .fns = mean    # viac funkcií na aplikovanie sa vloží cez list()
                 )
          )
##        mpg     disp       hp
## 1 20.09062 230.7219 146.6875

Funkcia summarize ukáže svoju plnú silu až v kombinácii s funkciou group_by. Vypočítajme napríklad priemerný dojazd a výkon motora podľa počtu valcov a ich uloženia:

summarize(group_by(mtcars, cyl, vs), 
          mpg_mean = mean(mpg), hp_mean = mean(hp), number_obs = n(),
          .groups = "drop"  # zruší vnútorné členenie na skupiny
)
## # A tibble: 5 × 5
##     cyl    vs mpg_mean hp_mean number_obs
##   <dbl> <dbl>    <dbl>   <dbl>      <int>
## 1     4     0     26      91            1
## 2     4     1     26.7    81.8         10
## 3     6     0     20.6   132.           3
## 4     6     1     19.1   115.           4
## 5     8     0     15.1   209.          14

Všimnime si, že výsledné dáta sú uložené v dátovej štruktúre podobnej dátovému rámcu, ktorá sa volá tibble a je súčasťou systému tidyverse. Rozdiely oproti data.frame sú také, že

  1. pri vzniku tibble sa nikdy nezmení názov ani typ premennej,
  2. vo výpise sa zobrazujú iba stĺpce, ktoré sa zmestia na obrazovku, niekoľko prvých riadkov a dátový typ stĺpca,
  3. volanie neexistujúceho stĺpca skončí chybou namiesto výsledku Null,
  4. výber prvkov pomocou [ vždy vráti tibble a [[ vždy vektor,
  5. pri definovaní stĺpca sa zrecykluje iba vektor dĺžky 1,
  6. tibble nevytvára ani nepoužíva názvy riadkov.

Na záver si predstavíme jeden veľmi užitočný a (subjektívne) návykový nástroj, ktorým sa dá vyhnúť vytvoreniu pomocných/dočasných objektov vo výpočtovom prostredí a celkovo sprehľadňuje zdrojový kód. Je ním pipe operátor %>% importovaný z balíku magrittr (kde má ešte niekoľko rôznych modifikácií) a slúži na reťazenie príkazov podobne ako skladáme funkcie, napr. x %>% f() %>% g() vykoná to isté ako g(f(x)). V prostredí RStudio sa vkladá klávesovou skratkou [Ctrl] + [Shift] + [M].

Vypočítajme napr. priemerný dojazd všetkých automobilov s priamou orientáciou valcov a to podľa typu prevodovky a v jednotkách km/l:

mtcars %>%                       # východiskový dátový objekt 
  filter(vs == 1) %>%            # ponechaj iba riadky so straight engine
  mutate(kmpl = 0.43 * mpg) %>%  # pridaj stĺpec dojazdu v iných jednotkách
  group_by(am) %>%               # zoskup podľa typu prevodovky
  summarize(priemerny_dojazd = mean(kmpl))  # vypočítaj priemer
## # A tibble: 2 × 2
##      am priemerny_dojazd
##   <dbl>            <dbl>
## 1     0             8.92
## 2     1            12.2

So základnými nástrojmi R bez použitia nástrojov balíka dplyr by to vyzeralo napr. takto:

tmp <- subset(mtcars, vs == 1)
aggregate(kmpl ~ am, 
          data = cbind(tmp, kmpl = 0.43*tmp$mpg), 
          FUN = function(x) mean(x)
)
##   am      kmpl
## 1  0  8.919429
## 2  1 12.199714

V balíku magrittr sú aj iné pipe operátory, napr. %T% posunie argument funkcii, ale nepočká na jej odpoveď (slúži napríklad na vykreslenie), alebo %$% nevloží objekt do prvého argumentu, len ho sprístupní, ďalej operátor %<>% prepíše východiskový objekt výsledkom reťaze príkazov:

library(magrittr)
x <- data.frame(A = 2, B = 3)
x %>% cbind(C = sum(.))     # použitie bodky ako zástupného symbolu (za vstup)
##   A B C
## 1 2 3 5
x %>% { c(D = .$A ^ .$B) }  # krútené zátvorky spolu s . zastupujú anonymnú funkciu
## D 
## 8
x %$% c(C = A + B)          # sprístupňuje prvky vstupného objeku po mene
## C 
## 5
x %T>% plot() %>% cbind(C = 4)  # použitie napr. pre zobrazenie medzivýsledku

##   A B C
## 1 2 3 4
x %<>% cbind(C = sum(.))        # prepíše x výsledkom 
x
##   A B C
## 1 2 3 5
detach("package:magrittr")

Jednoduchšou (a predvídateľnejšou) alternatívou ku %>% je napr. pipe operátor %>>% z balíku pipeR.

V roku 2021 – ako odpoveď na obrovskú popularitu pipe operátora z magritttr – bol do štandardných knižníc jazyka R implementovaný jednoduchý pipe operátor |>. Kvôli spôsobu jeho definície sú jeho možnosti zatiaľ (k 2.2.2022) pomerne obmedzené, neumožňuje použitie zástupného znaku pre vstupný objekt (.) ani funkcie odvodené od bežných aritmetických operátorov (napr. '+'), tieto prekážky sa však dajú obísť použitím anonymnej funkcie (na úkor prehľadnosti zápisu):

2 |> '+'(3)     # nefunguje, ale reťaz 2 %>% '+'(3) funguje
2 |> (function(x) x + 3)()
x |> (\(df) df$A + df$B)()   # \(x) je skrátený zápis function(x)
## Error: function '+' not supported in RHS call of a pipe

Hoci balík dplyr sprístupňuje pipe operátor %>%, tento v ďalších kapitolách bude používaný nezávisle (nebude načítaný ani dplyr ani magrittr), a to pomocou príkazu:

`%>%` <- magrittr::`%>%` 

4.3 Cvičenie

  1. Načítajte data frame Cars93 z balíka MASS.
  2. Vytvorte nový data frame auta93 výberom všetkých premenných, ktoré spĺňajú aspoň jednu z nasledujúcich podmienok:
    • prvé tri (použite operátor sekvencie :),
    • ich názov obsahuje reťazec „Price” ale neobsahuje „.Price” (contains, operator -),
    • ich názov sa začína na „MPG” (starts_with),
    • všetky od indikátora airbagov až po výkon motora okrem typu pohonu DriveTrain (operátory :,-),
    • hmotnosť a pôvod vozidla.
  3. Pipe operátorom vytvorte sekvenciu nasledujúcich príkazov:
    1. auta93 (východískový objekt),
    2. premenovanie premennej EngineSize na CylindersVolume (rename),
    3. prevod hmotnosti z libier na kilogramy (mutate),
    4. výber všetkých amerických automobilov s hmotnosťou do 1200 kg (filter),
    5. zoradenie primárne podľa kategórie auta Type a v druhom rade podľa ceny vzostupne (arrange),
    6. vypísanie (print),
    7. rozdelenie riadkov podľa kategórie auta a výpočet priemerneho dojazdu v meste (group_by, summarise).

Literatúra

Ismay, Chester, a Albert Y Kim. 2019. Statistical Inference via Data Science: A ModernDive into R and the Tidyverse. CRC Press.
Peng, Roger D. 2016. R programming for data science. Leanpub. https://bookdown.org/rdpeng/rprogdatascience.
Wickham, Hadley, Mara Averick, Jennifer Bryan, Winston Chang, Lucy D’Agostino McGowan, Romain François, Garrett Grolemund, et al. 2019. “Welcome to the Tidyverse”. Journal of Open Source Software 4 (43): 1686. https://doi.org/10.21105/joss.01686.
Wickham, Hadley, Romain François, Lionel Henry, a Kirill Müller. 2020. dplyr: A Grammar of Data Manipulation. https://cran.R-project.org/package=dplyr.
———. 2021. dplyr: A Grammar of Data Manipulation. https://CRAN.R-project.org/package=dplyr.
Wickham, Hadley, a Garrett Grolemund. 2016. R for Data Science: Import, Tidy, Transform, Visualize, and Model Data. 1st vyd. O’Reilly Media, Inc. https://r4ds.had.co.nz/.