一、背景

多数情况下,我们经常在R语言编程中,使用循环处理各种数据,已达到必要的结果。在R语言中,写循环的修仙道路:手动for循环—apply函数族—purr包的泛函数式编程。

关于purrr 与 apply 族:

  • purrr 提供了更多的一致性、规范性和便利性,更容易记住和使用。
  • 速度来说,apply 族稍微快可以忽略不计的一丢丢。

基于 purrr 包的泛函式循环迭代的核心思想及常用操作:

  1. 循环迭代,就是将一个函数依次应用(映射)到序列的每一个元素上。
  2. 常用操作:
  • map():依次应用一元函数到一个序列的每个元素上,基本等同 lapply()
  • map2():依次应用二元函数到两个序列的每对元素上
  • pmap():应用多元函数到多个序列的每组元素上,可以实现对数据框逐行迭代
  • map 系列默认返回列表型
  • 可根据想要的返回类型添加后缀:_int, _dbl, _lgl, _chr, _df
  • 可以接着对返回的数据框df做行/列合并:_dfr, _dfc
  • 如果只想要函数依次作用的过程,而不需要返回结果,改用 walk 系列即可
  • 所应用的函数,有 purrr公式风格简写(匿名函数),支持一元,二元,多元函数
  1. map_* 系列函数
  • map_chr(.x, .f): 返回字符型向量
  • map_lgl(.x, .f): 返回逻辑型向量
  • map_dbl(.x, .f): 返回实数型向量
  • map_int(.x, .f): 返回整数型向量
  • map_dfr(.x, .f): 返回数据框列表,再 bind_rows 按行合并为一个数据框
  • map_dfc(.x, .f): 返回数据框列表,再 bind_cols 按列合并为一个数据框

二、基本概念

(1)序列:可根据位置或名字进行索引的数据结构

包括:

  • 原子向量:各个值都是同类型的,包括 6 种类型:logical、integer、double、character、complex、raw,其中 integer 和 double 也统称为numeric
  • 列表:各个值是不同类型的
  • 数据框:每一列的数据类型必须相同

所谓循环迭代,就是依次在序列上做相同的操作。

(2) 泛函式编程:函数的函数称为泛函,在编程中表示函数作用在函数上,或者说函数包含其它函数作为参数。

  • 循环迭代,本质上就是将一个函数依次应用(映射)到序列的每一个元素上。如泛函式:map(x, f)

(3) 管道:管道可以将数据从一个函数传给另一个函数,形成若干函数构成的管道式数据流,依次变换数据。

例如:

x %>% f() %>% g()
# 等同于 g(f(x))
# 解读:依次对数据进行若干操作:先对 x 进行 f 操作, 接着对结果进行 g 操作

注意:

  • 使用管道的好处是:提高程序可读性,避免引入不必要的中间变量。
  • 数据经过管道默认传递给函数的第一个参数(表现为省略);若在非第一个参数处使用该数据,用 “.” 代替,这使得管道作用更加强大和灵活。

三、基于purr的匿名函数编程(泛函数式)

在序列上做循环迭代(应用函数),经常需要自定义函数,但有些简单的函数也用 function 定义,就会显得麻烦。所以,purrr 包提供了对 purrr 风格公式(匿名函数)的支持。

在上述描述中,purrr 包实现迭代循环是用 map(.x, .f).f是要应用的函数。如果想用匿名函数来写.f,并将.f应用在序列 .x 上(也就是将匿名函数.f和序列 .x 关联),那么就限定用序列参数名关联好了,即将序列参数名作为匿名函数的参数

详细的示例

一元函数:序列参数 .x

比如,f(x) = x^2 + 1, 其 purrr 风格公式(匿名函数)就写为:~ .x ^ 2 + 1

二元函数:序列参数是 .x, .y

比如,f(x, y) = x^2 - 3 y, 其 purrr 风格公式(匿名函数)就写为:~ .x ^ 2 - 3 * .y

多元函数:序列参数是 ..1, ..2, ..3,等

比如,f(x, y, z) = ln(x + y + z), 其 purrr 风格公式(匿名函数)就写为:~ log(…1 + …2 + …3)

注:所有序列参数,可以用 … 代替,比如,sum(…1, …2, …3) 同 sum(…)

四、map系列函数解读

4.1 map函数

map(.x, .f, ...)
map_*(.x, .f, ...)

其中,.x 为序列;.f为一元函数,或 purrr 风格公式(匿名函数);...可以设置函数 .f的其它参数。

示例1:计算每列的均值
  • 返回列表
# 依次将 mean() 函数,应用到第1列,第2列,...
> df <- iris[,1:4]
> map(df, mean)
# $Sepal.Length
# [1] 5.843333
# 
# $Sepal.Width
# [1] 3.057333
# 
# $Petal.Length
# [1] 3.758
# 
# $Petal.Width
# [1] 1.199333

注解:

  • df 是数据框(特殊的列表),作为序列其元素依次是:df[[1]], df[[2]], … 所以,map(df, mean) 相当于依次计算:mean(df[[1]]), mean(df[[2]]), …
  • 返回向量
> map_dbl(df, mean)
# Sepal.Length  Sepal.Width Petal.Length  Petal.Width 
# 5.843333     3.057333     3.758000     1.199333
  • 使用多个参数

mean()函数还有其它参数,如 na.rm,若上述计算过程需要设置忽略缺失值

> map_dbl(df, mean, na.rm=TRUE)
# Sepal.Length  Sepal.Width Petal.Length  Petal.Width 
# 5.843333     3.057333     3.758000     1.199333
  • purr 风格公式(匿名函数)
> map_dbl(df, ~mean(.x, na.rm=TRUE))
# Sepal.Length  Sepal.Width Petal.Length  Petal.Width 
# 5.843333     3.057333     3.758000     1.199333
示例2:批量读取数据文件并合并(列名相同)
files = list.files("datas/", pattern = "xlsx", full.names = TRUE)
df = map_dfr(files, read_xlsx)    # 批量读取+按行堆叠合并

注解

  • files 获取 datas 文件夹下所有 .xlsx 文件的路径,若嵌套只需设置参数 recursive = TRUR;
  • map_dfr(files, read_xlsx) 依次将 read_xlsx() 函数应用到各个文件路径上,即依次读取数据,返回结果是数据框,同时“r”表示再做按行合并,一步到位。若需要设置 read_xlsx() 的其它参数,只需在后面设置即可。
示例3:批量建模
  1. 根据分类变量对数据进行分组,对每组分别建模,再提取模型信息
> df <- mtcars %>% select(mpg, cyl, wt)
> head(df)
# mpg cyl    wt
# Mazda RX4         21.0   6 2.620
# Mazda RX4 Wag     21.0   6 2.875
# Datsun 710        22.8   4 2.320
# Hornet 4 Drive    21.4   6 3.215
# Hornet Sportabout 18.7   8 3.440
# Valiant           18.1   6 3.460
  1. 嵌套列表列:按照某列分组,并形成嵌套的数据结构
> df <- df %>% group_nest(cyl)
> df
# # A tibble: 3 × 2
# cyl               data
# <dbl> <list<tibble[,2]>>
# 1     4           [11 × 2]
# 2     6            [7 × 2]
# 3     8           [14 × 2]
> df$data[[1]]
# # A tibble: 11 × 2
# mpg    wt
# <dbl> <dbl>
#   1  22.8  2.32
# 2  24.4  3.19
# 3  22.8  3.15
# 4  32.4  2.2 
# 5  30.4  1.62
# 6  33.9  1.84
# 7  21.5  2.46
# 8  27.3  1.94
# 9  26    2.14
# 10  30.4  1.51
# 11  21.4  2.78
  1. 建模
> df <- df %>% mutate(
  # 序列参数名作为匿名函数的参数
  model=map(data, ~lm(mpg~wt, data = .x)),  # 分组建模
  pred=map(model, predict)					# 计算每个样本的预测值
  )
> df
# # A tibble: 3 × 4
# cyl               data model  pred      
# <dbl> <list<tibble[,2]>> <list> <list>    
#   1     4           [11 × 2] <lm>   <dbl [11]>
#   2     6            [7 × 2] <lm>   <dbl [7]> 
#   3     8           [14 × 2] <lm>   <dbl [14]>
  1. 模型基本信息
> df$model %>% map(summary)
# [[1]]
# 
# Call:
#   lm(formula = mpg ~ wt, data = .x)
# 
# Residuals:
#   Min      1Q  Median      3Q     Max 
# -4.1513 -1.9795 -0.6272  1.9299  5.2523 
# 
# Coefficients:
#   Estimate Std. Error t value Pr(>|t|)    
# (Intercept)   39.571      4.347   9.104 7.77e-06 ***
#   wt            -5.647      1.850  -3.052   0.0137 *  
#   ---
#   Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
# 
# Residual standard error: 3.332 on 9 degrees of freedom
# Multiple R-squared:  0.5086,	Adjusted R-squared:  0.454 
# F-statistic: 9.316 on 1 and 9 DF,  p-value: 0.01374
# 
# 
# [[2]]
# 
# Call:
#   lm(formula = mpg ~ wt, data = .x)
# 
# Residuals:
#   1       2       3       4       5       6       7 
# -0.1250  0.5840  1.9292 -0.6897  0.3547 -1.0453 -1.0080 
# 
# Coefficients:
#   Estimate Std. Error t value Pr(>|t|)   
# (Intercept)   28.409      4.184   6.789  0.00105 **
#   wt            -2.780      1.335  -2.083  0.09176 . 
# ---
#   Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
# 
# Residual standard error: 1.165 on 5 degrees of freedom
# Multiple R-squared:  0.4645,	Adjusted R-squared:  0.3574 
# F-statistic: 4.337 on 1 and 5 DF,  p-value: 0.09176
# 
# 
# [[3]]
# 
# Call:
#   lm(formula = mpg ~ wt, data = .x)
# 
# Residuals:
#   Min      1Q  Median      3Q     Max 
# -2.1491 -1.4664 -0.8458  1.5711  3.7619 
# 
# Coefficients:
#   Estimate Std. Error t value Pr(>|t|)    
# (Intercept)  23.8680     3.0055   7.942 4.05e-06 ***
#   wt           -2.1924     0.7392  -2.966   0.0118 *  
#   ---
#   Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
# 
# Residual standard error: 2.024 on 12 degrees of freedom
# Multiple R-squared:  0.423,	Adjusted R-squared:  0.3749 
# F-statistic: 8.796 on 1 and 12 DF,  p-value: 0.01179
> df$model %>% map(summary) %>% map_dbl("r.squared") 
# 用列表的元素名做 map 相当于提取该元素
# [1] 0.5086326 0.4645102 0.4229655

> df %>% unnest(c(data, pred))
# 解除嵌套
# # A tibble: 32 × 5
# cyl   mpg    wt model   pred
# <dbl> <dbl> <dbl> <list> <dbl>
# 1     4  22.8  2.32 <lm>    26.5
# 2     4  24.4  3.19 <lm>    21.6
# 3     4  22.8  3.15 <lm>    21.8
# 4     4  32.4  2.2  <lm>    27.1
# 5     4  30.4  1.62 <lm>    30.5
# 6     4  33.9  1.84 <lm>    29.2
# 7     4  21.5  2.46 <lm>    25.7
# 8     4  27.3  1.94 <lm>    28.6
# 9     4  26    2.14 <lm>    27.5
# 10     4  30.4  1.51 <lm>    31.0
# # … with 22 more rows

4.2 map2()函数:依次应用二元函数到两个序列的每对元素上

map2(.x, .y .f, ...)
map2_*(.x, .y, .f, ...)

其中,.x为序列1,.y为序列2,.f 为要应用的二元函数,或 purrr 风格公式(匿名函数),...可设置函数 .f 的其它参数。

示例:计算BMI指数
> height = c(1.58, 1.76, 1.64)
> weight = c(52, 73, 68)
> bmi = function(h, w) w/h^2
> map2_dbl(height, weight, bmi)
# [1] 20.83000 23.56663 25.28257

# purr风格公式(匿名函数)
> map2_dbl(height, weight, ~.y / .x^2)
# [1] 20.83000 23.56663 25.28257

说明:

  • 序列1其元素为:height[[1]], height[[2]], …
  • 序列2其元素为:weight[[1]], weight[[2]], …
  • 所以,map2_dbl(height, weight, cal_BMI) 相当于依次计算:cal_BMI(height[[1]], weight[[1]]), cal_BMI(height[[2]], weight[[2]]), …
> df = tibble(height = height, weight = weight)
> df %>% mutate(bmi=map2_dbl(height, weight, bmi))
# # A tibble: 3 × 3
# height weight   bmi
# <dbl>  <dbl> <dbl>
#   1   1.58     52  20.8
# 2   1.76     73  23.6
# 3   1.64     68  25.3

# purrr 风格公式(匿名函数)
> df %>% mutate(bmi=map2_dbl(height, weight, ~.y/.x^2))
# # A tibble: 3 × 3
# height weight   bmi
# <dbl>  <dbl> <dbl>
#   1   1.58     52  20.8
# 2   1.76     73  23.6
# 3   1.64     68  25.3

4.3 pmap(): 应用多元函数到多个序列的每组元素上,可以实现对数据框逐行迭代

pmap(.l, .f, ...)
pmap_*(.l, .f, ...)

其中,.l为数据框,.f为要应用的多元函数,...可设置函数 .f 的其它参数
注:.f是几元函数,对应数据框 .l有几列,.f将依次在数据框 .l的每一行上进行迭代。

示例5: 分别生成不同数量不同均值、标准差的正态分布随机数。
> df <- tibble(
  n = c(1,3,5),
  mean = c(5,10,-3),
  sd = c(1,5,10)
)
> pmap(df, rnorm)

说明:这里的 rnorm(n, mean, sd) 是三元函数,pmap(df, rnorm) 相当于将三元函数 rnorm() 依次应用到数据框 df 的每一行上,即依次执行:rnorm(1, 5, 1), rnorm(3, 10, 5), rnorm(5, -3, 10)

  • 特别注意,这里 df 中的列名,必须与 rnorm() 函数的参数名相同(列序随便)。若要避免这种局限,可以使用 purrr 风格公式写法:

pmap(df, ~ rnorm(..1, ..2, ..3)) # 结果同上(略), 或者简写为 pmap(df, ~ rnorm(...))