5 Manipulace s daty
V této kapitole vám ukážu, jak jde s daty v R manipulovat. Daty myslím jednu či více tabulek uložených v objektech třídy data frame a manipulací myslím zejména:
- výběr sloupců,
- filtrování neboli výběr řádků,
- řazení řádků,
- odvozování nových sloupců,
- seskupování řádků a agregaci,
- spojování více tabulek.
K tomu všemu používám zásadně balíčky z ekosystému tidyverse, konkrétně dplyr a tidyr. Vy si ale konkrétní balíčky nemusíte pamatovat, protože stačí, když na začátku deklarujete celou tidyverse.
library(tidyverse)Balík tidyverse mj. obsahuje i vzorový dataset starwars, na kterém většinu postupů ukážu. Nemusíte tedy nikde shánět a importovat jiná data. Pokud si vše vyzkoušíte i na nějakých vlastních datech, bude to jedině dobře.
5.1 Průzkum dat a výběr sloupců
Před dalším zpracováním je dobré si data pořádně prohlédnout a ujasnit si, co přesně popisují, jaké datové typy mají jednotlivé sloupce, zda jsou konzistentní, nejsou v nich chyby, něco neschází atd.
Nejprve si asi data frame zobrazíte, což můžete udělat několika způsoby. V R Markdownu ho většinou stačí vypsat názvem a tabulkou lze pak interaktivně listovat. To ale nefunguje na konzoli a ani výstup do finálního dokumentu není nejlepší.
starwarsLepší tedy asi bude funkce View, která otevře tabulkové zobrazení. Vyzkoušejte si to.
View(starwars)Kromě prostého zobrazení tabulky, můžete zkusit pár dalších funkcí, které vám toho o datech řeknou víc nebo přehledněji. Já mám rád funkci glimpse, protože hezky ukazuje názvy všech sloupců, jejich datové typy (třídy) a náhled několika prvních hodnot.
glimpse(starwars)## Rows: 87
## Columns: 14
## $ name <chr> "Luke Skywalker", "C-3PO", "R2-D2", "Darth Vader", "Leia Or~
## $ height <int> 172, 167, 96, 202, 150, 178, 165, 97, 183, 182, 188, 180, 2~
## $ mass <dbl> 77.0, 75.0, 32.0, 136.0, 49.0, 120.0, 75.0, 32.0, 84.0, 77.~
## $ hair_color <chr> "blond", NA, NA, "none", "brown", "brown, grey", "brown", N~
## $ skin_color <chr> "fair", "gold", "white, blue", "white", "light", "light", "~
## $ eye_color <chr> "blue", "yellow", "red", "yellow", "brown", "blue", "blue",~
## $ birth_year <dbl> 19.0, 112.0, 33.0, 41.9, 19.0, 52.0, 47.0, NA, 24.0, 57.0, ~
## $ sex <chr> "male", "none", "none", "male", "female", "male", "female",~
## $ gender <chr> "masculine", "masculine", "masculine", "masculine", "femini~
## $ homeworld <chr> "Tatooine", "Tatooine", "Naboo", "Tatooine", "Alderaan", "T~
## $ species <chr> "Human", "Droid", "Droid", "Human", "Human", "Human", "Huma~
## $ films <list> <"The Empire Strikes Back", "Revenge of the Sith", "Return~
## $ vehicles <list> <"Snowspeeder", "Imperial Speeder Bike">, <>, <>, <>, "Imp~
## $ starships <list> <"X-wing", "Imperial shuttle">, <>, <>, "TIE Advanced x1",~
Všimněte si, že poslední tři sloupce jsou třídy list. To znamená, že v každé buňce je uložen celý seznam více hodnot. Moc dobře se s tím nepracuje, v praxi na to asi hned tak nenarazíte, tak raději tyhle sloupce odstraníme.
Jak? Tím, že vybereme všechny ostatní funkcí select a výsledek uložíme do vlastního objektu.
my_starwars <- select(starwars, name:species)K funkci select se ještě za chvíli vrátím. Teď zkuste znovu glimps a všimněte si, že tři poslední sloupce zmizely.
glimpse(my_starwars)## Rows: 87
## Columns: 11
## $ name <chr> "Luke Skywalker", "C-3PO", "R2-D2", "Darth Vader", "Leia Or~
## $ height <int> 172, 167, 96, 202, 150, 178, 165, 97, 183, 182, 188, 180, 2~
## $ mass <dbl> 77.0, 75.0, 32.0, 136.0, 49.0, 120.0, 75.0, 32.0, 84.0, 77.~
## $ hair_color <chr> "blond", NA, NA, "none", "brown", "brown, grey", "brown", N~
## $ skin_color <chr> "fair", "gold", "white, blue", "white", "light", "light", "~
## $ eye_color <chr> "blue", "yellow", "red", "yellow", "brown", "blue", "blue",~
## $ birth_year <dbl> 19.0, 112.0, 33.0, 41.9, 19.0, 52.0, 47.0, NA, 24.0, 57.0, ~
## $ sex <chr> "male", "none", "none", "male", "female", "male", "female",~
## $ gender <chr> "masculine", "masculine", "masculine", "masculine", "femini~
## $ homeworld <chr> "Tatooine", "Tatooine", "Naboo", "Tatooine", "Alderaan", "T~
## $ species <chr> "Human", "Droid", "Droid", "Human", "Human", "Human", "Huma~
Občas ještě používám funkci summary, která u číselných sloupců (resp. obecně vektorů) ukáže jednoduchou statistiku – minimum, první kvartil, median, průměr, třetí kvartil, maximum a počet prázdných hodnot.
summary(my_starwars)## name height mass hair_color
## Length:87 Min. : 66.0 Min. : 15.00 Length:87
## Class :character 1st Qu.:167.0 1st Qu.: 55.60 Class :character
## Mode :character Median :180.0 Median : 79.00 Mode :character
## Mean :174.4 Mean : 97.31
## 3rd Qu.:191.0 3rd Qu.: 84.50
## Max. :264.0 Max. :1358.00
## NA's :6 NA's :28
## skin_color eye_color birth_year sex
## Length:87 Length:87 Min. : 8.00 Length:87
## Class :character Class :character 1st Qu.: 35.00 Class :character
## Mode :character Mode :character Median : 52.00 Mode :character
## Mean : 87.57
## 3rd Qu.: 72.00
## Max. :896.00
## NA's :44
## gender homeworld species
## Length:87 Length:87 Length:87
## Class :character Class :character Class :character
## Mode :character Mode :character Mode :character
##
##
##
##
5.1.1 Prázdné hodnoty
R rozlišuje prázdné (nedefinované, neznámé) hodnoty a označuje je hodnotou NA. Pozor, NA není ani nula, ani prázdný řetězec. Je to indikátor, že danou hodnotu neznáme, nebo neexistuje. Proto s NA nejde počítat.
NA + 10## [1] NA
sum(c(10, 20, NA, 30))## [1] NA
Všechny tyto a podobné operace vrací NA. A protože to je někdy dost nepraktické, jde to v určitých situacích obejít. Např. součet nebo průměr můžete vypočítat takto:
sum(c(10, 20, NA, 30), na.rm = TRUE)## [1] 60
mean(c(10, 20, NA, 30), na.rm = TRUE)## [1] 20
Existují i funkce, kterými můžete řádky s NA z data framu úplně odstranit
drop_na(my_starwars)nebo nahradit jinou hodnotou funkcí replace_na.
5.2 Výběr sloupců
Už jsem ukázal, že výběr sloupců provedete funkcí select. Ta má jako první parametr data frame a za ním následuje seznam sloupců, který můžete napsat několika způsoby. Většinou je nejjednodušší sloupce vyjmenovat a oddělit čárkou:
select(my_starwars, name, hair_color, eye_color)Můžete použít i souvislý rozsah sloupců oddělený dvojtečkou:
select(my_starwars, hair_color:eye_color)Případně oba způsoby zkombinovat:
select(my_starwars, name, hair_color:eye_color)Těch způsobů je ale mnohem víc a všechny je najdete v nápovědě:
?dplyr_tidy_select5.2.1 Funkcionální princip
Podstatné je, že funkce select ani žádná jiná nikdy nemění objekt, se kterým pracuje (zde data frame my_starwars). R je tzv. funkcionální jazyk, což znamená, že všechny funkce nějak zpracují parametry, vrátí výsledek jako nový objekt (který si můžete i nemusíte uložit), ale parametry nikdy nezmění.
Mimochodem, jazyk vzorců Excelu je taky funkcionální – funkce v něm taky něco vrátí, ale parametry nezmění.
5.2.2 Pipes (trubky, fajfky)
Představte si, že chcete na objekt (zde data frame) aplikovat nějakou funkci, a na výsledek, který dostanete, chcete aplikovat další funkci. Stejně jako v Excelu to jde udělat vnořováním funkcí:
glimpse(select(my_starwars, name, hair_color))## Rows: 87
## Columns: 2
## $ name <chr> "Luke Skywalker", "C-3PO", "R2-D2", "Darth Vader", "Leia Or~
## $ hair_color <chr> "blond", NA, NA, "none", "brown", "brown, grey", "brown", N~
Jenže vnořování mnoha funkcí do sebe v Excelu všichni nenávidíme. Jde tím sice udělat úžasné věci, ale je to příšerně nepřehledné a náchylné k chybám. Zápis totiž začíná tím, co se má udělat jako poslední, a to, co se má udělat jako první, je utopené někde uprostřed. Peklo.
Proto má R operátor pipe, se kterým předešlý výraz přepíšu takhle:
my_starwars |> select(name, hair_color) |> glimpse()## Rows: 87
## Columns: 2
## $ name <chr> "Luke Skywalker", "C-3PO", "R2-D2", "Darth Vader", "Leia Or~
## $ hair_color <chr> "blond", NA, NA, "none", "brown", "brown, grey", "brown", N~
případně ještě přehledněji:
my_starwars |>
select(name, hair_color) |>
glimpse()## Rows: 87
## Columns: 2
## $ name <chr> "Luke Skywalker", "C-3PO", "R2-D2", "Darth Vader", "Leia Or~
## $ hair_color <chr> "blond", NA, NA, "none", "brown", "brown, grey", "brown", N~
Operátor pipe (|>) vždy vezme výsledek výrazu vlevo a použije ho jako první parametr funkce vpravo. Celé se to tedy píše i čte přesně v pořadí zpracování: vezmu objekt my_starwars na něj aplikuji funkci select a na výsledek aplikuji funkci glimpse.
Pro porovnání obecně oba způsoby zápisu:
# Zápis s pipes
objekt |>
prvni_funkce(druhy_parametr_prvni_funkce, treti_parametr_prvni_funkce) |>
druha_funkce(druhy_parametr_druhe_funkce) |>
treti_funkce(druhy_parametr_treti_funkce) |>
ctvrta_funkce() |>
pata_funkce(druhy_parametr_pate_funkce)
# Zápis s vnořenými funkcemi
pata_funkce(
ctvrta_funkce(
treti_funkce(
druha_funkce(
prvni_funkce(
objekt, druhy_parametr_prvni_funkce, treti_parametr_prvni_funkce
), druhy_parametr_druhe_funkce
), druhy_parametr_treti_funkce
)
), druhy_parametr_pate funkce
)Kdyby se jednalo o skutečné funkce, oba zápisy by dělaly totéž, ale první je výrazně přehlednější a lépe se píše.
5.2.2.1 Starší pipe
Skoro ve všech návodech a dokumentaci k R najdete jiný operátor pipe: %>%. Dřív totiž R operátor pipe nemělo a nahrazoval se operátorem z knihovny magrittr. Teď už ale R má nativní operátor |> a já ho mám radši. Pro vás je důležité, že %>% a |> je v principu totéž a skoro vždy se to chová stejně.
5.3 Filtrování neboli výběr řádků
5.3.1 Funkce filter
Pokud nechcete zobrazit či dále zpracovat všechny řádky data framu, vyberete si jen některé. Nějčastěji se na to používá funkce filter. Tohle je její nejjednodušší podoba:
my_starwars |>
filter(hair_color == "blond")Jako parametr jsem napsal logickou podmínku, že se hodnota ve sloupci hair_color musí rovnat blond. Podmínky ale mohu i kombinovat, a pak musí být oddělené čárkou.
my_starwars |>
filter(
hair_color == "brown",
eye_color == "blue"
) |>
select(name, hair_color, eye_color)Na čísla fungují běžné srovnávací operátory > (větší), >= (větší nebo rovno), < (menší), <= (menší nebo rovno).
my_starwars |>
filter(height > 220) |>
select(name, height)V podmínkách ale můžete používat i různé funkce. Chcete třeba najít všechny postavy, které jsou vyšší než průměr?
my_starwars |>
filter(height > mean(height, na.rm = TRUE)) |>
select(name, height)Nebo všechny, jejichž výška je neznámá?
my_starwars |>
filter(is.na(height)) |>
select(name, height)5.3.2 Další funkce pro výběr řádků
Někdy chcete omezit počet řádků jinak než logickou podmínkou. Např. chcete vrátit jen prvních pět:
my_starwars |>
slice_head(n = 5) |>
select(name)nebo poslední 3:
my_starwars |>
slice_tail(n = 3) |>
select(name)nebo 3 nejvyšší:
my_starwars |>
slice_max(order_by = height, n = 3) |>
select(name, height)nebo 5 % nejvyšších:
my_starwars |>
slice_max(order_by = height, prop = 0.05) |>
select(name, height)5.4 Řazení řádků
Řádky se řadí funkcí arrange, ve které se jako parametry uvedou sloupce, podle kterých se má řadit, oddělené čárkou.
my_starwars |>
arrange(species, name) |>
select(species, name)Jde řadit i sestupně pomocí funkce desc.
my_starwars |>
arrange(species, desc(height)) |>
select(species, name, height)5.5 Odvozování nových sloupců
Funkcí mutate jde vypočítat hodnoty sloupců, ať už nových, nebo stávajících. V příkladu spočítám BMI z váhy dělené výškou v metrech na druhou a před jméno doplním slovo „tlouštík“, pokud má daná postava nadváhu.
my_starwars |>
select(name:mass) |>
mutate(
bmi = mass / (height / 100) ** 2,
name = paste0(if_else(bmi > 30, "tlouštík ", ""), name)
)K tomu pár vysvětlení:
- Operátor
**umocňuje. - Funkce
paste0spojuje víc textových řetězců do jednoho. Bez 0 na konci mezi ně dá mezeru případně jiný oddělovač dle parametru. - funkce
if_elsefunguje stejně jakoIFv Excelu – podle podmínky v první parametru vrátí buď druhý, nebo třetí parametr. - Zvykněte si, že data frame je vlastně seznam vektorů, které odpovídají sloupcům. Výraz
height / 100tedy vydělí celý vektor, tj. postupně jednotlivé jeho hodnoty, číslem 100. Výpočet tedy neprobíhá po řádcích, nýbrž po sloupcích.
Potřebujete-li složitější větvení, jde funkce if_else vnořovat do sebe, ale není to moc přehledné. Lepší je funkce case_when, která se používá takhle:
my_starwars |>
select(name:mass) |>
mutate(
bmi = round(mass / (height / 100) ** 2),
postava = case_when(
bmi < 25 ~ "hubeňour",
bmi >= 25 & bmi <= 30 ~ "akorát",
bmi > 30 ~ "tlouštík"
)
)5.6 Seskupování řádků a agregace
5.6.1 funkce sumarise
Začnu prostou agregací. Slouží k ní funkce summarise a používá se skoro stejně, jako funkce mutate. Jen je určená pro agregační funkce (počet, součet, průměr apod.) a z výsledku vynechá všechny sloupce, které se neagregují nebo neslouží jako seskupovací klíče (viz dále).
Řekněme, že chci vědět, kolik postav v datasetu je a jakou mají průměrnou výšku.
my_starwars |>
summarise(
n = n(),
height = mean(height, na.rm = TRUE)
)Všimněte si, že funkce n() vrátí počet řádků data framu. Šlo by s ní spočítat třeba počet planet? Nešlo. Na to slouží funkce n_distinct, která spočítá počet unikátních hodnot vektoru.
my_starwars |>
summarise(
n_homes = n_distinct(homeworld),
n_species = n_distinct(species)
)5.6.2 Funkce group_by
V reálné praxi potřebujete agregovat celý dataset do jednoho řádku málokdy. Častější jsou agregace podle nějakých skupin. K seskupení slouží funkce group_by a vypadá takhle:
my_starwars |>
group_by(species) |>
summarise(
n = n(),
height = mean(height, na.rm = TRUE)
)jde kombinovat i víc seskupovacích klíčů:
my_starwars |>
group_by(species, homeworld) |>
summarise(
n = n(),
height = mean(height, na.rm = TRUE)
)## `summarise()` has grouped output by 'species'. You can override using the
## `.groups` argument.
5.6.3 Funkce count
Protože se položky podle nějakého klíče počítají velmi často, existuje zkratka. Místo toho, abyste psali:
my_starwars |>
group_by(species) |>
summarise(n = n())můžete zvolit kratší zápis a výsledek rovnou sestupně setřídit:
my_starwars |>
count(species, sort = TRUE)5.6.4 Kombinace group_by s dalšími funkcemi pro manipulaci dat
Funkce group_by nemusí sloužit jen k agregaci. Respektive k agregaci slouží skoro vždycky, ale ta nemusí být hlavním cílem.
O něco výš jsem vám ukázal, jak pomocí funkcí filter a mean vybrat postavy, které jsou nadprůměrně vysoké:
my_starwars |>
filter(height > mean(height, na.rm = TRUE)) |>
select(name, height)Co kdybych ale chtěl vybrat jen ty postavy, které jsou nadprůměrně vysoké jen v rámci svého druhu? Použiju group_by a pro přehlednost výsledek ještě setřídím.
my_starwars |>
group_by(species) |>
filter(height > mean(height, na.rm = TRUE)) |>
select(name, species, height) |>
arrange(species, desc(height))Funkce group_by jde výborně dohromady i s funkcemi slice_xxx. Např. bych chtěl vidět z každého druhu jen nejvyšší postavu:
my_starwars |>
group_by(species) |>
slice_max(height) |>
select(species, name, height)A konečně jde group_by kombinovat i s mutate – do předešlého výstupu doplním, z kolika zástupců svého druhu je daný exemplár nejvyšší:
my_starwars |>
group_by(species) |>
mutate(out_of = n()) |>
slice_max(height) |>
select(species, name, tallest = height, out_of)Všimněte si, že jsem musel dát mutate ještě před slice_max, protože po něm by funkce n() vracela už jen 1. A ukázal jsem vám také, jak pomocí funkce select přejmenovat sloupec (což jde i samostatnou funkcí rename, ale tady je to jednodušší takhle).
5.7 Tahák s dalšími funkcemi
Vzal jsem to všechno jen z rychlíku, ve skutečnosti je těch možností mnohem víc. Perfektní tahák najdete v RStudiu v menu Help -> Cheat Sheets -> Data Transformation with dplyr, případně zde je on-line verze.