第 20 章 函数式编程1
很多教材都是讲函数和循环,都是从for, while, ifelse
讲起
,如果我也这样讲,又回到了Base R的老路上去了。考虑到大家都没有编程背景,也不会立志当程序员,所以我直接讲purrr包,留坑以后填吧。
20.1 简单回顾
大家知道R常用的数据结构是向量、矩阵、列表和数据框,如下图
他们构造起来,很多相似性。
list(a = 1, b = "a") # list
c(a = 1, b = 2) # named vector
data.frame(a = 1, b = 2) # data frame
tibble(a = 1, b = 2) # tibble
20.2 向量化运算
a <- c(2, 4, 3, 1, 5, 7)
用for()
循环,让向量的每个元素乘以2
## [1] 4
## [1] 8
## [1] 6
## [1] 2
## [1] 10
## [1] 14
事实上,R语言是支持向量化(将运算符或者函数作用在向量的每一个元素上),可以用向量化代替循环
a * 2
## [1] 4 8 6 2 10 14
达到同样的效果。
再比如,找出向量a中元素大于2的所有值
## [1] 4
## [1] 3
## [1] 5
## [1] 7
用向量化的运算,可以轻松实现
a[a > 2]
## [1] 4 3 5 7
向量是R中最基础的一种数据结构,有种说法是“一切都是向量”,R中的矩阵、数组甚至是列表都可以看成是某种意义上的向量。因此,使用向量化操作可以大大提高代码的运行效率。
20.3 多说说列表
我们构造一个列表
## $num
## [1] 8 9
##
## $log
## [1] TRUE
##
## $cha
## [1] "a" "b" "c"
要想访问某个元素,可以这样
a_list["num"]
## $num
## [1] 8 9
注意返回结果,第一行是$num
,说明返回的结果仍然是列表, 相比a_list
来说,a_list["num"]
是只包含一个元素的列表。
想将num元素里面的向量提取出来,就得用两个[[
a_list[["num"]]
## [1] 8 9
大家知道程序员都是偷懒的,为了节省体力,用一个美元符号$
代替[[" "]]
六个字符
a_list$num
## [1] 8 9
在tidyverse里,还可以用
## [1] 8 9
或者
## [1] 8 9
20.4 列表 vs 向量
假定一向量
v <- c(-2, -1, 0, 1, 2)
v
## [1] -2 -1 0 1 2
我们对元素分别取绝对值
abs(v)
## [1] 2 1 0 1 2
如果是列表形式,abs
函数应用到列表中就会报错
lst <- list(-2, -1, 0, 1, 2)
abs(lst)
## Error in abs(lst): non-numeric argument to mathematical function
报错了。用在向量的函数用在list上,往往行不通。
再来一个例子:我们模拟了5个学生的10次考试的成绩
exams <- list(
student1 = round(runif(10, 50, 100)),
student2 = round(runif(10, 50, 100)),
student3 = round(runif(10, 50, 100)),
student4 = round(runif(10, 50, 100)),
student5 = round(runif(10, 50, 100))
)
exams
## $student1
## [1] 90 64 92 51 93 67 73 89 71 88
##
## $student2
## [1] 52 84 92 74 75 81 98 99 71 88
##
## $student3
## [1] 65 66 59 79 73 62 63 74 51 98
##
## $student4
## [1] 60 80 83 77 63 90 73 93 76 94
##
## $student5
## [1] 70 63 55 74 79 80 77 59 94 81
很显然,exams
是一个列表。那么,每个学生的平均成绩是多呢?
我们可能会想到用mean函数,但是
mean(exams)
## Warning in mean.default(exams): argument is not numeric or logical: returning
## NA
## [1] NA
发现报错了,可以看看帮助文档看看问题出在什么地方
?mean()
帮助文档告诉我们,mean()
要求第一个参数是数值型或者逻辑型的向量。
而我们这里的exams
是列表,因此无法运行。
那好,我们就用笨办法吧
list(
student1 = mean(exams$student1),
student2 = mean(exams$student2),
student3 = mean(exams$student3),
student4 = mean(exams$student4),
student5 = mean(exams$student5)
)
## $student1
## [1] 77.8
##
## $student2
## [1] 81.4
##
## $student3
## [1] 69
##
## $student4
## [1] 78.9
##
## $student5
## [1] 73.2
成功了。但发现我们写了好多代码,如果有100个学生,那就得写更多的代码,如果是这样,程序员就不高兴了,这太累了啊。于是purrr
包的map
函数来解救我们,下面主角出场了。
20.5 purrr
介绍之前,先试试
map(exams, mean)
## $student1
## [1] 77.8
##
## $student2
## [1] 81.4
##
## $student3
## [1] 69
##
## $student4
## [1] 78.9
##
## $student5
## [1] 73.2
哇,短短一句话,得出了相同的结果。
20.5.1 map函数
map()
函数的第一个参数是list或者vector, 第二个参数是函数
函数 f
应用到list/vector 的每个元素
于是输入的 list/vector 中的每个元素,都对应一个输出
最后,所有的输出元素,聚合成一个新的list
整个过程,可以想象 list/vector 是生产线上的盒子,依次将里面的元素,送入加工机器。 函数决定了机器该如何处理每个元素,机器依次处理完毕后,结果打包成list,最后送出机器。
在我们这个例子,mean()
作用到每个学生的成绩向量,
调用一次mean()
, 返回一个数值,所以最终的结果是五个数值的列表。
map(exams, mean)
## $student1
## [1] 77.8
##
## $student2
## [1] 81.4
##
## $student3
## [1] 69
##
## $student4
## [1] 78.9
##
## $student5
## [1] 73.2
我们也可以使用管道
## $student1
## [1] 77.8
##
## $student2
## [1] 81.4
##
## $student3
## [1] 69
##
## $student4
## [1] 78.9
##
## $student5
## [1] 73.2
20.5.2 map函数家族
如果希望返回的是数值型的向量,可以这样写map_dbl()
## student1 student2 student3 student4 student5
## 77.8 81.4 69.0 78.9 73.2
map_dbl()
要求每个输出的元素必须是数值型
如果每个元素是数值型,map_dbl()
会聚合所有元素构成一个原子型向量
如果希望返回的结果是数据框
## # A tibble: 1 × 5
## student1 student2 student3 student4 student5
## <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 77.8 81.4 69 78.9 73.2
是不是很酷?是不是很灵活
20.5.3 小结
事实上,map
函数
- 第一个参数是向量或列表(数据框是列表的一种特殊形式,因此数据框也是可以的)
- 第二个参数是函数,这个函数会应用到列表的每一个元素,比如这里
map
函数执行过程如下 :
具体为,exams
有5个元素,一个元素装着一个学生的10次考试成绩,
运行map(exams, mean)
函数后, 首先取出exams第一个元素exams$student1
(它是向量),然后执行
mean(exams$student1)
, 然后将计算结果存放在列表result中的第一个位置result1
上;
做完第一个学生的,紧接着取出exams
第二个元素exams$student2
,执行
mean(exams$student2)
, 然后将计算结果存放在列表result
中的第一个位置result2
上;
如此这般,直到所有学生都处理完毕。我们得到了最终结果—一个新的列表result
。
当然,我们也可以根据需要,让map返回我们需要的数据格式, purrr也提供了方便的函数,具体如下
我们将mean
函数换成求方差var
函数试试,
## # A tibble: 1 × 5
## student1 student2 student3 student4 student5
## <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 212. 202. 168. 136. 137.
20.5.4 额外参数
将每位同学的成绩排序,默认的是升序。
map(exams, sort)
## $student1
## [1] 51 64 67 71 73 88 89 90 92 93
##
## $student2
## [1] 52 71 74 75 81 84 88 92 98 99
##
## $student3
## [1] 51 59 62 63 65 66 73 74 79 98
##
## $student4
## [1] 60 63 73 76 77 80 83 90 93 94
##
## $student5
## [1] 55 59 63 70 74 77 79 80 81 94
如果我们想降序排,需要在sort()
函数里添加参数 decreasing = TRUE
。比如
sort(exams$student1, decreasing = TRUE)
## [1] 93 92 90 89 88 73 71 67 64 51
map很人性化,可以让函数的参数直接跟随在函数名之和
map(exams, sort, decreasing = TRUE)
## $student1
## [1] 93 92 90 89 88 73 71 67 64 51
##
## $student2
## [1] 99 98 92 88 84 81 75 74 71 52
##
## $student3
## [1] 98 79 74 73 66 65 63 62 59 51
##
## $student4
## [1] 94 93 90 83 80 77 76 73 63 60
##
## $student5
## [1] 94 81 80 79 77 74 70 63 59 55
当然,也可以添加更多的参数,map()会自动的传递给函数。
20.5.5 匿名函数
刚才我们是让学生成绩执行求平均mean
,求方差var
等函数。我们也可以自定义函数。
比如我们这里定义了将向量中心化的函数(先求出10次考试的平均值,然后每次考试成绩去减这个平均值)
## # A tibble: 10 × 5
## student1 student2 student3 student4 student5
## <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 12.2 -29.4 -4 -18.9 -3.20
## 2 -13.8 2.60 -3 1.10 -10.2
## 3 14.2 10.6 -10 4.10 -18.2
## 4 -26.8 -7.40 10 -1.90 0.800
## 5 15.2 -6.40 4 -15.9 5.8
## 6 -10.8 -0.400 -7 11.1 6.8
## 7 -4.8 16.6 -6 -5.90 3.80
## 8 11.2 17.6 5 14.1 -14.2
## 9 -6.8 -10.4 -18 -2.90 20.8
## 10 10.2 6.60 29 15.1 7.8
我们也可以不用命名函数,而使用匿名函数。匿名函数顾名思义,就是没有名字的函数,
function(x) x - mean(x)
我们能将匿名函数直接放在map()函数中
## $student1
## [1] 12.2 -13.8 14.2 -26.8 15.2 -10.8 -4.8 11.2 -6.8 10.2
##
## $student2
## [1] -29.4 2.6 10.6 -7.4 -6.4 -0.4 16.6 17.6 -10.4 6.6
##
## $student3
## [1] -4 -3 -10 10 4 -7 -6 5 -18 29
##
## $student4
## [1] -18.9 1.1 4.1 -1.9 -15.9 11.1 -5.9 14.1 -2.9 15.1
##
## $student5
## [1] -3.2 -10.2 -18.2 0.8 5.8 6.8 3.8 -14.2 20.8 7.8
还可以更加偷懒,用~
代替function()
,但代价是参数必须是规定的写法,比如.x
## $student1
## [1] 12.2 -13.8 14.2 -26.8 15.2 -10.8 -4.8 11.2 -6.8 10.2
##
## $student2
## [1] -29.4 2.6 10.6 -7.4 -6.4 -0.4 16.6 17.6 -10.4 6.6
##
## $student3
## [1] -4 -3 -10 10 4 -7 -6 5 -18 29
##
## $student4
## [1] -18.9 1.1 4.1 -1.9 -15.9 11.1 -5.9 14.1 -2.9 15.1
##
## $student5
## [1] -3.2 -10.2 -18.2 0.8 5.8 6.8 3.8 -14.2 20.8 7.8
有时候,程序员觉得x
还是有点多余,于是更够懒一点,只用.
, 也是可以的
## $student1
## [1] 12.2 -13.8 14.2 -26.8 15.2 -10.8 -4.8 11.2 -6.8 10.2
##
## $student2
## [1] -29.4 2.6 10.6 -7.4 -6.4 -0.4 16.6 17.6 -10.4 6.6
##
## $student3
## [1] -4 -3 -10 10 4 -7 -6 5 -18 29
##
## $student4
## [1] -18.9 1.1 4.1 -1.9 -15.9 11.1 -5.9 14.1 -2.9 15.1
##
## $student5
## [1] -3.2 -10.2 -18.2 0.8 5.8 6.8 3.8 -14.2 20.8 7.8
~
告诉 map()
后面跟随的是一个匿名函数,.
对应函数的参数,可以认为是一个占位符,等待传送带的student1、student2到student5 依次传递到函数机器。
如果熟悉匿名函数的写法,会增强代码的可读性。比如下面这段代码,找出每位同学有多少门考试成绩是高于80分的
## student1 student2 student3 student4 student5
## 5 6 1 4 2
总之,有三种方法将函数传递给map()
- 直接传递
map(.x, mean, na.rm = TRUE)
- 匿名函数
- 使用
~
function(.x) {
.x *2
}
# Equivalent
~ .x * 2
简单点说 ::: {.rmdnote}
function(x) x^2 + 5
~ .x^2 + 5
这是两种等价的写法。
这里的 .x
又可以写成 .
~ .^2 + 5
最后一种是偷懒到极致的写法
:::
20.6 在dplyr函数中的运用map
20.6.1 在Tibble中
Tibble本质上是向量构成的列表,因此tibble也适用map。假定有tibble如下
map()
中的函数f
,可以作用到每一列
map_dbl(tb, median)
## col_1 col_2 col_3
## 2.0 200.0 0.2
在比如,找出企鹅数据中每列缺失值NA
的数量
## species island bill_length_mm bill_depth_mm
## 0 0 2 2
## flipper_length_mm body_mass_g sex year
## 2 2 11 0
20.6.2 在col-column中
如果想显示列表中每个元素的长度,可以这样写
## # A tibble: 3 × 2
## x l
## <list> <int>
## 1 <dbl [1]> 1
## 2 <int [2]> 2
## 3 <int [3]> 3
用于各种函数,比如产生随机数
## # A tibble: 3 × 2
## x r
## <dbl> <list>
## 1 3 <dbl [3]>
## 2 5 <dbl [5]>
## 3 6 <dbl [6]>
用于建模
mtcars %>%
group_by(cyl) %>%
nest() %>%
mutate(model = purrr::map(data, ~ lm(mpg ~ wt, data = .))) %>%
mutate(result = purrr::map(model, ~ broom::tidy(.))) %>%
unnest(result)
## # A tibble: 6 × 8
## # Groups: cyl [3]
## cyl data model term estimate std.error statistic p.value
## <dbl> <list> <list> <chr> <dbl> <dbl> <dbl> <dbl>
## 1 6 <tibble [7 × 10]> <lm> (Interce… 28.4 4.18 6.79 1.05e-3
## 2 6 <tibble [7 × 10]> <lm> wt -2.78 1.33 -2.08 9.18e-2
## 3 4 <tibble [11 × 10]> <lm> (Interce… 39.6 4.35 9.10 7.77e-6
## 4 4 <tibble [11 × 10]> <lm> wt -5.65 1.85 -3.05 1.37e-2
## 5 8 <tibble [14 × 10]> <lm> (Interce… 23.9 3.01 7.94 4.05e-6
## 6 8 <tibble [14 × 10]> <lm> wt -2.19 0.739 -2.97 1.18e-2
更多内容和方法可参考第 39 章数据框列方向和行方向。