一、背景
多数情况下,我们经常在R语言编程中,使用循环处理各种数据,已达到必要的结果。在R语言中,写循环的修仙道路:手动for循环—apply函数族—purr包的泛函数式编程。
关于purrr 与 apply 族:
- purrr 提供了更多的一致性、规范性和便利性,更容易记住和使用。
- 速度来说,apply 族稍微快可以忽略不计的一丢丢。
基于 purrr 包的泛函式循环迭代的核心思想及常用操作:
- 循环迭代,就是将一个函数依次应用(映射)到序列的每一个元素上。
- 常用操作:
- map():依次应用一元函数到一个序列的每个元素上,基本等同 lapply()
- map2():依次应用二元函数到两个序列的每对元素上
- pmap():应用多元函数到多个序列的每组元素上,可以实现对数据框逐行迭代
- map 系列默认返回列表型
- 可根据想要的返回类型添加后缀:_int, _dbl, _lgl, _chr, _df
- 可以接着对返回的数据框df做行/列合并:_dfr, _dfc
- 如果只想要函数依次作用的过程,而不需要返回结果,改用 walk 系列即可
- 所应用的函数,有 purrr公式风格简写(匿名函数),支持一元,二元,多元函数
- 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:批量建模
- 根据分类变量对数据进行分组,对每组分别建模,再提取模型信息
> 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
- 嵌套列表列:按照某列分组,并形成嵌套的数据结构
> 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
- 建模
> 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]>
- 模型基本信息
> 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(...))