Co budeme dělat

Často je potřeba provést nějakou operaci s každým prvkem vektoru, seznamu, matice apod.

Většina programovacích jazyků k tomu používá cykly. R však nabízí lepší možnosti.

  • operace nad prvky vektorů
  • operace nad prvky matic
  • srovnání s cykly

(V lecture notest je toho více!)

Vektorové funkce

Mnoho funkcí v R je vektorizovaných, tj. pracují s vektory po prvcích:

x <- (1:1000) / 10
y <- sin(x)
plot(x, y, type = "l")

Operace nad vektory

lapply(x, f, ...) spustí funkci f na každý prvek vektoru x a výsledky poskládá do seznamu.

Vektor x může být atomický vektor nebo seznam.

Parametry ... se předají funkci f jako 2., 3. a další parametr.

Operace nad vektory: příklad 1

Jakou délku mají jednotlivé vektory v seznamu:

v <- list(1, 1:2, 1:3, 1:4, 1:5)
lapply(v, length)
## [[1]]
## [1] 1
## 
## [[2]]
## [1] 2
## 
## [[3]]
## [1] 3
## 
## [[4]]
## [1] 4
## 
## [[5]]
## [1] 5

Operace nad vektory: příklad 2a

Jaký datový typ mají jednotlivé sloupce datasetu (data.frame je zároveň tabulka i seznam; funkce lapply() s ním zachází jako se seznamem sloupců):

df <- data.frame(x = 1:6,
                 y = c(rnorm(1:5), NA),
                 z = c(NA, letters[1:4], NA),
                 stringsAsFactors = FALSE)
df
##   x          y    z
## 1 1 -1.3681875 <NA>
## 2 2 -0.3120006    a
## 3 3  0.6331625    b
## 4 4 -0.2116848    c
## 5 5  1.0231018    d
## 6 6         NA <NA>

Operace nad vektory: příklad 2b

lapply(df, class)
## $x
## [1] "integer"
## 
## $y
## [1] "numeric"
## 
## $z
## [1] "character"

Operace nad vektory: příklad 2b

Kolik obsahuje který sloupec datasetu hodnot NA:

lapply(df, function(x) sum(is.na(x)))  # používá anonymní funkci
## $x
## [1] 0
## 
## $y
## [1] 1
## 
## $z
## [1] 2

Operace nad vektory: příklad 3

Chceme vytvořit několik vektorů náhodných čísel o různých délkách:

v1 <- lapply(1:5, rnorm)  # normální rozložení, mean = 0, sd = 1
v1
## [[1]]
## [1] 2.630638
## 
## [[2]]
## [1] 1.045436 1.789848
## 
## [[3]]
## [1]  0.3348189 -1.1796583 -1.0774210
## 
## [[4]]
## [1] -2.41223480  0.08406078  0.57553749 -1.28017514
## 
## [[5]]
## [1]  0.6406824  1.1447330  0.1219580 -0.3648760  0.8637794
v2 <- lapply(1:5, rnorm, mean = 10, sd = 10)  # normální rozložení, mean = 10, sd = 10
                                              # extra parametry předány do rnorm()
v2
## [[1]]
## [1] 12.27842
## 
## [[2]]
## [1]  2.797489 18.525450
## 
## [[3]]
## [1] 20.863774 17.499333  5.733281
## 
## [[4]]
## [1] 14.96041 17.82953 11.11295 18.00856
## 
## [[5]]
## [1] -19.484140  24.201079   2.534614 -20.105132  16.785621

Zjednodušení výsledku (1)

Často nechceme výsledek jako seznam, ale chceme jej zjednodušit na vektor nebo matici. K tomu slouží dvě funkce: sapply() a vapply().

Funkce sapply(x, f, ...) nejdříve spustí lapply(), a pak se pokusí její výsledek zjednodušit:

  • pokud je výsledkem lapply() seznam vektorů o délce 1, vrátí vektor
  • pokud je výsledkem seznam vektorů o stejných délkách, ale vyšších než 1, vrátí matici
  • jinak vrátí seznam

Zjednodušení výsledku: příklad 1

Počet hodnot NA v datasetu tak zjistíme jako (názvy hodnot odpovídají jménům sloupců datasetu):

sapply(df, function(x) sum(is.na(x)))  # používá anonymní funkci
## x y z 
## 0 1 2

Zjednodušení výsledku (2)

Funkce sapply() se dobře hodí pro interaktivní práci.

Pro psaní skriptů je nebezpečná, protože výsledek nemá jasnou datovou strukturu: pokud nedokáže seznam zjednodušit na vektor nebo matici, bez varování vrátí seznam.

Ve skriptech bezpečnější použít funkci vapply(x, f, fv, ...), které zadáme datový typ výsledku.

Typ výsledku se zadává jako vektor příkladů (tj. celé číslo např. jako 1L, reálné číslo jako numeric(1) nebo 1.0 apod.)

Pokud se konverze nezdaří, vydá funkce vapply() chybové hlášení.

Zjednodušení výsledku: příklad 2

Totéž, co předchozí příklad:

vapply(df, function(x) sum(is.na(x)), 1L)  
## x y z 
## 0 1 2

Práce nad sloupci datasetu

Někdy chceme provést nějakou operaci se všemi sloupci datasetu tak, aby výsledkem byl opět dataset.

Výsledek lapply() stačí převést na data.frame explicitní konverzí.

Práce nad sloupci datasetu: příklad a

Řekněme, že máte dataset, který obsahuje pouze číselné sloupce a že každý z nich chcete vydělit 1 000 (možná v důsledku měnové reformy):

df <- data.frame(name = LETTERS[1:3],
                 income = c(1.3, 1.5, 1.7) * 1e6,
                 rent = c(500, 450, 580) * 1e3,
                 loan = c(0, 250, 390) * 1e9,
                 stringsAsFactors = FALSE)
df
##   name  income   rent    loan
## 1    A 1300000 500000 0.0e+00
## 2    B 1500000 450000 2.5e+11
## 3    C 1700000 580000 3.9e+11

Práce nad sloupci datasetu: příklad b

# výsledek je seznam
lapply(df, function(x) if (is.numeric(x)) x / 1000 else x)
## $name
## [1] "A" "B" "C"
## 
## $income
## [1] 1300 1500 1700
## 
## $rent
## [1] 500 450 580
## 
## $loan
## [1] 0.0e+00 2.5e+08 3.9e+08

Práce nad sloupci datasetu: příklad c

# výsledek při explicitní konverzi
as.data.frame(lapply(df, function(x) if (is.numeric(x)) x / 1000 else x))
##   name income rent    loan
## 1    A   1300  500 0.0e+00
## 2    B   1500  450 2.5e+08
## 3    C   1700  580 3.9e+08
# při nahrazování in-place funguje i toto
df[,] <- lapply(df, function(x) if (is.numeric(x)) x / 1000 else x)
df
##   name income rent    loan
## 1    A   1300  500 0.0e+00
## 2    B   1500  450 2.5e+08
## 3    C   1700  580 3.9e+08

K procvičení

R obsahuje standardní dataset mtcars:

head(mtcars, n = 3)
##                mpg cyl disp  hp drat    wt  qsec vs am gear carb
## Mazda RX4     21.0   6  160 110 3.90 2.620 16.46  0  1    4    4
## Mazda RX4 Wag 21.0   6  160 110 3.90 2.875 17.02  0  1    4    4
## Datsun 710    22.8   4  108  93 3.85 2.320 18.61  1  1    4    1
  • mpg = Miles/(US) gallon
  • cyl = Number of cylinders
  • disp = Displacement (cu.in.)
  • hp = Gross horsepower

K procvičení (pokrač.)

Zjistěte:

  • jakého datového typu je který sloupec
  • kolik hodnot NA obsahuje který sloupec

Přidejte jména auta (jména řádků) do datasetu jako sloupce car, vymažte jména sloupců a následně standardizujte všechny numerické sloupce pomocí funkce scale().

Jakou datovou strukturu má výsledek tohoto výpočtu? Zajistěte, aby výsledek byl data.frame.

Funkce replicate() (1)

replicate(n, e) vyhodnotí \(n\)-krát výraz \(e\), který typicky obsahuje generátor náhodných čísel.

Spočítáme sto průměrů tisíce náhodných čísel s exponenciálním rozdělením a vykreslíme jeho histogram:

hist(replicate(100, mean(rexp(1000))))

Funkce replicate() (2)

Funkce replicate() se stejně jako sapply() snaží výsledek zjednodušit na vektor nebo matici.

To lze vypnout pomocí parametru simplify = FALSE.

Odhad mnoha lineárních modelů

Odhadneme mnoho modelů a vypíšeme jejich \(R^2\):

formulas <- list(mpg ~ disp,
                 mpg ~ I(1 / disp),
                 mpg ~ disp + wt,
                 mpg ~ I(1 / disp) + wt)
models <- lapply(formulas, lm, data = mtcars)
sapply(models, function(m) summary(m)$r.squared)
## [1] 0.7183433 0.8596865 0.7809306 0.8838038

Poznámka: modely byste neměli vybírat podle \(R^2\).

Funkce do.call() (1)

Funkce do.call(f, args) volá funkci f s argumenty uloženými v seznamu args. Funkce může být dokonce zadána jako řetězec obsahující jméno funkce.

Volání funkce mean(), pokud jsou její parametry uložené v seznamu:

l <- list(c(1, 3, 5, NA, 9), na.rm = TRUE)
str(l)
## List of 2
##  $      : num [1:5] 1 3 5 NA 9
##  $ na.rm: logi TRUE
do.call(mean, l)            # totéž -- mean(c(l[[1]], l[[2]]))
## [1] 4.5

Funkce do.call() (2)

Spojení mnoha datasetů, které jsou uložené v seznamu product_data:

product_data <- do.call(rbind, product_data)

Načtení a spojení mnoha .csv (1)

# načtení knihoven
library(readr)
library(stringr)
# adresář s daty
DATADIR <- file.path("..", "data")
# seznam jmen souborů products_*.csv.gz (regulární výraz)
PRODUCT_FILES <- dir(DATADIR, "products_.*\\.csv\\.gz",
                     full.names = TRUE)
# řetězec s dvaceti "c" -- chceme načíst 20 řetězců
PRODUCT_FILE_MASK <- str_dup("c", 20)
# načtení jednotlivých souborů
product_data <-
    lapply(PRODUCT_FILES,
           function(x) read_csv2(file = x,
                                 col_types = PRODUCT_FILE_MASK))

Načtení a spojení mnoha .csv (2)

# kontrola, že všechny datasety mají stejnou strukturu
product_col_names <- lapply(product_data, colnames)
product_col_names_test <-
    sapply(product_col_names,
           function(x) all.equal(product_col_names[[1]], x))
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 <- do.call(rbind, product_data)

Operace nad maticemi a poli (1)

apply(X, margin, f, ...) použije funkci f na každý řádek, sloupec apod. tabulky X.

X může být matice nebo pole.

margin určí použitý rozměr (řádek je 1, sloupec 2 atd.)

Parametry ... se předají funkci f.

Operace nad maticemi a poli (2)

m <- matrix(1:20, nrow = 4)
m
##      [,1] [,2] [,3] [,4] [,5]
## [1,]    1    5    9   13   17
## [2,]    2    6   10   14   18
## [3,]    3    7   11   15   19
## [4,]    4    8   12   16   20
apply(m, 1, mean)  # průměrná čísla na řádcích
## [1]  9 10 11 12
apply(m, 2, mean)  # průměrná čísla ve sloupcích
## [1]  2.5  6.5 10.5 14.5 18.5

K procvičení

Vytvořte data.frame, který obsahuje výšku, šířku a hloubku nějakých objektů (zde náhodná čísla):

df <- data.frame(id = 1:100,
                 h = runif(100),
                 w = runif(100),
                 d = runif(100))

Normalizujte výšku h, šířku w a hloubku w každého objektu (tj. po řádcích) tak, že ji vydělíte objemem objektu (objem = h * w * d), tj. h[i] <- h[i] / (h[i] * w[i] * d[i]) atd.

Hint: násobek prvků vektoru vrací funkce prod(), transpozici matice vrací funkce t(). Výsledek je třeba převést na data.frame.

Upoutávka na dplyr

Někdy je potřeba vektory, matice a datasety rozdělit podle nějaké externí proměnné (typicky faktoru) nebo na ně aplikovat nějaké funkce po skupinách daných nějakou externí proměnnou (opět typicky faktorem).

Dají se k tomu použít funkce tapply() a split().

Ve většině případů se však takové operace provádí nad datasety, kde je možné použít mnohem komfortnější funkce z balíku dplyr, kterým se budeme zabývat později.

Srovnání s cykly

R poskytuje i klasické cykly for(), while() a repeat().

Většinou je lepší použít funkce typu apply():

  • jsou mnohem rychlejší
  • výsledný kód podstatně přehlednější

Použití cyklů

Existují tři typické případy, kdy je potřeba použít klasické cykly:

  • náhrada prvků vektorů, matic a datasetů "na místě"
  • rekurzivní výpočty
  • cyklus s neznámým počtem opakování (while a repeat)

Příklady viz Hadley Wickham: Advanced R, oddíl 11.6.

Domácí úkol

Vytvořte funkci smartScale(df) která vezme data.frame df a vrátí nový dataset takový, že

  • všechny numerické sloupce budou normalizované, tj. bude z nich odečtena střední hodnota a výsledek bude podělen směrodatnou odchylkou (k normalizaci využijte funkci scale())
  • všechny ne-numerické sloupce zůstanou stejné

Poznámka: otestujte, zda vaše funkce funguje správně pro celá i reálná čísla, faktory, logické hodnoty i řetězce.