R 数据处理(十八)
1. 前言
本节我们将开始介绍 R
中的迭代。主要介绍两种重要的迭代:
-
命令式编程:
有像
for
和while
循环一样的工具,使迭代非常的明确以及比较容易理解。但是
for
循环一般代码较长,重复的代码较多 -
函数式编程(
FP,Functional programming
):函数式编程提供了提取重复代码的工具,每个循环模式都是自己的函数。
1.1 导入
在这里我们将要介绍另一个 tidyverse
核心包 purrr
。它提供了许多强大的编程工具
library(tidyverse)
2. for 循环
假设我们有下面一个简单的 tibble
df <- tibble(
a = rnorm(10),
b = rnorm(10),
c = rnorm(10),
d = rnorm(10)
)
计算每列的中位数
> median(df$a)
[1] 0.2037405
> median(df$b)
[1] -0.2373901
> median(df$c)
[1] 0.05839867
> median(df$d)
[1] 0.08679879
这样做可真不是个好选择。记住,只要复制粘贴代码两次以上,就要考虑使用 for
循环
> output <- vector("double", ncol(df)) # 1. output
> for (i in seq_along(df)) { # 2. sequence
+ output[[i]] <- median(df[[i]]) # 3. body
+ }
> output
[1] 0.20374050 -0.23739013 0.05839867 0.08679879
对于每个循环有三个组成部分
- 输出:
output <- vector("double", length(x))
在循环开始之前,必须为输出分配足够的空间。这是很重要的,如果每次都用 c()
来动态添加会极大拖慢程序的速度
通常使用 vector()
来创建给定长度的空向量。接受两个参数:向量的类型(logical
, integer
, double
, character
等)和长度。
- 序列:
i in seq_along(df)
遍历 1,2,3,4
。你可能没见过 seq_along()
,它是一个安全版本的 1:length(l)
。
它们之间的区别是,当传入的是一个空向量时,seq_along
是正确的
> y <- vector("double", 0)
> seq_along(y)
integer(0)
> 1:length(y)
[1] 1 0
- 循环体:
output[[i]] <- median(df[[i]])
每次获取不同的 i 值,并执行同样的操作。
2.1 思考练习
- 编写循环
- 计算
mtcars
每一列的均值 - 确定
nycflights13::flights
每一列的类型 - 计算
iris
每列唯一值的数目 - 从均值为
-10
、0
、10
和100
的分布中生成10
个随机正态分布
- 将下面的代码改写为向量函数而不是
for
循环
out <- ""
for (x in letters) {
out <- stringr::str_c(out, x)
}
x <- sample(100)
sd <- 0
for (i in seq_along(x)) {
sd <- sd + (x[i] - mean(x)) ^ 2
}
sd <- sqrt(sd / (length(x) - 1))
x <- runif(100)
out <- vector("numeric", length(x))
out[1] <- x[1]
for (i in 2:length(x)) {
out[i] <- out[i - 1] + x[i]
}
3. 变异 for 循环
for
循环主要包含 4
种变体:
- 修改一个现有对象,而不是创建一个新对象
- 遍历名称或值,而不是索引
- 处理长度未知的输出
- 处理长度未知的序列
3.1 修改现有对象
df <- tibble(
a = rnorm(10),
b = rnorm(10),
c = rnorm(10),
d = rnorm(10)
)
rescale01 <- function(x) {
rng <- range(x, na.rm = TRUE)
(x - rng[1]) / (rng[2] - rng[1])
}
df$a <- rescale01(df$a)
df$b <- rescale01(df$b)
df$c <- rescale01(df$c)
df$d <- rescale01(df$d)
改写为 for
循环
for (i in seq_along(df)) {
df[[i]] <- rescale01(df[[i]])
}
为什么在每个 for
循环内部我都使用 [[
而不是 [
呢?因为它清楚地表明,我想处理的是单个元素。
3.2 循环模式
遍历向量主要有三种基本方式,上面讲的是最常用的方式。还有另外两种:
- 遍历向量的元素:
for (x in xs)
- 遍历向量的名称:
for (n in names(xs))
一般遍历索引是最通用的形式,可以根据索引位置提取出名称和值
for (i in seq_along(x)) {
name <- names(x)[[i]]
value <- x[[i]]
}
3.3 输出长度未知
有时您可能不知道输出结果有多长,可以使用动态添加的方式
> means <- c(0, 1, 2)
>
> output <- double()
> for (i in seq_along(means)) {
+ n <- sample(100, 1)
+ output <- c(output, rnorm(n, means[[i]]))
+ }
> str(output)
num [1:153] -0.479 0.612 1.231 1.243 0.583 ...
但是会增加程序耗时。
一个改进的方法是,将结果保存在 list
当中,循环之后再合并为一个向量。
> out <- vector("list", length(means))
> for (i in seq_along(means)) {
+ n <- sample(100, 1)
+ out[[i]] <- rnorm(n, means[[i]])
+ }
> str(out)
List of 3
$ : num -1.52
$ : num [1:30] 0.163 -0.411 0.144 0.613 2.449 ...
$ : num [1:65] 3.037 1.725 1.879 3.329 0.978 ...
> str(unlist(out))
num [1:96] -1.522 0.163 -0.411 0.144 0.613 ...
在这里,我们使用 unlist
将一个列表向量展开为单个向量。
这种模式先考虑将输出保存在更复杂的对象中,在循环结束后合并到一起。
3.4 序列长度未知
有时你可能甚至不知道序列有多长,可以考虑使用 while
循环。
例如,计算连续得到三个 H
需要多少次数
> flip <- function() sample(c("T", "H"), 1)
>
> flips <- 0
> nheads <- 0
>
> while (nheads < 3) {
+ if (flip() == "H") {
+ nheads <- nheads + 1
+ } else {
+ nheads <- 0
+ }
+ flips <- flips + 1
+ }
> flips
[1] 58
3.5 思考练习
-
如果你使用
for (nm in names(x))
遍历,但是x
没有名称时会发生什么?如果只有一些元素有名称呢?如果名字不是唯一的呢? -
编写一个函数来打印数据框中每个数字列的均值及其名称。例如,
show_mean(iris)
将打印
show_mean(iris)
#> Sepal.Length: 5.84
#> Sepal.Width: 3.06
#> Petal.Length: 3.76
#> Petal.Width: 1.20
- 下面的代码的作用是什么?它是如何工作的?
trans <- list(
disp = function(x) x * 0.0163871,
am = function(x) {
factor(x, labels = c("auto", "manual"))
}
)
for (var in names(trans)) {
mtcars[[var]] <- trans[[var]](mtcars[[var]])
}
4. for VS 函数
for
循环在 R
中可能没有在其他语言中那么重要,因为 R
是函数式编程语言。
这意味着可以在函数中将 for
循环封装起来,然后调用该函数,而不是直接使用 for
循环。
为了理解这一点的重要性,让我们考虑下面这个数据框
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
[1] 0.41487173 -0.16774333 -0.05348092 0.01059490
你将会意识到,这一操作是会频繁的发生,所以我们将它封装为一个函数
col_mean <- function(df) {
output <- vector("double", length(df))
for (i in seq_along(df)) {
output[i] <- mean(df[[i]])
}
output
}
但同时,你认为计算中位数和标准差也会有所帮助,所以你复制并粘贴 col_mean()
函数,然后用 median
和 sd
替换 mean
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
}
你看,类似的代码虽然被包装为不同的函数,但是大部分代码还是复制粘贴,那我们该怎么改进呢?
考虑一下下面的简单例子
f1 <- function(x) abs(x - mean(x)) ^ 1
f2 <- function(x) abs(x - mean(x)) ^ 2
f3 <- function(x) abs(x - mean(x)) ^ 3
我们可以将这三个函数再抽象出来
f <- function(x, i) abs(x - mean(x)) ^ i
这样不仅减少了代码量,同时提高了函数的可扩展性。
现在,让我们来更改上面的三个函数
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)
#> [1] -0.51850298 0.02779864 0.17295591 -0.61163819
col_summary(df, mean)
#> [1] -0.3260369 0.1356639 0.4291403 -0.2498034
将一个函数传递给另一个函数,是 R
中非常重要的思想。
在后续的章节中,将介绍并使用 purrr
包中的函数来消除常见的 for
循环。
当然 R
提供的原生的 apply()
, lapply()
, tapply()
也可以解决类似的问题,但是 purrr
更容易学习使用。
使用 purrr
中的函数而不是 for
循环的目的是为了让你将常见的列表操作分解成独立的部分
-
如何解决列表中单个元素的问题?解决该问题后,
purrr
会将您的解决方案推广到列表中的每个元素 -
如果你正在解决一个复杂的问题,你如何把它分解成多个小块,从而让你更容易解决问题。然后使用
purrr
将许多小部件通过管道组合在一起
4.1 思考练习
-
阅读
apply()
的文档。在2d
情况下,它泛化了哪两个for
循环? -
调整
col_summary()
,使其仅适用于数值列。您可能需要使用is_numeric()
函数,该函数返回一个逻辑向量,每个数值列对应TRUE
。