感觉purrr 包的函数非常像py 中的匿名函数相关的函数。
而功能上,其起到的作用更像是简化和丰富了apply 家族函数的调用。
1. map 族
其实map 除了对向量有用,也可以作用于数据框或矩阵类型,相当于把其中的每一列作为一个单独的元素来看,有点像按列的apply:
代码语言:javascript复制> map(infos, typeof)
$family
[1] "character"
$name
[1] "character"
$born
[1] "double"
> apply(infos, 2, typeof)
family name born
"character" "character" "character"
2. purrr 中的无名函数
数据:
代码语言:javascript复制s <- c('10, 8, 7',
'5, 2, 2',
'3, 7, 8',
'8, 8, 9')
比如map 函数,如果需要使用自定义的无名函数,可以和apply 等使用类似:
代码语言:javascript复制map_dbl(strsplit(s, split=",", fixed=TRUE),
function(x) sum(as.numeric(x)))
## [1] 25 9 18 25
map 提供了简写用法:
代码语言:javascript复制map_dbl(strsplit(s, split=",", fixed=TRUE),
~ sum(as.numeric(.)))
将无名函数写成“~ 表达式”格式, 表达式就是无名函数定义, 用.表示只有一个自变量时的自变量名, 用.x和.y表示只有两个自变量时的自变量名, 用..1、..2、..3这样的名字表示有多个自变量时的自变量名。
需要注意的是, 如果map()等泛函中的无名函数需要访问其它变量的话, 需要理解其变量作用域或访问环境。另外, 无名函数中的其它变量在每次被map()应用到输入列表的元素时都会重新计算求值。建议这样的情况改用有名函数, 这样其中访问其它变量时作用域规则比较容易掌控, 也不会重复求值。(人话就是,如果要使用其他变量,就别简写啦)
ps: 其实也可以通过apply 族实现,不过确实代码有点儿乱七八糟了:
代码语言:javascript复制> lapply(s, function(x) sum(as.numeric(unlist(strsplit(x, ",")))))
[[1]]
[1] 25
[[2]]
[1] 9
[[3]]
[1] 18
[[4]]
[1] 25
而且apply 也不认识简写方法:
代码语言:javascript复制> lapply(s, ~ sum(as.numeric(unlist(strsplit(., ",")))))
Error in match.fun(FUN) :
'~sum(as.numeric(unlist(strsplit(., ","))))'不是函数,也不是字符,也不是符號
3. 提取列表元素的简写
map 除了调用无名函数时可以简写,在提取列表元素时也有简写的方法。
较为复杂的数据, 有时表现为列表的列表, 每个列表元素都是列表或者向量。JSON、YAML等格式转换为R对象就经常具有这种嵌套结构。一般这种类型的数据,导入的R 后就表现为嵌套列表的格式,也就是列表中的每个元素也都是列表。
代码语言:javascript复制od <- list(
list(
101, name="李明", age=15,
hobbies=c("绘画", "音乐")),
list(
102, name="张聪", age=17,
hobbies=c("足球"),
birth="2002-10-01")
)
为了取出每个列表元素的第一项,本来应该写成:
代码语言:javascript复制map_dbl(od, function(x) x[[1]])
## [1] 101 102
map_dbl(od, ~ .[[1]])
## [1] 101 102
purrr包提供了进一步的简化写法, 在需要一个函数或者一个“~ 表达式”的地方, 可以用整数下标值表示对每个列表元素提取其中的指定成分,如:
代码语言:javascript复制map_dbl(od, 1)
## [1] 101 102
> map_chr(od, "name")
[1] "李明" "张聪"
我们还可以指定一个列表,列表为成员序号或者成员名,实现逐层挖掘:
代码语言:javascript复制map_chr(od, list("hobbies", 1))
## [1] "绘画" "足球"
表示取出每个列表元素的hobbies成员的第一个元素(每人的第一个业余爱好)。
取出不存在的成员会出错, 但可以用一个.default
选项指定查找不到成员时的选项, 如:
map_chr(od, "birth", .default=NA)
## [1] NA "2002-10-01"
4. map 函数的变种
map的变种:
代码语言:javascript复制map_lgl():返回逻辑向量;
map_int():返回整型向量;
map_dbl(): 返回双精度浮点型向量(double类型);
map_chr(): 返回字符型向量。
除此之外,map 还有其他的变种:
代码语言:javascript复制modify(),输入一个数据自变量和一个函数, 输出与输入数据同类型的结果;
map2()可以输入两个数据自变量和一个函数, 将两个自变量相同下标的元素用函数进行变换, 输出列表;
imap()根据一个下标遍历;
walk()输入一个数据自变量和一个函数, 不返回任何结果,仅利用输入的函数的副作用;
输入若干个数据自变量和一个函数, 对数据自变量相同下标的元素用函数进行变换;
按照map 的输入类型,又可分:
代码语言:javascript复制一个数据自变量,代表为map();
两个自变量,代表为map2();
一个自变量和一个下标变量,代表为imap();
多个自变量,代表为pmap()。
输入类型和输出类型两两搭配, purrr包提供了27种map类函数。
modify
modify 的用法同map差不多,好处就是可以返回同类型数据,如果是数据框输入,输出也还是数据框:
代码语言:javascript复制> d1 <- modify(d2, ~ if(is.numeric(.)) . - median(.) else .)
> d1
x1 x2 sex
1 -6 -4 M
2 1 8 F
3 2 -1 M
4 -1 1 F
purrr包还提供了一个modify_if()函数, 可以对满足条件的列进行修改,如:
代码语言:javascript复制> d2 <- modify_if(d2, is.numeric, ~ .x - median(.x))
>
> d2
x1 x2 sex
1 -6 -4 M
2 1 8 F
3 2 -1 M
4 -1 1 F
也就是多了一个返回逻辑值结果的函数作为参数。
walk
walk 函数并不会返回任何结果,有时仅需要遍历一个数据结构调用函数进行一些显示、绘图, 这称为函数的副作用, 不需要返回结果。purrr的walk函数针对这种情形。比如用cat查看并输出数据框中的变量类别:
代码语言:javascript复制walk(d.class, ~ cat(typeof(.x), "n"))
## character
## character
## double
## double
## double
walk2()函数可以接受两个数据自变量, 类似于map2()。例如, 需要对一组数据分别保存到文件中, 就可以将数据列表与保存文件名的字符型向量作为walk2()的两个数据自变量。
代码语言:javascript复制dl <- split(d.class, d.class[["sex"]])
walk2(dl, paste0("class-", names(dl), ".csv"),
~ write.csv(.x, file=.y))
也可以更加直观的用管道符号:
代码语言:javascript复制d.class %>%
split(d.class[["sex"]]) %>%
walk2(paste0("class-", names(.), ".csv"), ~ write.csv(.x, file=.y))
ps: walk 这个函数在操作保存时挺好用的,可以省去循环的麻烦,而且基本R 也没有提供类似walk的功能。
iwalk/imap
这一族函数可同时访问下标或元素名与元素值。相当于每次遍历数据,都会获取两个变量,一个是元素值,一个是元素下标(有元素名则为元素名),如果x有元素名, imap(x, f)相当于imap2(x, names(x), f);如果x没有元素名, imap(x, f)相当于imap2(x, seq_along(x), f)。:
代码语言:javascript复制例如, 显示数据框各列的变量名:
iwalk(d.class, ~ cat(.y, ": ", typeof(.x), "n"))
## name : character
## sex : character
## age : double
## height : double
## weight : double
pmap
R的向量化可以很好地处理各个自变量是向量的情形, 但是对于列表、数据框等多个自变量则不能自动进行向量化处理。purrr包的pmap类函数支持对多个列表、数据框、向量等进行向量化处理。pmap不是将多个列表等作为多个自变量, 而是将它们打包为一个列表。所以, map2(x, y, f)用pmap()表示为pmap(list(x, y), f)。
相当于多维度的遍历map :
代码语言:javascript复制x <- list(101, name="李明")
y <- list(102, name="张聪")
z <- list(103, name="王国")
pmap(list(x, y, z), c)
## [[1]]
## [1] 101 102 103
##
## $name
## [1] "李明" "张聪" "王国"
对于数据框, 对数据框的每一行执行函数(之于map 对列执行,有点类似于apply 选择行or列)。例如:
代码语言:javascript复制d <- tibble::tibble(
x = 101:103,
y=c("李明", "张聪", "王国"))
pmap_chr(d, function(...) paste(..., sep=":"))
## [1] "101:李明" "102:张聪" "103:王国"
5. reduce 类函数
代码语言:javascript复制reduce(1:4, ` `)
## [1] 10
执行的其实是 1 2 3 4。
虽然结果和sum 一致,但是reduce 可以对元素为复杂类型的列表进行逐项合并计算。
比如如果要取下面数据的交集:
代码语言:javascript复制set.seed(5)
x <- replicate(4, sample(
1:5, size=5, replace=TRUE), simplify=FALSE); x
## [[1]]
## [1] 2 3 1 3 1
##
## [[2]]
## [1] 1 5 3 3 2
##
## [[3]]
## [1] 5 4 2 5 3
##
## [[4]]
## [1] 1 4 3 2 5
我们可以用管道符号,非常繁琐的进行:
代码语言:javascript复制x[[1]] %>% intersect(x[[2]]) %>% intersect(x[[3]]) %>% intersect(x[[4]])
而reduce 直接一句话的事情:
代码语言:javascript复制reduce(x, intersect)
## [1] 2 3
ps:reduce()支持...参数, 所以可以给要调用的函数额外的自变量或选项。那么对于ifelse,是不是可以增加参数,如果对于复杂的内容,就不用一层层套娃了。
- reduce2
reduce2(x, y, f) 中的x是要进行连续运算的数据列表或向量, 而y是给这些运算提供不同的参数。如果没有.init初始值, f仅需调用length(x)-1次, 所以y仅需要有length(x)-1个元素;如果有.init初始值, f需要调用length(x)次, y也需要与x等长。
- accumulate
accumulate 之于reduce, 类似cumsum 之于sum。它会返回每一步进行函数运算后的结果:
代码语言:javascript复制accumulate(x, union)
## [[1]]
## [1] 2 3 1 3 1
##
## [[2]]
## [1] 2 3 1 5
##
## [[3]]
## [1] 2 3 1 5 4
##
## [[4]]
## [1] 2 3 1 5 4
- Map-reduce算法
Map-reduce是大数据技术中的重要算法, 在Hadoop分布式数据库中主要使用此算法思想。将数据分散存储在不同计算节点中, 将需要的操作先映射到每台计算节点, 进行信息提取压缩, 最后用reduce的思想将不同节点的信息整合在一起。
6. 使用示性函数的泛函
some
some(.x, .p),对数据列表或向量.x的每一个元素用.p判断, 只要至少有一个为真,结果就为真;every(.x, .p)与some类似,但需要所有元素的结果都为真结果才为真。这些函数与any(map_lgl(.x, .p))和all(map_lgl(.x, .p))类似, 但是只要在遍历过程中能提前确定返回值就提前结束计算, 比如some 只要遇到一个真值就不再继续判断, every只要遇到一个假值就不再继续判断。(更加灵活的any 或all)
代码语言:javascript复制> d1
# A tibble: 4 x 2
x1 x2
<dbl> <dbl>
1 106 101
2 108 112
3 103 107
4 110 105
> some(d1, is.numeric)
[1] TRUE
detect
detect(.x, .p)返回数据.x的元素中第一个用.p判断为真的元素值, 而detect_index(.x, .p)返回第一个为真的下标值。
代码语言:javascript复制返回向量中的第一个超过100的元素的值:
detect(c(1, 5, 77, 105, 99, 123), ~ . >= 100)
## [1] 105
返回向量中的第一个超过100的元素的下标:
detect_index(c(1, 5, 77, 105, 99, 123),
~ . >= 100)
## [1] 4
keep/discard
keep(.x, .p)选取数据.x的元素中用.p判断为真的元素的子集;discard(.x, .p)返回不满足条件的元素子集。
x. 其他有用的函数
比如keep, 可以专门用来选择数据框各列或列表元素中满足某种条件的子集, 这个条件用一个返回逻辑值的函数来给出。如:
代码语言:javascript复制> keep(infos, is.character)
# A tibble: 4 x 2
family name
<chr> <chr>
1 张 三
2 李 四
3 王 五
4 赵 六
其实用其他向量化的方法也不错。
代码语言:javascript复制> tmp2 = unlist(map(infos, typeof)) %in% "character"
> infos[,tmp2]
# A tibble: 4 x 2
family name
<chr> <chr>
1 张 三
2 李 四
3 王 五
4 赵 六