16.6 Slovesa pracující se dvěma (nebo více) tabulkami

Data bývají často dostupná ve více tabulkách, které mohou například mohou pocházet z různých zdrojů: HDP ze Světové banky, migrace z Eurostatu, atp. Pro účely datové analýzy je nutné takové tabulky spojovat do jednoho celku.

dplyr podporuje dva druhy spojovacích operací:

  • bind spojuje tabulky, které mají stejnou strukturu – v podstatě přidává sloupce (bind_cols()) nebo řádky (bind_rows())
  • join slučuje tabulky podle určitého definovaného klíče – například sloučí k sobě údaje o jednom člověku z více tabulek, které mohou mít naprosto odlišnou strukturu (více funkcí *_join())

16.6.1 Spojování tabulek s bind_*()

Funkcí bind_rows() a bind_cols() je spojovat tabulky se stejnou strukturou. V případě, že jsou pozorování se stejnými proměnnými rozděleny do více tabulek, je potřeba tabulky poskládat “pod sebe”. Jinými slovy přidávat další a další řádky s dodatečnými pozorováními. V tomto případě se hodí použít funkci bind_rows(...). Ta jako argument přijímá jména (neomezeného počtu) tabulek a nebo seznam (list) tabulek. Fungování bind_rows() můžeme demonstrovat na tabulce dplyr::band_members:

band_members
## # A tibble: 3 x 2
##   name  band   
##   <chr> <chr>  
## 1 Mick  Stones 
## 2 John  Beatles
## 3 Paul  Beatles

Do bind_rows() můžeme vložit více tabulek:

bind_rows(band_members, band_members, band_members)
## # A tibble: 9 x 2
##   name  band   
##   <chr> <chr>  
## 1 Mick  Stones 
## 2 John  Beatles
## 3 Paul  Beatles
## 4 Mick  Stones 
## 5 John  Beatles
## 6 Paul  Beatles
## 7 Mick  Stones 
## 8 John  Beatles
## 9 Paul  Beatles

…nebo list tabulek:

bind_rows(
    list(band_members, band_members, band_members)
)
## # A tibble: 9 x 2
##   name  band   
##   <chr> <chr>  
## 1 Mick  Stones 
## 2 John  Beatles
## 3 Paul  Beatles
## 4 Mick  Stones 
## 5 John  Beatles
## 6 Paul  Beatles
## 7 Mick  Stones 
## 8 John  Beatles
## 9 Paul  Beatles

Výsledky jsou pochopitelně stejné. Schopnost spojit tabulky uložené v seznamu je zvláště užitečná v případě, že pracujeme například s výstupem funkce lapply().

Předchozí příklady byly bezproblémové, protože tabulky měly stejnou strukturu – tedy stejně pojmenované sloupce se stejnými datovými typy. Co se stane v případě volání bind_rows() na nekonzistentní tabulky ukazuje následující příklad:

band_members %>% 
    rename(NAME = name) %>% 
    bind_rows(., band_members)
## # A tibble: 6 x 3
##   NAME  band    name 
##   <chr> <chr>   <chr>
## 1 Mick  Stones  <NA> 
## 2 John  Beatles <NA> 
## 3 Paul  Beatles <NA> 
## 4 <NA>  Stones  Mick 
## 5 <NA>  Beatles John 
## 6 <NA>  Beatles Paul

bind_rows() pod sebe složil hodnoty ze sloupců stejného jména. Sloupce s neshodujícími se jmény zachoval, ale do tabulky doplnil NA.

band_members %>% 
    rename(NAME = name) %>% 
    bind_rows(., band_members)
## # A tibble: 6 x 3
##   NAME  band    name 
##   <chr> <chr>   <chr>
## 1 Mick  Stones  <NA> 
## 2 John  Beatles <NA> 
## 3 Paul  Beatles <NA> 
## 4 <NA>  Stones  Mick 
## 5 <NA>  Beatles John 
## 6 <NA>  Beatles Paul

V případě nekonzistentních datových typů je situace zajímavější. V následujícím příkladu je sloupec name konvertován z character na factor a následně je tabulka spojena s nezměněnou tabulkou band_members:

band_members %>% 
    mutate(
        name = as.factor(name)
        ) %>% 
    bind_rows(., band_members)
## # A tibble: 6 x 2
##   name  band   
##   <chr> <chr>  
## 1 Mick  Stones 
## 2 John  Beatles
## 3 Paul  Beatles
## 4 Mick  Stones 
## 5 John  Beatles
## 6 Paul  Beatles

R umí provést automatickou konverzi faktorů na znaky. Provede ji, ale vypíše i upovídané varování. Výsledek je nicméně perfektně použitelný a ve většině případů bude odpovídat přání uživatele.

Větší problém nastane, pokud se ve spojovaných tabulkách vyskytnou sloupce stejného jména a rozdílných datových typů, u kterých R neumí provést automatickou konverzi. V tomto případě jde o double a character:

band_members %>% 
    mutate(
        name = rnorm1)
        ) %>% 
    bind_rows(., band_members)
## Error: <text>:4:9: unexpected ')'
## 3:         name = rnorm1)
## 4:         )
##            ^

Tato operace se neprovede a R vrátí chybu.

Podobně jako bind_rows() funguje funkce bind_cols(). Tabulky ovšem neskládá “pod sebe”“, ale”vedle sebe". Předpokladem jejího použití je opět shodná struktura tabulek. To v tomto případě znamená zejména to, že jedno pozorování je vždy na stejném řádku. Pozorování je tak vlastně identifikováno číslem řádku. Syntaxe je stejná jako u bind_rows():

bind_cols(band_members, band_members, band_members)
## New names:
## * name -> name...1
## * band -> band...2
## * name -> name...3
## * band -> band...4
## * name -> name...5
## * ...
## # A tibble: 3 x 6
##   name...1 band...2 name...3 band...4 name...5 band...6
##   <chr>    <chr>    <chr>    <chr>    <chr>    <chr>   
## 1 Mick     Stones   Mick     Stones   Mick     Stones  
## 2 John     Beatles  John     Beatles  John     Beatles 
## 3 Paul     Beatles  Paul     Beatles  Paul     Beatles

Za povšimnutí stojí, že pokud jsou ve spojovaných tabulkách shodná jména sloupců, bind_cols() je do výsledné tabulky přidá všechny. Aby se však zabránilo nepřípustné duplicitě ve jménech sloupců, rozšíří duplicitní jména o příponu.

Z logiky věci nejsou problém odlišné datové typy ve spojovaných tabulkách, ale problém můžou představovat tabulky s různým počtem řádků:

band_members %>% 
    sample_n(2) %>% 
    bind_cols(band_members,.)
## Error: Can't recycle `..1` (size 3) to match `..2` (size 2).

Protože u bind_cols() jsou pozorování fakticky identifikována číslem řádku, tak není možné takovou operaci smysluplně provést. bind_cols() v takovém případě nic nehádá, nic nepředpokládá ani nerecykluje, ale poctivě vyvěsí bílou vlajku a vrátí chybu.

16.6.2 Slučování tabulek s *_join()

16.6.2.1 Mutating joins

Slučování tabulek lze provádět pomocí různých slučovacích funkcí. Ty jsou v dplyr jednotně pojmenovány tak, že končí řetězcem *_join. Jejich základní skupina – tzv. mutating joins*, tj. slučovací funkce, které přidávají sloupce, se v zásadě chová jako “inteligentní” varianta bind_cols(). Pozorování však není definováno číslem řádku, ale proměnnou nebo kombinací více proměnných.

Pro ilustraci slučování jsou potřeba dvě tabulky. Vedle band_members využijeme dplyr::band_instruments:

band_instruments
## # A tibble: 3 x 2
##   name  plays 
##   <chr> <chr> 
## 1 John  guitar
## 2 Paul  bass  
## 3 Keith guitar

Pravděpodobně nejčastěji používanou slučovací funkcí je left_join():

left_join(x, y, by = NULL, copy = FALSE, suffix = c(".x", ".y"), ...)

Jako argument přijímá právě dvě tabulky x a y. Slučovací funkce v dplyr obecně umí pracovat pouze se dvěma tabulkami. Toto omezení je však možné obejít – viz dále.

Dalším důležitým parametrem je by jeho hodnota určuje, podle kterých sloupců se má provést slučování – tedy které sloupce definují pozorování. V případě, že je hodnota parametru NULL, potom se sloučení provede na základě všech sloupců, jejich jméno je v obou tabulkách. V tomto případě vrátí dplyr informaci o tom, které sloupce použil.

Příklad použití left_join():

left_join(band_members, band_instruments)
## Joining, by = "name"
## # A tibble: 3 x 3
##   name  band    plays 
##   <chr> <chr>   <chr> 
## 1 Mick  Stones  <NA>  
## 2 John  Beatles guitar
## 3 Paul  Beatles bass

left_join() funguje tak, že vrací všechny řádky z x a všechny sloupce z tabulky x i y. Řádky z x, pro které neexistuje shoda v tabulce y mají v připojených sloupcích NA.

V tomto příkladu nebyl vyplněn parametr by. left_join() tedy jako klíč pro slučování použil sloupec name, který se jako jediný vyskytoval v obou slučovaných tabulkách. Volání

left_join(band_members, band_instruments, by = "name")
## # A tibble: 3 x 3
##   name  band    plays 
##   <chr> <chr>   <chr> 
## 1 Mick  Stones  <NA>  
## 2 John  Beatles guitar
## 3 Paul  Beatles bass

by pochopitelně vedlo ke stejným výsledkům.

V případě slučování tabulek, ve kterých se vyskytují shodná jména sloupců, která ovšem neidentifikují pozorování je nutné parametr by specifikovat. Sloučení tabulek se stejnými názvy sloupců by jinak dopadlo například takto:

left_join(band_members, band_members)
## Joining, by = c("name", "band")
## # A tibble: 3 x 2
##   name  band   
##   <chr> <chr>  
## 1 Mick  Stones 
## 2 John  Beatles
## 3 Paul  Beatles

Výsledkem by byla vstupní tabulka x. Pří specifikování identifikačního sloupce (například name) bude výsledek odlišný:

left_join(band_members, band_members, by = "name")
## # A tibble: 3 x 3
##   name  band.x  band.y 
##   <chr> <chr>   <chr>  
## 1 Mick  Stones  Stones 
## 2 John  Beatles Beatles
## 3 Paul  Beatles Beatles

Slučované tabulky v tomto případě obsahují sloupec se shodným jménem, který není použit ke slučování. V tom případě je tento sloupec (podobně jako u bind_cols()) přejmenován připojením přípony. Podobu přípony je možné specifikovat parametrem suffix:

left_join(band_members, band_members, by = "name", suffix = c(".prvni",".druhy"))
## # A tibble: 3 x 3
##   name  band.prvni band.druhy
##   <chr> <chr>      <chr>     
## 1 Mick  Stones     Stones    
## 2 John  Beatles    Beatles   
## 3 Paul  Beatles    Beatles

V předchozích příkladech byl parametr by použit pouze pro specifikaci jednoho sloupce, jehož jméno bylo přítomno v obou slučovaných tabulkách. Možností nastavení je však více:

  • by může obsahovat jména více sloupců zadaných jako nepojmenovaný vektor, například by = c("name","band")
  • by může obsahovat i pojmenovaný vektor. Ten má ale zvláštní interpretaci. Jméno každého prvku odpovídá v takovém případě jménu sloupce z tabulky x a hodnota jménu sloupce z tabulky y. To umožňuje slučování tabulek, i když tyto neobsahují ani jeden sloupec se shodným jménem. Praktickou ukázkou je následující příklad, který využívá tabulku dplyr::band_instruments2:
band_instruments2
## # A tibble: 3 x 2
##   artist plays 
##   <chr>  <chr> 
## 1 John   guitar
## 2 Paul   bass  
## 3 Keith  guitar

Cílem je sloučit tabulky band_members a band_instruments2 podle jména hudebníka. Tato proměnná se však jmenuje name v band_members a artist v band_instruments2:

left_join(band_members, band_instruments2, by = c("name" = "artist"))
## # A tibble: 3 x 3
##   name  band    plays 
##   <chr> <chr>   <chr> 
## 1 Mick  Stones  <NA>  
## 2 John  Beatles guitar
## 3 Paul  Beatles bass

Používání slučovacích funkcí však má svoje úskalí. Představme si situaci, kdy identifikace pozorování není jednoznačná – tedy situaci, kdy identifikátor pozorování odpovídá více řádkům. Vytvoříme tabulku band_instruments3, která bude právě tuto podmínku splňovat:

band_instruments3 <- bind_rows(band_instruments, band_instruments)

print(band_instruments3)
## # A tibble: 6 x 2
##   name  plays 
##   <chr> <chr> 
## 1 John  guitar
## 2 Paul  bass  
## 3 Keith guitar
## 4 John  guitar
## 5 Paul  bass  
## 6 Keith guitar

Identifikátor – jméno hudebníka – teď není unikátní. Přináleží mu právě dva řádky. Následně tuto novou tabulku sloučíme s band_members:

left_join(band_members, band_instruments3, by = "name")
## # A tibble: 5 x 3
##   name  band    plays 
##   <chr> <chr>   <chr> 
## 1 Mick  Stones  <NA>  
## 2 John  Beatles guitar
## 3 John  Beatles guitar
## 4 Paul  Beatles bass  
## 5 Paul  Beatles bass

Zde je patrné, že výsledek může být problematický. Výsledná tabulka má větší počet řádků, než vstupní tabulka x – řádky, pro které bylo v tabulce y více záznamů se namnožily. dplyr tuto operaci provedl bez jakéhokoliv varování. Je proto kritické kontrolovat v průběhu sestavování datasetu konzistenci dat. Jinak se lehko může stát, že výsledná tabulka a analýza na ni postavená bude bezcenná.

left_join() není jediný mutating join implementovaný v dplyr, další jsou následující:

  • right_join() je bratr left_join(). Vrací sloupce z x i y, ale řádky z y.
  • inner_join() vrací sloupce z x i y, ale pouze řádky, která jsou jak v x, tak v y.
  • full_join() vrací všechny sloupce a všechny řádky z x a y.

16.6.2.2 Filtering joins

Druhou skupinou slučovacích funkcí jsou tzv. filtering joins. Tyto funkce opět pracují nad dvěma tabulkami x a y, ale vždy vrací sloupce pouze z tabulky x.

První takovou funkcí je semi_join(), který vrací pouze ty řádky, které existují v obou tabulkách x i y. Je to vlastně blízký příbuzný inner_join().

Pro ilustraci fungování filtering joins můžeme porovnat výsledky těchto funkcí:

inner_join(band_members, band_instruments, by = "name")
## # A tibble: 2 x 3
##   name  band    plays 
##   <chr> <chr>   <chr> 
## 1 John  Beatles guitar
## 2 Paul  Beatles bass
semi_join(band_members, band_instruments, by = "name")
## # A tibble: 2 x 2
##   name  band   
##   <chr> <chr>  
## 1 John  Beatles
## 2 Paul  Beatles

inner_join() vrací sloupce z obou tabulek – skutečně “slučuje”. semi_join() vrací sloupce pouze z první tabulky – spíše tedy filtruje na základě informací z druhé tabulky.

Druhou funkcí z této skupiny slučovacích funkcí je anti_join(), která je svým způsobem inverzní k semi_join(). Vrací řádky z x, které nejsou obsaženy v y:

anti_join(band_members, band_instruments, by = "name")
## # A tibble: 1 x 2
##   name  band  
##   <chr> <chr> 
## 1 Mick  Stones

16.6.2.3 Slučování více tabulek

Všechny funkce *_join() pracují s dvěma tabulkami. V drtivé většině případů je to zcela postačující, nicméně najdou se i výjimky. V takovém případě je možné využit funkci reduce() z balíku purrr (součást tidyverse).

reduce(.x, .f, ..., .init)

Funkce má dva základní argumenty:

-.x…je seznam (list) nebo atomický vektor -.f…je funkce přijímající 2 vstupy -...…dodatečné argumenty pro funkci .f

reduce() funguje tak, že provádí “akumulaci”. Nejprve aplikuje funkci .f na první dva prvky .x. V druhé iteraci aplikuje .f na výstup první iterace a na třetí prvek .x a tak dále.

list(band_members, band_members, band_members, band_instruments) %>% 
    reduce(., left_join, by = "name")
## # A tibble: 3 x 5
##   name  band.x  band.y  band    plays 
##   <chr> <chr>   <chr>   <chr>   <chr> 
## 1 Mick  Stones  Stones  Stones  <NA>  
## 2 John  Beatles Beatles Beatles guitar
## 3 Paul  Beatles Beatles Beatles bass

Za pozornost stojí varianty jména sloupce band, které přesně odpovídají mechanismu fungování reduce(). V první iteraci nastal konflikt jmen sloupců. Obě slučované tabulky obsahovaly sloupec pojmenovaný band a proto ve výsledné tabulce dostaly příponu. V druhé iteraci již ke konfliktu nedošlo. Stará tabulka totiž obsahovala jména band.x a band.y a nová band. Sloupec z nové tabulky tak byl připojen bez změny jména.

Příklad by mohl pokračovat:

list(band_members, band_members, band_members, band_members) %>% 
    reduce(., left_join, by = "name")
## # A tibble: 3 x 5
##   name  band.x  band.y  band.x.x band.y.y
##   <chr> <chr>   <chr>   <chr>    <chr>   
## 1 Mick  Stones  Stones  Stones   Stones  
## 2 John  Beatles Beatles Beatles  Beatles 
## 3 Paul  Beatles Beatles Beatles  Beatles