《R数据科学》学习笔记|Note15:使用purrr实现迭代(上)

purrr_封面.jpg

本系列为《R数据科学》(R for Data Science)的学习笔记。相较于其他R语言教程来说,本书一个很大的优势就是直接从实用的R包出发,来熟悉R及数据科学。更新过程中,读者朋友如发现错误,欢迎指正。如果有疑问,也可以后台私信。希望各位读者朋友能学有所得!

使用purrr实现迭代

15.1 简介

函数是减少重复代码的一种工具,其减少重复代码的方法是,先识别出代码中的重复模式,然后将其提取出来,成为更容易修改和重用的独立部分。减少重复代码的另一种工具是迭代,它的作用在于可以对多个输入执行同一种处理,比如对多个列或多个数据集进行同样的操作。

library(tidyverse)

15.2 for循环

举个例子,计算下面数据框的每列中位数

df <- tibble(
 a = rnorm(10),
 b = rnorm(10),
 c = rnorm(10),
 d = rnorm(10)
)
> df
# A tibble: 10 x 4
         a       b      c       d
     <dbl>   <dbl>  <dbl>   <dbl>
 1 -0.0803  0.0689  0.548  1.37  
 2 -0.998   2.14    0.222  0.244 
 3 -0.882  -0.229  -0.776 -1.24  
 4 -0.293   0.781   0.482  1.69  
 5 -0.875   0.0440  0.964 -0.196 
 6 -0.461  -1.29   -2.17  -1.62  
 7 -0.417   0.206   0.230 -0.564 
 8 -2.21   -4.66    0.957  1.30  
 9 -1.41   -2.05    0.859 -1.03  
10 -0.820   1.19    0.101 -0.0891

可以一列一列重复的算:

> median(df$a)
[1] -0.8474354
> median(df$b)
[1] 0.05642144
> median(df$c)
[1] 0.3562925
> median(df$d)
[1] -0.1424249

也可以使用for循环:

output <- vector("double", ncol(df)) # 1. 输出
for (i in seq_along(df)) { # 2. 序列
  output[[i]] <- median(df[[i]]) # 3. 循环体
}
output
> output
[1] -0.84743543  0.05642144  0.35629250
[4] -0.14242489

每个 for 循环都包括 3 个部分。

  • 输出output <- vector("double", length(x))

    在开始循环前,必须为输出结果分配足够的空间。这对循环效率非常重要,如果在每次迭代中都使用 c() 来保存循环的结果,那么 for 循环的速度就会特别慢。创建给定长度的空向量的一般方法是使用 vector() 函数,该函数有两个参数:向量类型("logical"、"integer"、"double"、"character" 等)和向量的长度

  • 序列i in seq_along(df)

    这部分确定了使用哪些值来进行循环:每一轮 for 循环都会赋予 i 一个来自于 seq_ along(df) 的不同的值。我们可以将 i 看作一个代词,和 it 类似。 seq_along() 函数的作用与 1:length(l) 的作用基本相同,但最重要的区别是更加安全。如果我们有一个长度为 0 的向量,那么 seq_along() 会进行正确的处理,而1:length(l)则会出错:

> y <- vector("double", 0)
> seq_along(y)
integer(0)
> 1:length(y)
[1] 1 0
  • 循环体output[[i]] <- median(df[[i]])

    这部分就是执行具体操作的代码。它们会重复运行,每次运行都使用一个不同的 i 值。第一次迭代运行的是 output[[1]] <- median(df[[1]]),第二次迭代运行的是 output[[2]] <- median[df[[2]]],以此类推。

15.3 for循环的变体

15.3.1 修改现有对象

比如《R数据科学》学习笔记|Note13:函数中讲过的标准化例子:

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 循环中使用的都是 [[。因为它可以明确表示我们要处理的是单个元素

15.3.2 循环模式

除了通过 for (i in seq_along(xs)) 使用数值索引进行循环,并使用 x[[i]] 提取出相应的值这种最常用的循环方式外,还有另外两种循环方式:

  • 使用元素进行循环:for (x in xs)。如果只关心副作用,比如绘图或保存文件,那么这种方式是最适合的,因为有效率地保存输出结果是非常困难的。

  • 使用名称进行循环:for (nm in names(xs))。这种方式会给出一个名称,你可以使用这个名称和 x[[nm]] 来访问元素的值。如果想要在图表标题或文件名中使用元素名称,那么你就应该使用这种方式。

    创建命名的输出向量:

    results <- vector("list", length(x))
    names(results) <- names(x)
    

    使用数值索引进行循环是最常用的方式,因为给定位置后,就可以提取出元素的名称和值:

    for (i in seq_along(x)) {
     name <- names(x)[[i]]
     value <- x[[i]]
    }
    

15.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)
> str(output)
 num [1:172] -0.976 0.235 0.626 0.291 -0.552 ...

但这并不是一种非常高效的方式,因为 R 要在每次迭代中复制上一次迭代中的所有数据。 从技术角度来看,你执行了一种“平方”操作,这意味着,如果元素数量增加到原来的 3 倍,那么循环时间就要增加到原来的 9 倍。

更好的解决方式是将结果保存在一个列表中,循环结束后再组合成一个向量:

out <- vector("list", length(means))
for (i in seq_along(means)) {
  n <- sample(100, 1)
  out[[i]] <- rnorm(n, means[[i]])
}
str(out)
str(unlist(out))
> str(out)
List of 3
 $ : num [1:94] -0.2441 -1.9689 -0.8901 0.1324 -0.0745 ...
 $ : num [1:22] 0.6286 0.0227 0.9343 1.5355 1.013 ...
 $ : num [1:37] 3.116 1.539 -0.472 2.6 3.13 ...
> str(unlist(out))
 num [1:153] -0.2441 -1.9689 -0.8901 0.1324 -0.0745 ...

这里我们使用了 unlist() 函数将一个向量列表转换为单个向量。

  • 如果生成一个的是很长的字符串,不要使用 paste() 函数将每次迭代的结果与上一 次连接起来,而应该将每次迭代结果保存在字符向量中,然后再使用 paste(output, collapse = "") 将这个字符向量组合成一个字符串。
  • 如果生成一个的是很大的数据框,不要在每次迭代中依次使用 rbind() 函数,而应该将每次迭代结果保存在列表中,再使用 dplyr::bind_rows(output) 将结果组合成数据框。

15.3.4 未知的序列长度

有时你甚至不知道输入序列的长度。例如,在掷硬币时,你想要循环到连续 3 次掷出正面向上。这种迭代不能使用 for 循环来实现,而应该使用 while 循环。while 循环比 for 循环更简单,因为前者只需要 2 个部分:条件循环体

while (condition) {
 # 循环体
}
for (i in seq_along(x)) {
 # 循环体
}
# 等价于
i <- 1
while (i <= length(x)) {
 # 循环体
 i <- i + 1
}

使用 while 循环找出了连续 3 次掷出正面向上的硬币所需的投掷次数:

flip <- function() sample(c("T", "H"), 1) 
# sample(x, size, replace = FALSE)
# x 整体数据,以向量形式给出
# size 抽取样本的数目
# replace 如果为F(默认),则是不重复抽样,此时size不能大于x的长度;
# sample(c("T", "H"), 1)相当于随机扔硬币
flips <- 0  #总次数
nheads <- 0 #连续投掷正面次数
while (nheads < 3) { #连续投掷正面次数 <3
 if (flip() == "H") {  #如果投掷到正面
 nheads <- nheads + 1 #连续投掷正面次数 +1
 } else {
 nheads <- 0  #否则连续投掷正面次数归零
 }
 flips <- flips + 1 #总次数+1
} #循环至nheads = 3,即投掷正面次数连续三次
flips #输出投掷总次数

15.4 for循环与函数式编程

for 循环在 R 中不像在其他语言中那么重要,因为 R 是一门函数式编程语言。这意味着可以先将 for 循环包装在函数中,然后再调用这个函数,而不是直接使用 for 循环。

如计算每列均值:

df <- tibble(
 a = rnorm(10),
 b = rnorm(10),
 c = rnorm(10),
 d = rnorm(10)
)
output <- vector("double", length(df))
for (i in seq_along(df)) {
 output[[i]] <- mean(df[[i]])
}
output

可以将这段代码提取出来,转换成一个函数:

col_mean <- function(df) {
 output <- vector("double", length(df))
 for (i in seq_along(df)) {
 output[i] <- mean(df[[i]])
 }
 output
}

也可以计算出每列的中位数和标准差:

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
}

通过添加支持函数应用到每列的一个参数,我们可以使用同一个函数完成与 col_mean()col_median()col_sd() 函数相同的操作:

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.84743543  0.05642144  0.35629250
[4] -0.14242489
> col_summary(df, mean)
[1] -0.84475995 -0.38086077  0.14231605
[4] -0.01268487

将函数作为参数传入另一个函数的这种做法是一种非常强大的功能,它是促使 R 成为函数式编程语言的因素之一。


往期内容:

《R数据科学》学习笔记|Note14:向量

《R数据科学》学习笔记|Note13:函数

R绘图实战|GSEA富集分析图

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容