3  Transformácia a súhrn

V tejto kapitole preberieme moderné nástroje na manipuláciu so súbormi takých údajov, ktoré už sú v čistej tabuľkovej forme. (O tom, ako dáta dostať do čistej formy, budeme hovoriť neskôr.) Informácie pochádzajú najmä zo zdrojov (Wickham a Grolemund 2016, kap. 5; Ismay a Kim 2019, kap. 3; Peng 2016, kap. 13).

3.1 Všeobecne

Základnou dátovou štruktúrou (čiže kontajnerom na údaje) 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…). V predošlých kapitolách 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ť únavné a neprehľadné. Syntax jazyka R totiž nie je veľmi intuitívna. Tieto nevýhody sa snaží odstrániť balík dplyr (Wickham et al. 2022), v ktorom je implementovaná konzistentná „gramatika” (v súlade s názvoslovím databázového jazyka SQL - Structured Query Language) a ktorý je tiež veľmi rýchly. Kľúčovými sú funkcie pre

  • prácu so stĺpcami,
    • select – výber stĺpcov,
    • relocate – zmena poradia sĺpcov,
    • rename – premenovanie stĺpcov (premenných),
    • mutate – pridanie nových stĺpcov napr. transformáciou iných,
  • prácu s riadkami,
    • filter – výber riadkov na základe logických podmienok,
    • slice – výber riadkov indexom,
    • distinct – odstránenie duplicitných riadkov,
    • arrange – zoradenie riadkov podľa hodnôt zvolených stĺpcov,
  • tvorbu súhrnov,
    • summarise – pre zvolené stĺpce,
    • reframe – súhrn v skupine môže mať viac ako jeden riadok (zovšeobecňuje funkciu summarise)
  • vytváranie skupín
    • group_by – skupiny riadkov podľa hodnôt zvolených stĺpcov,
    • rowwise – izolovanie riadkov, takže sa dajú vykonať operácie nad stĺpcami aj pomocou nie-vektorizovaných funkcií
  • spájanie viacerých tabuliek,
    • bind_cols, bind_rows – vedľa seba, pod seba,
    • \(*\)_join – zlúčením prostredníctvom spoločných znakov,
  • reťazenie príkazov
    • %>% (pipe operátor) – ako 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).

Oproti základným nástrojom jazyka R prináša dplyr niekoľko výhod, najmä unifikovanú prácu s dátovými rámcami, jednoduchší výber stĺpcov, špecializáciu, podporu reťazenia a skupín.

Okrem spomínaných obsahuje balík aj množstvo pomocných funkcií, ktoré sú na príkladoch popísané v konkrétnych vignette: opererácie po stĺpcoch, riadkoch, skupinách, nad dvoma tabuľkami, a práca s tzv. window funkciami.

3.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 |, logického 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 používa 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 je opäť možné použiť viacero pomocných funkcií, napr. recode, if_else, case_when … (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 ukážeme reťazenie príkazov pomocou pipe operátora %>%, ktorým sa dá vyhnúť vytvoreniu pomocných/dočasných objektov vo výpočtovom prostredí, čo výrazne sprehľadňuje zdrojový kód. Princíp je podobný ako pri skladaní matematických funkcií \((g \circ f)(x)\), čiže napr. príkaz x %>% f() %>% g() vykoná to isté ako g(f(x)). V prostredí RStudio sa pipe operátor vkladá klávesovou skratkou [Ctrl] + [Shift] + [M].

Pre ilustráciu vypočítajme 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

Operátor %>% je v skutočnosti importovaný z balíku magrittr, v ktorom sú definované aj ďalšie príbuzné 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 %<>% východiskový objekt prepíše výsledkom reťaze príkazov.

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

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

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ľ pomerne obmedzené, neumožňuje napr. viacnásobné použitie zástupného znaku pre vstupný objekt (_)1, to sa musí obísť pomocou anonymnej funkcie. Menej elegantné je tiež použitie funkcií odvodených od bežných aritmetických operátorov (napr. +), ktoré musia byť uzavreté do opačne zošikmených apostrofov aj zátvoriek.

# nasledujúci príkaz nefunguje, kým reťaz `2 %>% '+'(3)` áno
2 |> '+'(3)     
Error in +3: function '+' not supported in RHS call of a pipe (<input>:2:6)
2 |> (`+`)(3)
[1] 5
# použitie anonymnej funkcie
2 |> (function(x) x + 3)()
[1] 5
# `\(x)` je skrátený zápis konštrukcie `function(x)`
x |> (\(df) df$A + df$B)()   
[1] 5
# zástupný znak `_` musí byť umiestnený adresne
2 |> log(8, base = _)
[1] 3
# zástupný znak sa dá použiť aj na výber časti objektu pomocou [, [[, $, @
0:4 |> _[2:3]
[1] 1 2

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

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

Väčšinou si však vystačíme so vstavaným operátorom. V nastaveniach prostredia RStudio sa mu dá priradiť rovnaká klávesová skratka ako predvolenému pipe operátoru.

3.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ýchodiskový 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).

  1. Na pozadí je totiž príkaz x |> f() priamo prepísaný na f(x). Ak by vstupoval do viacerých argumentov, musel by sa vykonať viackrát.↩︎