---
title: "Iterace nad prvky atomických vektorů a seznamů"
author: "Michal Kvasnička"
documentclass: article
output:
html_document:
theme: cerulean
toc: yes
toc_float: yes
pdf_document: default
fontsize: 10pt
classoption: a4paper
---
# Iterace nad prvky atomických vektorů a seznamů {#kap:iterace-nad-vektory}
Často je potřeba provést nějakou operaci s\ každým prvkem atomického vektoru, seznamu apod. Většina programovacích jazyků k\ tomuto účelu používá cyklus `for` (viz oddíl\ \@ref(sec:for)). R\ však nabízí dvě lepší řešení. Zaprvé, mnohé funkce jsou v\ R vektorizované, tj.\ pracují s\ vektorem po prvcích a vrátí vektor stejné délky, kde každý prvek je výsledkem aplikace dané funkce na odpovídající prvek původního vektoru. Příkladem takové funkce je např.\ funkce `log()`. Druhou možností je použití funkce `map()` a jí podobných z\ balíku **purrr**. Tyto funkce berou jako svůj vstup nejen data, ale také jinou (nevektorizovanou) funkci a aplikují ji na každý prvek dat.
Základní R sice poskytuje podobné funkce jako balík **purrr**, my se však zaměříme na funkce z\ balíku **purrr**, a\ to z\ několika důvodů: 1)\ jejich ovládání je jednodušší, 2)\ je kompatibilní s\ dalšími funkcemi ve skupině balíků **tidyverse**, 3)\ funkce usnadňují řešení problémů a ladění kódu a 4)\ umožňují snadnou paralelizaci pomocí balíku **furrr**.
V\ této kapitole se naučíte
- aplikovat nevektorizované funkce na prvky vektorů
- aplikovat funkce na sloupce tabulek
- zjednodušovat výsledků na atomické vektory
- filtrovat prvky vektorů
- redukovat prvky vektorů
- paralelizovat svůj výpočet
Před vlastní prací musíme načíst balík **purrr**:
```r
library(purrr)
```
## Základní iterace nad prvky vektorů pomocí `map()`
Nejčastějším případem je, že potřebujeme pomocí nějaké funkce transformovat každý prvek vektoru (atomického nebo seznamu). Základní funkce pro iterace nad vektory je funkce `map(.x, .f, ...)`. Její první parametr je vektor\ `.x` (atomický nebo seznam) a druhý parametr je funkce\ `.f`. Funkce `map()` spustí funkci\ `.f` na každý prvek vektoru\ `.x` a výsledky poskládá do seznamu stejné délky, jako má vektor\ `.x`. Funkci `map()` si tedy můžete představit jako výrobní linku, kde nad pásovým dopravníkem pracuje robot. Dopravník nejdříve robotovi doručí první prvek\ `.x`, tj.\ `.x[[1]]`. Robot na doručeném prvku provede funkci\ `.f()`, tj.\ vyhodnotí `.f(x[[1]])` a výsledek uloží zpátky na dopravníkový pás, tj.\ jako první prvek nově vytvářeného seznamu. Pak se dopravník posune, robot spustí funkci\ `.f()` na `.x[[2]]` atd., dokud linka nezpracuje všechny prvky vektoru\ `.x`. Fungování linky ukazuje obrázek\ \@ref(fig:lapply).
Všimněte si dvou věcí: 1)\ Funkci\ `.f()` předáváme funkci `map()` jako proměnnou, tj.\ bez kulatých závorek. 2)\ Funkce `.f()` od `map()` vždy dostane jeden prvek vektoru\ `.x` jako svůj první parametr.
Ukážeme si to nejdříve na jednoduchém příkladu. Máme seznam\ `v`, který obsahuje různě dlouhé atomické vektory. Chceme zjistit délku jednotlivých vektorů v\ seznamu:
```r
v <- list(1, 1:2, 1:3, 1:4, 1:5) # vytvoří seznam vektorů
map(v, length) # zjistí délku jednotlivých vektorů v seznamu v
```
```
## [[1]]
## [1] 1
##
## [[2]]
## [1] 2
##
## [[3]]
## [1] 3
##
## [[4]]
## [1] 4
##
## [[5]]
## [1] 5
```
V\ dalším příkladu chceme zjistit, jaký datový typ mají jednotlivé sloupce tabulky. Využijeme toho, že tabulky tříd *data.frame* i\ *tibble* jsou technicky implementované jako seznamy sloupců a že funkce `map()` s\ nimi takto zachází:
```r
df <- tibble::tibble(x = 1:6, # celá čísla
y = c(rnorm(1:5), NA), # reálná čísla
z = c(NA, letters[1:4], NA) # řetězce
)
df
```
```
## # A tibble: 6 × 3
## x y z
##
## 1 1 -1.49
## 2 2 -1.26 a
## 3 3 -1.21 b
## 4 4 1.38 c
## 5 5 -1.68 d
## 6 6 NA
```
```r
map(df, class) # funkce class() je použita postupně na všechny sloupce df
```
```
## $x
## [1] "integer"
##
## $y
## [1] "numeric"
##
## $z
## [1] "character"
```
Výše uvedené příklady ukazují, jak pomocí funkce `map()` aplikovat na jednotlivé prvky vektoru nějakou existující funkci. Často však chceme na prvky vektoru aplikovat nějaký vlastní výpočet. Pokud jej chceme provést jen na jednom vektoru, nestojí za to vytvářet pojmenovanou funkci, která by zaplevelila naše pracovní prostředí. V\ takovém případě můžeme použít anonymní funkci, jak ukazuje následující příklad. V\ něm chceme zjistit, kolik každý sloupec tabulky obsahuje hodnot\ `NA`. Druhým parametrem funkce `map()` je anonymní funkce, tj.\ funkce, kterou jsme před tím neuložili do žádné proměnné. Naše anonymní funkce musí brát jako svůj první parametr jeden prvek vektoru, nad kterým se iteruje:
```r
map(df, function(x) sum(is.na(x))) # x nabývá vždy hodnotu jednoho sloupce z df
```
```
## $x
## [1] 0
##
## $y
## [1] 1
##
## $z
## [1] 2
```
Od verze\ 4.1 je samozřejmě možné využít i\ zkrácené syntaxe pro tvorbu anonymních funkcí\ -- se stejným výsledkem:
```r
map(df, \(x) sum(is.na(x))) # x nabývá vždy hodnotu jednoho sloupce z df
```
Protože tvorba anonymních funkcí může být protivná, nabízí funkce `map()` "zkratku": místo funkce zadat pravostrannou formuli, kterou `map()` automaticky převede na funkci. Syntaxe této formule je jednoduchá. Začíná symbolem `~` (vlnkou) za kterou následuje výraz, který se má vyhodnotit. Hodnotu prvku vektoru\ `.x`, který funkce právě vyhodnocuje, zadáme jako `.` nebo `.x`. Předchozí výpočet je tedy možné zadat i takto:
```r
map(df, ~ sum(is.na(.)))
```
nebo
```r
map(df, ~ sum(is.na(.x)))
```
```
## $x
## [1] 0
##
## $y
## [1] 1
##
## $z
## [1] 2
```
Funkce `map()` umožňuje využít i\ další zajímavou zkratku, která je užitečná v\ případě, že chceme ze seznamu extrahovat prvky, které se nachází na určité pozici nebo mají určité jméno. V\ takovém případě zadáme místo funkce `.f` vektor celých čísel, řetězců nebo jejich kombinaci. Pokud zadáme celé číslo, `map()` vrátí z\ každého prvku vektoru\ `.x` jeho prvek na této pozici; pokud zadáme řetězec, pak jeho prvek s\ tímto jménem. Pokud zadáme víc pozic, `map()` postupuje rekurzivně. Ukažme si to na příkladu. Vytvoříme datovou strukturu, která obsahuje vybrané informace o\ hráčích nějaké hry. Všimněte si, že vnější seznam obsahuje dílčí seznamy, které mají stejné pojmenované položky:
```r
dungeon <- list(
list(id = 11, name = "Karel", items = list("sword", "key")),
list(id = 12, name = "Emma", items = list("mirror", "potion", "dagger"))
)
```
Nejdříve chceme získat seznam id jednotlivých hráčů. Protože každý vnitřní seznam má `id` na prvním místě, můžeme jejich seznam získat takto:
```r
map(dungeon, 1)
```
```
## [[1]]
## [1] 11
##
## [[2]]
## [1] 12
```
Pokud chceme získat seznam jmen hráčů, můžeme jej získat buď pomocí pozice (jména jsou ve vnitřních seznamech na druhém místě) výrazem `map(dungeon, 2)`. Můžeme je však extrahovat i\ jménem:
```r
map(dungeon, "name")
```
```
## [[1]]
## [1] "Karel"
##
## [[2]]
## [1] "Emma"
```
Podobně můžeme získat i\ seznam artefaktů, které má každý hráč k\ dispozici (výsledek je seznamem seznamů):
```r
map(dungeon, "items")
```
```
## [[1]]
## [[1]][[1]]
## [1] "sword"
##
## [[1]][[2]]
## [1] "key"
##
##
## [[2]]
## [[2]][[1]]
## [1] "mirror"
##
## [[2]][[2]]
## [1] "potion"
##
## [[2]][[3]]
## [1] "dagger"
```
Jednotlivé artefakty pak můžeme získat zadáním více pozic, které použijí rekurzivně:
```r
map(dungeon, c(3, 1))
```
```
## [[1]]
## [1] "sword"
##
## [[2]]
## [1] "mirror"
```
```r
map(dungeon, list("items", 1))
```
```
## [[1]]
## [1] "sword"
##
## [[2]]
## [1] "mirror"
```
Oba předchozí výrazy vrátili seznam artefaktů, které mají oba hráči na první pozici mezi svými položkami. Všimněte si, že pokud jsme chtěli adresovat `items` jménem, museli jsme jméno a pozici spojit pomocí seznamu, protože funkce `c()` by převedla obě pozice na řetězce.
Pokud požádáme o\ prvky, které neexistují, dostaneme výsledek `NULL`:
```r
map(dungeon, list("items", 3))
```
```
## [[1]]
## NULL
##
## [[2]]
## [1] "dagger"
```
```r
map(dungeon, "aloha")
```
```
## [[1]]
## NULL
##
## [[2]]
## NULL
```
Pokud chceme, aby `map()` v\ takovém případě vrátil jinou hodnotu, můžeme ji nastavit pomocí parametru `.default`:
```r
map(dungeon, list("items", 3), .default = NA_character_)
```
```
## [[1]]
## [1] NA
##
## [[2]]
## [1] "dagger"
```
Funkci, kterou spouštíme nad jednotlivými prvky vektoru, můžeme předat i\ další parametry. Ty se zadají na pozici `...`, tedy jako třetí a případně další parametry ve funkci `map()`, jak ukazuje následující příklad. V\ něm chceme vytvořit několik vektorů gaussovských náhodných čísel o\ různých délkách a průměru\ 10 a směrodatné odchylce také\ 10. K tvorbě náhodných vektorů s\ normálním rozložením použijeme funkci\ `rnorm()`:
```r
map(1:5, rnorm, mean = 10, sd = 10)
```
```
## [[1]]
## [1] -0.4339624
##
## [[2]]
## [1] -7.515927 2.461931
##
## [[3]]
## [1] 20.259172 -8.843781 3.837213
##
## [[4]]
## [1] 4.08876620 -0.08912992 14.06446468 7.87739747
##
## [[5]]
## [1] 19.880268 20.103576 -7.350148 24.694877 12.686155
```
Funkce `map()` v\ našem příkladu iteruje nad vektorem `1:5`, jehož jednotlivé prvky se předají funkci `rnorm()` jako její první parametr, tj.\ jako délka vytvářeného vektoru. Střední hodnotu a směrodatnou odchylku vytvářených náhodných vektorů jsme ve funkci `rnorm()` nastavili pomocí pojmenovaných parametrů `mean` a `sd`. Funkce `map()` tedy postupně vyhodnocovala výrazy `rnorm(1, mean = 10, sd = 10`), `rnorm(2, mean = 10, sd = 10)` atd.
Jak jsme viděli, funkce `map()` vrací svůj výsledek jako seznam. To je často užitečné, protože seznamy umožňují v\ jedné datové struktuře skladovat hodnoty různých délek, datových typů a struktur. Někdy však funkce `.f()` vrací na našich datech vždy atomický vektor stejného datového typu a délky\ 1. V\ tom případě můžeme chtít výsledek zjednodušit na atomický vektor. K\ tomu slouží funkce `map_lgl()`, `map_int()`, `map_dbl()` a `map_chr()`, které mají stejné vstupní parametry jako funkce `map()`, ale svůj výsledek zjednoduší na logický, celočíselný nebo reálný vektor nebo vektor řetězců.
Ukážeme si to nejdříve na příkladu, který jsme viděli výše: Chceme zjistit počet chybějících hodnot v\ jednotlivých sloupcích tabulky, výsledek však chceme mít uložený v\ atomickém vektoru (názvy hodnot odpovídají jménům sloupců tabulky):
```r
map_int(df, ~ sum(is.na(.)))
```
```
## x y z
## 0 1 2
```
Pokud by výsledek nebylo možné zjednodušit na daný datový typ, volání funkce by skončilo chybou, jak ukazuje následující příklad:
```r
map_int(1:5, log)
```
```
## Error: Can't coerce element 1 from a double to a integer
```
Podívejme se nyní na komplexnější ukázku použití funkce `map()` převzatou z\ její dokumentace. Řekněme, že chceme zjistit, jak silně závisí spotřeba aut na jejich váze\ -- ve smyslu, kolik procent rozptylu spotřeby vysvětlí váha aut, a\ to zvlášť pro každý počet válců. K\ tomu použijeme standardní dataset `mtcars` (třída *data.frame*). Rozdělíme jej pomocí funkce `split(x, f, drop = FALSE, ...)`. Funkce `split()` bere na vstupu atomický vektor, seznam nebo tabulku `x` a vektor `f`, který určí rozdělení `x` do skupin. Funkce vrací seznam, jehož jednotlivé prvky obsahují části proměnné `x`\ -- každý prvek obsahuje část\ `x`, pro kterou má `f` stejnou hodnotu. Následující kód tedy rozdělí tabulku `mtcars` na tři tabulky uložené v\ jednom pojmenovaném seznamu `cars`. Každá z\ těchto dílčích tabulek bude obsahovat jen pozorování se stejným počtem válců: první tabulka\ 4, druhá\ 6 a třetí 8\ válců:
```r
cars <- split(mtcars, mtcars$cyl)
```
Od verze\ 4.1 může být\ `f` i\ pravostranná formule, která za vlnkou obsahuje jméno proměnné ze zvoleného datasetu, podle jejíchž hodnot se má dataset rozdělit; o\ formulích najdete více v\ oddíle\ \@ref(sec:formula). Stejnou operaci je pak možné zapsat kompaktněji takto:
```r
cars <- split(mtcars, ~cyl)
```
Na každé této tabulce zvlášť odhadneme lineární model, který vysvětluje spotřebu (`mpg`, počet mil, který vůz ujede na galon paliva) pomocí váhy vozidla (`wt`) a úrovňové konstanty. Vlastní odhad provede funkce `lm()`. Její první parametr je formule `mpg ~ wt`, která popisuje odhadovanou rovnici (zde $\textrm{mpg}_i = b_0 + b_1 \textrm{wt}_i + \epsilon_i$). Druhý parametr jsou data, na který se má model odhadnout. Pomocí funkce `map()` aplikujeme funkci `lm()` na každý prvek seznamu `cars`, tj.\ na tři tabulky, které jsme vytvořili v\ předchozím kroku:
```r
estimates <- map(cars, ~ lm(mpg ~ wt, data = .))
```
Výsledkem je seznam, který obsahuje opět tři prvky: objekty, které popisují odhad modelu na třech částech rozdělené tabulky. Nás z\ nich zajímá jediná hodnota: koeficient determinace\ $R^2$, který říká, kolik procent rozptylu vysvětlované veličiny model vysvětlil. Ten získáme jako slot `r.squared` objektu, který vrací funkce `summary()`. Výpočet opět musíme spustit pro všechny prvky seznamu `estimates`:
```r
s <- map(estimates, summary)
map_dbl(s, "r.squared")
```
```
## 4 6 8
## 0.5086326 0.4645102 0.4229655
```
Vidíme, že sama váha vysvětluje velkou část spotřeby. Celý výpočet můžeme zapsat kompaktněji pomocí operátoru `|>` nebo `%>%`, který předá výsledek předchozího výpočtu (svou levou stranu) jako první parametr do funkce v\ následném výpočtu (na své pravé straně), viz oddíl\ \@ref(sec:pipes):
```r
mtcars %>%
split(~ cyl) %>%
map(~ lm(mpg ~ wt, data = .x)) %>%
map(summary) %>%
map_dbl("r.squared")
```
```
## 4 6 8
## 0.5086326 0.4645102 0.4229655
```
Více se o\ ekonometrii dozvíte v\ kapitole\ \@ref(kap:ekonometrie).
Funkce `map_dfc(x, f)` a `map_dfr(x, f)` fungují podobně jako `map()`, ale výsledek transformují na tabulku po sloupcích a řádcích respektive. Detaily najdete v\ dokumentaci. Zde si ukážeme jen několik příkladů. Řekněme, že máme pojmenovaný seznam několika atomických vektorů. Chceme je nějak transformovat (např.\ spočítat jejich druhou mocninu) a výsledky složit vedle sebe jako sloupce tabulky:
Podobně můžeme rozdělit tabulku pomocí funkce `split()` na části, které uložíme do seznamu. Potom, co na každé části tabulky provedeme nějaké operace, můžeme výsledek opět spojit pomocí `map_df()`:
Nakonec si ukážeme poněkud složitější příklad. Řekněme, že máme seznam atomických vektorů\ `s` (zde si jej nasimulujeme) a pro každý vektor v\ seznamu chceme zjistit základní popisné statistiky. Pro jeden vektor nám je spočítá funkce `summary()`. My však chceme tyto statistiky spočítat pro každý jednotlivý vektor a uložit je do tabulky, kde bude každý řádek odpovídat jednomu původnímu vektoru a sloupec jedné statistice. To můžeme udělat takto:
```r
s <- replicate(5, rnorm(100), simplify = FALSE)
map_dfr(s, ~ set_names(as.list(summary(.)), nm = names(summary(.))))
```
```
## # A tibble: 5 × 6
## Min. `1st Qu.` Median Mean `3rd Qu.` Max.
##
## 1 -2.83 -0.568 0.0385 0.134 0.890 3.08
## 2 -2.56 -0.605 0.0540 0.0388 0.840 2.26
## 3 -1.58 -0.706 -0.140 -0.0347 0.634 1.88
## 4 -2.61 -0.659 -0.291 -0.0477 0.586 2.88
## 5 -3.12 -0.691 -0.0541 -0.128 0.288 2.62
```
Výraz `as.list(summary(.)` spočítá statistiky pro právě zpracovávaný prvek a převede je na seznam. Funkce `set_names()` pojmenuje prvky seznamu podle jednotlivých prvků objektu, který vrací funkce `summary()`. Funkce `map_dfr()` tak pro každý vektor v\ seznamu\ `s` získá pojmenovaný seznam statistik. Ten převede na tabulku a jednotlivé tabulky spojí.
Někdy nechceme transformovat všechny, ale jen vybrané prvky nějakého vektoru. K\ tomu slouží funkce `map_if(.x, .p, .f, ..., .else = NULL)` a `map_at(.x, .at, .f, ...)`. Tyto funkce fungují podobně jako `map()`, liší se však tím, na které prvky se použije funkce\ `.f`: `map()` ji použije všechny prvky vektoru\ `.x`, `map_at()` pouze na vyjmenované prvky a `map_if()` pouze na prvky, které splňují nějakou podmínku.
Funkce `map_if()` má stejné parametry jako `map()` plus dva navíc: `.p` je *predikátová funkce* (tj.\ funkce, která vrací logické hodnoty), která určí, které prvky se budou transformovat; funkce `.f` se použije na ty prvky `.x`, kde `.p()` vrací `TRUE`. Pokud je zadaná i\ funkce `.else`, pak se použije na ty prvky `.x`, kde `.p()` vrací `FALSE`. Pokud parametr `.else` nezadáme, pak se tyto prvky ponechají beze změny.
Ukažme si to na příkladu. Řekněme, že máme seznam, který obsahuje vektory čísel a řetězců. Numerické vektory chceme standardizovat tak, že od nich odečteme jejich střední hodnotu, zatímco řetězce chceme ponechat beze změny. To můžeme snadno udělat takto:
```r
v <- list(1:5, rnorm(5), LETTERS[1:5]) # tvorba seznamu
v
```
```
## [[1]]
## [1] 1 2 3 4 5
##
## [[2]]
## [1] -0.9800479 -0.2850950 -0.6050690 -0.2439833 1.3108718
##
## [[3]]
## [1] "A" "B" "C" "D" "E"
```
```r
map_if(v, is.numeric, ~ . - mean(.))
```
```
## [[1]]
## [1] -2 -1 0 1 2
##
## [[2]]
## [1] -0.81938326 -0.12443032 -0.44440432 -0.08331858 1.47153648
##
## [[3]]
## [1] "A" "B" "C" "D" "E"
```
Pokud bychom chtěli v\ jednom kroku zároveň odečíst střední hodnotu od numerických vektorů a řetězce převést na malá písmena, mohli bychom to provést takto:
```r
map_if(v, is.numeric, ~ . - mean(.), .else = stringr::str_to_lower)
```
```
## [[1]]
## [1] -2 -1 0 1 2
##
## [[2]]
## [1] -0.81938326 -0.12443032 -0.44440432 -0.08331858 1.47153648
##
## [[3]]
## [1] "a" "b" "c" "d" "e"
```
Protože tabulky jsou v\ R implementované jako seznam sloupců, můžeme totéž provést i\ s\ tabulkou:
```r
df # původní tabulka
```
```
## # A tibble: 6 × 3
## x y z
##
## 1 1 -1.49
## 2 2 -1.26 a
## 3 3 -1.21 b
## 4 4 1.38 c
## 5 5 -1.68 d
## 6 6 NA
```
```r
map_if(df, is.numeric, ~ . - mean(., na.rm = TRUE))
```
```
## $x
## [1] -2.5 -1.5 -0.5 0.5 1.5 2.5
##
## $y
## [1] -0.6362982 -0.4102703 -0.3580913 2.2309963 -0.8263364 NA
##
## $z
## [1] NA "a" "b" "c" "d" NA
```
Funkce `map_at()` funguje podobně, ale které prvky se mají transformovat, musíme zadat jejich jménem nebo pozicí. Stejně jako u\ výběru prvků vektoru je možné používat kladné i záporné vektory pozic: kladné určují, které se mají transformovat, záporné říkají, které prvky se nemají transformovat. Odečíst střední hodnoty od numerických vektorů tedy můžeme i\ takto:
```r
map_at(df, 1:2, ~ . - mean(., na.rm = TRUE))
```
```
## $x
## [1] -2.5 -1.5 -0.5 0.5 1.5 2.5
##
## $y
## [1] -0.6362982 -0.4102703 -0.3580913 2.2309963 -0.8263364 NA
##
## $z
## [1] NA "a" "b" "c" "d" NA
```
```r
map_at(df, -3, ~ . - mean(., na.rm = TRUE)) # totéž
map_at(df, c("x", "y"), ~ . - mean(., na.rm = TRUE)) # totéž
```
Funkce `map()` a její odvozeniny vracejí vždy konkrétní objekt: `map()` vrací seznam, `map_lgl()` vrací logický vektor apod. Pokud potřebujeme, aby funkce vrátila stejnou datovou strukturu, jako dostala na vstupu, můžete místo nich použít funkci `modify(.x, .f, ...)` a její odvozeniny, `modify_if(.x, .p, .f, ..., .else = NULL)`, `modify_at(.x, .at, .f, ...)`. Tyto funkce se používají stejně jako odpovídající funkce z\ rodiny `map()`. Srovnejte rozdíl:
```r
map(1:3, ~ . + 2L)
```
```
## [[1]]
## [1] 3
##
## [[2]]
## [1] 4
##
## [[3]]
## [1] 5
```
```r
modify(1:3, ~ . + 2L)
```
```
## [1] 3 4 5
```
```r
map_if(df, is.numeric, ~ . ^ 2) %>% str()
```
```
## List of 3
## $ x: num [1:6] 1 4 9 16 25 36
## $ y: num [1:6] 2.21 1.59 1.46 1.9 2.81 ...
## $ z: chr [1:6] NA "a" "b" "c" ...
```
```r
modify_if(df, is.numeric, ~ . ^ 2) %>% str()
```
```
## tibble [6 × 3] (S3: tbl_df/tbl/data.frame)
## $ x: num [1:6] 1 4 9 16 25 36
## $ y: num [1:6] 2.21 1.59 1.46 1.9 2.81 ...
## $ z: chr [1:6] NA "a" "b" "c" ...
```
Funkce `map()` aplikovaná na celočíselný vektor vrací seznam, zatímco funkce `modify()` vrací celočíselný vektor. (Zkuste, co se stane, pokud se pokusíte místo `2L` přičíst `2`.) Podobně funkce `map_if()` aplikovaná na tabulku vrací seznam, zatímco funkce `modify_if()` vrací tabulku stejné třídy, jako je její vstup.
Funkce typu `modify()` se tedy hodí např.\ pro transformace vybraných sloupců tabulek. Řekněme, že máme tabulku s\ příjmy a výdaji z\ hyperinflační ekonomiky. Pokud by došlo k\ měnové reformě, potřebujeme u\ všech numerických sloupců škrtnout tři nuly. To můžeme provést např.\ takto:
```r
df <- tibble::tibble(names = c("Adam", "Bětka", "Cyril"),
income = c(1.3, 1.5, 1.7) * 1e6,
rent = c(500, 450, 580) * 1e3,
loan = c(0, 250, 390) * 1e9)
df <- modify_if(df, is.numeric, ~ . / 1000)
df
```
```
## # A tibble: 3 × 4
## names income rent loan
##
## 1 Adam 1300 500 0
## 2 Bětka 1500 450 250000000
## 3 Cyril 1700 580 390000000
```
V\ kapitole\ \@ref(kap:dplyr) se však naučíte jiné funkce specializované pro práci s\ tabulkami z\ balíku **dplyr**.
Někdy nechceme při iterování nad prvky vektoru vracet hodnoty, nýbrž provést nějaké jiné operace, které iterovaná funkce provádí jako své vedlejší účinky. K\ tomuto účelu slouží funkce `walk(x, f, ...)`. Její syntaxe je stejná jako u\ funkce `map()`. Řekněme, že chceme vypsat všechny prvky nějakého vektoru. To můžeme pomocí funkce `walk()` udělat např.\ takto:
```r
v <- list(1, "a", 3)
walk(v, print)
```
```
## [1] 1
## [1] "a"
## [1] 3
```
Funkce `walk()` tiše vrací vektor\ `v`, takže je možné i\ zařadit i\ do proudu "trubek" (viz\ oddíl\ \@ref(sec:pipes)):
```r
v %>% walk(print) %>% map_int(length)
```
```
## [1] 1
## [1] "a"
## [1] 3
```
```
## [1] 1 1 1
```
To je užitečné např.\ při ladění dlouhého výrazu s\ mnoha "trubkami", protože do proudu můžeme zařadit výpis nebo vykreslení výsledků bez toho, abychom museli "potrubí" narušit.
## Iterace nad více vektory současně
Někdy potřebujeme iterovat nad více vektory současně. Můžeme např.\ chtít vytvořit seznam vektorů tisíce gaussovských náhodných čísel, kde každý vektor bude mít jinou střední hodnotu a směrodatnou odchylku. Pomocí funkce `map()` bychom to mohli udělat např.\ takto:
```r
m <- 0:5 # požadované střední hodnoty
std <- 1:6 # požadované směrodatné odchylky
z <- map(seq_along(m), ~ rnorm(1000, mean = m[.], sd = std[.]))
str(z) # struktura výsledného seznamu
```
```
## List of 6
## $ : num [1:1000] -0.461 0.33 -1.43 -1.874 -1.43 ...
## $ : num [1:1000] -0.325 1.245 1.07 -0.79 0.861 ...
## $ : num [1:1000] 5.626 2.735 6.351 3.167 -0.885 ...
## $ : num [1:1000] 4.3235 3.2419 3.5528 1.0416 -0.0604 ...
## $ : num [1:1000] 5.1324 7.4692 7.7001 8.7294 -0.0565 ...
## $ : num [1:1000] 3.65 16.85 7.11 6.41 13.06 ...
```
```r
z %>% map_dbl(mean) # střední hodnoty jednotlivých vektorů v seznamu
```
```
## [1] -0.008845889 0.987705763 1.996043806 2.876261756 4.047597629
## [6] 5.173388493
```
Balík **purrr** však pro tyto účely nabízí příjemnější funkce. Pro iterace nad dvěma vektory zavádí funkci `map2(.x, .y, .f, ...)` a odpovídající zjednodušující funkce `map2_lgl()`, `map2_int()` atd. Všechny tyto funkce berou vektory\ `.x` a `.y`, nad kterými mají iterovat, jako své první dva parametry. Třetí parametr je jméno iterované funkce (musí brát aspoň dva parametry). Případné další parametry jsou předány funkci\ `.f()` jako její třetí a další parametr. Postup výpočtu ukazuje obrázek\ \@ref(fig:map2).
Pokud chceme stejně jako výše vytvořit pět náhodných gaussovsky rozdělených vektorů se středními hodnotami 0, 1 atd. a směrodatnými odchylkami 1, 2 atd., můžeme je sestavit takto (všimněte si, že další parametry, zde délka vytvářených vektorů, musejí být uvedeny až za jménem iterované funkce):
```r
z <- map2(0:5, 1:6, rnorm, n = 1000)
```
Funkce opět umožňuje zadat místo funkce `.f()` pravostrannou formuli, kterou na funkci sama převede. Zpracovávaný prvek vektoru `.x` v\ zadáme jako `.x`, prvek vektoru `.y` jako `.y`. Řekněme tedy, že chce vytvořit tisíc vektorů náhodných čísel s\ gaussovským rozdělením a různými středními hodnotami a směrodatnými odchylkami, a\ z\ těchto vektorů spočítat jejich střední hodnotu. To můžeme udělat takto:
```r
map2_dbl(0:5, 1:6, ~ mean(rnorm(n = 1000, mean = .x, sd = .y)))
```
```
## [1] -0.0090844 0.9548081 2.1689152 2.9193635 4.0238804 4.5798181
```
Pro iterace nad větším počtem vektorů nabízí **purrr** funkci `pmap(.l, .f, ...)` a její zjednodušující varianty `pmap_lgl()` atd., kde `.l` je buď seznam nebo tabulka vektorů, nad kterými se má iterovat, a\ `.f` je buď funkce, která bere příslušný počet parametrů, nebo pravostranná formule, kterou `pmap()` převede na funkci.
Pokud je `.f` funkce a jednotlivé vektory v `.l` nejsou pojmenované, pak se předávají do `.f` podle svého pořadí. Pokud jsou pojmenované, pak se předávají jménem, takže na jejich fyzickém pořadí v `.l` nezáleží.
Řekněme, že chceme opět vytvořit seznam náhodných výběrů z\ gaussovského rozdělení. Každý výběr bude mít různý počet prvků, různou střední hodnotu a různou směrodatnou odchylku. Pokud seznam parametrů nepojmenujeme, musíme mít jednotlivé parametry v\ seznamu v\ tom pořadí, v\ jakém je očekává funkce `rnorm()`, která vygeneruje náhodná čísla:
```r
n <- (1:5) * 100 # počet pozorování je 100, 200, ..., 500
mu <- 0:4 # střední hodnota je 0, 1, ..., 4
sd <- 1:5 # směrodatná odchylka je 1, 2, ..., 5
pars <- list(n, mu, sd) # nepojmenovaný seznam parametrů v pořadí
z <- pmap(pars, rnorm)
str(z) # struktura výsledku
```
```
## List of 5
## $ : num [1:100] -1.491 -0.686 -0.403 1.684 0.678 ...
## $ : num [1:200] 2.729 -1.024 -1.127 3.067 -0.653 ...
## $ : num [1:300] 6.69 1.83 3.44 8.44 -5.8 ...
## $ : num [1:400] -1.236 0.603 1.435 2.264 6.821 ...
## $ : num [1:500] -0.0399 0.7949 1.5612 -4.9282 -0.2502 ...
```
Pokud jednotlivé parametry v\ seznamu pojmenujeme, na jejich pořadí nezáleží, protože se předají jménem:
```r
pars <- list(sd = sd, mean = mu, n = n) # pojmenovaný seznam parametrů
z <- pmap(pars, rnorm)
str(z) # struktura výsledku
```
```
## List of 5
## $ : num [1:100] -0.103 -1.442 -0.362 -1.134 0.928 ...
## $ : num [1:200] 2.108 -0.947 2.054 3.163 0.216 ...
## $ : num [1:300] 0.151 7.578 5.123 0.777 3.386 ...
## $ : num [1:400] -0.409 6.513 1.538 6.413 -0.164 ...
## $ : num [1:500] 0.982 2.231 4.519 8.021 9.917 ...
```
Pohodlnější je však zadat parametry jako tabulku:
```r
pars <- tibble::tibble(sd = sd, mean = mu, n = n)
z <- pmap(pars, rnorm)
str(z) # struktura výsledku
```
```
## List of 5
## $ : num [1:100] 0.618 -0.934 -0.318 -1.369 -0.685 ...
## $ : num [1:200] 0.41 2.915 1.974 -1.212 0.868 ...
## $ : num [1:300] 6.905 1.235 -0.694 -1.288 1.977 ...
## $ : num [1:400] 3.88 4.36 -4.25 5.63 2.31 ...
## $ : num [1:500] 2.36 6.33 6.09 11.64 10.09 ...
```
Pokud místo funkce zadáme `.f` jako pravostrannou formuli, pak první vektor v\ seznamu nebo tabulce označíme jako `..1`, druhý jako `..2` atd.:
```r
z <- pmap(pars, ~ rnorm(n = ..3, mean = ..2, sd = ..1))
str(z) # struktura výsledku
```
```
## List of 5
## $ : num [1:100] -0.0653 -0.3456 1.2299 1.8481 2.2397 ...
## $ : num [1:200] 3.05 2.68 2.01 4.31 1.96 ...
## $ : num [1:300] 1.987 3.7 0.576 -2.06 6.629 ...
## $ : num [1:400] 1.9583 12.9714 -2.9716 1.2447 0.0701 ...
## $ : num [1:500] 0.123 5.13 4.082 8.409 4.041 ...
```
Balík **purrr** implementuje i\ funkci `modify2()` a funkce `walk2()` a `pwalk()`, které umožňují iterovat vedlejší efekty nad více vektory.
## Filtrace a detekce prvků vektorů
Balík **purrr** implementuje i\ několik funkcí určených k\ filtraci hodnot vektorů.
Funkce `keep(.x, .p, ...)` vrací ty prvky vektoru\ `.x`, pro které predikátová funkce\ `.p()` vrací hodnotu `TRUE`.
Naopak funkce `discard(.x, .p, ...)` vrací ty prvky vektoru\ `.x`, pro které predikátová funkce\ `.p()` vrací hodnotu `FALSE`, tj.\ zahazuje prvky, pro které podmínka platí.
Funkce `head_while(.x, .p, ...)` a `tail_while(.x, .p, ...)` vrací všechny prvky od začátku nebo od konce, pro které funkce\ `.p()` souvisle vrací hodnotu\ `TRUE`. Ve všech těchto funkcích nemusí být `.p` funkce: může to být i\ logický vektor stejné délky jako\ `.x` nebo pravostranná formule, která vrací logickou hodnotu. Jejich použití ukazuje následující příklad:
```r
v <- 1:10
is.odd <- function(x) x %% 2 != 0 # vrací TRUE, když je číslo liché
keep(v, is.odd) # výběr lichých hodnot z vektoru v
```
```
## [1] 1 3 5 7 9
```
```r
keep(v, ~ . %% 2 != 0) # totéž pomocí pravostranné formule
```
```
## [1] 1 3 5 7 9
```
```r
discard(v, is.odd) # vrácení vektoru v bez lichých hodnot
```
```
## [1] 2 4 6 8 10
```
```r
head_while(v, ~ . < 5) # vrácení prvních hodnot menších než 5
```
```
## [1] 1 2 3 4
```
Funkce `compact(.x, .p = identity)` umožňuje ze seznamu vypustit ty prvky, které mají buď hodnotu `NULL` nebo nulovou délku.
```r
compact(list(a = 1, b = 2, c = NULL, d = 4, e = numeric(0)))
```
```
## $a
## [1] 1
##
## $b
## [1] 2
##
## $d
## [1] 4
```
Parametr `.p` umožňuje zadat funkci nebo formuli. Pokud tato funkce vrátí `NULL` nebo prázdný vektor, pak funkce `compact()` vynechá odpovídající prvek. Zbývající hodnoty však nejsou funkcí `.p` nijak transformované. Použití ukazuje triviální příklad:
```r
compact(1:5, .p = ~ .[. < 4]) # zachová pouze prvky menší než 4
```
```
## [1] 1 2 3
```
Funkce `detect(.x, .f, ..., .dir = c("forward", "backward"), .default = NULL)` vrací první položku vektoru\ `.x`, pro kterou vrací `.f` hodnotu\ `TRUE`. Funkce `detect_index(.x, .f, ..., .dir = c("forward", "backward"))` vrací index této položky. Stejně jako výše může `.f` být funkce nebo pravostranná formule, která vrací logickou hodnotu.
```r
detect(v, is.odd) # první lichá hodnota ve vektoru v
```
```
## [1] 1
```
```r
detect(v, ~ . > 1) # první hodnota větší než 1
```
```
## [1] 2
```
```r
detect_index(v, is.odd) # index prvního lichého prvku vektoru v
```
```
## [1] 1
```
```r
detect_index(v, ~ . > 1) # index prvního prvku většího než 1
```
```
## [1] 2
```
Dva zbývající parametry určují směr, odkud se budou hodnoty hledat (parametr `.dir`, implicitně zepředu), a\ jaká hodnota se vrátí, pokud žádný prvek vektoru nesplňuje zadaný predikát (parametr `.default`).
Funkce `every(.x, .p, ...)` a `some(.x, .p, ...)` zobecňují logické funkce `all()` a `any()`. `every()` vrací `TRUE`, pokud zadaná predikátová funkce `.p` vrací pro každý prvek vektoru `.x` hodnotu `TRUE`; funkce `some()` vrací `TRUE`, pokud `.f` vrací `TRUE` aspoň pro jeden prvek `.x`. Pomocí těchto funkcí můžeme např.\ otestovat, zda tabulka `df` obsahuje aspoň jeden numerický sloupec (`some()`) nebo jen numerické sloupce (`every()`):
```r
df %>% some(is.numeric) # obsahuje df aspoň jeden numerický sloupec?
```
```
## [1] TRUE
```
```r
df %>% every(is.numeric) # obsahuje df pouze numerické sloupce?
```
```
## [1] FALSE
```
Funkce `has_element(.x, .y)` zobecňuje operátor `%in%`. Vrací `TRUE`, pokud vektor `.x` obsahuje objekt `.y`.
```r
x <- list(1:5, "a") # prvky x jsou vektory 1:5 a "a"
has_element(x, 1:5)
```
```
## [1] TRUE
```
```r
has_element(x, 3)
```
```
## [1] FALSE
```
Balík **purrr** nabízí i\ užitečnou funkci `negate()`, která transformuje zadanou funkci tak, že vrací její negaci. Pokud bychom chtěli pomocí `keep()` a naší funkce `is.odd()` vybrat sudé prvky, museli bychom použít formuli:
```r
keep(1:10, ~ !is.odd(.))
```
```
## [1] 2 4 6 8 10
```
Pomocí funkce `negate()` však můžeme negovat celou predikátovou funkci `is.odd()`:
```r
keep(1:10, negate(is.odd))
```
```
## [1] 2 4 6 8 10
```
## Výběr a úpravy prvků vektorů
Často potřebujeme ze složitější struktury získat jeden její prvek. Obecně k\ tomu slouží funkce `[[` (dvojité hranaté závorky). Funkce `pluck(.x, ..., .default = NULL)` tuto myšlenku zobecňuje. Umožňuje vybrat libovolně zanořený prvek vektoru `.x`. Ukážeme si to na příkladu vektoru hráčů:
```r
dungeon %>% str()
```
```
## List of 2
## $ :List of 3
## ..$ id : num 11
## ..$ name : chr "Karel"
## ..$ items:List of 2
## .. ..$ : chr "sword"
## .. ..$ : chr "key"
## $ :List of 3
## ..$ id : num 12
## ..$ name : chr "Emma"
## ..$ items:List of 3
## .. ..$ : chr "mirror"
## .. ..$ : chr "potion"
## .. ..$ : chr "dagger"
```
První parametr `pluck()` je vektor, ze kterého vybíráme. Další parametry jsou pozice nebo jména prvků, které vybíráme. Pokud zadáme víc položek, pak výběr funguje rekurzivně: druhá položka vybírá z\ výsledku prvního výběru atd.:
```r
pluck(dungeon, 1)
```
```
## $id
## [1] 11
##
## $name
## [1] "Karel"
##
## $items
## $items[[1]]
## [1] "sword"
##
## $items[[2]]
## [1] "key"
```
```r
pluck(dungeon, 1, "name")
```
```
## [1] "Karel"
```
```r
pluck(dungeon, 1, "items")
```
```
## [[1]]
## [1] "sword"
##
## [[2]]
## [1] "key"
```
```r
pluck(dungeon, 1, "items", 1)
```
```
## [1] "sword"
```
K\ výběru můžeme použít i\ funkci, která vrací výběr z\ vektoru:
```r
artefact <- function(x) x[["items"]] # funkce vrací artefakty vybraného hráče
artefact(dungeon[[1]]) # seznam všech artefaktů prvního hráče
```
```
## [[1]]
## [1] "sword"
##
## [[2]]
## [1] "key"
```
```r
pluck(dungeon, 1, artefact, 1) # 1. artefakt prvního hráče
```
```
## [1] "sword"
```
Všechny tyto výběry můžeme samozřejmě provést i\ pomocí základních funkcí\ R, syntaxe `pluck()` je však přehlednější. Poslední výběr bychom např.\ museli zadat takto:
```r
artefact(dungeon[[1]])[[1]]
```
```
## [1] "sword"
```
Pokud hledaný prvek ve vektoru neexistuje, funkce `pluck()` vrátí `NULL`. Tuto hodnotu můžeme změnit pomocí parametru `.default`:
```r
pluck(dungeon, 3)
```
```
## NULL
```
```r
pluck(dungeon, 3, .default = NA)
```
```
## [1] NA
```
Pokud bychom potřebovali, aby funkce raději zhavarovala, můžeme místo `pluck()` použít funkci `chuck(x, ...)`:
```r
chuck(dungeon, 3)
```
```
## Error: Index 1 exceeds the length of plucked object (3 > 2)
```
Funkce `pluck()` umožňuje i\ měnit hodnotu vybraného prvku (zde bohužel není možné při výběru použít funkci jako je např.\ naše funkce `artefact()`):
```r
pluck(dungeon, 1, "items", 1) <- "megaweapon"
str(dungeon)
```
```
## List of 2
## $ :List of 3
## ..$ id : num 11
## ..$ name : chr "Karel"
## ..$ items:List of 2
## .. ..$ : chr "megaweapon"
## .. ..$ : chr "key"
## $ :List of 3
## ..$ id : num 12
## ..$ name : chr "Emma"
## ..$ items:List of 3
## .. ..$ : chr "mirror"
## .. ..$ : chr "potion"
## .. ..$ : chr "dagger"
```
První hráč má nyní místo meče nějakou "megazbraň".
Pokud potřebujete změnit nějaký prvek vektoru, můžete použít funkce `assign_in()` a `modify_in()`. Na jejich použití se podívejte do dokumentace.
## Zabezpečení iterací proti chybám
Pokud jedna z\ iterací ve funkci `map()` a spol. skončí chybou, skončí chybou celé volání této funkce. Pak může být obtížné zjistit, který prvek vektoru chybu způsobil. Jedním z\ přínosů balíku **purrr** je to, že zavádí několik speciálních funkcí, které umožňují tento problém vyřešit. Všechny tyto funkce fungují tak, že transformují funkci `.f` ještě před tím, než vstoupí do `map()`\ -- na vstupu vezmou funkci a vrací její zabezpečenou verzi odolnou vůči selhání.
První z\ těchto funkcí je `safely(.f, otherwise = NULL, quiet = TRUE)`. Tato funkce bere na vstupu iterovanou funkci a vrací její modifikovanou verzi, která nikdy neskončí chybou a která vrací seznam dvou prvků: výsledku a chybového objektu. Pokud původní funkce proběhla, má chybová hlášení hodnotu\ `NULL`, pokud skončila chybou, má hodnotu\ `NULL` výsledek. Protože funkce vrací seznam, je možné ji použít pouze ve funkci `map()`, ne v\ jejích zjednodušujících variantách jako je `map_lgl()` apod.
Ukážeme si použití této funkce na příkladu. Máme seznam\ `v`, který obsahuj většinou čísla, mezi která je však přimíchaný jeden řetězec. Pro každý prvek\ `v` chceme spočítat vektor jeho logaritmů (pomocí funkce `map()` přesto, že funkce `log()` je vektorizovaná). Přímé volání funkce `log()` skončí chybou:
```r
v <- list(1, 2, "a", 5)
map(v, log)
```
```
## Error in .Primitive("log")(x, base): non-numeric argument to mathematical function
```
Pokud funkci `log()` "obalíme" funkcí `safely()`, výpočet proběhne až do konce a výsledkem bude struktura popsaná výše. Všimněte si, že v\ 1., 2. a 4.\ prvku struktury má chybová složka hodnotu\ `NULL`. Ve 3.\ prvku, který zhavaroval, obsahuje chybová složka seznam, který obsahuje objekt třídy *error*. Ruční prohlídka našeho výsledku by nám umožnila zjistit, že je to 3.\ prvek vektoru\ `v`, který způsobuje chyby:
```r
result <- map(v, safely(log))
str(result) # struktura výsledku
```
```
## List of 4
## $ :List of 2
## ..$ result: num 0
## ..$ error : NULL
## $ :List of 2
## ..$ result: num 0.693
## ..$ error : NULL
## $ :List of 2
## ..$ result: NULL
## ..$ error :List of 2
## .. ..$ message: chr "non-numeric argument to mathematical function"
## .. ..$ call : language .Primitive("log")(x, base)
## .. ..- attr(*, "class")= chr [1:3] "simpleError" "error" "condition"
## $ :List of 2
## ..$ result: num 1.61
## ..$ error : NULL
```
Ruční hledání chyb je však možné jen v\ případě, že zpracovávaná data jsou velmi malá. Balík **purrr** naštěstí umožňuje proces hledání chyby zautomatizovat. Stačí na výsledek předchozího výpočtu použít funkci `transpose()`, která změní seznam párů na pár seznamů. Výraz `transpose(result)` tedy vrátí seznam dvou prvků: první obsahuje všechny výsledky a druhý všechna chybová hlášení (oboje uložené jako seznamy):
```r
transpose(result)
```
```
## $result
## $result[[1]]
## [1] 0
##
## $result[[2]]
## [1] 0.6931472
##
## $result[[3]]
## NULL
##
## $result[[4]]
## [1] 1.609438
##
##
## $error
## $error[[1]]
## NULL
##
## $error[[2]]
## NULL
##
## $error[[3]]
##
##
## $error[[4]]
## NULL
```
To nám umožní najít problematické prvky vektoru\ `v` např.\ tak, že vybereme pouze část chybových hlášení a z\ ní sestavíme logický vektor, který bude mít hodnotu\ `TRUE` tam, kde hodnota chyby není\ `NULL`, tj.\ volání funkce selhalo. Logický vektor pak můžeme použít k\ nalezení indexů prvků vektoru, kde k\ chybě došlo (pomocí funkce `which()`), nebo k\ vypsání hodnot, které chybu způsobily:
```r
bugs <- transpose(result)$error # transpozice a výběr chybové složky
bugs <- !map_lgl(bugs, is.null) # TRUE, kde error není NULL, tj. kde je chyba
which(bugs) # index prvků v, kde nastala chyba (jako vektor)
```
```
## [1] 3
```
```r
x[bugs] # hodnoty prvků v, kde nastala chyba (jako seznam)
```
```
## [[1]]
## NULL
```
Pokud nás nezajímají chyby, ale pouze ty výsledky, které se skutečně spočítaly, můžeme použít funkci `possibly(.f, otherwise, quiet = TRUE)`. Tato funkce zabezpečí funkci `.f` tak, že nikdy nezhavaruje. Pokud není schopná spočítat výsledek, vrátí místo něj hodnotu zadanou v\ parametru `otherwise` (pokud není zadána, funkce zhavaruje). Díky tomu funkce `possibly()` vrací jen vlastní výsledky, takže může být využitá i\ ve zjednodušujících variantách `map()`:
```r
map_dbl(v, possibly(log, otherwise = NA_real_)) # chybná hodnota je nahrazena NA
```
```
## [1] 0.0000000 0.6931472 NA 1.6094379
```
Pokud byste místo zachytávání chyb potřebovali zachytit zprávy a varování, která vrací iterovaná funkce, můžete použít funkci `quietly(.f)`.
Poslední funkce, kterou balík **purrr** nabízí k\ ovlivnění výpočtu, je funkce `auto_browse(.f)`, která transformuje funkci\ `.f` tak, že v\ případě chyby, automaticky spustí ladící mechanismus:
```r
map(v, auto_browse(log))
```
## Rekurzivní kombinace prvků vektorů {#sec:reduce}
Někdy máme seznam objektů, na které potřebujeme aplikovat funkci, která však bere jen dva vstupy. Patrně nejdůležitější příklad takového užití je spojení mnoha tabulek uložených v\ seznamu pomocí funkce `left_join()`, se kterým se seznámíte v\ kapitole\ \@ref(kap:dplyr). V\ takovém případě chceme aplikovat funkci postupně: nejprve spojit první dvě tabulky, pak k\ výsledku připojit třetí tabulku, k\ výsledku tohoto spojení čtvrtou atd. Balík **purrr** k\ tomuto účelu nabízí čtyři funkce: `reduce(.x, .f, ..., .init, .dir = c("forward", "backward"))` a `reduce2(.x, .y, .f, ..., .init)` a `accumulate(.x, .f, ..., .init, .dir = c("forward", "backward"))` a `accumulate2(.x, .y, .f, ..., .init)`. My se zde podíváme jen na funkce `reduce()` a `accumulate()`, které pracují nad jedním seznamem; druhé dvě funkce pracují paralelně nad dvěma.
Funkce `accumulate()` postupně aplikuje na vektor `.x` funkci `.f` a vrací seznam stejné délky jako `.x` (nebo o\ 1\ delší, viz dále). Prvním prvkem výsledného vektoru je `.x[[1]]`, druhým `.f(.x[[1]], .x[[2]])`, třetím `.f(.f(.x[[1]], .x[[2]]), .x[[3]])` atd. Pokud tedy "akumulujeme" atomický vektor čísel $1, 2, \ldots, N$ pomocí funkce součtu\ `+`, pak dostaneme atomický vektor čísel $1, 1 + 2, 1 + 2 + 3, \dots$, tedy totéž, co by nám vrátila funkce `cumsum()`:
```r
accumulate(1:5, `+`)
```
```
## [1] 1 3 6 10 15
```
Funkce `reduce()` funguje podobně, ale vrací jen finální výsledek akumulace, zde tedy součet všech prvků vektoru:
```r
reduce(1:5, `+`)
```
```
## [1] 15
```
Podívejme se nyní na realističtější příklad. Řekněme, že máme seznam atomických vektorů, které obsahují id jedinců, kteří se zúčastnili nějakých akcí. Zajímá nás, kteří jedinci, se zúčastnili *všech* těchto akcí, tj.\ hledáme průnik všech těchto množin. K\ tomu slouží funkce `intersect(x, y)`, která však umí vrátit pouze průnik dvou množin. Musíme ji tedy použít na všechny prvky seznamu rekurzivně. Protože nás zajímá jen finální průnik, použijeme funkci `reduce()`:
```r
riots <- list( # seznam id účastníků různých akcí
c(1, 2, 3, 7, 9),
c(1, 4, 8, 9),
c(1, 3, 5, 9)
)
reduce(riots, intersect) # celkový průsečík množin
```
```
## [1] 1 9
```
Funkce `reduce()` a `accumulate()` umožňují zadat i\ počáteční hodnotu pomocí parametru `.init`. To se hodí v\ případě, kdy by akumulovaný vektor mohl být prázdný a my nechceme, aby výpočet zhavaroval.
```r
v <- numeric(0)
reduce(v, `+`)
```
```
## Error: `.x` is empty, and no `.init` supplied
```
```r
reduce(v, `+`, .init = 0)
```
```
## [1] 0
```
Pokud zadáme parametr `.init`, bude výsledek funkce `accumulate()` o\ 1\ delší než vstupní vektor.
Parametr `.dir` umožňuje nastavit směr akumulace (implicitně se akumuluje od prvního prvku vektoru po poslední). Detaily najdete v dokumentaci.
## Paralelizace výpočtu
Funkce `map()` a spol. pracují s\ každým prvkem vektoru samostatně a izolovaně, takže nezáleží na pořadí, v\ jakém jsou tyto prvky zpracovány. To, mimo jiné, umožňuje paralelizaci výpočtu, tj.\ zpracování každého prvku na jiném jádře počítače nebo jiném prvku počítačového klastru. Pokud jsou data tak velká, že se to vyplatí, je možné použít balík **furrr**.
Balík **furrr** implementuje paralelizované ekvivalenty funkcí `map()`, `map2()`, `pmap()`, `modify()` a všech jejich variant, které zjednodušují výsledek na atomický vektor nebo tabulku jako je `map_dbl()` nebo transformují jen vybrané prvky jako `map_at()`. Tyto alternativní funkce mají konzistentní pojmenování: před jméno dané funkce připojují `future_`, takže místo `map()` máme `future_map()` apod. Všechny tyto funkce také vracejí stejné výsledky a berou stejné parametry jako odpovídající funkce z\ balíku **purrr** (plus parametr `.progress`, který umožňuje sledovat průběh výpočtu pomocí progress baru, a parametr `.options`, který umožňuje předávat dodatečné informace paralelizačnímu stroji).
Před použitím těchto funkcí je třeba nejen načíst balík **furrr**, ale i\ nastavit paralelizaci. K\ tomu slouží funkce `plan()`. Její hlavní parametr určuje, jak se bude paralelizovat. Implicitní hodnota je `sequential`, tj.\ normální výpočet na jednom jádře bez paralelizace. Typické nastavení je
```r
library(furrr)
plan(multiprocess)
```
které nastaví nejlepší dostupnou paralelizaci na daném stroji. Vlastní iterace jsou pak stejné jako s\ balíkem **purrr**, stačí jen přidat `future_` ke jménu funkce, tj.\ např.\ volat:
```r
future_map_dbl(1:5, ~ . ^ 2)
```
```
## [1] 1 4 9 16 25
```
Na velmi stylizovaném příkladu si ukážeme, jak paralelizace zrychluje výpočet. Pomocí funkce `map()` a `future_map()` spustíme třikrát výraz `Sys.sleep(1)`, který na 1\ sekundu pozastaví výpočet. Pomocí funkce `system.time()` změříme, jak dlouho tento "výpočet" trval:
```r
system.time(map(1:3, ~ Sys.sleep(1)))
```
```
## user system elapsed
## 0.001 0.000 3.004
```
```r
system.time(future_map(1:3, ~ Sys.sleep(1)))
```
```
## user system elapsed
## 0.042 0.000 1.087
```
Zatímco s\ pomocí `map()` trval, jak bychom očekávali, zhruba tři sekundy, s\ pomocí `future_map()` zabral jen o\ málo víc než 1\ sekundu, protože každé jednotlivé volání `Sys.sleep(1)` proběhlo na mém osmijádrovém počítači v\ jiném sezení\ R. Výsledek by byl ještě rychlejší, kdybych na svém Linuxovém stroji nekompiloval tento text v\ RStudiu, viz dále.
Implicitně se používají všechna jádra počítače. To je možné změnit tak, že nejprve pomocí funkce `availableCores()` zjistíme počet dostupných jader, a\ pak nastavíme počet jader použitých výpočtu ve funkci `plan()` pomocí parametru `workers`. Pokud tedy chceme např.\ jedno jádro ušetřit pro ostatní procesy běžící na počítači, můžeme paralelizaci naplánovat takto:
```r
n_cores <- availableCores()
plan(multiprocess, workers = n_cores - 1)
```
Popis podporovaných způsobů paralelizace najdete v\ dokumentaci k\ funkci `plan()` a v\ základní vinětě k\ balíku **future**, který se stará o\ backend. Základní způsoby paralelizace jsou čtyři: `sequential` provádí jednotlivé výpočty normálně bez paralelizace na jednom jádře, `multisession` spouští jednotlivé výpočty v\ nových kopiích\ R, `multicore` vytváří forky procesů\ R a `cluster` používá počítačový klastr. Ve většině případů budeme pracovat na jednom počítači, takže volíme mezi `multisession` a `multicore`. `mulitcore` je při tom efektivnější, protože nemusí kopírovat pracovní prostředí (globální proměnné, balíky apod.) do nového procesu, zatímco `multisession` musí překopírovat potřebné objekty do nového sezení\ R. `multicore` však není dostupný ve Windows, které fork vůbec nepodporují, ani při spuštění výpočtu v\ rámci RStudia. Nejrozumnější je proto použít `multiprocess`, který buď o\ zavolá `multicore` (na Linuxu nebo macOS při spuštění R mimo RStudio) nebo `multisession` (jinak).
Při spuštění `multisession` musí `future_map()` a spol. překopírovat do nového sezení potřebné proměnné. Většinou to funguje bezproblémově automaticky. Pokud však něco selže, přečtěte si viněty k\ balíku **future**, které vysvětlují, jak potřebné proměnné do nového sezení nakopírovat ručně a jak vyřešit i\ další případné problémy. Ani jeden z\ autorů tohoto textu však při použití **furrr** zatím na takový problém nenarazil.
Potřeba přesouvat data do a z\ nového procesu také znamená, že paralelní výpočet na čtyřech jádrech nebude typicky čtyřikrát rychlejší než na jednom. Pokud byste spouštěli jen malý počet velmi rychlých výpočtů na velkých datech, mohlo by se dokonce stát, že výpočet bude *pomalejší* než na jednom jádře. Paralelizovat se tak vyplatí jen výpočty, kde každé spuštění funkce nad prvkem seznamu trvá poměrně dlouho nebo se zpracovává poměrně hodně prvků.
## Srovnání map() s\ cyklem for {#sec:lapply-for}
Většinu začátečníků funkce typu `map()` děsí a snaží se daný problém řešit pomocí cyklů. To je samozřejmě možné. Vraťme se k\ příkladu, kdy máme nějakou tabulku a chceme zjistit, kolik který její sloupec obsahuje chybějících hodnot.
Pomocí funkce `map_int()` to uděláme na jednom řádku:
```r
map_int(df, ~ sum(is.na(.)))
```
```
## names income rent loan
## 0 0 0 0
```
Pokud budeme místo toho chtít použít cyklus `for`, bude náš kód vypadat takto:
```r
result <- integer(ncol(df))
for (k in seq_len(ncol(df)))
result[k] <- sum(is.na(df[[k]]))
names(result) <- names(df)
result
```
```
## names income rent loan
## 0 0 0 0
```
Výsledek je v\ obou případech stejný, ale kód napsaný pomocí cyklů má několik nevýhod: 1)\ Kód napsaný pomocí cyklu `for` bývá obvykle delší a podstatně méně přehledný. Funkce `map()` jasně ukazuje, na jakých datech se iteruje a co se na nich iteruje. V\ kódu cyklu to není zdaleka tak jasně vidět. 2)\ Při použití cyklu `for` musíte myslet na víc věcí: musíte si předalokovat vektor pro uložení výsledku, musíte vektor pojmenovat (pokud o\ to stojíte) a musíte přemýšlet, jestli použijete jednoduché nebo dvojité hranaté závorky (podle toho, zda je původní vektor a výsledek atomický vektor, seznam, nebo tabulka typu *data.frame* nebo *tibble*). 3)\ Zaplevelíte si pracovní prostředí nechtěnými proměnnými: možná proměnnou `result`, určitě však počítadlem cyklu\ `k`. A\ 4)\ Mapovací funkce je mnohem jednodušší paralelizovat. Obecně je proto lepší vždy používat funkce typu `map()` raději než explicitní cyklus\ `for`.
Přesto však existují situace, kdy je využití cyklů nezbytné. Hlavní případy jsou dva: 1)\ Když výpočet $i$-té hodnoty záleží na některé z\ hodnot spočítaných dříve a 2)\ když pro ušetření času nahrazujeme hodnoty přímo v\ původní datové struktuře, místo abychom celou strukturu vytvářeli znovu. V\ ostatních situacích je téměř vždy rozumnější použít některou z\ funkcí typu `map()`.
## Aplikace
Někdy potřebujeme načíst velké množství tabulek z\ jednotlivých `.csv` souborů, zkontrolovat jejich konzistenci a spojit je dohromady. To dělá následující kód, který představuje zjednodušenou verze části jednoho mého projektu:
```r
# načtení potřebných balíků
library(readr)
library(purrr)
# adresář s daty
DATADIR <- file.path("..", "testdata")
# seznam jmen souborů, které splňují určitou masku danou regulárním výrazem
PRODUCT_FILES <- dir(DATADIR, "products_.*\\.csv\\.gz", full.names = TRUE)
# načtení jednotlivých souborů, sloupcům vnutíme typ řetězec
product_data <- map(PRODUCT_FILES,
~ read_csv2(file = ., col_types = cols(.default = "c"))
# kontrola, že všechny tabulky mají stejnou strukturu
product_col_names <- map(product_data, colnames)
product_col_names_test <- map_chr(product_col_names,
~ all.equal(product_col_names[[1]], .))
if (!(all(identical(product_col_names_test,
rep(TRUE, length = length(product_col_names))))))
stop("Product data sets have different columns!")
# spojení jednotlivých tabulek do jedné
product_data <- reduce(product_data, rbind)
```
Vlastní spojení dat je možné provést efektivněji pomocí funkce `bind_rows()` z\ balíku **dplyr**, která je efektivnější a rychlejší:
```r
product_data <- dplyr::bind_rows(product_data)
```