40. R 数据整理(十一: 用purrr包实现更花样的匿名函数使用)

2021-12-17 09:24:50 浏览数 (1)

感觉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选项指定查找不到成员时的选项, 如:

代码语言:javascript复制
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 赵     六   

0 人点赞