函數(shù)有3個(gè)好處:
- 更容易看清代碼意圖
- 更容易對需求變化做出反應(yīng)(改變)
- 更容易減少程序bug
除了函數(shù),減少重復(fù)代碼的另一種工具是迭代,它的作用在于可以對多個(gè)輸入執(zhí)行同一種處理,比如對多個(gè)列或多個(gè)數(shù)據(jù)集進(jìn)行同樣的操作妥畏。
迭代方式主要有兩種:
- 命令式編程 - for和while
- 函數(shù)式編程 - purrr
準(zhǔn)備工作
purrr是tidyverse的核心r包之一,提供了一些更加強(qiáng)大的編程工具及舍。
library(tidyverse)
#> ─ Attaching packages ─────────────────────────────────────────────────── tidyverse 1.2.1 ─
#> ? ggplot2 3.0.0 ? purrr 0.2.5
#> ? tibble 1.4.2 ? dplyr 0.7.6
#> ? tidyr 0.8.1 ? stringr 1.3.1
#> ? readr 1.1.1 ? forcats 0.3.0
#> ─ Conflicts ──────────────────────────────────────────────────── tidyverse_conflicts() ─
#> ? dplyr::filter() masks stats::filter()
#> ? dplyr::lag() masks stats::lag()
for循環(huán)與函數(shù)式編程
因?yàn)镽是一門函數(shù)式編程語言,我們可以先將for循環(huán)包裝在函數(shù)中窟绷,然后再調(diào)用函數(shù)锯玛,而不是使用for循環(huán),因此for循環(huán)在R中不像在其他編程語言中那么重要兼蜈。
為了說明函數(shù)式編程攘残,我們先利用下面簡單的數(shù)據(jù)框進(jìn)行一些思考:
df = tibble(
a = rnorm(10),
b = rnorm(10),
c = rnorm(10),
d = rnorm(10)
)
如果想要計(jì)算每列的均值,我們使用for循環(huán)完成任務(wù):
output = vector("double", length(df))
for (i in seq_along(df)) {
output[[i]] = mean(df[[i]])
}
output
#> [1] 0.45635 -0.17938 0.32879 0.00263
然后我們可能意識到需要頻繁地計(jì)算每列的均值饭尝,因此將代碼提取出來肯腕,轉(zhuǎn)換為一個(gè)函數(shù):
col_mean = function(df) {
output = vector("double", length(df))
for ( i in seq_along(df)) {
output[i] = mean(df[[i]])
}
output
}
然后我們覺得可能還需要這樣計(jì)算每列的中位數(shù)和標(biāo)準(zhǔn)差,因此復(fù)制粘貼了col_mean()
钥平,并使用相應(yīng)的median()
和sd()
函數(shù)替換了mean()
函數(shù):
col_median = function(df) {
output = vector("double", length(df))
for ( i in seq_along(df)) {
output[i] = median(df[[i]])
}
output
}
col_sd = function(df) {
output = vector("double", length(df))
for ( i in seq_along(df)) {
output[i] = sd(df[[i]])
}
output
}
(有時(shí)候我還真這么干的实撒。)
哎呀姊途,我們又復(fù)制粘貼了2次代碼,因此是不是該思考下如何擴(kuò)展一個(gè)代碼讓它同時(shí)發(fā)揮幾個(gè)函數(shù)的功能呢知态?這段代碼的大部分是一個(gè)for循環(huán)捷兰,而且如果不仔細(xì)很難看出3個(gè)函數(shù)有什么差別。
通過添加支持函數(shù)到每列的參數(shù)负敏,我們可以使用同一個(gè)函數(shù)解決3個(gè)問題:
col_summary = function(df, fun){
out = vector("double", length(df))
for (i in seq_along(df)) {
out[i] = fun(df[[i]])
}
out
}
col_summary(df, median)
#> [1] 0.4666 0.0269 0.6161 0.0573
col_summary(df, mean)
#> [1] 0.45635 -0.17938 0.32879 0.00263
將函數(shù)作為參數(shù)傳入另一個(gè)函數(shù)的做法是一種非常強(qiáng)大的功能贡茅,我們需要花些時(shí)間理解這種思想,但絕對是值得的其做。接下來我們將學(xué)習(xí)和使用purrr包顶考,它提供的函數(shù)可以替代很多常見的for循環(huán)應(yīng)用。R基礎(chǔ)包中的apply應(yīng)用函數(shù)族也可以完成類似的任務(wù)妖泄,但purrr包的函數(shù)更一致驹沿,也更容易學(xué)習(xí)。
使用purrr函數(shù)替代for循環(huán)的目的是將常見的列表問題分解為獨(dú)立的幾部分:
- 對于列表的單個(gè)元素蹈胡,我們能找到解決辦法嗎渊季?如果可以,我們就能使用purrr將該方法擴(kuò)展到列表的所有元素罚渐。
- 如果我們面臨的是一個(gè)復(fù)雜的問題却汉,那么將其分解為可行的子問題,然后依次解決荷并。使用purrr合砂,我們可以解決子問題,然后用管道將其組合起來源织。
映射函數(shù)
先對向量進(jìn)行循環(huán)既穆,然后對其每一個(gè)元素進(jìn)行一番處理,最后保存結(jié)果雀鹃。這種模式太普遍了,因而purrr包提供了一個(gè)函數(shù)族替我們完成這種操作励两。每種類型的輸出都有一個(gè)相應(yīng)的函數(shù):
-
map()
用于輸出列表 -
map_lgl()
用于輸出邏輯型向量 -
map_dbl()
用于輸出雙精度型向量 -
map_chr()
用于輸出字符型向量
每個(gè)函數(shù)都使用一個(gè)向量(注意列表可以作為遞歸向量看待)作為輸入黎茎,并對向量的每個(gè)元素應(yīng)用一個(gè)函數(shù),然后返回和輸入向量同樣長度的一個(gè)新向量当悔。向量的類型由映射函數(shù)的后綴決定傅瞻。
使用map()
函數(shù)族的優(yōu)勢不是速度,而是簡潔:它可以讓我們的代碼更易編寫盲憎,也更易閱讀嗅骄。
下面是進(jìn)行上一節(jié)一樣的操作:
library(purrr)
map_dbl(df, mean)
#> a b c d
#> 0.45635 -0.17938 0.32879 0.00263
map_dbl(df, median)
#> a b c d
#> 0.4666 0.0269 0.6161 0.0573
map_dbl(df, sd)
#> a b c d
#> 0.608 1.086 0.797 0.873
**與for循環(huán)相比,映射函數(shù)的重點(diǎn)在于需要執(zhí)行的操作(即mean()
饼疙、median()
和sd()
)溺森,而不是在所有元素中循環(huán)所需的跟蹤記錄以及保存結(jié)果。使用管道時(shí)這一點(diǎn)尤為突出:
df %>% map_dbl(mean)
#> a b c d
#> 0.45635 -0.17938 0.32879 0.00263
df %>% map_dbl(median)
#> a b c d
#> 0.4666 0.0269 0.6161 0.0573
df %>% map_dbl(sd)
#> a b c d
#> 0.608 1.086 0.797 0.873
map_*()
和col_summary()
具有以下幾點(diǎn)區(qū)別:
- 所有的purrr函數(shù)都是用C實(shí)現(xiàn)的,這讓它們的速度非称粱快医窿,但犧牲了一些可讀性。
- 第二個(gè)參數(shù)可以是一個(gè)公式炊林、一個(gè)字符向量或一個(gè)整型向量姥卢。
-
map_*()
使用...
向.f
傳遞一些附加參數(shù),供每次調(diào)用時(shí)使用 - 映射函數(shù)還保留名稱
快捷方式
對于第二個(gè)參數(shù).f
渣聚,我們可以使用幾種快捷方式來減少輸入量独榴。比如我們現(xiàn)在想對某個(gè)數(shù)據(jù)集中的每一個(gè)分組都擬合一個(gè)線性模型,下面示例將mtcars數(shù)據(jù)集拆分為3個(gè)部分(按照氣缸值分類)奕枝,并對每個(gè)部分?jǐn)M合一個(gè)線性模型:
models = mtcars %>%
split(.$cyl) %>%
map(function(df) lm(mpg ~ wt, data = df))
因?yàn)樵赗中創(chuàng)建匿名函數(shù)的語法比較復(fù)雜棺榔,所以purrr提供了一種更方便的快捷方式——單側(cè)公式:
models = mtcars %>%
split(.$cyl) %>%
map(~lm(mpg ~ wt, data = .))
上面.
作為一個(gè)代詞:它表示當(dāng)前列表元素(與for循環(huán)中用i表示當(dāng)前索引是一樣的)。
當(dāng)檢查多個(gè)模型時(shí)倍权,有時(shí)候我們需要提取像R方這樣的摘要統(tǒng)計(jì)量掷豺,要想完成這個(gè)任務(wù),我們需要先運(yùn)行summary()
函數(shù)薄声,然后提取結(jié)果中的r.squared:
models %>%
map(summary) %>%
map_dbl(~.$r.squared)
#> 4 6 8
#> 0.509 0.465 0.423
因?yàn)樘崛∶煞植僮鞣浅F毡榈贝詐urrr提供了一種更簡單的快捷方式:使用字符串。
models %>%
map(summary) %>%
map_dbl("r.squared")
#> 4 6 8
#> 0.509 0.465 0.423
對操作失敗的處理
當(dāng)使用映射函數(shù)重復(fù)多次操作時(shí)默辨,某次操作失敗的概率大大增加德频。這個(gè)時(shí)候我們會收到一條錯(cuò)誤信息,但得不到任何結(jié)果缩幸。這讓人很惱火壹置!我們怎么保證不會出現(xiàn)一條魚腥了一鍋湯?
safely()
是一個(gè)修飾函數(shù)(副詞)表谊,它接收一個(gè)函數(shù)(動詞)钞护,對其進(jìn)行修改并返回修改后的函數(shù)。這樣爆办,修改后的函數(shù)就不會拋出錯(cuò)誤难咕,相反,它總是返回由下面兩個(gè)元素組成的列表:
- result - 原始結(jié)果距辆。如果出現(xiàn)錯(cuò)誤余佃,那么它就是NULL
- error - 錯(cuò)誤對象。如果操作成功跨算,那么它就是NULL
下面用log()
函數(shù)進(jìn)行說明:
safe_log = safely(log)
str(safe_log(10))
#> List of 2
#> $ result: num 2.3
#> $ error : NULL
str(safe_log("a"))
#> List of 2
#> $ result: NULL
#> $ error :List of 2
#> ..$ message: chr "數(shù)學(xué)函數(shù)中用了非數(shù)值參數(shù)"
#> ..$ call : language log(x = x, base = base)
#> ..- attr(*, "class")= chr [1:3] "simpleError" "error" "condition"
safely()
函數(shù)也可以與map()
共同使用:
x = list(1, 10, "a")
y = x %>% map(safely(log))
str(y)
#> List of 3
#> $ :List of 2
#> ..$ result: num 0
#> ..$ error : NULL
#> $ :List of 2
#> ..$ result: num 2.3
#> ..$ error : NULL
#> $ :List of 2
#> ..$ result: NULL
#> ..$ error :List of 2
#> .. ..$ message: chr "數(shù)學(xué)函數(shù)中用了非數(shù)值參數(shù)"
#> .. ..$ call : language log(x = x, base = base)
#> .. ..- attr(*, "class")= chr [1:3] "simpleError" "error" "condition"
如果將以上結(jié)果轉(zhuǎn)換為2個(gè)列表爆土,一個(gè)列表包含所有錯(cuò)誤對象,另一個(gè)列表包含所有原始結(jié)果诸蚕,那么處理起來就會更容易步势。我們可以使用purrr::transpose()函數(shù)輕松完成該任務(wù):
y = y %>% transpose()
str(y)
#> List of 2
#> $ result:List of 3
#> ..$ : num 0
#> ..$ : num 2.3
#> ..$ : NULL
#> $ error :List of 3
#> ..$ : NULL
#> ..$ : NULL
#> ..$ :List of 2
#> .. ..$ message: chr "數(shù)學(xué)函數(shù)中用了非數(shù)值參數(shù)"
#> .. ..$ call : language log(x = x, base = base)
#> .. ..- attr(*, "class")= chr [1:3] "simpleError" "error" "condition"
我們可以自行決定如何處理錯(cuò)誤對象氧猬,一般來說,我們應(yīng)該檢查一下y中錯(cuò)誤對象所對應(yīng)的x值立润,或者使用y中的正常結(jié)果進(jìn)行一些處理:
is_ok = y$error %>% map_lgl(is_null)
x[!is_ok]
#> [[1]]
#> [1] "a"
y$result[is_ok] %>% flatten_dbl()
#> [1] 0.0 2.3
purrr還提供了兩個(gè)有用的修飾函數(shù):
- 與
safely()
類似狂窑,possibly()
函數(shù)總是會成功返回。它比safely()
還要簡單一些桑腮,因?yàn)榭梢栽O(shè)定出現(xiàn)錯(cuò)誤時(shí)返回一個(gè)默認(rèn)值:
x = list(1, 10, "a")
x %>% map_dbl(possibly(log, NA_real_))
#> [1] 0.0 2.3 NA
-
quietly()
函數(shù)與safely()
的作用基本相同泉哈,但前者結(jié)果不包含錯(cuò)誤對象,而是包含輸出破讨、消息和警告:
x = list(1, -1)
x %>% map(quietly(log)) %>% str()
#> List of 2
#> $ :List of 4
#> ..$ result : num 0
#> ..$ output : chr ""
#> ..$ warnings: chr(0)
#> ..$ messages: chr(0)
#> $ :List of 4
#> ..$ result : num NaN
#> ..$ output : chr ""
#> ..$ warnings: chr "產(chǎn)生了NaNs"
#> ..$ messages: chr(0)
x %>% map(safely(log)) %>% str()
#> Warning in .f(...): 產(chǎn)生了NaNs
#> List of 2
#> $ :List of 2
#> ..$ result: num 0
#> ..$ error : NULL
#> $ :List of 2
#> ..$ result: num NaN
#> ..$ error : NULL
多參數(shù)映射
前面我們提到的映射函數(shù)都是對單個(gè)輸入進(jìn)行映射丛晦,但有時(shí)候我們需要多個(gè)相關(guān)輸入同步迭代,這就是map2()和pmap()函數(shù)的用武之地提陶。
例如我們想模擬幾個(gè)均值不同的隨機(jī)正態(tài)分布烫沙,我們可以使用map
完成這個(gè)任務(wù):
mu = list(5, 10, -3)
mu %>%
map(rnorm, n = 5) %>%
str()
#> List of 3
#> $ : num [1:5] 5.65 6.48 6.35 4.61 4.74
#> $ : num [1:5] 8.93 8.93 10.67 10.98 8.72
#> $ : num [1:5] -4.04 -3.25 -2.16 -3.02 -2.53
如果我們想讓標(biāo)準(zhǔn)差也不同,一種方法是使用均值向量和標(biāo)準(zhǔn)差向量的索引進(jìn)行迭代:
sigma = list(1, 5, 10)
seq_along(mu) %>%
map(~rnorm(5, mu[[.]], sigma[[.]])) %>%
str()
#> List of 3
#> $ : num [1:5] 4.5 4.73 4.43 6.19 5.47
#> $ : num [1:5] 8.71 8.59 18.26 7.93 4.93
#> $ : num [1:5] -21.46 -7.94 -21.41 5.66 2.38
但這種方式比較難理解隙笆,我們使用map2()
進(jìn)行同步迭代:
map2(mu, sigma, rnorm, n = 5) %>% str()
#> List of 3
#> $ : num [1:5] 6.08 6.72 7.59 5.21 3.99
#> $ : num [1:5] 13.44 6.81 3.61 22.29 14.29
#> $ : num [1:5] 4.05 -1.77 -2.77 0.69 -23.91
注意這里每次調(diào)用時(shí)值發(fā)生變換的參數(shù)要放在映射函數(shù)前面锌蓄,值不變的參數(shù)要放在映射函數(shù)后面。
和map()
函數(shù)一樣撑柔,map2()
函數(shù)也是對for循環(huán)的包裝:
map2 = function(x, y, f, ...){
out = vector("list", length(x))
for (i in seq_along(x)) {
out[[i]] = f(x[[i]], y[[i]], ...)
}
out
}
(實(shí)際的map2()
并不是這樣的瘸爽,此處是給出R實(shí)現(xiàn)的一種思想)
根據(jù)這個(gè)函數(shù),我們可以涉及map3()
铅忿、map4()
等等剪决,但這樣實(shí)在無聊。purrr提供了pmap()
函數(shù)檀训,它可以將列表作為參數(shù)柑潦。如果我們想要生成均值、標(biāo)準(zhǔn)差和樣本數(shù)都不同的正態(tài)分布峻凫,可以使用:
n = list(1, 3, 5)
args1 = list(n, mu, sigma)
args1 %>%
pmap(rnorm) %>%
str()
#> List of 3
#> $ : num 3.55
#> $ : num [1:3] 8.4 10.9 -3.3
#> $ : num [1:5] 3.9 -11.61 2.06 7.14 -16.25
如果沒有為列表元素命名渗鬼,那么pmap()在調(diào)用函數(shù)時(shí)會按照位置匹配。這樣做容易出錯(cuò)而且可讀性差荧琼,因此最后使用命名參數(shù):
args2 = list(mean = mu, sd = sigma, n = n)
args2 %>%
pmap(rnorm) %>%
str()
#> List of 3
#> $ : num 6.18
#> $ : num [1:3] 11.2 18 14.8
#> $ : num [1:5] -5.27 6.57 1.88 6.53 -8.35
這樣更加安全乍钻。
因?yàn)殚L度都相同,所以將各個(gè)參數(shù)保存在一個(gè)數(shù)據(jù)框中:
params = tibble::tribble(
~mean, ~sd, ~n,
5, 1, 1,
10, 5, 3,
-3, 10, 5
)
params %>%
pmap(rnorm)
#> [[1]]
#> [1] 5.41
#>
#> [[2]]
#> [1] 5.4 10.2 14.4
#>
#> [[3]]
#> [1] -8.653 -4.457 9.747 -4.916 -0.436
調(diào)用不同的函數(shù)
還有一種更復(fù)雜的情況:不但傳給函數(shù)的參數(shù)不同铭腕,甚至函數(shù)本身也是不同的。
f = c("runif", "rnorm", "rpois")
param = list(
list(min = -1, max = 1),
list(sd = 5),
list(lambda = 10)
)
為了處理這種情況多糠,我們使用invoke_map()
函數(shù):
invoke_map(f, param, n = 5) %>% str()
#> List of 3
#> $ : num [1:5] 0.167 -0.235 -0.366 -0.933 0.304
#> $ : num [1:5] 6.961 3.642 13.405 0.536 -2.078
#> $ : int [1:5] 8 8 8 6 11
第1個(gè)參數(shù)是一個(gè)函數(shù)列表或包含函數(shù)名稱的字符串向量累舷。第2個(gè)參數(shù)是列表的一個(gè)列表,給出了要傳給各個(gè)函數(shù)的不同參數(shù)夹孔。隨后的參數(shù)要傳給每個(gè)函數(shù)被盈。
我們使用tribble()
讓參數(shù)配對更容易:
sim = tibble::tribble(
~f, ~params,
"runif", list(min = -1, max = 1),
"rnorm", list(sd = 5),
"rpois", list(lambda = 10)
)
sim %>%
dplyr::mutate(sim = invoke_map(f, params, n = 10))
#> # A tibble: 3 x 3
#> f params sim
#> <chr> <list> <list>
#> 1 runif <list [2]> <dbl [10]>
#> 2 rnorm <list [1]> <dbl [10]>
#> 3 rpois <list [1]> <int [10]>
游走函數(shù)
當(dāng)使用函數(shù)的目的是向屏幕提供輸出或?qū)⑽募4娴酱疟P——重要的是操作過程而不是返回值析孽,我們應(yīng)該使用游走函數(shù),而不是映射函數(shù)只怎。
下面是一個(gè)示例:
x = list(1, "a", 3)
x %>%
walk(print)
#> [1] 1
#> [1] "a"
#> [1] 3
一般來說袜瞬,walk()函數(shù)不如walk2()和pwalk()實(shí)用。例如有一個(gè)圖形列表和一個(gè)文件名向量身堡,那么我們就可以使用pwalk()
將每個(gè)文件保存到相應(yīng)的磁盤位置:
library(ggplot2)
plots = mtcars %>%
split(.$cyl) %>%
map(~ggplot(., aes(mpg, wt)) + geom_point())
paths = stringr::str_c(names(plots), ".pdf")
pwalk(list(paths, plots), ggsave, path = tempdir())
#> Saving 7 x 5 in image
#> Saving 7 x 5 in image
#> Saving 7 x 5 in image
我們來查看一下是不是建立好了:
dir(tempdir())
#> [1] "4.pdf" "6.pdf" "8.pdf"
for循環(huán)的其他模式
purrr還提供了其他一些函數(shù)邓尤,雖然這些函數(shù)的使用率低,但了解還是有必要的贴谎。本節(jié)就是對它們進(jìn)行簡單介紹
預(yù)測函數(shù)
一些函數(shù)可以與返回TRUE
或FALSE
的預(yù)測函數(shù)一同使用汞扎。
keep()
和discard()
函數(shù)可以分別保留輸入中預(yù)測值為TRUE
和FALSE
的元素(在數(shù)據(jù)框中就是指列):
iris %>%
keep(is.factor) %>%
str()
#> 'data.frame': 150 obs. of 1 variable:
#> $ Species: Factor w/ 3 levels "setosa","versicolor",..: 1 1 1 1 1 1 1 1 1 1 ...
iris %>%
discard(is.factor) %>%
str()
#> 'data.frame': 150 obs. of 4 variables:
#> $ Sepal.Length: num 5.1 4.9 4.7 4.6 5 5.4 4.6 5 4.4 4.9 ...
#> $ Sepal.Width : num 3.5 3 3.2 3.1 3.6 3.9 3.4 3.4 2.9 3.1 ...
#> $ Petal.Length: num 1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 ...
#> $ Petal.Width : num 0.2 0.2 0.2 0.2 0.2 0.4 0.3 0.2 0.2 0.1 ...
some()
和every()
函數(shù)分別用來確定預(yù)測值是否對某個(gè)元素為真以及是否對所有元素為真:
x = list(1:5, letters, list(10))
x %>%
some(is_character)
#> [1] TRUE
x %>%
every(is_vector)
#> [1] TRUE
detect()
可以找出預(yù)測值為真的第一個(gè)元素,detect_index()
可以返回該元素的索引擅这。
x = sample(10)
x
#> [1] 10 8 5 7 4 1 2 9 3 6
x %>%
detect(~ . >5)
#> [1] 10
x %>%
detect_index(~ . >5)
#> [1] 1
head_while()
和tail_while()
分別從向量的開頭和結(jié)尾找出預(yù)測值為真的元素:
x %>%
head_while(~ . > 5)
#> [1] 10 8
x %>%
tail_while(~ . > 5)
#> [1] 6
歸約和累計(jì)
操作一個(gè)復(fù)雜的列表澈魄,有時(shí)候我們想要不斷合并兩個(gè)預(yù)算兩個(gè)元素(基礎(chǔ)函數(shù)Reduce
干的事情)。
dfs = list(
age = tibble(name = "John", age = 30),
sex = tibble(name = c("John", "Mary"), sex = c("M", "F")),
trt = tibble(name = "Mary", treatment = "A")
)
dfs %>% reduce(full_join)
#> Joining, by = "name"
#> Joining, by = "name"
#> # A tibble: 2 x 4
#> name age sex treatment
#> <chr> <dbl> <chr> <chr>
#> 1 John 30 M <NA>
#> 2 Mary NA F A
這里我們使用reduce
結(jié)合dplyr
中的full_join()
將它們輕松合并為一個(gè)數(shù)據(jù)框仲翎。
reduce()
函數(shù)使用一個(gè)“二元函數(shù)”(即兩個(gè)基本輸入)痹扇,將其不斷應(yīng)用于一個(gè)列表,直到最后只剩下一個(gè)元素溯香。
累計(jì)函數(shù)與歸約函數(shù)類似鲫构,但會保留中間結(jié)果,比如下面求取累計(jì)和:
x = sample(10)
x
#> [1] 9 10 8 5 6 2 3 4 7 1
x %>% accumulate(`+`)
#> [1] 9 19 27 32 38 40 43 47 54 55
文章作者 王詩翔
上次更新 2018-10-04
許可協(xié)議 CC BY-NC-ND 4.0