R学习:R for Data Science 循环-迭代 purrr 函数代替 for 循环
R学习往期回顾:
R学习:R for Data Science 循环-迭代(for while))
R学习 从Tidyverse学起,入门R语言 dplyr合并数据
R学习 从Tidyverse学起,入门R语言(tidyr和stringr)
R学习 从Tidyverse学起,入门R语言(tibble,readr和dplyr)
for循环与函数式编程
for 循环在 R 中不像在其他语言中那么重要,因为 R 是一门函数式编程语言。这意味着可以先将 for 循环包装在函数中,然后再调用这个函数,而不是直接使用 for 循环
library(tidyverse)
df <- tibble(
a = rnorm(10),
b = rnorm(10),
c = rnorm(10),
d = rnorm(10)
)
假设想要计算每列的均值。你可以使用 for 循环来完成这个任务
output <- vector("double", length(df))
for (i in seq_along(df)) {
output[[i]] <- mean(df[[i]])
}
output
将这段代码提取出来,转换成一个函数:
col_mean <- function(df) {
output <- vector("double", length(df))
for (i in seq_along(df)) {
output[i] <- mean(df[[i]])
}
output
}
还可以算每列的中位数和标准差
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
}
通过添加支持函数应用到每列的一个参数,我们可以使用同一个函数完成与 col_mean()、col_median() 和 col_sd() 函数相同的操作:
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)
col_summary(df, mean)
将函数作为参数传入另一个函数的这种做法是一种非常强大的功能,它是促使 R 成为函数式编程语言的因素之一。
我们将学习和使用 purrr 包,它提供的函数可以替代很多常见的 for 循环应用。 R 基础包中的应用函数族(apply()、 lapply()、 tapply() 等)也可以完成类似的任务,但 purrr 包中的函数更一致,也更易于学习。
使用 purrr 函数代替 for 循环的目的是将常见的列表处理问题分解为独立的几个部分。
· 对于列表中的单个元素,你能找到解决问题的方法吗?如果找到了解决方法,那么你就可以使用 purrr 将这种方法扩展到列表中的所有元素。
· 如果你面临的是一个非常复杂的问题,那么如何将其分解为几个可行的子问题,然后循序渐进地解决,直至完成最终的解决方案?使用 purrr,你可以解决很多子问题,然后再通过管道操作将这些问题的结果组合起来。
映射函数
先对向量进行循环、然后对其每个元素进行一番处理,最后保存结果。这种模式太普遍了,因此 purrr 包提供了一个函数族来替你完成这种操作。每种类型的输出都有一个相应的函数:
· map() 用于输出列表;
· map_lgl() 用于输出逻辑型向量;
· map_int() 用于输出整型向量;
· map_dbl() 用于输出双精度型向量;
· map_chr() 用于输出字符型向量。
每个函数都使用一个向量作为输入,并对向量的每个元素应用一个函数,然后返回和输入向量同样长度(同样名称)的一个新向量。向量的类型由映射函数的后缀决定。
我们可以使用这些函数来执行与最后一个 for 循环相同的操作。因为那些摘要函数返回的是双精度数,所以我们需要使用 map_dbl() 函数:
map_dbl(df, median)
map_dbl(df, mean)
map_*() 和 col_summary() 具有以下几点区别
· 所有 purrr 函数都是用 C 实现的。这使得它们的速度非常快,但牺牲了一些可读性
· 第二个参数(即 .f,要应用的函数)可以是一个公式、一个字符向量或一个整型向量
· map_*() 使用向 .f 传递一些附加参数,供其在每次调用时使用
· 映射函数还可以保留名称
快捷方式
对于参数 .f,你可以使用几种快捷方式来减少输入量。假设你想对某个数据集中的每个分组都拟合一个线性模型。以下这个简单示例将 mtcars 数据集拆分成 3 个部分(按照气缸的值分类),并对每个部分拟合一个线性模型:
models <- mtcars %>%
split(.$cyl) %>%
map(function(df) lm(mpg ~ wt, data = df))
因为 R 中创建匿名函数的语法比较繁琐,所以 purrr 提供了一种更方便的快捷方式——单侧公式:
models <- mtcars %>%
split(.$cyl) %>%
map(~lm(mpg ~ wt, data = .))
我们在以上示例中使用了 . 作为一个代词:它表示当前列表元素(与 for 循环中用 i 表示当前索引是一样的)
需要提取出 R平方 这样的摘要统计量
需要先运行 summary() 函数,然后提取出结果中的 r.squared。我们可以使用匿名函数的快捷方式来完成这个操作:
models %>%
map(summary) %>%
map_dbl(~.$r.squared)
因为提取命名成分的这种操作非常普遍,所以 purrr 提供了一种更为简洁的快捷方式:使用字符串
models %>%
map(summary) %>%
map_dbl("r.squared")
后面需要有apply()家族函数的基础知识,为了循序渐进,后面我们将会介绍apply家族,下回见。