10.1 Základní iterace nad prvky vektorů pomocí map()

Nejcastejsim pripadem je, ze potrebujeme pomoci nejake funkce transformovat kazdy prvek vektoru (atomickeho nebo seznamu). Zakladni funkce pro iterace nad vektory je funkce map(.x, .f, ...). Jeji prvni parametr je vektor .x (atomicky nebo seznam) a druhy parametr je funkce .f. Funkce map() spusti funkci .f na kazdy prvek vektoru .x a vysledky posklada do seznamu stejne delky, jako ma vektor .x. Funkci map() si tedy muzete predstavit jako vyrobni linku, kde nad pasovym dopravnikem pracuje robot. Dopravnik nejdrive robotovi doruci prvni prvek .x, tj. .x[[1]]. Robot na dorucenem prvku provede funkci .f(), tj. vyhodnoti .f(x[[1]]) a vysledek ulozi zpatky na dopravnikovy pas, tj. jako prvni prvek nove vytvareneho seznamu. Pak se dopravnik posune, robot spusti funkci .f() na .x[[2]] atd., dokud linka nezpracuje vsechny prvky vektoru .x. Fungovani linky ukazuje obrazek 10.1.

Funkce `map(.x, .f)` aplikuje funkci `.f()` na každý prvek vektoru `.x` a vrací seznam stejné délky.

Obrázek 10.1: Funkce map(.x, .f) aplikuje funkci .f() na každý prvek vektoru .x a vrací seznam stejné délky.

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:

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.frametibble jsou technicky implementované jako seznamy sloupců a že funkce map() s nimi takto zachází:

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    
##   <int>   <dbl> <chr>
## 1     1 -0.0509 <NA> 
## 2     2 -0.0864 a    
## 3     3  0.0728 b    
## 4     4  0.179  c    
## 5     5 -0.113  d    
## 6     6 NA      <NA>
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:

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:

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:

map(df, ~ sum(is.na(.)))

nebo

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:

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:

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:

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ů):

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ě:

map(dungeon, c(3, 1))
## [[1]]
## [1] "sword"
## 
## [[2]]
## [1] "mirror"
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:

map(dungeon, list("items", 3))
## [[1]]
## NULL
## 
## [[2]]
## [1] "dagger"
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:

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():

map(1:5, rnorm, mean = 10, sd = 10)
## [[1]]
## [1] 8.961861
## 
## [[2]]
## [1] 20.34482  7.81152
## 
## [[3]]
## [1] -10.83198  14.18439  15.38682
## 
## [[4]]
## [1] 17.042953 20.462867  7.032497 16.098338
## 
## [[5]]
## [1] 12.856612  9.467170  1.192188  8.717749 27.916013

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):

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:

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ů:

cars <- split(mtcars, mtcars$cyl)

Od verze 4.1 muze byt f i pravostranna formule, ktera za vlnkou obsahuje jmeno promenne ze zvoleneho datasetu, podle jejichz hodnot se ma dataset rozdelit; o formulich najdete vice v oddile 18.2. Stejnou operaci je pak možné zapsat kompaktněji takto:

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:

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:

s <- map(estimates, summary)
map_dbl(s, "r.squared")
##         4         6         8 
## 0.5086326 0.4645102 0.4229655

Vidime, ze sama vaha vysvetluje velkou cast spotreby. Cely vypocet muzeme zapsat kompaktneji pomoci operatoru |> nebo %>%, ktery preda vysledek predchoziho vypoctu (svou levou stranu) jako prvni parametr do funkce v naslednem vypoctu (na sve prave strane), viz oddil 5.6:

mtcars %>%
  split(~ cyl) %>%
  map(~ lm(mpg ~ wt, data = .x)) %>%
  map(summary) %>%
  map_dbl("r.squared")
##         4         6         8 
## 0.5086326 0.4645102 0.4229655

Vice se o ekonometrii dozvite v kapitole 18.

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:

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.
##   <dbl>     <dbl>   <dbl>   <dbl>     <dbl> <dbl>
## 1 -2.85    -0.393  0.294   0.204      0.944  2.54
## 2 -2.47    -0.673 -0.0701 -0.0889     0.648  1.86
## 3 -2.31    -0.648 -0.119  -0.0530     0.526  2.27
## 4 -1.70    -0.407  0.210   0.210      0.902  2.11
## 5 -3.08    -0.571  0.130   0.0737     0.740  2.80

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:

v <- list(1:5, rnorm(5), LETTERS[1:5])  # tvorba seznamu
v
## [[1]]
## [1] 1 2 3 4 5
## 
## [[2]]
## [1]  0.4863157  0.4915921 -0.9870787 -0.6880310  0.3773071
## 
## [[3]]
## [1] "A" "B" "C" "D" "E"
map_if(v, is.numeric, ~ . - mean(.))
## [[1]]
## [1] -2 -1  0  1  2
## 
## [[2]]
## [1]  0.5502946  0.5555711 -0.9230998 -0.6240520  0.4412861
## 
## [[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:

map_if(v, is.numeric, ~ . - mean(.), .else = stringr::str_to_lower)
## [[1]]
## [1] -2 -1  0  1  2
## 
## [[2]]
## [1]  0.5502946  0.5555711 -0.9230998 -0.6240520  0.4412861
## 
## [[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:

df  # původní tabulka
## # A tibble: 6 × 3
##       x       y z    
##   <int>   <dbl> <chr>
## 1     1 -0.0509 <NA> 
## 2     2 -0.0864 a    
## 3     3  0.0728 b    
## 4     4  0.179  c    
## 5     5 -0.113  d    
## 6     6 NA      <NA>
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.05120135 -0.08667126  0.07249691  0.17891993 -0.11354423          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:

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.05120135 -0.08667126  0.07249691  0.17891993 -0.11354423          NA
## 
## $z
## [1] NA  "a" "b" "c" "d" NA
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:

map(1:3, ~ . + 2L)
## [[1]]
## [1] 3
## 
## [[2]]
## [1] 4
## 
## [[3]]
## [1] 5
modify(1:3, ~ . + 2L)
## [1] 3 4 5
map_if(df, is.numeric, ~ . ^ 2) %>% str()
## List of 3
##  $ x: num [1:6] 1 4 9 16 25 36
##  $ y: num [1:6] 0.00259 0.00746 0.0053 0.03212 0.01282 ...
##  $ z: chr [1:6] NA "a" "b" "c" ...
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] 0.00259 0.00746 0.0053 0.03212 0.01282 ...
##  $ 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:

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
##   <chr>  <dbl> <dbl>     <dbl>
## 1 Adam    1300   500         0
## 2 Bětka   1500   450 250000000
## 3 Cyril   1700   580 390000000

V kapitole 16 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:

v <- list(1, "a", 3)
walk(v, print)
## [1] 1
## [1] "a"
## [1] 3

Funkce walk() tise vraci vektor v, takze je mozne i zaradit i do proudu "trubek" (viz oddil 5.6):

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.