本章中,我们主要使用purrr和dplyr两个包来实现循环和迭代。在R中,迭代通常与其他编程语言看起来截然不同,例如,如果您想要将数值向量x中的每个元素都加倍,在R中,您只需编写2 * x即可。而在大多数其他编程语言中,您需要使用某种形式的for循环来显式地将x的每个元素加倍。类似的情况在使用purrr的时候尤为明显。
purrr泛函式循环迭代
purrr包是一个非常强大的包,它提供了一种更简洁的方式来处理循环和迭代。它的核心思想是使用函数式编程的方式来处理数据,而不是使用传统的for循环。
循环迭代 ,本质上是将一个函数依次应用到序列 的每一个元素上,在purrr中,这个函数就是map() 。map() 函数的基本语法为purrr::map(.x, .f, ...)。
在最新的purrr中,为使逻辑更清晰一致,已经建议弃用map_df/map_dfr/map_dfc这些一步到位的操作,改用map+list_bind/list_cbind。
修改多列
选择多列并调用单个函数
# A tibble: 1 × 5
n a b c d
<int> <dbl> <dbl> <dbl> <dbl>
1 10 0.529 0.108 -0.0540 -0.102
上述代码在计算df各列时,median() 函数被重复执行了四次,意味着我们进行了多次的复制粘贴 操作,这与“在代码中永远不要复制粘贴超过2次”的原则相违背。试想如果数据集中有10个以上的列,多次进行复制粘贴 不仅繁琐,而且容易产生错误。幸好,在tidyverse中的acorss()函数就排上了用场。
across() 是一个功能异常强大的函数,它有3个关键的参数:
.cols,操作的目标列,可以是多列,where() 、which() 、everything() 、starts_with() 等在此处同样适用。
.fns,对目标列进行操作的函数,同样可以是多个函数,如果是多个函数,则需要以列表的形式传入。
.names,控制输出列的名称,在与mutate() 连用时非常有用。
那么上述代码可以使用across() 进行改写。
# A tibble: 1 × 4
a b c d
<dbl> <dbl> <dbl> <dbl>
1 0.529 0.108 -0.0540 -0.102
选择多列并调用多个函数
# 生成一个可操作的数据集
rnorm_na <- function ( n , n_na , mean = 0 , sd = 1 ) {
sample ( c ( rnorm ( n - n_na , mean = mean , sd = sd ) , rep ( NA , n_na ) ) )
} # 生成包含缺失值的正态分布数列,项数为n-n_na,其中有n-n_na项缺失值
df_miss <- tibble (
a = rnorm_na ( 5 , 1 ) ,
b = rnorm_na ( 5 , 1 ) ,
c = rnorm_na ( 5 , 2 ) ,
d = rnorm ( 5 )
)
# 计算df_miss各列的中数,计算时需要去掉缺失值
df_miss |>
summarise (
across ( a : d , \( x ) median ( x , na.rm = TRUE ) )
)
# A tibble: 1 × 4
a b c d
<dbl> <dbl> <dbl> <dbl>
1 0.00670 0.492 -0.0768 -0.897
上述代码的匿名函数写作了\(x) median(x, na.rm = TRUE),而我们更熟悉的写法是~median(.x, na.rm = TRUE)。Hadley之所以修改写法基于以下两个方面:
.x的写法只适用于tidyverse函数内。
.x有时指代比较抽象,不便理解。
基于此,Hadley建议使用\(x)的写法,例如~.x + 1建议写为\(x) x + 1
如果我们系统同时计算数据集中每列的中数并求和呢?这就需要调用两个函数。
# A tibble: 1 × 8
a_median a_sum b_median b_sum c_median c_sum d_median d_sum
<dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 0.00670 -1.79 0.492 1.23 -0.0768 -0.187 -0.897 -2.91
上述代码输出的结果列的名称是使用一个类似于 {.col}_{.fn} 的 glue 规范 命名的,其中 .col 是原始列的名称,.fn 是函数的名称。我们可以使用 .names 参数来提供您自己的 glue 规范。
df_miss |>
summarise (
across (
a : d , # 一定要指定列
list (
median = \( x ) median ( x , na.rm = TRUE ) ,
sum = \( x ) sum ( x , na.rm = TRUE )
) ,
.names = "{.fn}-{.col}"
)
)
# A tibble: 1 × 8
`median-a` `sum-a` `median-b` `sum-b` `median-c` `sum-c` `median-d` `sum-d`
<dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 0.00670 -1.79 0.492 1.23 -0.0768 -0.187 -0.897 -2.91
.names参数在与mutate() 连用时非常有用,通常用来对比新生成的列与原有列的不同。
# 求数据集的绝对值
df_miss |>
mutate (
across ( a : d , \( x ) abs ( x ) , .names = "{.col}_abs" )
)
# A tibble: 5 × 8
a b c d a_abs b_abs c_abs d_abs
<dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 -0.242 -0.0278 NA -1.06 0.242 0.0278 NA 1.06
2 0.255 1.63 -1.59 -0.897 0.255 1.63 1.59 0.897
3 0.266 NA -0.0768 0.199 0.266 NA 0.0768 0.199
4 NA 1.01 NA -0.0204 NA 1.01 NA 0.0204
5 -2.07 -1.38 1.48 -1.13 2.07 1.38 1.48 1.13
行操作
across() 和summarize() ,mutate() 匹配度很好,但都是针对列的操作,有没有针对filter() 的行的操作呢?
dplyr中if_any() ,if_all() 两个函数功能就是across() 针对行操作的变种。我们具体来看:
# A tibble: 0 × 4
# ℹ 4 variables: a <dbl>, b <dbl>, c <dbl>, d <dbl>
这部分更详细的说明可参看:https://bookdown.org/wangminjie/R4DS/tidyverse-beauty-of-across2.html#dplyr-1.0.4-if_any-and-if_all。
在自定义函数中使用across()
expand_dates <- function ( df ) {
df |>
mutate ( across ( where ( is.Date ) , list ( year = year , month = month , day = mday ) ) )
}
上述函数的作用为:将所有日期列扩展为年、月和日列:
df_date <- tibble (
name = c ( "Amy" , "Bob" ) ,
date = ymd ( c ( "2009-08-03" , "2010-01-16" ) )
)
df_date
# A tibble: 2 × 2
name date
<chr> <date>
1 Amy 2009-08-03
2 Bob 2010-01-16
df_date |>
expand_dates ( )
# A tibble: 2 × 5
name date date_year date_month date_day
<chr> <date> <dbl> <dbl> <int>
1 Amy 2009-08-03 2009 8 3
2 Bob 2010-01-16 2010 1 16
across() 还可以与非标准性评估(tidy eval) 结合,实现给一个参数传入多个列的操作,只要注意将这些传入多个列的参数用两个大括号括起来 。关于非标准性评估(tidy eval) ,可参看这里 和 这里 。还有我自己的blog
TODO: 补充自己blog链接
与pivot_longer()连用
pivot_longer() 和across() 间有许多有趣的联系,它们可以实现相同的操作并得到一致的结果。我们看一个例子,例如我们需要计算df数据集每列的中数和众数。
使用across() :我们可以看到结果是一个宽表格,每列的名称的形式为”列名_函数名”。
# A tibble: 1 × 8
a_median a_mean b_median b_mean c_median c_mean d_median d_mean
<dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 0.529 0.389 0.108 0.527 -0.0540 -0.0844 -0.102 -0.243
# A tibble: 4 × 3
name median mean
<chr> <dbl> <dbl>
1 a 0.529 0.389
2 b 0.108 0.527
3 c -0.0540 -0.0844
4 d -0.102 -0.243
long |>
pivot_wider (
names_from = name ,
values_from = c ( median , mean ) ,
names_vary = "slowest" ,
names_glue = "{name}_{.value}"
)
# A tibble: 1 × 8
a_median a_mean b_median b_mean c_median c_mean d_median d_mean
<dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 0.529 0.389 0.108 0.527 -0.0540 -0.0844 -0.102 -0.243
这个操作在我们无法使用across() 的时候非常有用:即当我们需要同时计算一组多列的时候。例如,数据框同时包含了数值列和权重列,我们需要计算加权平均值时(我理解其实就是数据不整洁的时候):
# A tibble: 10 × 8
a_val a_wts b_val b_wts c_val c_wts d_val d_wts
<dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 0.142 0.713 0.868 0.921 -1.04 0.711 0.104 0.913
2 -0.887 0.701 -1.07 0.00904 0.401 0.353 -0.394 0.854
3 1.22 0.482 -0.477 0.744 -0.763 0.533 2.04 0.402
4 -0.810 0.360 -0.596 0.0532 1.48 0.277 -1.08 0.672
5 -0.259 0.883 -0.659 0.896 -0.262 0.000178 -1.15 0.466
6 0.700 0.575 0.747 0.954 -0.601 0.383 1.44 0.402
7 0.0426 0.254 0.677 0.170 -0.848 0.997 -0.176 0.180
8 0.713 0.935 0.646 0.961 0.155 0.268 0.759 0.541
9 -0.364 0.851 1.41 0.234 -0.0721 0.736 -0.234 0.00948
10 0.543 0.443 1.87 0.254 1.12 0.0446 0.669 0.283
df_paired数据框是不整洁的,我们无法使用across() 函数,此时我们需要进行转换:
# A tibble: 40 × 3
group val wts
<chr> <dbl> <dbl>
1 a 0.142 0.713
2 b 0.868 0.921
3 c -1.04 0.711
4 d 0.104 0.913
5 a -0.887 0.701
6 b -1.07 0.00904
7 c 0.401 0.353
8 d -0.394 0.854
9 a 1.22 0.482
10 b -0.477 0.744
# ℹ 30 more rows
# A tibble: 1 × 4
a_mean b_mean c_mean d_mean
<dbl> <dbl> <dbl> <dbl>
1 0.0901 0.397 -0.380 0.0977
案例:探索ariquality数据集
ariquality数据集是一个关于美国各州空气质量和健康状况的数据集。我们将使用purrr和dplyr来探索这个数据集。
掌握 map 循环迭代解决问题的分解逻辑和一般流程,能够灵活运用 map 系列函数,通过调试解决各种具体问题。
体会数据结构的重要性,特别是通过恰当的后缀,控制结果的返回类型。
我们主要从以下几个方面来探索airquality数据集:
Temp 温度列为华氏度(𝐹 ),用 map 迭代转化为摄氏度(𝐶),保留 1 位小数。
Month 和 Day 列分别是月和日,结合 1973 年,用 map 迭代从这两列计算日期列。
探索数据框 airquality 各列的基本信息:类型、缺失值个数。
对前 4 列做归一化,需要区分正向指标和逆向指标。
计算数据框 airquality 每一行缺失比例,保留 3 位小数。
批量写出数据到文件。
温度列转换
# 取一个元素调试解决问题
x <- df $ Temp [ 1 ]
x
# 编写函数
temp_convert <- function ( temp_f ) {
round ( ( temp_f - 32 ) / 1.8 , 1 )
}
# 使用 map系列 函数将函数应用到数据框的 Temp 列
df %>%
mutate ( Temp_C = map_dbl ( Temp , temp_convert ) ) %>%
select ( Temp , Temp_C )
Temp Temp_C
1 67 19.4
2 72 22.2
3 74 23.3
4 62 16.7
5 56 13.3
6 66 18.9
7 65 18.3
8 59 15.0
9 61 16.1
10 69 20.6
11 74 23.3
12 69 20.6
13 66 18.9
14 68 20.0
15 58 14.4
16 64 17.8
17 66 18.9
18 57 13.9
19 68 20.0
20 62 16.7
21 59 15.0
22 73 22.8
23 61 16.1
24 61 16.1
25 57 13.9
26 58 14.4
27 57 13.9
28 67 19.4
29 81 27.2
30 79 26.1
31 76 24.4
32 78 25.6
33 74 23.3
34 67 19.4
35 84 28.9
36 85 29.4
37 79 26.1
38 82 27.8
39 87 30.6
40 90 32.2
41 87 30.6
42 93 33.9
43 92 33.3
44 82 27.8
45 80 26.7
46 79 26.1
47 77 25.0
48 72 22.2
49 65 18.3
50 73 22.8
51 76 24.4
52 77 25.0
53 76 24.4
54 76 24.4
55 76 24.4
56 75 23.9
57 78 25.6
58 73 22.8
59 80 26.7
60 77 25.0
61 83 28.3
62 84 28.9
63 85 29.4
64 81 27.2
65 84 28.9
66 83 28.3
67 83 28.3
68 88 31.1
69 92 33.3
70 92 33.3
71 89 31.7
72 82 27.8
73 73 22.8
74 81 27.2
75 91 32.8
76 80 26.7
77 81 27.2
78 82 27.8
79 84 28.9
80 87 30.6
81 85 29.4
82 74 23.3
83 81 27.2
84 82 27.8
85 86 30.0
86 85 29.4
87 82 27.8
88 86 30.0
89 88 31.1
90 86 30.0
91 83 28.3
92 81 27.2
93 81 27.2
94 81 27.2
95 82 27.8
96 86 30.0
97 85 29.4
98 87 30.6
99 89 31.7
100 90 32.2
101 90 32.2
102 92 33.3
103 86 30.0
104 86 30.0
105 82 27.8
106 80 26.7
107 79 26.1
108 77 25.0
109 79 26.1
110 76 24.4
111 78 25.6
112 78 25.6
113 77 25.0
114 72 22.2
115 75 23.9
116 79 26.1
117 81 27.2
118 86 30.0
119 88 31.1
120 97 36.1
121 94 34.4
122 96 35.6
123 94 34.4
124 91 32.8
125 92 33.3
126 93 33.9
127 93 33.9
128 87 30.6
129 84 28.9
130 80 26.7
131 78 25.6
132 75 23.9
133 73 22.8
134 81 27.2
135 76 24.4
136 77 25.0
137 71 21.7
138 71 21.7
139 78 25.6
140 67 19.4
141 76 24.4
142 68 20.0
143 82 27.8
144 64 17.8
145 71 21.7
146 81 27.2
147 69 20.6
148 63 17.2
149 70 21.1
150 77 25.0
151 75 23.9
152 76 24.4
153 68 20.0
# 当函数比较简单时,使用匿名函数直接计算
map_dbl ( df $ Temp , \( x ) round ( ( x - 32 ) / 1.8 , 1 ) )
[1] 19.4 22.2 23.3 16.7 13.3 18.9 18.3 15.0 16.1 20.6 23.3 20.6 18.9 20.0 14.4
[16] 17.8 18.9 13.9 20.0 16.7 15.0 22.8 16.1 16.1 13.9 14.4 13.9 19.4 27.2 26.1
[31] 24.4 25.6 23.3 19.4 28.9 29.4 26.1 27.8 30.6 32.2 30.6 33.9 33.3 27.8 26.7
[46] 26.1 25.0 22.2 18.3 22.8 24.4 25.0 24.4 24.4 24.4 23.9 25.6 22.8 26.7 25.0
[61] 28.3 28.9 29.4 27.2 28.9 28.3 28.3 31.1 33.3 33.3 31.7 27.8 22.8 27.2 32.8
[76] 26.7 27.2 27.8 28.9 30.6 29.4 23.3 27.2 27.8 30.0 29.4 27.8 30.0 31.1 30.0
[91] 28.3 27.2 27.2 27.2 27.8 30.0 29.4 30.6 31.7 32.2 32.2 33.3 30.0 30.0 27.8
[106] 26.7 26.1 25.0 26.1 24.4 25.6 25.6 25.0 22.2 23.9 26.1 27.2 30.0 31.1 36.1
[121] 34.4 35.6 34.4 32.8 33.3 33.9 33.9 30.6 28.9 26.7 25.6 23.9 22.8 27.2 24.4
[136] 25.0 21.7 21.7 25.6 19.4 24.4 20.0 27.8 17.8 21.7 27.2 20.6 17.2 21.1 25.0
[151] 23.9 24.4 20.0
# 对于向量,可以直接使用向量计算,没必要使用map迭代核算
round ( ( df $ Temp - 32 ) / 1.8 , 1 )
[1] 19.4 22.2 23.3 16.7 13.3 18.9 18.3 15.0 16.1 20.6 23.3 20.6 18.9 20.0 14.4
[16] 17.8 18.9 13.9 20.0 16.7 15.0 22.8 16.1 16.1 13.9 14.4 13.9 19.4 27.2 26.1
[31] 24.4 25.6 23.3 19.4 28.9 29.4 26.1 27.8 30.6 32.2 30.6 33.9 33.3 27.8 26.7
[46] 26.1 25.0 22.2 18.3 22.8 24.4 25.0 24.4 24.4 24.4 23.9 25.6 22.8 26.7 25.0
[61] 28.3 28.9 29.4 27.2 28.9 28.3 28.3 31.1 33.3 33.3 31.7 27.8 22.8 27.2 32.8
[76] 26.7 27.2 27.8 28.9 30.6 29.4 23.3 27.2 27.8 30.0 29.4 27.8 30.0 31.1 30.0
[91] 28.3 27.2 27.2 27.2 27.8 30.0 29.4 30.6 31.7 32.2 32.2 33.3 30.0 30.0 27.8
[106] 26.7 26.1 25.0 26.1 24.4 25.6 25.6 25.0 22.2 23.9 26.1 27.2 30.0 31.1 36.1
[121] 34.4 35.6 34.4 32.8 33.3 33.9 33.9 30.6 28.9 26.7 25.6 23.9 22.8 27.2 24.4
[136] 25.0 21.7 21.7 25.6 19.4 24.4 20.0 27.8 17.8 21.7 27.2 20.6 17.2 21.1 25.0
[151] 23.9 24.4 20.0
日期列计算
date_convert <- function ( month , day ) {
as.Date ( paste ( "1973" , month , day , sep = "-" ) )
}
map2_vec ( df $ Month , df $ Day , date_convert )
[1] "1973-05-01" "1973-05-02" "1973-05-03" "1973-05-04" "1973-05-05"
[6] "1973-05-06" "1973-05-07" "1973-05-08" "1973-05-09" "1973-05-10"
[11] "1973-05-11" "1973-05-12" "1973-05-13" "1973-05-14" "1973-05-15"
[16] "1973-05-16" "1973-05-17" "1973-05-18" "1973-05-19" "1973-05-20"
[21] "1973-05-21" "1973-05-22" "1973-05-23" "1973-05-24" "1973-05-25"
[26] "1973-05-26" "1973-05-27" "1973-05-28" "1973-05-29" "1973-05-30"
[31] "1973-05-31" "1973-06-01" "1973-06-02" "1973-06-03" "1973-06-04"
[36] "1973-06-05" "1973-06-06" "1973-06-07" "1973-06-08" "1973-06-09"
[41] "1973-06-10" "1973-06-11" "1973-06-12" "1973-06-13" "1973-06-14"
[46] "1973-06-15" "1973-06-16" "1973-06-17" "1973-06-18" "1973-06-19"
[51] "1973-06-20" "1973-06-21" "1973-06-22" "1973-06-23" "1973-06-24"
[56] "1973-06-25" "1973-06-26" "1973-06-27" "1973-06-28" "1973-06-29"
[61] "1973-06-30" "1973-07-01" "1973-07-02" "1973-07-03" "1973-07-04"
[66] "1973-07-05" "1973-07-06" "1973-07-07" "1973-07-08" "1973-07-09"
[71] "1973-07-10" "1973-07-11" "1973-07-12" "1973-07-13" "1973-07-14"
[76] "1973-07-15" "1973-07-16" "1973-07-17" "1973-07-18" "1973-07-19"
[81] "1973-07-20" "1973-07-21" "1973-07-22" "1973-07-23" "1973-07-24"
[86] "1973-07-25" "1973-07-26" "1973-07-27" "1973-07-28" "1973-07-29"
[91] "1973-07-30" "1973-07-31" "1973-08-01" "1973-08-02" "1973-08-03"
[96] "1973-08-04" "1973-08-05" "1973-08-06" "1973-08-07" "1973-08-08"
[101] "1973-08-09" "1973-08-10" "1973-08-11" "1973-08-12" "1973-08-13"
[106] "1973-08-14" "1973-08-15" "1973-08-16" "1973-08-17" "1973-08-18"
[111] "1973-08-19" "1973-08-20" "1973-08-21" "1973-08-22" "1973-08-23"
[116] "1973-08-24" "1973-08-25" "1973-08-26" "1973-08-27" "1973-08-28"
[121] "1973-08-29" "1973-08-30" "1973-08-31" "1973-09-01" "1973-09-02"
[126] "1973-09-03" "1973-09-04" "1973-09-05" "1973-09-06" "1973-09-07"
[131] "1973-09-08" "1973-09-09" "1973-09-10" "1973-09-11" "1973-09-12"
[136] "1973-09-13" "1973-09-14" "1973-09-15" "1973-09-16" "1973-09-17"
[141] "1973-09-18" "1973-09-19" "1973-09-20" "1973-09-21" "1973-09-22"
[146] "1973-09-23" "1973-09-24" "1973-09-25" "1973-09-26" "1973-09-27"
[151] "1973-09-28" "1973-09-29" "1973-09-30"