4  Základní datové struktury

Data, která budeme zkoumat, většinou netvoří skaláry (jednotlivá izolovaná čísla), nýbrž větší množství hodnot, které mají nějaký vztah. Výsledkem ekonomického experimentu může být např. datový soubor, který obsahuje pro každý subjekt experimentu identifikační číslo daného subjektu, hodnotu jeho treatmentu, identifikaci skupiny, do které patřil, seznam akcí, které zahrál, a výplat, kterých dosáhl. Takový datový soubor můžeme zorganizovat jako tabulku, ve které řádky odpovídají jednotlivých subjektům a sloupce jednotlivým proměnným. Pokud jsou všechny proměnné číselné, představuje tato tabulka vlastně matici. Alternativně můžeme uspořádat každou proměnnou zvlášť jako jednotlivé vektory. V každém případě však potřebujeme k uchování hodnot získaných z experimentu určitý typ datové struktury.

Základní datové struktury, které nám R nabízí, lze roztřídit podle dvou charakteristik: 1) podle jejich dimensionality na jednorozměrné, dvourozměrné a vícerozměrné objekty a 2) podle homogenity použitých datových typů na homogenní a heterogenní struktury. Jednotlivé kombinace uvádí tabulka 4.1. Homogenní struktury mají všechny položky stejného typu, např. celá čísla. Mezi homogenní struktury patří zejména atomické vektory a matice. Heterogenní struktury mohou mít jednotlivé položky různých typů, takže mohou najednou obsahovat např. reálná čísla i řetězce. Mezi nejdůležitější heterogenní struktury patří seznamy a různé typy tabulek, jako jsou tabulky tříd data.frame a tibble. Jednorozměrné datové struktury mají jen jeden rozměr, délku. Sem patří zejména atomické vektory a seznamy (seznamy jsou neatomické vektory). Dvourozměrné struktury mají dva rozměry, takže tvoří tabulku. Nejdůležitější dvourozměrné struktury jsou homogenní matice a nehomogenní tabulky.

Tabulka 4.1: Význam základních logických operací.
dimenze homogenní heterogenní
1 atomický vektor seznam
2 matice tabulka
více pole \(\strut\)

V této kapitole se

4.1 Atomické vektory

Nejzákladnější datovou strukturou je v R atomický vektor. R nemá datovou strukturu pro skalár (jedinou logickou hodnotu, jediné číslo, znak nebo řetězec) – každý skalár je ve skutečnosti atomický vektor s jediným prvkem. Atomický vektor je vektor hodnot, jehož všechny prvky mají stejný typ (např. celé číslo).

Tvorba atomických vektorů

Atomické vektory se vytvářejí funkcí c() (od “concatenate”), která “slepí” jednotlivé hodnoty dohromady, přičemž provede automatickou konverzi (pokud je potřeba).

x <- c(1, 2, 3, 17)
print(x)
[1]  1  2  3 17

Pomocí funkce c() je možné “slepit” i celé vektory:

x1 <- c(1, 2, 3)
x2 <- c(4, 5, 6, NA)
x <- c(x1, x2)
print(x)
[1]  1  2  3  4  5  6 NA

Všechny prvky atomického vektoru musejí mít stejný typ. Pokud tedy při tvorbě atomického vektoru smícháte proměnné různých typů, dojde k automatické konverzi:

c(TRUE, 1, "ahoj")  # výsledek je vektor tří řetězců
[1] "TRUE" "1"    "ahoj"

Jednotlivé prvky atomických vektorů mohou mít jména. Jména jsou uložena v atributu names. Je možné je zadat čtyřmi různými způsoby: přímo ve funkci c(), pomocí funkce attr(), nebo pomocí speciálních funkcí names() a setNames():

x <- c(a = 1, "good parameter" = 7, c = 17)
x
             a good parameter              c 
             1              7             17 
attr(x, "names") <- c("A", "Good Parameter", "C")
x
             A Good Parameter              C 
             1              7             17 
names(x) <- c("aa", "bb", "cc")
names(x)
[1] "aa" "bb" "cc"
x
aa bb cc 
 1  7 17 
setNames(x, c("A", "B", "C"))
 A  B  C 
 1  7 17 

Pokud mají jednotlivé prvky vektorů přiřazená jména, vypisují se na obrazovku nad vlastní hodnoty.

Délku vektoru je možné zjistit pomocí funkce length():

length(x)
[1] 3

Pozor: atomický vektor může mít i nulovou délku, pokud neobsahuje žádné prvky. (Podobně i další datové struktury mohou mít nulové rozměry, např. nulový počet řádků apod.) Prázdný vektor vznikne často tak, že z existujícího vektoru vyberete hodnoty pomocí podmínky, kterou žádný prvek vektoru nesplní (jak se vybírá část datové struktury uvidíte níže):

x <- 1:9
length(x)
[1] 9
y <- x[x > 10]  # vybereme prvky větší než 10, viz dále
y
integer(0)
length(y)
[1] 0

Prázdný vektor je možné vytvořit pomocí konstruktorových funkcí logical(), integer(), numeric(), character() apod., které mají jediný parametr, počet prvků. Pokud je zadaný počet prvků nulový, funkce vrátí prázdný vektor. Pokud je počet prvků kladný, vznikne vektor se zadanou délkou. To je užitečné např. v situaci, kdy si chcete dopředu připravit vektor určité délky, a později jej naplnit hodnotami. (Funkce numeric() má poněkud nekonzistentní název, protože vytváří vektor typu double.)

z <- numeric(0)  # parametr je délka vektoru
z
numeric(0)
z <- numeric(10)  # vektor 10 hodnot
z
 [1] 0 0 0 0 0 0 0 0 0 0

Některé vektory obsahují předvídatelné sekvence čísel. Pro vytváření takových vektorů existuje operátor dvojtečka (:) a speciální funkce seq() a rep() a jejich specializované varianty:

1:10   # vektor celých čísel 1 až 10
 [1]  1  2  3  4  5  6  7  8  9 10
10:1   # vektor celých čísel sestupně 10 až 1
 [1] 10  9  8  7  6  5  4  3  2  1
# sekvence od ... do
seq(from = 1, to = 10, by = 3)  # s daným krokem
[1]  1  4  7 10
seq(from = 1, to = 10, length.out = 4)  # s danou délkou výsledku
[1]  1  4  7 10
seq_along(c(1, 3, 17, 31))  # celá čísla od 1 do délky zadaného vektoru
[1] 1 2 3 4
seq_len(7)  # celá čísla od 1 nahoru se zadanou nezápornou délkou
[1] 1 2 3 4 5 6 7
# opakování hodnot ve vektoru
rep(c(1, 3), times = 5)  # celý vektor 5 krát
 [1] 1 3 1 3 1 3 1 3 1 3
rep_len(c(1, 3), length.out = 5)  # celý vektor do délky 5
[1] 1 3 1 3 1
rep(c(1, 3), each = 3)  # každý prvek 3 krát
[1] 1 1 1 3 3 3
rep(1:3, each = 2, times = 3)
 [1] 1 1 2 2 3 3 1 1 2 2 3 3 1 1 2 2 3 3

Složitější varianty použití funkce rep() najdete v dokumentaci.

Pozor! Konstrukce vektorů pomocí operátoru dvojtečka je někdy nebezpečná. Řekněme, že chcete provést nějakou operaci pro každý prvek vektoru x a že to chcete udělat pomocí cyklu for, viz oddíl 6.2.1. Při psaní cyklů se často prochází hodnoty pomocného vektoru k = 1:length(x). Pokud má vektor x kladnou délku, je vše v pořádku. Pokud však vektor x neobsahuje žádné hodnoty, pak má nulovou délku. Čekali bychom, že pomocný vektor k bude mít také nulovou délku, takže cyklus neproběhne ani jednou. To však není pravda. Vektor k je v tomto případě zkonstruován jako 1:0, má tedy hodnotu c(1, 0) a délku 2! Taková věc je zdrojem špatně dohledatelných chyb. Je lepší použít k = seq_along(x). Ještě lepší je cyklům se vyhýbat. R k tomu má velmi užitečné funkce typu map(), viz kapitola  9.

Výpis atomických vektorů

Na obrazovku se vektory vypisují tak, že se jejich jednotlivé hodnoty skládají vedle sebe do řádku. Pokud se všechny hodnoty na řádek nevejdou, začne se vypisovat na dalším řádku. Pokud nejsou jednotlivé prvky vektoru pojmenované, pak je každý řádek uvozen číslem v hranatých závorkách. To je index prvního prvku vektoru na daném řádku. Protože jsou prvky vektorů číslované přirozenými čísly (tj. první prvek má index 1), bude první řádek začínat [1]. Pokud další řádek začíná např. [31], znamená to, že první číslo na daném řádku je 31. prvek daného vektoru atd.

1:100
  [1]   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18
 [19]  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36
 [37]  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54
 [55]  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72
 [73]  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90
 [91]  91  92  93  94  95  96  97  98  99 100

Pokud jsou prvky vektoru pojmenované, pak R nevypisuje na začátek řádku indexy prvního prvku, ale vypisuje nad jednotlivé prvky jejich jména:

setNames(1:26, letters)
 a  b  c  d  e  f  g  h  i  j  k  l  m  n  o  p  q  r  s  t  u  v  w  x  y  z 
 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 

Poznámka: I když se vektory vypisují na obrazovku po řádcích, neznamená to že jsou v R vektory řádkové – nejsou ani řádkové, ani sloupcové (jako je to implicitně třeba v Matlabu). Hodnoty se vypisují po řádcích prostě pro úsporu místa.

Operace s atomickými vektory

R je vektorizovaný jazyk. To znamená, že veškeré aritmetické a logické operace a většina funkcí, která pracuje s vektory, provádí danou operaci na celých vektorech po prvcích. Pokud např. vynásobíte dva vektory \(x\) a \(y\), výsledkem bude vektor, jehož prvky budou násobky odpovídajících prvků obou vektorů, takže \(z_i = x_i \cdot y_i\):

x <- 1:6
y <- 2:7
x * y
[1]  2  6 12 20 30 42
x ^ 2
[1]  1  4  9 16 25 36

Pozor: pokud se délka vektorů liší, pak R automaticky “recykluje” kratší vektor, tj. opakuje jeho hodnoty znovu a znovu. Při tom vydá R varování jen v případě, že délka delšího vektoru není celočíselným násobkem délky kratšího vektoru:

x <- 1:2
y <- 1:6
x + y
[1] 2 4 4 6 6 8
x * y
[1]  1  4  3  8  5 12
y <- 1:7
x + y
Warning in x + y: longer object length is not a multiple of shorter object
length
[1] 2 4 4 6 6 8 8

Testování atomických vektorů

Otestovat, zda je proměnná atomický vektor, není úplně snadné. R nabízí dvě potřebné funkce, z nichž však žádná sama o sobě nestačí. Funkce is.atomic() vrátí hodnotu TRUE, pokud je daná proměnná atomická (tj. všechny její prvky mají stejný datový typ). Tuto hodnotu však nevrací jen pro atomické vektory, ale i pro atomické matice, viz oddíl 4.2. Funkce is.vector() vrací logickou hodnotu TRUE, pokud je proměnná vektor; vektory však v R nemusejí být nutně atomické; funkce proto vrací TRUE i pro neatomické vektory (seznamy), viz oddíl 4.3. Zda proměnná obsahuje atomický vektor proto musíme otestovat spojením těchto funkcí:

is.atomic(x)  # x je atomická, tj.\ homogenní proměnná
[1] TRUE
is.vector(x)  # x je vektor
[1] TRUE
is.atomic(x) & is.vector(x)  # x je atomický vektor
[1] TRUE

Výběry a nahrazování hodnot v atomických vektorech

Někdy je potřeba získat z datové struktury jen vybrané prvky: z vektoru jednotlivé prvky, z matice vybrané prvky, řádky nebo sloupce apod. K tomu slouží výběry (“subsetování” nebo indexování). Výběry lze použít nejen k získání vybraných prvků z datové struktury, ale také k jejich nahrazení nebo doplnění. K základním výběrům slouží hranaté závorky ([]). V nich se určí indexy prvků, které je třeba vybrat. Prvky mohou být vybrány třemi způsoby: pomocí svých indexů, pomocí svých jmen a nebo pomocí logických hodnot. Nyní si to ukážeme na atomických vektorech.

1. Výběr pomocí číselných indexů. Prvky atomických vektorů jsou číslované přirozenými čísly \(1,\ldots,N\), kde \(N\) je délka vektoru (tj. první prvek vektoru má index 1, nikoli 0). Při výběru prvků pomocí indexů se vyberou prvky s danými indexy (pokud jsou indexy kladné), nebo se vynechají prvky s danými indexy (pokud jsou indexy záporné). Indexování pomocí kladných a záporných čísel nelze míchat. Index 0 se tiše ignoruje.

# vektor letters obsahuje 26 malých písmen anglické abecedy
x <- letters[1:12]  # prvních dvanáct písmen abecedy
x
 [1] "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l"
x[1]  # první prvek
[1] "a"
x[3]  # třetí prvek
[1] "c"
x[length(x)]  # poslední prvek
[1] "l"
x[3:6]  # třetí až šestý prvek včetně
[1] "c" "d" "e" "f"
x[c(2, 3, 7)]  # druhý, třetí a sedmý prvek
[1] "b" "c" "g"
x[c(-1, -3)]  # vynechají se první a třetí prvek
 [1] "b" "d" "e" "f" "g" "h" "i" "j" "k" "l"

2. Výběr pomocí jmen prvků. Pokud mají prvky vektoru jména, je možné vybírat pomocí vektoru jejich jmen (zde samozřejmě nejde vynechávat pomocí znaménka minus, protože R nemá záporné řetězce):

x <- c(c = 1, b = 2, a = 3)
x
c b a 
1 2 3 
x["a"]  # prvek s názvem a, tj. zde poslední prvek
a 
3 
x[c("b", "c")]  # prvky s názvy b a c
b c 
2 1 

3. Výběr pomocí logických hodnot. R vybere prvky, které jsou indexovány logickou hodnotou TRUE a vynechá ostatní. Pozor: pokud je logický vektor kratší než subsetovaný vektor, pak se recykluje! To jde využít např. v situaci, kdy chceme vybrat dva prvky, pak jeden vynechat, opět vybrat dva prvky atd.:

x <- 1:12
x
 [1]  1  2  3  4  5  6  7  8  9 10 11 12
x[c(TRUE, TRUE, FALSE)]
[1]  1  2  4  5  7  8 10 11

Výběr pomocí logických hodnot je užitečný zejména v situaci, kdy chceme vybrat prvky, které splňují nějakou podmínku:

# vybere prvky, které jsou větší než tři a menší než 11
x[x > 3 & x < 11]
[1]  4  5  6  7  8  9 10
# vybere prvky, které jsou menší než tři nebo větší než 11
x[x < 3 | x > 11]
[1]  1  2 12

Výběry je možné využít i k nahrazení prvků. Přitom se do výběru uloží nová hodnota, která nahradí starou:

x <- c(1:3, NA, 5:7)
x
[1]  1  2  3 NA  5  6  7
x[7] <- Inf  # nahrazení poslední hodnoty nekonečnem
x
[1]   1   2   3  NA   5   6 Inf
x[is.na(x)] <- 0  # nahrazení všech hodnot NA nulou
x
[1]   1   2   3   0   5   6 Inf

Pokud se nová hodnota přidá za konec existujícího vektoru, vektor se automaticky prodlouží:

x[length(x) + 1] <- 8  # přidání nové hodnoty za konec vektoru
x
[1]   1   2   3   0   5   6 Inf   8

Pokud se nový prvek nevloží hned za konec existujícího, R doplní nezadané prvky hodnota NA:

x[length(x) + 3] <- 9
x
 [1]   1   2   3   0   5   6 Inf   8  NA  NA   9

Pozor: Postupné rozšiřování datových struktur vždy o několik málo prvků je výpočetně velmi neefektivní, protože R musí (téměř) pokaždé alokovat nové místo v paměti, do něj zkopírovat staré hodnoty a na konec přidat nový prvek. Mnohem efektivnější je naráz alokovat velký blok paměti, do něj postupně uložit hodnoty a blok na konci případně zkrátit:

x <- numeric(1e6)  # alokace prázdného vektoru o milonu prvků
x[1] <- 1          # přidání prvků (další řádky vynechány)
n <- 7654          # skutečný počet vložených prvků
x <- x[1:n]        # zkrácení vektoru na potřebnou délku

Ekvivalentně je vhodné postupovat v případě všech homogenních datových struktur.

Pozor: Funkce numeric() vytvoří vektor samých nul. Možná je lepší použít rep(NA_real_, 1e6), které vytvoří reálný vektor hodnot NA. Většina lidí však používá funkce numeric(), character() apod.

V R je možné výběry ze všech datových struktur řetězit – následující výběr vybírá z výsledku předchozího výběru:

v <- 1:10
v[6:10]     # 6. až 10. prvek v
[1]  6  7  8  9 10
v[6:10][2]  # druhý prvek z výběru 6. až 10. prvku v
[1] 7

Do zřetězeného výběru je obvykle možné dosazovat hodnoty:

v[6:10][2] <- NA
v
 [1]  1  2  3  4  5  6 NA  8  9 10

4.2 Atomické matice

Atomická matice je matice (tj. dvourozměrná tabulka), jejíž všechny prvky mají stejný datový typ (např. celé číslo). Pro datovou analýzu nejsou matice příliš důležité, někdy se však hodí pro rychlou maticovou algebru a také některé funkce vracejí nebo očekávají jako vstup matice.

Tvorba matic

Nejjednodušší způsob, jak vytvořit atomickou matici je pomocí funkce matrix(). Prvním parametrem je atomický vektor, který obsahuje data. Další parametry určují počet řádků a počet sloupců matice, způsob, jak budou data do matice skládaná (zda podle řádků či sloupců; implicitně se data do matic skládají po sloupcích) a pojmenování dimenzí matice. Není potřeba zadávat všechny parametry, pokud R dokáže odhadnout hodnotu jednoho parametru z hodnot ostatních parametrů. R např. umí z délky zadaného vektoru a zadaného počtu řádků odhadnout počet sloupců.

# matice se třemi řádky a čtyřmi sloupci, po sloupcích
matrix(1:12, nrow = 3)
     [,1] [,2] [,3] [,4]
[1,]    1    4    7   10
[2,]    2    5    8   11
[3,]    3    6    9   12
# stejný rozměr, data po řádcích
matrix(1:12, ncol = 4, byrow = TRUE)
     [,1] [,2] [,3] [,4]
[1,]    1    2    3    4
[2,]    5    6    7    8
[3,]    9   10   11   12

Při tvorbě matic R recykluje data. To znamená, že pokud je zadaných hodnot méně, než vyžadují rozměry matice, R začne číst datový vektor znovu od začátku. To může být zdrojem nepříjemných chyb. Naštěstí R vypíše varování, ovšem pouze v případě, že počet prvků matice není celočíselným násobkem délky zadaného vektoru.

matrix(1:9, nrow = 3, ncol = 4)
Warning in matrix(1:9, nrow = 3, ncol = 4): data length [9] is not a
sub-multiple or multiple of the number of columns [4]
     [,1] [,2] [,3] [,4]
[1,]    1    4    7    1
[2,]    2    5    8    2
[3,]    3    6    9    3

Otestovat, zda je objekt matice, je možné pomocí funkce is.matrix(); převést data na matici je možné pomocí konverzní funkce as.matrix().

Zjistit rozměry matice je možné pomocí následujících funkcí: nrow() vrátí počet řádků matice, ncol() vrátí počet sloupců matice, dim() vrací vektor s počtem řádků a sloupců matice a length() vrací počet prvků matice. (Pro vektory vrací funkce nrow(), ncol() a dim() hodnotu NULL.)

m <- matrix(1:12, nrow = 3)
m
     [,1] [,2] [,3] [,4]
[1,]    1    4    7   10
[2,]    2    5    8   11
[3,]    3    6    9   12
nrow(m)
[1] 3
ncol(m)
[1] 4
dim(m)
[1] 3 4
length(m)
[1] 12

Matice a podobné objekty je možné skládat pomocí funkcí rbind() a cbind(). Funkce rbind() (od “row bind”) spojuje matice po řádcích, tj. skládá je pod sebe. Funkce cbind() (od “column bind”) je spojuje po sloupcích, tj. skládá je vedle sebe:

A <- matrix(1:12, nrow = 3)
B <- matrix(101:112, nrow = 3)
rbind(A, B)
     [,1] [,2] [,3] [,4]
[1,]    1    4    7   10
[2,]    2    5    8   11
[3,]    3    6    9   12
[4,]  101  104  107  110
[5,]  102  105  108  111
[6,]  103  106  109  112
cbind(A, B)
     [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8]
[1,]    1    4    7   10  101  104  107  110
[2,]    2    5    8   11  102  105  108  111
[3,]    3    6    9   12  103  106  109  112

Matice mohou mít následující atributy: dim je celočíselný vektor rozměrů (viz výše), jména řádků (čte i nastavuje se funkcí rownames()), jména sloupců (čte i nastavuje se funkcí colnames()) a jména dimenzí včetně jmen řádků a sloupců (čte i nastavuje se funkcí dimnames()):

rownames(A) <- c("a", "b", "c")
colnames(A) <- c("alpha", "beta", "gamma", "delta")
A
  alpha beta gamma delta
a     1    4     7    10
b     2    5     8    11
c     3    6     9    12
dimnames(A) <- list(id = c("A", "B", "C"), variables = c("Alpha", "Beta", "Gamma", "Delta"))
A
   variables
id  Alpha Beta Gamma Delta
  A     1    4     7    10
  B     2    5     8    11
  C     3    6     9    12
attributes(A)
$dim
[1] 3 4

$dimnames
$dimnames$id
[1] "A" "B" "C"

$dimnames$variables
[1] "Alpha" "Beta"  "Gamma" "Delta"

Atomická matice je implementována jako atomický vektor, který má přiřazený atribut dim, který je celočíselný vektor délky dva. Pokud tedy vezmeme atomický vektor a přiřadíme mu atribut dim, vznikne matice:

M <- 1:12
M
 [1]  1  2  3  4  5  6  7  8  9 10 11 12
dim(M) <- c(3, 4)
M
     [,1] [,2] [,3] [,4]
[1,]    1    4    7   10
[2,]    2    5    8   11
[3,]    3    6    9   12
is.matrix(M)
[1] TRUE

Podobně lze zrušením atributu dim převést matici zpět na vektor (matice se vektorizuje po sloupcích). Stejného výsledku jde dosáhnout pomocí funkce as.vector():

as.vector(M)
 [1]  1  2  3  4  5  6  7  8  9 10 11 12
dim(M) <- NULL
M
 [1]  1  2  3  4  5  6  7  8  9 10 11 12

Protože matice je atomický vektor, který má přiřazený atribut dim, funkce is.atomic() vrací pro matici hodnotu TRUE; funkce is.vector() však samozřejmě vrací FALSE, protože testuje, zda je v proměnné uložen vektor.

M <- matrix(1:12, nrow = 3)
is.atomic(M)
[1] TRUE
is.vector(M)
[1] FALSE

Maticová aritmetika

Obyčejné symboly násobení (*), dělení (/) a umocňování (^) pracují “po prvcích”. Při násobení se např. vynásobí odpovídající prvky matice. Matice tedy musejí mít stejné rozměry (stejný počet řádků a sloupců).

A <- matrix(1:12, nrow = 3)
B <- matrix(101:112, nrow = 3)
A * B
     [,1] [,2] [,3] [,4]
[1,]  101  416  749 1100
[2,]  204  525  864 1221
[3,]  309  636  981 1344
A ^ 2
     [,1] [,2] [,3] [,4]
[1,]    1   16   49  100
[2,]    4   25   64  121
[3,]    9   36   81  144

Pro skutečné maticové násobení se používá operátor %*%. Inverzní matici vrací funkce solve() (obecně tato funkce řeší soustavy lineárních rovnic). K transponování matice slouží funkce t(). Hlavní diagonálu matice vrací funkce diag().

Speciální matice:

# jednotková matice
diag(1, nrow = 3, ncol = 3)
     [,1] [,2] [,3]
[1,]    1    0    0
[2,]    0    1    0
[3,]    0    0    1
# nulová matice (využívá recyklace)
matrix(0, nrow = 3, ncol = 3)
     [,1] [,2] [,3]
[1,]    0    0    0
[2,]    0    0    0
[3,]    0    0    0

Příklad inverze:

M <- matrix(c(1:8, 0), nrow = 3)
invM <- solve(M)
E <- diag(1, nrow = nrow(M), ncol = ncol(M))
# součin matice a její inverze je jednotková matice
all.equal(M %*% invM, E)
[1] TRUE
all.equal(invM %*% M, E)
[1] TRUE

Výběry z matic

Výběry z matic fungují podobně jako výběry z atomických vektorů s jedním rozdílem: protože má matice řádky a sloupce, je třeba indexovat pomocí dvou indexů. První index vybírá řádky, druhý sloupce:

M <- matrix(1:12, nrow = 3)
M
     [,1] [,2] [,3] [,4]
[1,]    1    4    7   10
[2,]    2    5    8   11
[3,]    3    6    9   12
# prvek ve druhém řádku a třetím sloupci
M[2, 3]
[1] 8
# prvky na prvních dvou řádcích a v prvním a čtvrtém sloupci
M[1:2, c(1,4)]  
     [,1] [,2]
[1,]    1   10
[2,]    2   11
# matice bez prvního řádku a sloupce
M[-1, -1]
     [,1] [,2] [,3]
[1,]    5    8   11
[2,]    6    9   12

Pokud je jeden z indexů prázdný, vybírá celý řádek nebo sloupec:

M[1:2, ]  # celé první dva řádky
     [,1] [,2] [,3] [,4]
[1,]    1    4    7   10
[2,]    2    5    8   11
M[, c(1, 3)]  # první a třetí sloupec
     [,1] [,2]
[1,]    1    7
[2,]    2    8
[3,]    3    9
# všechny řádky, ve kterých je prvek v prvním sloupci >= 2
M[M[, 1] >= 2, ]
     [,1] [,2] [,3] [,4]
[1,]    2    5    8   11
[2,]    3    6    9   12
M[M[, 1] >= 2, M[1, ] < 6]  # submatice
     [,1] [,2]
[1,]    2    5
[2,]    3    6
M[ , ]  # celá matice M
     [,1] [,2] [,3] [,4]
[1,]    1    4    7   10
[2,]    2    5    8   11
[3,]    3    6    9   12

Pokud se matice indexuje jen jedním indexem, R ji tiše převede na jeden vektor (spojí sloupce matice za sebe) a vybere prvky z takto vzniklého vektoru:

# vrací 1. prvek ve 2. sloupci, protože je to 4. prvek vektoru
M[4]
[1] 4
M[c(1, 4:7)]
[1] 1 4 5 6 7

Indexování může nejen vybírat hodnoty, ale také měnit jejich pořadí. Zadáním indexů můžeme např. otočit pořadí sloupců matice:

M <- matrix(c(8, 5, 7, 2, 3, 11, 6, 12, 1, 4, 9, 10), nrow = 3)
M
     [,1] [,2] [,3] [,4]
[1,]    8    2    6    4
[2,]    5    3   12    9
[3,]    7   11    1   10
M[, 4:1]
     [,1] [,2] [,3] [,4]
[1,]    4    6    2    8
[2,]    9   12    3    5
[3,]   10    1   11    7

Stejným způsobem můžeme dosáhnout i zajímavějších efektů: např. seřadit řádky nebo sloupce podle hodnot vybraného vektoru nebo je náhodně permutovat. Funkce order() vrací indexy uspořádané podle velikosti původního vektoru. Funkce např. umožňuje setřídit hodnoty všech sloupců matice podle jednoho sloupce:

# indexy prvků 1. sloupce matice M seřazené podle velikosti prvků,
# tj. na 1. místě je 2. prvek původního vektoru (5), pak 3. prvek
# (7) atd.
order(M[, 1]) 
[1] 2 3 1
# řádky matice seřazené podle prvního sloupce
M[order(M[, 1]), ]
     [,1] [,2] [,3] [,4]
[1,]    5    3   12    9
[2,]    7   11    1   10
[3,]    8    2    6    4

Funkce sample() náhodně permutuje zadaná čísla. Lze jí tak mimo jiné využít k náhodné permutaci sloupců matice:

# čísla 1:ncol(M) v náhodném pořadí
o <- sample(ncol(M))
o
[1] 1 4 2 3
M[, o]
     [,1] [,2] [,3] [,4]
[1,]    8    4    2    6
[2,]    5    9    3   12
[3,]    7   10   11    1

Výběry se vždy snaží snížit rozměry matice – pokud počet řádků nebo sloupců klesne na 1, matice se změní ve vektor. Pokud tomu chceme zabránit, je třeba přidat parametr drop = FALSE. (Nemělo by vám být divné, že je možné hranatým závorkám přidávat parametry – jako vše v R je i použití hranatých závorek volání funkce – a funkce mohou mít parametry.)

M[, 1]
[1] 8 5 7
M[, 1, drop = FALSE]
     [,1]
[1,]    8
[2,]    5
[3,]    7

4.3 Neatomické vektory (seznamy)

Neatomické vektory (častěji nazývané seznamy) jsou vektory, jejichž jednotlivé prvky mohou mít různé datové typy a dokonce i obsahovat jiné datové struktury: atomické vektory, seznamy, matice, tabulky atd. Seznamy tedy umožňují v jedné proměnné skladovat různé typy dat. Jejich hlavní význam však spočívá v tom, že se používají jako základ pro tvorbu většiny objektů v systému S3, viz kapitola 8.

Seznamy se vytvářejí pomocí funkce list():

l <- list(1L, 11, 1:3, "ahoj", list(1, 1:3, "ahoj"))
l
[[1]]
[1] 1

[[2]]
[1] 11

[[3]]
[1] 1 2 3

[[4]]
[1] "ahoj"

[[5]]
[[5]][[1]]
[1] 1

[[5]][[2]]
[1] 1 2 3

[[5]][[3]]
[1] "ahoj"

Stejně jako atomické vektory, i seznamy mohou mít atribut names, tj. jednotlivé prvky seznamu mohou mít svá jména. Ta se přiřazují stejně jako v případě atomických vektorů:

l <- list(a = 1, b = "ahoj", c = 1:3, d = list(1:3, "ahoj"))
names(l)
[1] "a" "b" "c" "d"
l
$a
[1] 1

$b
[1] "ahoj"

$c
[1] 1 2 3

$d
$d[[1]]
[1] 1 2 3

$d[[2]]
[1] "ahoj"

Délku seznamu zjistíme pomocí funkce length():

length(l)
[1] 4

K otestování, zda je proměnná seznam, slouží funkce is.list(). Funkce is.vector() vrací hodnotu TRUE jak pro atomické vektory, tak i pro seznamy.

is.list(l)
[1] TRUE
is.vector(l)
[1] TRUE

Strukturu seznamu je možné přehledně zobrazit pomocí funkce str(). Podobný výsledek dostanete v RStudiu pomocí funkce View() nebo tak, že v záložce Environment kliknete na trojúhelníček v kolečku vedle jména seznamu. Musíte však být v módu List.

str(l)
List of 4
 $ a: num 1
 $ b: chr "ahoj"
 $ c: int [1:3] 1 2 3
 $ d:List of 2
  ..$ : int [1:3] 1 2 3
  ..$ : chr "ahoj"

Alternativně můžete přehledně zobrazit strukturu každého objektu pomocí funkce tree() z balíku lobstr:

lobstr::tree(l)
<list>
├─a: 1
├─b: "ahoj"
├─c<int [3]>: 1, 2, 3
└─d: <list>
  ├─<int [3]>1, 2, 3
  └─"ahoj"

Výběry ze seznamů jsou poněkud složitější, než je tomu u atomických proměnných. Indexování pomocí hranatých závorek zachovává mód proměnné. To znamená, že použití hranatých závorek na seznam vrací opět seznam. Pokud chceme získat přímo prvek uložený v seznamu, musíme použít dvojité hranaté závorky ([[]]). Podobnou funkci plní i operátor dolar ($).

Výběr ze seznamu pomocí hranatých závorek vrací prvky opět zabalené do seznamu:

l <- list(a = 1, b = 1:3, c = "ahoj")
l
$a
[1] 1

$b
[1] 1 2 3

$c
[1] "ahoj"
l[1]
$a
[1] 1
is.list(l[1])
[1] TRUE
l[1:2]
$a
[1] 1

$b
[1] 1 2 3

Pokud chceme získat vlastní prvek seznamu, musíme použít dvojité hranaté závorky. Dvojité hranaté závorky “vybalí” daný prvek ze seznamu ven:

l[[2]]
[1] 1 2 3
is.list(l[[2]])
[1] FALSE
is.numeric(l[[2]])
[1] TRUE

Syntaxe dvojitých hranatých závorek je poněkud nečekaná. Pokud je argumentem vektor, nevrací dvojité hranaté závorky vektor hodnot (to ani nejde, protože výsledkem by musel být opět seznam), nýbrž se vektor přeloží na rekurentní volání dvojitých hranatých závorek:

l[[2]][[3]]  # třetí prvek vektoru, který je druhým prvkem seznamu
[1] 3
l[[2:3]]     # totéž
[1] 3
# protože druhým prvkem seznamu je zde atomický vektor,
# mohou být druhé závorky jednoduché:
l[[2]][3]
[1] 3

Pokud jsou prvky seznamu pojmenované, nabízí R zkratku ke dvojitým hranatým závorkám: operátor dolar ($): l[["b"]] je totéž jako l$b:

l[["b"]]  # prvek se jménem b
[1] 1 2 3
l$b       # totéž (uvozovky se zde neuvádějí)
[1] 1 2 3

Použití dolaru od dvojitých závorek v jednom ohledu liší: pokud máme jméno prvku, který chceme získat uloženo v proměnné, je třeba použít hranaté závorky – operátor dolar zde nelze použít:

element <- "c"
# element není v uvozovkách, protože vybíráme hodnotu, která je v něm uložená:
l[[element]]
[1] "ahoj"

Pokud indexujeme jménem prvek seznamu, který v seznamu chybí, dostaneme hodnotu NULL. Pokud jej však indexujeme číselným indexem, dostaneme chybu:

l[["d"]]
NULL
l$d
NULL
l[[4]]  # chybný řádek
Error in l[[4]]: subscript out of bounds

Seznamy umožňují používat i jen části jmen prvků, pokud jsou určeny jednoznačně (tomu se říká “partial matching”). S dolarem partial matching zapnutý vždy; s dvojitými hranatými závorkami jen v případě, že o to požádáte parametrem exact = FALSE.

l <- list(prvni_prvek = 1, druhy_prvek = 2)
l$p
[1] 1
l[["p"]]
NULL
l[["p", exact = FALSE]]
[1] 1

Doporučujeme partial matching nikdy nevyužívat – může být zdrojem špatně dohledatelných chyb!

4.4 Tabulky třídy data.frame

Pro analýzu dat jsou nejdůležitější datovou strukturou tabulky. Každý sloupec tabulky může obsahovat proměnné jiného typu, v rámci sloupce však musí být typ stejný. Tím se tabulky liší od atomických matic, které musí mít všechny prvky stejného typu. Obvyklé použití tabulek je takové, že řádky tabulky představují jednotlivá pozorování a sloupce jednotlivé proměnné.

R má několik implementací tabulek. Základní třída tabulek, které se budeme věnovat v tomto oddíle, se nazývá data.frame. Technicky je implementovaná jako seznam vektorů o stejné délce, které jsou spojené vedle sebe. V příštím oddíle se podíváme na poněkud příjemnější variantu tabulek třídy tibble.

Tvorba tabulek třídy data.frame

Tabulky třídy data.frame se tvoří pomocí funkce data.frame(). Řekněme, že chceme zaznamenat údaje o subjektech, které se zúčastnily nějakého experimentu. Pro každý subjekt pozorujeme jeho identifikační číslo id, výšku a váhu. Pokud máme čtyři subjekty, můžeme vytvořit tabulku např. takto:

experiment <- data.frame(id = c(1, 2, 3, 41),
                         vyska = c(158, 174, 167, 203),
                         vaha = c(51, 110, 68, 97))
experiment
  id vyska vaha
1  1   158   51
2  2   174  110
3  3   167   68
4 41   203   97

Při zadávání vektorů do tabulek můžeme zadat jejich jména, která pak R vypíše. R samo přidá jména řádků (automaticky jim dá přirozená čísla od 1 do počtu pozorování).

I při konstrukci tabulek R recykluje proměnné, pokud není zadáno dost hodnot. Pokud jsou všechny naše subjekty muži, stačí zadat tuto hodnotu jen jednou – R ji zrecykluje.

experiment <- data.frame(
    id = c(1, 2, 3, 41),
    gender = "muž",
    vyska = c(158, 174, 167, 203),
    vaha = c(51, 110, 68, 97),
    zdravy = c(TRUE, TRUE, FALSE, TRUE)
)
experiment
  id gender vyska vaha zdravy
1  1    muž   158   51   TRUE
2  2    muž   174  110   TRUE
3  3    muž   167   68  FALSE
4 41    muž   203   97   TRUE

Poznámka: Starší verze R při zadání dat do tabulek pomocí funkce data.frame() automaticky převedly všechny řetězce na faktory, viz oddíl 5.1. Této konverzi šlo zabránit nastavením parametru stringsAsFactors = FALSE. Od verze R 4.0 má parametr stringsAsFactors implicitně hodnotu FALSE. Pokud tedy chceme v novějším R převést řetězce na faktory, musíme nastavit parametr stringsAsFactors na hodnotu TRUE nebo je převést pomocí funkce factor(), což je zřejmě rozumnější, viz oddíl 5.1.

Někdy se hodí vytvořit tabulku, která obsahuje všechny možné kombinace hodnot nějakého vektoru. K tomu slouží funkce expand.grid():

expand.grid(
    x = 1:3,
    y = factor(c("male", "female")),
    z = c(TRUE, FALSE)
)
   x      y     z
1  1   male  TRUE
2  2   male  TRUE
3  3   male  TRUE
4  1 female  TRUE
5  2 female  TRUE
6  3 female  TRUE
7  1   male FALSE
8  2   male FALSE
9  3   male FALSE
10 1 female FALSE
11 2 female FALSE
12 3 female FALSE

Počet řádků tabulky zjistíme pomocí funkce nrow(), počet sloupců funkcí ncol() nebo length(); funguje i funkce dim():

nrow(experiment)    # počet řádků
[1] 4
ncol(experiment)    # počet sloupců
[1] 5
length(experiment)  # počet sloupců
[1] 5
dim(experiment)     # vektor počtu řádků a sloupců
[1] 4 5

Tabulky mají standardně tři atributy: class (jméno třídy – data set je totiž objekt), names obsahuje jména sloupců (tj. jednotlivých proměnných) a row.names obsahuje jména jednotlivých řádků (tj. pozorování, implicitně mají hodnoty 1, 2 atd.).

attributes(experiment)
$names
[1] "id"     "gender" "vyska"  "vaha"   "zdravy"

$class
[1] "data.frame"

$row.names
[1] 1 2 3 4

Jména řádků můžete zjistit i změnit pomocí funkcí rownames() a row.names(), jména sloupců pomocí funkcí colnames() nebo names():

colnames(experiment) <- c("id", "sex", "height", "weight", "healthy")
experiment
  id sex height weight healthy
1  1 muž    158     51    TRUE
2  2 muž    174    110    TRUE
3  3 muž    167     68   FALSE
4 41 muž    203     97    TRUE

Jména řádků vypadají na první pohled jako dobrý způsob, jak uložit nějakou identifikaci pozorování, např. id subjektu v experimentu. Nedělejte to! Veškeré informace o pozorování ukládejte přímo do tabulky. Je to filosoficky správnější a i praktičtější: některé funkce, které se používají ke zpracování tabulek, jména řádků odstraní. Stejně tak některé formáty, do kterých se data ukládají, jména řádků nepodporují, takže byste přišli o důležité informace. Naproti tomu jména sloupců (tj. proměnných) jsou bezpečná.

Někdy je užitečné moci převést tabulku na matici a matici na tabulku. K převodu tabulky na matici slouží funkce as.matrix() a data.matrix(). Funkce as.matrix() převede všechny sloupce tabulky automatickou konverzí na stejný typ, a pak na matici. Naproti tomu funkce data.matrix() převede logické hodnoty a faktory na celá čísla, řetězce na faktory a ty na celá čísla. Pokud jsou po této konverzi všechny sloupce typu integer, má výsledná matice tento typ, jinak má typ double. Při automatické konverzi můžeme skončit s řetězci, což nemusí být žádoucí; s explicitní konverzí na reálná čísla můžeme řetězce ztratit a faktory mohou být zavádějící (faktory se převedou na čísla jako při konverzi na celé číslo).

# použije automatickou konverzi na stejný typ
as.matrix(experiment)
     id   sex   height weight healthy
[1,] " 1" "muž" "158"  " 51"  "TRUE" 
[2,] " 2" "muž" "174"  "110"  "TRUE" 
[3,] " 3" "muž" "167"  " 68"  "FALSE"
[4,] "41" "muž" "203"  " 97"  "TRUE" 
# použije explicitní konverzi na reálná čísla
data.matrix(experiment)
     id sex height weight healthy
[1,]  1   1    158     51       1
[2,]  2   1    174    110       1
[3,]  3   1    167     68       0
[4,] 41   1    203     97       1

Matici lze převést na tabulku pomocí funkcí as.data.frame() i data.frame(). Pokud má matice pojmenované sloupce, jejich jména jsou v tabulce zachována; v opačném případě je R samo pojmenuje V1, V2 atd nebo X1, X2 atd.

M <- matrix(1:12, nrow = 3)
as.data.frame(M)
  V1 V2 V3 V4
1  1  4  7 10
2  2  5  8 11
3  3  6  9 12
data.frame(M)
  X1 X2 X3 X4
1  1  4  7 10
2  2  5  8 11
3  3  6  9 12
colnames(M) <- c("a", "b", "c", "d")
as.data.frame(M)
  a b c  d
1 1 4 7 10
2 2 5 8 11
3 3 6 9 12
data.frame(M)
  a b c  d
1 1 4 7 10
2 2 5 8 11
3 3 6 9 12

Výběry z tabulek třídy data.frame

Tabulky jsou “kříženec” mezi seznamy a maticemi, takže je možné je indexovat jako matice i jako seznamy. Pokud použijete jeden index, pak je indexujete jako seznamy (a vybíráte sloupce), pokud dva indexy, pak je indexujete jako matice. V prvním případě tedy [ vrátí tabulku, ve druhém může vrátit tabulku (pokud se vybere více sloupců), nebo vektor (pokud se vybere jen jeden sloupec). Dolar i dvojité hranaté závorky vrací jeden sloupec jako vektor:

d <- data.frame(x = 1:7,
                y = c(3, 1, NA, 7, 5, 12, NA))
d
  x  y
1 1  3
2 2  1
3 3 NA
4 4  7
5 5  5
6 6 12
7 7 NA
d$x      # vektor x
[1] 1 2 3 4 5 6 7
d[["x"]] # totéž
[1] 1 2 3 4 5 6 7
d[[1]]   # totéž
[1] 1 2 3 4 5 6 7
d["x"]   # tabulka s jediným sloupcem
  x
1 1
2 2
3 3
4 4
5 5
6 6
7 7
d[1]     # opět tabulka s jediným sloupcem
  x
1 1
2 2
3 3
4 4
5 5
6 6
7 7
# vektor prvních dvou hodnot z vektoru x
d[1:2, "x"]              
[1] 1 2
d[1:2, 1]                # totéž
[1] 1 2
# tabulka složená z prvních dvou hodnot vektoru x
d[1:2, 1, drop = FALSE]  
  x
1 1
2 2
# tabulka složená z prvních dvou řádků
d[1:2, 1:2]
  x y
1 1 3
2 2 1
# tabulka složená z prvních dvou řádků
d[1:2, c("x", "y")]
  x y
1 1 3
2 2 1
# tabulka složená z prvních dvou řádků
d[1:2, ]
  x y
1 1 3
2 2 1

Samozřejmě je možné použít i indexování pomocí logických hodnot:

# výběr řádků, kde je hodnota y menší než 7
d[d[, "y"] < 7, ]
      x  y
1     1  3
2     2  1
NA   NA NA
5     5  5
NA.1 NA NA
d[d$y < 7 , ]  # totéž
      x  y
1     1  3
2     2  1
NA   NA NA
5     5  5
NA.1 NA NA

Výběr zachová i řádky, kde je hodnota \(y\) NA. To jde vyřešit např. takto:

# vybíráme pouze prvky, kde y zároveň není NA a
# zároveň je menší než 7 nebo
d[!is.na(d$y) & d$y < 7, ]
  x y
1 1 3
2 2 1
5 5 5

K vyřazení neúplných hodnot z tabulky a podobných struktur slouží funkce complete.cases(). V případě tabulky vrací vektor logických hodnot, který je TRUE pro každý řádek tabulky, který má všechny hodnoty známé, a FALSE jinak.

complete.cases(d)
[1]  TRUE  TRUE FALSE  TRUE  TRUE  TRUE FALSE
d[complete.cases(d), ]
  x  y
1 1  3
2 2  1
4 4  7
5 5  5
6 6 12

Pro složitější výběry z tabulek existuje funkce subset(). Té však nebudeme věnovat pozornost, protože se později naučíte mnohem příjemnější a rychlejší funkce implementované v balíku dplyr, viz kapitola 13.

Do existující tabulky přidáte novou proměnnou (nový sloupec) tak, že do nové proměnné přidáte hodnoty vektoru:

d$z <- letters[1:nrow(d)]
d
  x  y z
1 1  3 a
2 2  1 b
3 3 NA c
4 4  7 d
5 5  5 e
6 6 12 f
7 7 NA g

Nová proměnná se přidá jako poslední sloupec.

Pokud do existující proměnné přiřadíte hodnotu NULL, vyřadíte tím proměnnou z tabulky:

d$z <- NULL
d
  x  y
1 1  3
2 2  1
3 3 NA
4 4  7
5 5  5
6 6 12
7 7 NA

Jiná možnost, jak vynechat proměnnou nebo změnit jejich pořadí, je využít indexování sloupců tabulky.

Indexování je možné použít i ke změně pořadí řádků nebo sloupců. Řekněme, že chceme řádky tabulky d seřadit podle proměnné “y” a zároveň vyměnit pořadí sloupců. To můžeme udělat např. takto:

d[order(d$y), 2:1]
   y x
2  1 2
1  3 1
5  5 5
4  7 4
6 12 6
3 NA 3
7 NA 7

Hodnoty NA skončí implicitně na konci (to je možné změnit ve funkci order()).

4.5 Tabulky třídy tibble

Kromě tabulek třídy data.frame existuje v R ještě několik dalších typů tabulek. Nejpohodlnější z nich je třída tibble, která je součástí tidyverse, skupiny balíků určených pro datovou analýzu, kterými se budeme zabývat v pozdějších částech této knihy. Tato třída tabulek má některé velmi příjemné vlastnosti, pro které je programování s tabulkami třídy tibble pohodlnější než programování s tabulkami třídy data.frame.

Tabulku třídy tibble vytvoříte pomocí funkce tibble():

library(tibble)
ds <- tibble(
    x = 1:1e6,
    y = 2 * x,
    z = x / 3 + 1.5 * y - 7)
ds
# A tibble: 1,000,000 × 3
       x     y      z
   <int> <dbl>  <dbl>
 1     1     2 -3.67 
 2     2     4 -0.333
 3     3     6  3    
 4     4     8  6.33 
 5     5    10  9.67 
 6     6    12 13    
 7     7    14 16.3  
 8     8    16 19.7  
 9     9    18 23    
10    10    20 26.3  
# ℹ 999,990 more rows

Pokud potřebujete vytvořit tabulku třídy tibble ručně, existuje i příjemná funkce tribble(), která umožňuje zadávat data po řádcích:

tribble(
    ~name, ~weight, ~height,
    "Adam", 68, 193,
    "Bětka", 55, 163,
    "Cyril", 103, 159
)
# A tibble: 3 × 3
  name  weight height
  <chr>  <dbl>  <dbl>
1 Adam      68    193
2 Bětka     55    163
3 Cyril    103    159

Ke konverzi jiných tříd na tibble slouží funkce as_tibble().

Vytvoření tibble se od vytvoření data.frame v několika ohledech liší: tibble

  1. nemění “nepovolená” jména sloupců na povolená nahrazením divných znaků tečkami,
  2. vyhodnocuje své argumenty postupně, takže můžete později zadaný argument použít při tvorbě dříve zadaného argumentu (jako v příkladu výše),
  3. podporuje jen omezenou recyklaci: všechny zadané vektory musejí mít buď stejnou délku, nebo délku 1,
  4. nepoužívá jména řádků (která jsou ostatně nebezpečná k uchovávání dat) a
  5. převod na tibble pomocí as_tibble() je rychlejší než převod na data.frame pomocí as.data.frame().

Liší se také to, jak tibble vypisuje svou hodnotu do konzole. Oproti data.frame zobrazí tibble navíc rozměr tabulky a typ jednotlivých proměnných. Naopak vypíše jen prvních deset řádků a jen takový počet sloupců, které se vejdou na obrazovku. Počet vypsaných řádků je možné ovlivnit ve funkci print() pomocí parametru n; počet sloupců pomocí parametru width, kde width je maximální počet znaků, které může tibble při tisku použít:

print(ds, n = 5)
# A tibble: 1,000,000 × 3
      x     y      z
  <int> <dbl>  <dbl>
1     1     2 -3.67 
2     2     4 -0.333
3     3     6  3    
4     4     8  6.33 
5     5    10  9.67 
# ℹ 999,995 more rows

Hlavní rozdíl mezi tibble a data.frame se však týká indexování: tibble má konzistentnější chování a (na rozdíl od data.frame) vrací vždy stejnou datovou strukturu, což je výhodné zejména při programování. tibble nikdy nezahazuje zbytečné rozměry a ani je nepřidává. To znamená, že [] vždy vrací tibble, zatímco [[]] a $ vždy vrací vektor. Navíc tibble nikdy nepodporuje partial matching:

ds <- ds[1:6, ]  # omezíme ds na prvních 6 řádků
ds[, 1]
# A tibble: 6 × 1
      x
  <int>
1     1
2     2
3     3
4     4
5     5
6     6
ds[[1]]
[1] 1 2 3 4 5 6
ds$z
[1] -3.6666667 -0.3333333  3.0000000  6.3333333  9.6666667 13.0000000

Tato příjemná konzistence má však i svá nebezpečí. Technicky je tibble (stejně jako data.frame) seznamem vektorů. Přestože jsme zatím vždy uvažovali atomické vektory, ve skutečnosti mohou být sloupci tibble i seznamy. To znamená, že následují kód ošklivě selže, protože do proměnné vloží celou tabulku:

ds <- tibble(a = 1:3, b = 11:13)
ds$c <- ds[, "a"]  # vybere se tibble s jedním sloupcem a vloží do sloupce b
ds
# A tibble: 3 × 3
      a     b   c$a
  <int> <int> <int>
1     1    11     1
2     2    12     2
3     3    13     3
ds$c
# A tibble: 3 × 1
      a
  <int>
1     1
2     2
3     3
class(ds$c)
[1] "tbl_df"     "tbl"        "data.frame"

Správný postup je opět použít operátor, který z dat vybere jeden sloupec:

ds$d <- ds$a
ds$e <- ds[["a"]]
ds
# A tibble: 3 × 5
      a     b   c$a     d     e
  <int> <int> <int> <int> <int>
1     1    11     1     1     1
2     2    12     2     2     2
3     3    13     3     3     3
class(ds$d)
[1] "integer"
class(ds$e)
[1] "integer"

Možnost využívající dvojité hranaté závorky se hodí zejména v případě, kdy máte jméno sloupce uložené v nějaké proměnné, takže je nemůžete zapsat přímo:

name <- "a"
ds$f <- ds[[name]]
ds
# A tibble: 3 × 6
      a     b   c$a     d     e     f
  <int> <int> <int> <int> <int> <int>
1     1    11     1     1     1     1
2     2    12     2     2     2     2
3     3    13     3     3     3     3

Někdy se nehodí pracovat s tibble – např. proto, že některé funkce očekávají jiný výsledek indexování. V takovém případě můžete tibble převést na data.frame pomocí konverzní funkce as.data.frame():

as.data.frame(ds)
Warning in format.data.frame(if (omit) x[seq_len(n0), , drop = FALSE] else x, :
corrupt data frame: columns will be truncated or padded with NAs
  a  b                 c d e f
1 1 11 # A tibble: 3 × 1 1 1 1
2 2 12                 a 2 2 2
3 3 13             <int> 3 3 3

4.6 Operátor trubka (|> a %>%)

Při datové analýze se často hodí používat operátor “trubka”. R má dnes dvě verze tohoto operátoru: operátor |>, který je součástí základního R (od verze 4.1) a operátor %>% definovaný v balíku magrittr (načtou jej však i některé další balíky, jako např. dplyr). RStudio zavádí pro “trubku” speciální klávesovou zkratku Ctrl-Shift-M. V menu Tools\(\rightarrow\)Global Options...\(\rightarrow\)Code\(\rightarrow\)Editing si můžete vybrat, zda se použije nativní verze, nebo verze z balíku magrittr.

Trubky umožňují zapsat výraz se složitě zanořenými funkcemi poněkud čitelnějším způsobem. Řekněme, že chceme na proměnnou x aplikovat funkci f() a na výsledek funkci g(). Standardním způsobem bychom to zapsali takto:

g(f(x))

Operátor trubka umožní výraz přepsat do čitelnější podoby:

library(magrittr)
x |> f() |> g()  # nebo
x %>% f() %>% g()

Tato syntaxe lépe vystihuje to, co chceme provést. Doslova říká: vezmi proměnnou x a vlož ji jako první argument do funkce f(). Výsledek vlož jako první argument do funkce g(). Výrazy x |> f() a x %>% f() jsou tedy ekvivalentní výrazu f(x). Funkce f() a g() samozřejmě mohou obsahovat i další parametry. Ty se zařadí za proměnnou x, takže x |> mean(na.rm = TRUE) je ekvivalentní výrazu mean(x, na.rm = TRUE); stejně tak výraz s %>%.

Podívejme se na typický příklad použití trubek. Řekněme, že máme tabulku df (zde si ji vytvoříme ručně) a chceme pro každou unikátní hodnotu identifikátoru x spočítat průměrné hodnoty proměnných y a z. To můžeme udělat např. takto:

library(tibble)
library(dplyr)
df <- tribble(
    ~x, ~y, ~z,
     1,  1,  1,
     1,  2,  3,
     2,  3,  5,
     2,  4,  6,
     3,  5,  7
)
df |> group_by(x) |> summarize(my = mean(y), mz = mean(z))
# A tibble: 3 × 3
      x    my    mz
  <dbl> <dbl> <dbl>
1     1   1.5   2  
2     2   3.5   5.5
3     3   5     7  

Výraz s použitím trubky %>% dá stejný výsledek:

df %>% group_by(x) %>% summarize(my = mean(y), mz = mean(z))

Detaily fungování tohoto kódu si vysvětlíme v kapitole 13. Nyní nás zajímá jen fungování trubek: proměnná df se nejdříve vloží do funkce group_by() a vyhodnotí se jako group_by(df, x). Výsledek této operace se pak vloží na první místo do funkce summarize() a vyhodnotí se jako summarize(., my = mean(y), mz = mean(z)), kde tečka označuje výsledek předchozího výpočtu, tj. funkce group_by().

Mnoho funkcí, se kterými budeme pracovat, je přizpůsobeno používání trubek, takže data do nich vstupují jako první argument. Některé funkce však berou data na jiné pozici. V takovém případě je potřeba určit, na místo kterého parametru mají data předaná funkci zleva vstoupit. K tomu slouží tzv. “placeholder”. V případě nativní trubky |> slouží od verze R 4.2 jako “placeholder” podtržítko (znak _). Parametr však musí být “pojmenovaný”, tj. volaný svým jménem. Naproti tomu verze %>% z balíku magrittr používá jako “placeholder” speciální proměnnou tečka (.). Parametr je v tomto případě možné volat jeho pozicí, tj. i bez uvedení jména parametru. (Více o použití parametrů ve funkcích najdete v kapitole 7.)

Ukažme si to na příkladu. Funkce lm() odhadne lineární regresní model. Jako první parametr bere formuli, která popisuje odhadovaný model, a jako druhý parametr data (více o regresní analýze najdete v kapitole 15). Řekněme, že chceme odhadnout jednoduchý lineární model, který vysvětluje, kolik mil dokáže vůz dojet na jeden galon paliva v závislosti na objemu jeho motoru. Dále řekněme, že data mtcars chceme do funkce lm() předat pomocí trubky. Pak to můžeme udělat jedním z těchto způsobů:

mtcars |> lm(mpg ~ disp, data = _)
mtcars %>% lm(mpg ~ disp, .)

Call:
lm(formula = mpg ~ disp, data = mtcars)

Coefficients:
(Intercept)         disp  
   29.59985     -0.04122  

Někdy se hodí extrahovat nějakou část výsledku vráceného trubkou. U nativních trubek k tomu (v R od verze 4.3) slouží výraz _$x, u magrittrových trubek výraz .$x. Řekněme např., že u předchozího regresního modelu chceme z odhadu získat vektor bodových odhadů koeficientů. Funkce lm() vrací objekt třídy lm, což je fakticky seznam, který (mimo jiné) obsahuje i bodový odhad parametrů uložený v prvku se jménem coef. Ten pak můžeme získat např. takto:

mtcars |> lm(mpg ~ disp, data = _) |> _$coef
mtcars %>% lm(mpg ~ disp, .) %>% .$coef
(Intercept)        disp 
29.59985476 -0.04121512 

Pozor: “Placeholders” fungují uvedeným způsobem pouze v případě, že jsou ve funkci uvedeny přímo (jak jsme to viděli výše), tj. ne jako součást většího výrazu. Nativní trubka |> neumožňuje, aby byl “placeholder” součástí výrazu – pokud to potřebujeme, musíme použít anonymní funkci (viz kapitola 7). Operátor %>% z balíku magrittr je flexibilnější: Pokud je “placeholder” tečka použit jako součást nějakého výrazu, pak trubka vloží data tekoucí zleva na první místo ve funkci. To znamená, že x %>% f(y = nrow(.), z = ncol(.)) je ekvivalentní s f(x, y = nrow(x), z = ncol(x)). Pokud tomu chcete zabránit, musíte funkci f() uzavřít do bloku. Takže f(y = nrow(x), z = ncol(x)) musíme zapsat pomocí trubky jako x %>% {f(y = nrow(.), z = ncol(.))}.

Obecně platí, že nativní trubka |> je o něco méně flexibilní, je však rychlejší. Ve většině případů na tom nezáleží, protože volání trubek trvá v řádu nano až mikrosekund. Pokud však opakovaně simulujete velké množství vzorků dat, může být i takový drobný rozdíl užitečný. (Druhou výhodou nativní trubky je fakt, že pokud použijete v RStudiu vhodný font s ligagurami, pak se bude nativní trubka zobrazovat jako pěkný trojúhelníček.)

Balík magrittr definuje i některé další operátory. Seznámit se s nimi můžete na webu balíku http://magrittr.tidyverse.org/ a v kapitole “Pipes” ve Wickham a Grolemund (2017) dostupné na http://r4ds.had.co.nz/pipes.html. Doporučuji však omezit se více méně jen na základní operátory |> a %>%.

4.7 Poznámka k doplňování hodnot do tabulek

Stejně jako v případě ostatních základních datových struktur v R, je i v případě tabulek možné přiřadit novou hodnotu do výběru. To se hodí např. v situaci, kdy potřebujete nějakou hodnotu opravit nebo aktualizovat. Často se také hodí ukládat vypočtené nebo stahované hodnoty do tabulky. V tomto posledním případě je však potřeba jisté opatrnosti, protože ukládání do výběru tabulky je výpočetně nákladná operace (kvůli nákladům na vyhledání dané hodnoty). V případě malých tabulek to není problém, v případě tabulek o milionech řádků to už však může být problematické. Ukažme si to na příkladu (nenechte se vyvést z míry tím, že nebudete rozumět všem detailům; většině porozumíte při čtení následujících kapitol této knihy).

Nejdříve vytvoříme dva vektory o délce 10 milionů hodnot, první celočíselný a druhý reálný. Vektory naplníme chybějícími hodnotami NA. Vytvoříme i dvě tabulky, které naplníme těmito vektory. První tabulka bude třídy data.frame, druhá třídy tibble. Náš test spočívá v tom, že do každého vektoru budeme chtít vložit na stejnou (1000.) pozici jedničku. Funkce microbenchmark() ze stejnojmenného balíku nám změří, jak dlouho bude tato operace trvat.

library(microbenchmark)
library(dplyr)
library(tibble)

x <- rep(NA_integer_, 1e7)
y <- rep(NA_real_, 1e7)
df <- data.frame(x = x, y = y)
dt <- tibble(x = x, y = y)

performance <- microbenchmark(
    "vektory" = {x[1000] <- 1L; y[1000] <- 1},
    "data.frame 1" = {df[1000, "x"] <- 1L; df[1000, "y"] <- 1},
    "data.frame 2" = {df$x[1000] <- 1L; df$y[1000] <- 1},
    "data.frame 3" = df[1000, ] <- data.frame(x = 1L, y = 1),
    "tibble 1" = {dt[1000, "x"] <- 1L; dt[1000, "y"] <- 1},
    "tibble 2" = {dt$x[1000] <- 1L; dt$y[1000] <- 1},
    "tibble 3" = dt[1000, ] <- tibble(x = 1L, y = 1),
    unit = "ms"
) |>
    summary() |>
    select(expr, min, mean, median, max)
Tabulka 4.2: Čas potřebný na vložení dvou hodnot do vektorů a tabulek v milisekundách.
expr min mean median max
vektory 0.000622 0.5417682 0.007198 53.54337
data.frame 1 52.189962 75.1441825 61.026193 137.34468
data.frame 2 34.846538 66.8054057 53.276575 136.41073
data.frame 3 52.371052 73.3322471 60.610198 134.05430
tibble 1 52.447796 70.0556024 59.992705 137.01712
tibble 2 52.384948 70.3947987 58.773759 136.24085
tibble 3 53.121079 73.9137323 61.260989 135.01659

Výsledky jsou dramaticky odlišné, jak ukazuje tabulka 4.2. Vložení dvou hodnot do tabulek trvá (na mém poměrně výkonném počítači) v průměru několik desítek milisekund, zatímco vložení do vektoru trvá na stejném počítači tisíciny milisekund (pokud se díváme na mediány). Praktický výsledek je zřejmý: pokud vkládáte do tabulky jednotlivé hodnoty, nevadí to, pokud je tabulka malá nebo vkládáte jen velmi málo hodnot. V opačném případě se výrazně vyplatí pracovat s vektory. Jednou jsem potřeboval zjistit vzdálenosti vzdušnou čarou mezi asi 5 000 čerpacími stanicemi. Bylo tedy potřeba spočítat a uložit asi 12 milionů vzdáleností (vzdálenost vzdušnou čarou je symetrická). Vlastní výpočet vzdálenosti je extrémně rychlý. Přesto však celý výpočet neskončil ani za čtyři dny – spočítalo se asi jen 1.5 milionu hodnot. Na vině bylo to, že jsem doplňoval hodnoty přímo do tabulky. Když jsem kód přepsal tak, aby pracoval s vektory, které jsem spojil do tabulky až nakonec, výpočet proběhl zhruba za půl hodiny.

Všimněte si také toho, že jsem vektory a tabulky celé předalokoval. Kdybychom chtěli datové struktury zvětšovat postupně přidáváním dalších a dalších hodnot nebo řádků, byl by výpočet ještě pomalejší, protože by R muselo neustále alokovat nové bloky paměti a do nich nejdříve překopírovat stará data, a teprve potom přidat nové hodnoty. To by se opakovalo při každém zvětšení vektoru nebo tabulky.

Pokud se chcete dozvědět víc o efektivnosti kódu v R, doporučuji zejména Wickham (2014), kap. Performance a Profiling. Užitečné je také Burns (2011).

4.8 Volba datové struktury

Vstupní data pro jakoukoli analýzu budou mít ve většině případů formát tabulky. Naproti tomu si strukturu dat, která vzniknou transformacemi původních dat, můžete zvolit sami. Tuto strukturu byste si měli dopředu pořádně rozmyslet. Vhodně zvolená struktura vám umožní s daty pracovat jednoduše; špatně zvolená struktura může v následné práci dělat problémy.

Ve většině případů doporučuji používat pro uschování jakýchkoli dat tabulky (nejlépe třídy tibble). Oproti jiným strukturám mají několik výhod: 1) snadno se ukládají, čtou a převádí do jiného software, 2) snadno se z nich dělají výběry, 3) snadno se transformují a 4) snadno se vizualizují, ať už jako tabulky nebo v grafech. R nabízí mnoho balíků pro transformace tabulek (zejména tidyr a dplyr) a pro jejich vizualizaci (zejména ggplot2); o těchto balících bude řeč později. Práce s ostatními datovými strukturami je mnohem méně standardizovaná, takže si víc kódu budete muset napsat sami.

Použití jiné datové struktury k úschově dat má smysl pouze ve speciálních situacích: 1) pokud jinou strukturu (typicky vektory nebo matice) vyžadují použité funkce jako své vstupy, 2) když potřebujete rychlou maticovou aritmetiku a 3) když jsou složitější objekty výsledkem výpočtů (např. modelové objekty v ekonometrii). Pro dočasnou úschovu nehomogenních dat se hodí i seznamy, které typicky vznikají iteracemi nad vektory. Přesto vám výrazně doporučuji, abyste si pokaždé, když budete chtít zvolit jinou datovou strukturu než tabulku, tuto volbu raději několikrát promysleli.