今天看啥  ›  专栏  ›  名本无名

R 数据处理(十八)

名本无名  · 简书  ·  · 2021-01-29 22:20

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

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

  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. 编写循环
  • 计算 mtcars 每一列的均值
  • 确定 nycflights13::flights 每一列的类型
  • 计算 iris 每列唯一值的数目
  • 从均值为 -10 0 10 100 的分布中生成 10 个随机正态分布
  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() 函数,然后用 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 循环的目的是为了让你将常见的列表操作分解成独立的部分

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

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

4.1 思考练习

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

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




原文地址:访问原文地址
快照地址: 访问文章快照