R

R 数据处理(十八)

2021-01-29  本文已影响0人  名本无名

1. 前言

本节我们将开始介绍 R 中的迭代。主要介绍两种重要的迭代:

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

对于每个循环有三个组成部分

  1. 输出:output <- vector("double", length(x))

在循环开始之前,必须为输出分配足够的空间。这是很重要的,如果每次都用 c() 来动态添加会极大拖慢程序的速度

通常使用 vector() 来创建给定长度的空向量。接受两个参数:向量的类型(logical, integer, double, character 等)和长度。

  1. 序列: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
  1. 循环体:output[[i]] <- median(df[[i]])

每次获取不同的 i 值,并执行同样的操作。

2.1 思考练习

  1. 编写循环
  1. 将下面的代码改写为向量函数而不是 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 种变体:

  1. 修改一个现有对象,而不是创建一个新对象
  2. 遍历名称或值,而不是索引
  3. 处理长度未知的输出
  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 循环模式

遍历向量主要有三种基本方式,上面讲的是最常用的方式。还有另外两种:

  1. 遍历向量的元素:for (x in xs)
  2. 遍历向量的名称: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 思考练习

  1. 如果你使用 for (nm in names(x)) 遍历,但是 x 没有名称时会发生什么?如果只有一些元素有名称呢?如果名字不是唯一的呢?

  2. 编写一个函数来打印数据框中每个数字列的均值及其名称。例如,show_mean(iris) 将打印

show_mean(iris)
#> Sepal.Length: 5.84
#> Sepal.Width:  3.06
#> Petal.Length: 3.76
#> Petal.Width:  1.20
  1. 下面的代码的作用是什么?它是如何工作的?
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() 函数,然后用 mediansd 替换 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 循环的目的是为了让你将常见的列表操作分解成独立的部分

  1. 如何解决列表中单个元素的问题?解决该问题后,purrr 会将您的解决方案推广到列表中的每个元素

  2. 如果你正在解决一个复杂的问题,你如何把它分解成多个小块,从而让你更容易解决问题。然后使用 purrr 将许多小部件通过管道组合在一起

4.1 思考练习

  1. 阅读 apply() 的文档。在 2d 情况下,它泛化了哪两个 for 循环?

  2. 调整 col_summary(),使其仅适用于数值列。您可能需要使用 is_numeric() 函数,该函数返回一个逻辑向量,每个数值列对应 TRUE

上一篇下一篇

猜你喜欢

热点阅读