7.2 Opakování kódu

V některých situacích potřebujeme, aby se nějaký kus kódu provedl vícekrát. K tomu slouží cykly. Existuje několik typů cyklů, které se liší podle toho, kolikrát se má daný kus kódu zopakovat.

7.2.1 Cyklus se známým počtem opakování

Nejjednodussi je cyklus for, ktery pouzijeme, kdyz chceme nejaky kus kodu provest \(x\)-krat, kde \(x\) je libovolne cislo zname pred zapocetim cyklu. Cykly se pouzivaji nejcasteji k rekurzivnim vypoctum. Rekneme napr., ze potrebujete nasimulovat data o nahodne prochazce. Pocatecni hodnota \(y_1 = 0\), dalsi hodnoty jsou konstruovane rekurzivne jako \(y_t = y_{t-1} + \epsilon_t\) pro \(t>1\), kde \(\epsilon_t\) je nahodna chyba vygenerovana ze standardizovaneho normalniho rozdeleni. Vektor tisice pozorovani je pak mozne nasimulovat napr. takto (vysledek ukazuje obrazek 7.1):

N <- 1000
y <- numeric(N)
y[1] <- 0  # zbytečné, ale pro přehlednost lepší
for (t in 2:N)  # vlastní cyklus
    y[t] <- y[t - 1] + rnorm(1)  # kód, který se opakuje 999x
plot(y, type = "l")  # vykreslení výsledku
Výsledek simulace náhodné procházky pomocí cyklu.

Obrázek 7.1: Výsledek simulace náhodné procházky pomocí cyklu.

Náš kód funguje takto: Nejdříve ze všeho vytvoříme celý vektor y. Sice bychom mohli začít vektorem délky 1 a postupně jej prodlužovat, to by však nutilo R neustále doalokovávat další paměť a výrazně by to zpomalilo výsledek. Proto je vždy lepší si dopředu předalokovat celou potřebnou paměť tím, že vytvoříme celé datové struktury ve velikosti, jakou bude mít výsledek výpočtu.

Vlastní cyklus začíná klíčovým slovem for. Proměnná t je počítadlo cyklu a postupně nabývá hodnot z vektoru uvedeného za klíčovým slovem in. Protože chceme, aby t postupně nabývalo hodnot 2 až 1 000, mohlo by se zdát jednodušší napsat t in 2:1000. To by však nebyl dobrý nápad. Nyní sice chceme simulovat právě tisíc pozorování, ale v budoucnu se možná rozhodneme jinak. Bezpečnější tak určitě je napsat t in 2:N, protože pak můžeme počet pozorování změnit na jediném řádku, a vše bude korektně fungovat. Cyklus for proběhne v našem případě tak, že \(N-1\) krát spustí řádek y[t] <- y[t - 1] + rnorm(1), přičemž počítadlo t bude postupně nabývat hodnot 2, 3, 4 atd., až se naplní celý vektor y. (Výsledek simulace záleží na generátoru pseudonáhodných čísel, takže bude pokaždé jiný.)

Stejně jako podmínka if i cyklus for provádí vždy jen jeden výraz. Pokud chceme, aby provedl více výrazů, pak musíme tyto výrazy uzavřít do složených závorek, tj. vytvořit z nich blok, jak ukazuje následující příklad:

N <- 1000
y <- numeric(N)
z <- numeric(N)
for (t in 2:1000) {
    y[t] <- y[t - 1] + rnorm(1)
    z[t] <- sum(y[1:t])
}

Opět je vhodné kód výrazně odsadit, abychom zlepšili jeho čitelnost.

Někdy potřebujeme iterovat nad nějakým vektorem y. Na první pohled by se mohlo zdát, že počítadlo by mělo nabývat hodnot t in 1:lenght(y). To však není dobrý nápad. Někdy se totiž může stát, že vektor y bude mít nulovou délku. Pak chceme, aby cyklus vůbec neproběhl. Pokud však počítadlo nastavíme výše uvedeným způsobem, cyklus překvapivě proběhne (a pravděpodobně skončí chybou). Důvod je ten, že dvojtečka má v cyklu svůj normální výraz konstruktoru vektorů. Pokud je lenght(y) rovno nule, pak má cyklus iterovat přes prvky vektoru 1:0, což je vektor o délce 2 a hodnotách c(1, 0). Správně tedy musí být počítadlo nastavené pomocí funkce seq_along() jako t in seq_along(y).

Iterovat nad prvky nějakého vektoru x můžeme třemi různými způsoby: 1) můžeme iterovat nad indexy prvků, jako jsme to dělali v našem příkladu (použijeme např. for (k in seq_along(x))), 2) můžeme iterovat přímo nad hodnotami daného vektoru (použijeme for (k in x)) nebo 3) můžeme iterovat nad jmény prvků vektoru (použijeme for (k in names(x))). První způsob se používá nejčastěji, ale i další varianty jsou někdy užitečné.

Existují situace, kdy se bez cyklu for neobejdeme. V R se však tento cyklus používá mnohem méně než v jiných jazycích. Důvody jsou dva: Zaprvé, R je vektorizovaný jazyk se spoustou vektorizovaných funkcí, takže mnoho operací, které je v jiných jazycích nutné psát pomocí cyklu, v R vyřeší vektorizace. Dokonce i náš první příklad cyklu je vlastně zbytečný a simulaci je možné provést takto:

N <- 1000
e <- c(0, rnorm(N - 1))  # simulace náhodné složky
y <- cumsum(e)  # kumulativní součet

Za druhe, pro iteraci nad vektory ma R jiny velmi silny nastroj, a to funkce typy map(), se kterymi se seznamite v kapitole 10.

Dokumentaci k cyklu for najdete pomocí help("for").

7.2.2 Cykly s neznámým počtem opakování

V některých situacích nevíme, kolikrát bude potřeba kus kódu opakovat. Pro tyto účely slouží dva standardní typy cyklů: cyklus while opakuje kus kódu, dokud je splněná nějaká podmínka; naproti tomu cyklus repeat opakuje kus kódu donekonečna s možností cyklus přerušit, pokud je nějaká podmínka splněná. Rozdíl mezi cykly spočívá v tom, kdy se podmínka vyhodnotí: v cyklu while se podmínka vyhodnocuje na začátku, takže cyklus nemusí proběhnout ani jednou; naproti tomu v cyklu repeat se podmínka vyhodnocuje v principu až na konci, takže cyklus vždy proběhne aspoň jednou. Tyto cykly se při datové analýze nepoužívají příliš často. Zato jsou velmi užitečné v simulacích, optimalizacích a podobných úlohách. Zde se podíváme pouze na jednodušší a častěji používaný cyklus while. Více se o obou cyklech můžete dozvědět z dokumentace (help("while")).

Použití cyklu while si ukážeme na následujícím příkladu: Předpokládejme, že chceme zjistit, kolikrát musíme hodit kostkou, než padne šestka. To můžeme provést např. následujícím kódem:

pocet <- 0
kostka <- 0
while (kostka != 6) {
    pocet <- pocet + 1
    kostka <- sample(6, size = 1)
}
print(pocet)
## [1] 8

Skript funguje takto: nejdříve si vytvoříme proměnnou pocet, do které budeme shromažďovat uplynulý počet hodů. Dále vytvoříme proměnnou kostka, do které uložíme hod kostkou. Vlastní cyklus začíná klíčovým slovem while. V závorce za ním je podmínka, tj. výraz, který se musí vyhodnotit na logický vektor délky 1. Pokud je podmínka splněná (logický výraz se vyhodnotí na TRUE), vyhodnotí se výraz, který následuje. Protože chceme vyhodnotit dva výrazy, musíme je pomocí složených závorek uzavřít do bloku.

Při prvním průchodu cyklu je kostka rovna nule, proto se cyklus provede: počítadlo pocet se zvýší o 1 a “hodíme kostkou” (funkce sample() v našem případě vygeneruje náhodné celé číslo od 1 do 6). Pokud “padlo” jiné číslo než 6, cyklus proběhne znovu (počítadlo se zvýší o další 1 a znovu se hodí kostkou). To se opakuje, dokud je podmínka splněná (tj. kostka je různá od 6). Jakmile se kostka rovná šesti, cyklus už neproběhne a R přejde na vypsání hodnoty pocet. (Výsledek simulace záleží na generátoru pseudonáhodných čísel, takže bude pokaždé jiný.)

Podívejme se na jiný stylizovaný příklad. Řekněme, že chceme zjistit, pro jaký vstup z určitého intervalu nabývá nějaká funkce určité hodnoty. Aby byla úloha jednoduchá, budeme předpokládat, že funkce je monotónně rostoucí. V takovém případě můžeme použít primitivní algoritmus půlení intervalů. Jako funkci budeme v našem příkladu pro jednoduchost uvažovat přirozený logaritmus a budeme hledat takovou hodnotu \(x\) z intervalu \([0, 10]\), pro kterou je \(log(x) = 1\):

hodnota <- 1
funkce <- log
dint <- 0
hint <- 10
tolerance <- 1e-10
chyba <- Inf
while (abs(chyba) > tolerance) {
    vysledek <- (dint + hint) / 2
    pokusna_hodnota <- funkce(vysledek)
    if (pokusna_hodnota < hodnota)
        dint <- vysledek
    if (pokusna_hodnota > hodnota)
        hint <- vysledek
    chyba <- hodnota - pokusna_hodnota
}
vysledek
## [1] 2.718282

Nejprve jsme zadali hledanou hodnotu (hodnota), použitou funkci (funkce), horní a dolní mez prohledávaného intervalu (dint a hint) a toleranci (tolerance), se kterou má algoritmus pracovat. Zadali jsme i počáteční velikost chyby (chyba). Vlastní výpočet funguje takto: pokud je absolutní hodnota chyby větší než zadaná tolerance, provedeme úpravu mezí, a to tak, že 1. najdeme hodnotu uprostřed intervalu, 2. vyhodnotíme hodnotu funkce v tomto bodě, 3. pokud je výsledek nižší než požadovaná hodnota, posuneme dolní mez na úroveň středu intervalu; v opačném případě takto posuneme horní mez, 4. spočítáme velikost chyby. Cyklus upravuje meze tak dlouho, dokud není chyba menší než zadaná tolerance. Nakonec vypíšeme hledanou hodnotu, která zůstala v proměnné vysledek. Výsledek si můžeme snadno ověřit “zkouškou”; můžeme se také podívat, že chyba je opravdu menší než zadaná tolerance (protože pro přirozený logaritmus máme k dispozici inverzní funkci):

log(vysledek)
## [1] 1
exp(1) - vysledek
## [1] -1.064779e-10

Algoritmus je velmi rychlý, ale samozřejmě není příliš obecný: funguje jen pro monotonně rostoucí funkce. (R má naštěstí celou řadu funkcí, které dokážou numericky optimalizovat zadanou funkci.)