library(tidyverse)
26 Iteration
简介
在tidyverse中有一些常见的循环操作,如:
- 绘制分面——
facet_wrap()
,facet_grid()
- 分组总结–
group_by()
,summarise()
- list-column解压——
unnest_wider()
,unnest_longer()
下面我们学习另外一些循环技巧。
循环操作数据框中的列
假设现在有一个多列的数据框,你要对某些列进行中位数统计。
<- tibble(
df a = rnorm(10),
b = rnorm(10),
c = rnorm(10),
d = rnorm(10)
)
你首先会想到对每一列执行相同的操作,如下。
|> summarize(
df n = n(),
a = median(a),
b = median(b),
c = median(c),
d = median(d),
)#> # A tibble: 1 × 5
#> n a b c d
#> <int> <dbl> <dbl> <dbl> <dbl>
#> 1 10 -0.246 -0.287 -0.0567 0.144
如果再增加很多列呢,上述的写法会及其冗长,dplyr
提供了across
函数,能更加方便地实现目的。
|> summarize(
df n = n(),
across(a:d, median),
)#> # A tibble: 1 × 5
#> n a b c d
#> <int> <dbl> <dbl> <dbl> <dbl>
#> 1 10 -0.246 -0.287 -0.0567 0.144
下面介绍across()
的三个主要参数:.cols
、.fns
和.names
。
选择列
.cols
控制选择哪几列,是一个tidy-select
类,类似于dplyr
中的select()
,可以使用适配select()
的函数,如starts_with()
、contains()
等。
更多有过关于tidy-select
的内容,可以参考?dplyr_tidy_select
。
<- tibble(
df grp = sample(2, 10, replace = TRUE),
a = rnorm(10),
b = rnorm(10),
c = rnorm(10),
d = rnorm(10)
)
|>
df group_by(grp) |>
summarize(across(everything(), median))
#> # A tibble: 2 × 5
#> grp a b c d
#> <int> <dbl> <dbl> <dbl> <dbl>
#> 1 1 -0.0935 -0.0163 0.363 0.364
#> 2 2 0.312 -0.0576 0.208 0.565
函数
.fns
参数提供across()
进行call的函数,可以是 - 函数名,例如mean
,注意此处函数后不能有()
- purrr-style lambda,例如~mean(.x, na.rm = TRUE)
- 上述两种函数组成的named list,例如list(mean = mean,n_miss = ~ sum(is.na(.x))
函数名
# 函数名后有`()`会报错。
|>
df group_by(grp) |>
summarize(across(everything(), median()))
#> Error in `summarize()`:
#> ℹ In argument: `across(everything(), median())`.
#> Caused by error in `median.default()`:
#> ! argument "x" is missing, with no default
purrr-style lambda
<- function(n, n_na, mean = 0, sd = 1) {
rnorm_na sample(c(rnorm(n - n_na, mean = mean, sd = sd), rep(NA, n_na)))
}
<- tibble(
df_miss a = rnorm_na(5, 1),
b = rnorm_na(5, 1),
c = rnorm_na(5, 2),
d = rnorm(5)
)
|>
df_miss summarize(
across(a:d, ~ median(., na.rm = TRUE)),
n = n()
)#> # A tibble: 1 × 5
#> a b c d n
#> <dbl> <dbl> <dbl> <dbl> <int>
#> 1 0.139 -1.11 -0.387 1.15 5
|>
df_miss summarize(
across(a:d, function(x) median(x, na.rm = TRUE)),
n = n()
)#> # A tibble: 1 × 5
#> a b c d n
#> <dbl> <dbl> <dbl> <dbl> <int>
#> 1 0.139 -1.11 -0.387 1.15 5
|>
df_miss summarize(
across(a:d, \(x) median(x, na.rm = TRUE)),
n = n()
)#> # A tibble: 1 × 5
#> a b c d n
#> <dbl> <dbl> <dbl> <dbl> <int>
#> 1 0.139 -1.11 -0.387 1.15 5
函数list
|>
df_miss summarize(
across(a:d, list(
median = \(x) median(x, na.rm = TRUE),
n_miss = \(x) sum(is.na(x))
)),n = n()
)#> # A tibble: 1 × 9
#> a_median a_n_miss b_median b_n_miss c_median c_n_miss d_median d_n_miss
#> <dbl> <int> <dbl> <int> <dbl> <int> <dbl> <int>
#> 1 0.139 1 -1.11 1 -0.387 2 1.15 0
#> # ℹ 1 more variable: n <int>
列名
如果你仔细观察,上面例子中的列名类似于{.col}_{.fn}
(这也是为什么函数list是要有name属性的原因)。
.names
参数控制列名定义规则,使用{.col}
表示原始列名,{.fn}
表示函数名;单个函数默认{.col}
,多个函数默认{.col}_{.fn}
。
across()
在与mutate()
联用时会将原来的列覆盖,因为.names()
默认是{.col}
,所以看不出来,当参数值改变后就会直观的体现出来:
与mutate联用
|>
df_miss mutate(
across(a:d, \(x) coalesce(x, 0), .names = "{.col}_na_zero")
)#> # A tibble: 5 × 8
#> a b c d a_na_zero b_na_zero c_na_zero d_na_zero
#> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 0.434 -1.25 NA 1.60 0.434 -1.25 0 1.60
#> 2 NA -1.43 -0.297 0.776 0 -1.43 -0.297 0.776
#> 3 -0.156 -0.980 NA 1.15 -0.156 -0.980 0 1.15
#> 4 -2.61 -0.683 -0.785 2.13 -2.61 -0.683 -0.785 2.13
#> 5 1.11 NA -0.387 0.704 1.11 0 -0.387 0.704
filter()中的循环
dplyr 提供了两个across()
的变体:
if_any()
:至少有一个条件返回TRUE
if_all()
:所有条件返回TRUE
# same as df_miss |> filter(is.na(a) | is.na(b) | is.na(c) | is.na(d))
|> filter(if_any(a:d, is.na))
df_miss #> # A tibble: 4 × 4
#> a b c d
#> <dbl> <dbl> <dbl> <dbl>
#> 1 0.434 -1.25 NA 1.60
#> 2 NA -1.43 -0.297 0.776
#> 3 -0.156 -0.980 NA 1.15
#> 4 1.11 NA -0.387 0.704
# same as df_miss |> filter(is.na(a) & is.na(b) & is.na(c) & is.na(d))
|> filter(if_all(a:d, is.na))
df_miss #> # A tibble: 0 × 4
#> # ℹ 4 variables: a <dbl>, b <dbl>, c <dbl>, d <dbl>
与piovt_longer()
对比
使用piovt_longer()
可以实现across()
,反之则不成立。例如下面的两个例子
across
|>
df summarize(across(a:d, list(median = median, mean = mean)))
#> # 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.0380 0.205 -0.0163 0.0910 0.260 0.0716 0.540 0.508
pivot_longer 复现
<- df |>
long pivot_longer(a:d) |>
group_by(name) |>
summarize(
median = median(value),
mean = mean(value)
)|>
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.0380 0.205 -0.0163 0.0910 0.260 0.0716 0.540 0.508
只能使用pivot_longer
<- tibble(
df_paired a_val = rnorm(10),
a_wts = runif(10),
b_val = rnorm(10),
b_wts = runif(10),
c_val = rnorm(10),
c_wts = runif(10),
d_val = rnorm(10),
d_wts = runif(10)
)
<- df_paired |>
df_long pivot_longer(
everything(),
names_to = c("group", ".value"),
names_sep = "_"
)
df_long#> # A tibble: 40 × 3
#> group val wts
#> <chr> <dbl> <dbl>
#> 1 a 0.715 0.518
#> 2 b -0.709 0.691
#> 3 c 0.718 0.216
#> 4 d -0.217 0.733
#> 5 a -1.09 0.979
#> 6 b -0.209 0.675
#> # ℹ 34 more rows
|>
df_long group_by(group) |>
summarize(mean = weighted.mean(val, wts))
#> # A tibble: 4 × 2
#> group mean
#> <chr> <dbl>
#> 1 a 0.126
#> 2 b -0.0704
#> 3 c -0.360
#> 4 d -0.248
purrr::map() 类函数
原文中有关map()
的内容是一个循环读取文件的例子,由于实际工作中,循环读取文件的操作极少见,所以不再这里赘述,可以参考原文中的例子,主要使用了purrr::map()
和purrr::list_rbind()
。
map()
函数循环对一个list或原子向量进行操作,生成一个与输入等长的对象;如果输入有name属性,那么输出也会保留name属性。
函数有两个重要的参数:
.x
:原子向量或list.f
:- 普通函数,例如
mean
。 - purrr-style lambda,例如
~mean(.x, na.rm = TRUE)
。 - 匿名函数,例如
\(x) x + 1
或function(x) x + 1.
。 - 字符串、正数或者list,例如
"idx"
,1
或list("idx", 1)
;其作用是类似于purrr::pluck()
,提取x
的idx
或x[[1]]
;当提取的元素为NULL
时,可以配置.default
。
- 普通函数,例如
当使用管道符时,.x
通常是管道前的结果。
# 普通函数
1:5 |>
map(rnorm, n = 5)
#> [[1]]
#> [1] 2.444187 1.242889 1.168512 1.239065 1.236321
#>
#> [[2]]
#> [1] 1.7408808 2.6490457 0.7823590 2.8419704 0.3807894
#>
#> [[3]]
#> [1] 3.264166 3.097311 4.788865 2.011317 4.890903
#>
#> [[4]]
#> [1] 5.200827 2.186789 4.370432 3.517323 4.101284
#>
#> [[5]]
#> [1] 5.039077 6.064826 1.290151 6.600178 6.179670
# 匿名函数
1:5 |>
map(\(x) rnorm(5, x))
#> [[1]]
#> [1] -1.6505155 0.2252239 1.6058778 0.6997194 2.0104924
#>
#> [[2]]
#> [1] 0.2306168 2.6722027 1.1737427 1.7081041 1.2221829
#>
#> [[3]]
#> [1] 2.651446 4.011717 2.685228 2.994618 1.641697
#>
#> [[4]]
#> [1] 5.572928 2.800616 3.927344 3.975916 5.584497
#>
#> [[5]]
#> [1] 5.005676 4.351796 6.858663 5.828303 6.199035
# 定位函数
<- list(
l2 "name1" = list(num = 1:3, letters[1:3]),
"name2" = list(num = 101:103, letters[4:6]),
"name3" = list()
)|> map(c(2, 2), .default = "none")
l2 #> $name1
#> [1] "b"
#>
#> $name2
#> [1] "e"
#>
#> $name3
#> [1] "none"
map()
函数有很多变体,我将其归为下面三大类。
返回结果不同
map()
函数会返回一个list,其变体则不同。
返回输入值
walk()
函数只能隐性返回输入,对.f
生成的结果没有处理,这在循环绘图中十分便利。
<- 1:4 |>
res walk(\(x) {
hist(rnorm(100, x))
})
res#> [1] 1 2 3 4
返回结果结构与输入相同
modify()
函数会输出一个与输入结构完全相同的结果,常用来修改元素类型。
# Convert to characters
|> str()
iris #> 'data.frame': 150 obs. of 5 variables:
#> $ Sepal.Length: num 5.1 4.9 4.7 4.6 5 5.4 4.6 5 4.4 4.9 ...
#> $ Sepal.Width : num 3.5 3 3.2 3.1 3.6 3.9 3.4 3.4 2.9 3.1 ...
#> $ Petal.Length: num 1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 ...
#> $ Petal.Width : num 0.2 0.2 0.2 0.2 0.2 0.4 0.3 0.2 0.2 0.1 ...
#> $ Species : Factor w/ 3 levels "setosa","versicolor",..: 1 1 1 1 1 1 1 1 1 1 ...
|>
iris modify(as.character) |>
str()
#> 'data.frame': 150 obs. of 5 variables:
#> $ Sepal.Length: chr "5.1" "4.9" "4.7" "4.6" ...
#> $ Sepal.Width : chr "3.5" "3" "3.2" "3.1" ...
#> $ Petal.Length: chr "1.4" "1.4" "1.3" "1.5" ...
#> $ Petal.Width : chr "0.2" "0.2" "0.2" "0.2" ...
#> $ Species : chr "setosa" "setosa" "setosa" "setosa" ...
其还有变体modify_if()
,modify_at()
,modify2()
,imodify()
,使用方法相似,具体差异同下面讲到的一样。
返回原子向量
这一类有map_lgl()
,map_int()
,map_dbl()
,map_chr()
,map_vec()
;要求返回的原子向量类型为函数名后缀规定的类型,否则报错。例如:map_lgl()
函数要求返回结果为由TRUE
和FALSE
构成的向量,否则报错。map_vec()
函数提供了参数.ptype
,可以指定返回结果的类型。
<- list(1, 2, 3, 4)
numbers
map_lgl(numbers, ~ .x %% 2 == 0)
#> [1] FALSE TRUE FALSE TRUE
map_lgl(numbers, ~ .x * 2)
#> Error in `map_lgl()`:
#> ℹ In index: 1.
#> Caused by error:
#> ! Can't coerce from a number to a logical.
返回数据框
这一类有map_dfc()
,map_dfr()
,这两类函数在官方文档中已经不再推荐使用,而是使用map()
+ list_rbind()
/list_cbind()
的组合方式。
# map ---------------------------------------------
# Was:
|>
mtcars split(mtcars$cyl) |>
map(\(df) lm(mpg ~ wt, data = df)) |>
map_dfr(\(mod) as.data.frame(t(as.matrix(coef(mod)))))
#> (Intercept) wt
#> 1 39.57120 -5.647025
#> 2 28.40884 -2.780106
#> 3 23.86803 -2.192438
# Now:
|>
mtcars split(mtcars$cyl) |>
map(\(df) lm(mpg ~ wt, data = df)) |>
map(\(mod) as.data.frame(t(as.matrix(coef(mod))))) |>
list_rbind()
#> (Intercept) wt
#> 1 39.57120 -5.647025
#> 2 28.40884 -2.780106
#> 3 23.86803 -2.192438
输入元素不同
这一类有:
- 对不同位置元素操作:
map_at()
,map_if()
,map_depth()
。 - 要求元素类型为长度是1的list:
lmap()
和其变体lmap_at()
,lmap_if()
。
map_at()
通过参数.at
,提供元素在输入中的位置或name属性,针对特定元素进行操作。
<- list(
l3 a = 1:3,
b = LETTERS[1:3],
c = 7:9
)
|>
l3 map_at(.at = c("a", "c"), ~ .x + 1)
#> $a
#> [1] 2 3 4
#>
#> $b
#> [1] "A" "B" "C"
#>
#> $c
#> [1] 8 9 10
map_if()
通过参数.p
,提供元素要满足的条件,针对特定元素进行操作,可以是判断函数,也可以是逻辑向量。
|>
l3 map_if(.p = is.numeric, ~ .x + 1)
#> $a
#> [1] 2 3 4
#>
#> $b
#> [1] "A" "B" "C"
#>
#> $c
#> [1] 8 9 10
map_depth()
通过参数.depth
,提供元素的深度,针对该深度处的元素进行操作。
<- list(a = list(foo = 1:2, bar = 3:4), b = list(baz = 5:6))
x |> str()
x #> List of 2
#> $ a:List of 2
#> ..$ foo: int [1:2] 1 2
#> ..$ bar: int [1:2] 3 4
#> $ b:List of 1
#> ..$ baz: int [1:2] 5 6
|>
x map_depth(2, \(y) paste(y, collapse = "/")) |>
str()
#> List of 2
#> $ a:List of 2
#> ..$ foo: chr "1/2"
#> ..$ bar: chr "3/4"
#> $ b:List of 1
#> ..$ baz: chr "5/6"
# Equivalent to:
|>
x map(\(y) map(y, \(z) paste(z, collapse = "/"))) |>
str()
#> List of 2
#> $ a:List of 2
#> ..$ foo: chr "1/2"
#> ..$ bar: chr "3/4"
#> $ b:List of 1
#> ..$ baz: chr "5/6"
lmap() 类
lmap()
与map()
的不同之处在于,后者提取使用[[i]]
来提取list中的元素,返回一个向量,前者使用[i]
提取list中的元素,返回一个list。
由于lmap()
操作对象是list,所以它可以使用list中的name属性;在返回结果时,它会使用c()
将list合并。下面是一个示例:
<- function(x) {
add_minus <- lapply(x, function(y) y - 1)
res1 names(res1) <- paste0(names(x), "_minus")
<- lapply(x, function(y) y + 1)
res2 names(res2) <- paste0(names(x), "_plus")
c(res1, res2)
}
# The output size varies each time we map f()
<- list(a = 1:4, c = 8:9)
test |>
test lmap(add_minus) |>
str()
#> List of 4
#> $ a_minus: num [1:4] 0 1 2 3
#> $ a_plus : num [1:4] 2 3 4 5
#> $ c_minus: num [1:2] 7 8
#> $ c_plus : num [1:2] 9 10
所以,lmap()
对参数.x
,.f
有不同的要求:
.x
:list 或 data.frame.f
:该函数输入为长度是1的list,输出是任意长度的list。
函数参数数目
这一类函数有map2()
、pmap()
和map2()
的变体imap()
。这些函数与map()
的使用方法类似,只是多提供了一组或多组.f
函数的参数。同时这些函数都有*_lgl()
,*_int()
, *_dbl()
, *_chr()
, *_vec()
, *_dfc()
, *_dfr()
,*walk()
等变体,使用方法同上。
imap()
imap()
是map2(x, names(x), ...)
或map2(x, seq_along(x), ...)
的简写,其余使用方法相同。
set.seed(123)
imap_chr(sample(10), paste)
#> [1] "3 1" "10 2" "2 3" "8 4" "6 5" "9 6" "1 7" "7 8" "5 9" "4 10"