https://haoeric.gitbooks.io/r-advanced/content/qu_zi_ji.html

http://adv-r.had.co.nz/Subsetting.html


取子集

R的取子集操作非常快捷灵活。掌握R中的取子集操作能让你用简洁的方式对数据进行复杂的操作,这是其他编程语言所望成莫及的。R的取子集不是那么容易学习,这之前你需要先了解几个相关的概念:

  • 三个取子集操作符。

  • 六种取子集的索引方法。

  • 对不同数据类型(比如向量,列表,因子,矩阵和数据框)取子集结果的不同。

  • 取子集和任务分派的结合使用。

这章将帮助你一步一步掌握R的取子集操作。首先我们从最简单的取子集(即使用[对原子向量取子集)开始讲解,然后慢慢展开,学习对较复杂的数据结构比如数组和列表取子集以及使用其他取子集操作符[[$。接下来会讲解如何结合取子集和任务分派来修改对象的内容。最后我们来看看一些有用的取子集应用实例。

取子集是str()函数的补充。str()函数帮助你了解对象的数据结构,取子集让你从对象中提取感兴趣的数据片段。

测试

做做这个简单的测试看看你是否需要阅读本章内容。如果你能很快地得到答案,你可以轻松地跳过本章。本章最后提供参考答案。

  1. 使用正数索引,负数索引,逻辑向量索引以及字符串向量索引取子集分别有什么不同?

  2. 对列表使用[[[或者$有什么不同?

  3. 什么时候需要使用drop = FALSE

  4. 如果x是一个矩阵,x[] <- 0会得到什么结果,这和x <- 0的结果有什么不同?

  5. 如何使用向量的名字来重新标记分类变量?

概要
  • 数据结构 首先介绍如何使用[以及六种对原子向量取子集的索引方法。然后讲解如何将这六种索引方法应用到列表,矩阵,数据框和S3对象。 

  • 取子集操作符 介绍另外两种取子集操作符[[$,着重介绍简化与保留的原则。

  • 在取子集和任务分派学习子分配的艺术,结合使用取子集和分派来修改对象的部分类容。 

  • 实例运用 带你了解数据分析中取子集的八种常见运用。

索引类型 

学习原子向量的取子集是最简单的,原子向量的取子集操作可以很容易地被引申运用到高维和其他更复杂的数据结构。这里我们将从最常用的取子集操作符[开始讲解。后面的取子集操作符一节会介绍另外两种操作符,[[$

原子向量

以下用一个简单的向量x来讲解不同的取子集方式。

x <- c(2.1, 4.2, 3.3, 5.4) #注意:小数点后面的数实际标明了向量中元素的位置。

你可以用如下六种索引方式对一个向量进行取子集操作:

  • 正整数索引 返回向量中特定位置的元素: 

    x[c(3, 1)]
    x[order(x)]# 重复的索引返回重复的值x[c(1, 1)]# 实数默认被去尾为整数x[c(2.1, 2.9)]
  • 负整数索引 去除向量中特定位置的元素:

    x[-c(3, 1)]

    正整数和负整数不可以在同一个取子集操作中结合使用:

    x[c(-1, 2)]
  • 逻辑向量索引 选择对应值为TRUE的元素。这可能是最有用的取子集操作,因为你在代码中常常得到逻辑向量。

    x[c(TRUE, TRUE, FALSE, FALSE)]
    x[x > 3]

    如果使用的逻辑向量的长度比被取子集的向量长度短,逻辑向量会被循环到与该向量相同的长度。

    x[c(TRUE, FALSE)]# 等同于x[c(TRUE, FALSE, TRUE, FALSE)]

    索引中如果出现缺失值,结果中也会对应返回缺失值:

    x[c(TRUE, TRUE, NA, FALSE)]
  • 空索引 返回原向量。这对向量取子集没有什么用处,可是对于矩阵,数据框和数组却非常有用。并且还可以和任务分派联合使用。

    x[]
  • 零索引 返回一个长度为零的向量。这个不常用,但是可以用来生成测试数据。

    x[0]
  • 字符串向量索引 如果向量有名字,你也可以使用字符串向量索引返回与名字相匹配的元素:

    (y <- setNames(x, letters[1:4]))
    y[c("d", "c", "a")]# 和整数索引一样,你也可以使用重复字符串y[c("a", "a", "a")]# 使用[取子集时,名字必须是完全匹配的z <- c(abc = 1, def = 2)
    z[c("a", "d")]

列表

对列表取子集与对原子向量取子集原理相同。使用[将会始终返回一个向量;后面要讲解的[[$则会提取一个向量中的元素。

矩阵和数组 

可以使用如下三种方法对高维数据取子集:

  • 多个向量

  • 单个向量

  • 矩阵

最常用的对矩阵和数组取子集就是对一维向量取子集的简单衍生,即对每一个维度提供一个用逗号彼此隔开的索引。空索引这时就有用处了,它意味着保留所有行,或者所有列。

a <- matrix(1:9, nrow = 3)
colnames(a) <- c("A", "B", "C")
a[1:2, ]
a[c(T, F, T), c("B", "A")]
a[0, -2]

默认情况下,使用[会对结果进行简化和降维。查看简化与保留一节来学习如何避免这种情况。

因为矩阵和数组是由带特殊属性的向量构建成的,这也就意味着你也可以使用一个简单的向量来对它们进行取子集。这中情况下矩阵和数组可以被视为一个向量,注意R中的数组是按列优先顺序排列存储的:

vals <- outer(1:5, 1:5, FUN = "paste", sep = ",")
vals[c(4, 15)]

你也可以使用×××矩阵来对高维数据进行取子集(如果高维数据有名字属性,也可以使用字符串类矩阵)。矩阵中的每一行标明一个元素在高维数据中的坐标,每一列则对应着该高维数据的某一个维度。也就是说,你要使用一个两列的矩阵来对一个矩阵取子集,一个三列的矩阵来对一个三维数组取子集,依此类推。它们输出的结果是一个向量:

vals <- outer(1:5, 1:5, FUN = "paste", sep = ",")
select <- matrix(ncol = 2, byrow = TRUE, c(  1, 1,  3, 1,  2, 4))
vals[select]

数据框 

数据框同时拥有列表和矩阵的特性:如果你用单个向量来取子集,那么数据框就表现为列表;如果使用两个向量,数据框则表现为矩阵。

df <- data.frame(x = 1:3, y = 3:1, z = letters[1:3])

df[df$x == 2, ]
df[c(1, 3), ]# 有两种方法对一个数据框的列取子集# 同列表一样df[c("x", "z")]# 同矩阵一样df[, c("x", "z")]# 如果仅取数据框的某一列:使用同矩阵一样的方法则返回值会被简化为向量,但是使用同列表一样方法则不会简化。str(df["x"])
str(df[, "x"])

S3对象

S3对象是由原子向量,数组和列表构成的,因此你可以使用上面介绍的方法以及str()的帮助来对S3对象取子集。

S4对象

对于S4对象有另外的两种取子集的操作符:@(等同于$)和slot()(等同于[[)。@相对于$更严谨,如果对应所取位置不存在则会报错。这在面相对象指南一章中会详细介绍。

练习

  1. 找出并修改如下代码中的错误:

    mtcars[mtcars$cyl = 4, ]
    mtcars[-1:4, ]
    mtcars[mtcars$cyl <= 5]
    mtcars[mtcars$cyl == 4 | 6, ]
  2. 为什么x <- 1:5; x[NA]会返回五个缺失值?(提示:这和x[NA_real_]有什么不同)

  3. upper.tri()返回什么值?使用它对矩阵取子集是如何操作的?我们需要其他的取子集原则来描述它么?

    x <- outer(1:5, 1:5, FUN = "*")
    x[upper.tri(x)]
  4. 为什么mtcars[1:20]会报错,它和mtcars[1:20, ]有什么不同?

  5. 自己编写一个对矩阵取对角元素的函数(要和对矩阵x使用diag(x)的返回值相同)。

  6. df[is.na(df)] <- 0做了什么操作,怎么解释这个代码?

取子集操作符 

另外两种取子集操作符分别是[[$[[[相似,使用[[可以提取列表中的元素,但是每次只能返回单个元素。$可以看做是[[的简化,同时它还能结合字符串取子集。

对列表使用[返回值始终是一个列表,然而使用[[则返回列表中的元素。因此,提取列表中的元素时要使用[[:

“如果列表x是一个满载货物的火车,x`5`表示在第五节车厢 中的货物,而x[4:6]则表示由四,五,六号车厢组成的小火车。” --- @RLangTip

因为使用[[只能返回单个值,所以使用的索引必须是正整数或者字符串。

a <- list(a = 1, b = 2)
a`1`
a[["a"]]# 如果[[里是一个向量则会迭代索引b <- list(a = list(b = list(c = list(d = 1))))
b[[c("a", "b", "c", "d")]]# 等同于b[["a"]][["b"]][["c"]][["d"]]

因为数据框本质上是由多个列向量构成的列表,所以你也可以使用[[来提取数据框中的某一列,比如mtcars`1`mtcars[["cyl"]]

使用[[[对S3和S4对象进行操作时,他们的结果会因受对象的重写而不同。关键的不同在于简化与保留。所以知道什么是默认操作很重要。

简化与保留 

理解简化与保留的不同非常重要。对结果进行简化会将输出信息转化为最简单的数据结构。简化有时候很有用,因为很多时候简化后的返回值会恰好是你想要的结构。对结果进行保留则会保证输出与输入的数据结构类型一致,这对提高程序的稳定性非常重要。在对矩阵和数据框取子集时忽略drop = FALSE是导致程序出错的一种常见原因。(可能在你的测试数据中不会有错误,当别人输入单列的数据框时则会出现错误)

如何切换简化或者保留因数据类型的差异而不同。具体的操作概括如下表:


简化 保留
向量 x`1` x[1]
列表 x`1` x[1]
因子 x[1:4, drop = T] x[1:4]
数组 x[1, ] or x[, 1] x[1, , drop = F] or x[, 1, drop = F]
数据框 x[, 1] or x`1` x[, 1, drop = F] or x[1]

保留操作对于所有数据类型都是一样的:你得到和输入同样类型的输出。简化操作则对不同的数据类型会有些不同:

  • 原向量:去除名字。

    x <- c(a = 1, b = 2)
    x[1]
    x`1`
  • 列表:返回列表中的元素而不是单个元素的列表。

    y <- list(a = 1, b = 2)
    str(y[1])
    str(y`1`)
  • 因子:去掉多余的水平。

    z <- factor(c("a", "b"))
    z[1]
    z[1, drop = TRUE]
  • 矩阵数组:去掉长度为一的维度。

    a <- matrix(1:4, nrow = 2)
    a[1, , drop = FALSE]
    a[1, ]
  • 数据框:若返回值是单列,则返回一个向量而不是数据框。 

    df <- data.frame(a = 1:2, b = 1:2)
    str(df[1])
    str(df`1`)
    str(df[, "a", drop = FALSE])
    str(df[, "a"])

$

$是一个简化操作符,x$y等同于x[["y", exact = FALSE]]。多用于对数据框取子集,比如mtcars$cyldiamonds$carat

使用$的一个常用错误是使用一个变量替代某一列的名字:

var <- "cyl"# mtcars$var等同于mtcars[["var"]],这样返回nullmtcars$var# 换用[[mtcars`var`

$[[使用上最大的不同是,$采用不完整配对:

x <- list(abc = 1)
x$a
x[["a"]]

你可以修改全域设置,将warnPartialMatchDollar设为TRUE来避免这种操作。但是小心这样设置给其他导入代码(比如其他包中的代码)带来的影响。

缺失索引与出界索引

当使用的索引超出范围(OOB)时,使用[[[会表现的有所不同。比如,你试图提取一个长度为四的向量的第五个元素,或者使用NANULL作为索引:

x <- 1:4str(x[5])
str(x[NA_real_])
str(x[NULL])

下面的表格归纳了在对向量或列表使用[[[时,当出现出界索引(OOB)或缺失索引时结果的差异:

操作符 索引 原子向量 列表
[ OOB NA list(NULL)
[ NA_real_ NA list(NULL)
[ NULL x[0] list(NULL)
[[ OOB Error Error
[[ NA_real_ Error NULL
[[ NULL Error Error

如果输入向量有名字,那么出界索引(OOB)或缺失索引的名字为""

numeric()[1]
numeric()[NA_real_]
numeric()[NULL]
numeric()`1`
numeric()`NA_real_`
numeric()`NULL`

list()[1]
list()[NA_real_]
list()[NULL]
list()`1`
list()`NA_real_`
list()`NULL`

练习

  1. 比如一个线性模型mod <- lm(mpg ~ wt, data = mtcars),如何对它提取模型中的残余自由度,如何提取summary(mod)中的R平方值。

取子集与任务分派 

所有的取子集操作都可以和任务分派结合起来对输入的向量进行选择性地修改。

2


x <- 1:5x[c(1, 2)] <- 2:3x# LHS的长度必须和RHS一致x[-1] <- 4:1x# 注意:重复的索引不会被除掉,会覆盖前面的赋值x[c(1, 1)] <- 2:3x# 整型索引不能和NA一同使用x[c(1, NA)] <- c(1, 2)# 但是NA可以和逻辑索引一同使用 (这时,NA会被视为false)x[c(T, F, NA)] <- 1x# 这对修改向量中修改符合某种条件的元素很有用处df <- data.frame(a = c(1, 10, NA))
df$a[df$a < 5] <- 0df$a

使用空索引取子集搭配任务分派能保有原对象的类型和结构。比较如下两行代码。第一行中mtcars将保持原类型为数据框,而第二行中mtcars将成为一个列表。

mtcars[] <- lapply(mtcars, as.integer)
mtcars <- lapply(mtcars, as.integer)

对于列表,可以使用取子集+任务分派+NULL来去除向量中的某个特定元素。如果要添加一个NULL到一个列表,则可以使用[list(NULL)

x <- list(a = 1, b = 2)
x[["b"]] <- NULLstr(x)

y <- list(a = 1)
y["b"] <- list(NULL)
str(y)

实例运用 

上面介绍的取子集的基础知识能够被应用到很多的场景中。以下我们会介绍其中最重要的几个运用。有些特定的运用虽然有对应的专门的函数(比如,subset()merge()plyr::arrange()),但是了解这些函数是如何通过基础取子集操作来实现的对我们非常有帮助。这让我们能够应对那些没有专门函数来处理的新环境。

查寻表 (字符串取子集) 

字符匹配为制作查询表提供了一个强大的机制。比如你想转换一些缩写:

x <- c("m", "f", "u", "f", "f", "m", "m")
lookup <- c(m = "Male", f = "Female", u = NA)
lookup[x]
unname(lookup[x])# 或者更简单的输出c(m = "Known", f = "Known", u = "Unknown")[x]

如果不想在结果汇总出现名字,你可以使用unname()来把它们去掉。

手动匹配和融合 (×××取子集) 

你可能有一个更复杂的多列的查询表。比如我们有一个表示成绩的向量,和一个描述它的特性表:

grades <- c(1, 2, 2, 3, 1)

info <- data.frame(
  grade = 3:1,
  desc = c("Excellent", "Good", "Poor"),
  fail = c(F, F, T)
)

我们想要得到每个成绩在特性表中对应的信息。我们有两种途径来获得,一种是使用match()做×××取子集,另外一种是使用rownames()做字符串取子集: 

# 使用 matchid <- match(grades, info$grade)
info[id, ]# 使用 rownamesrownames(info) <- info$grade
info[as.character(grades), ]

如果你有多列需要匹配,那么你需要先使用interaction()paste()或者plyr::id()将它们转换成单列。你也可以使用merge()plyr::join()来做同样的事。请查看对应函数的源代码来学习如何实现。

随机取样/自助法 (整型取子集)

你可以使用×××索引来对一个向量或者数据框进行随机取样和自助取样。首先使用sample()函数生成一个随机索引向量,然后对对象取子集。 

df <- data.frame(x = rep(1:3, each = 2), y = 6:1, z = letters[1:6])# 为可重复性操作设置种子set.seed(10)# 随机重排df[sample(nrow(df)), ]# 随机取3排df[sample(nrow(df), 3), ]# 取6个自助样本df[sample(nrow(df), 6, rep = T), ]

设置sample()函数的参数来调整取样的个数,以及是否重复取样。

排序 (×××取子集)

order()函数的输入是一个向量,返回一个存储该向量排列顺序的整型向量。

x <- c("b", "c", "a")
order(x)
x[order(x)]

可以给order()函数提供额外参数来重排并列值的顺序。可以使用decreasing = TRUE将返回结果变成降序排列。默认情况下,缺失值会被排在最后;可以使用na.last = NA来去除它们,或者使用na.last = FALSE将它们放在最前面。

当目标对象是二维或更高维时,可以使用order()和×××索引来简单地对行或者列排序:

# 随机重排dfdf2 <- df[sample(nrow(df)), 3:1]
df2

df2[order(df2$x), ]
df2[, order(names(df2))]

使用sort()可以对向量进行排序,plyr::arrange()则可以对数据框排序。

展开汇总计数 (整型取子集)

有时候你的数据框中的重复行可能被汇总为一行,同时添加一列来标记重复的次数。可以使用rep()生成有重复的行×××索引来展开汇总计数:

df <- data.frame(x = c(2, 4, 1), y = c(9, 11, 6), n = c(3, 5, 1))
rep(1:nrow(df), df$n)
df[rep(1:nrow(df), df$n), ]

去除数据框中的某列 (字符串取子集)

有两种方法来去除数据框中的某列。一种是将该列设为NULL:

df <- data.frame(x = 1:3, y = 3:1, z = letters[1:3])
df$z <- NULL

另外一种是生成只包含你想要的列的新数据框:

df <- data.frame(x = 1:3, y = 3:1, z = letters[1:3])
df[c("x", "y")]

如果你知道你不想要的列信息,使用setdiff筛选出你想要保留的列:

df[setdiff(names(df), "z")]

有条件的行筛选 (逻辑型取子集)

因为我们能很容易地整合多列的条件判断,所以逻辑型取子集应该是对数据框进行行筛选的最常用的方法。

mtcars[mtcars$gear == 5, ]
mtcars[mtcars$gear == 5 & mtcars$cyl == 4, ]

注意使用向量型逻辑运算符&|, 而不是缩短的标量型逻辑运算符&&||&&||if条件判断中比较有用。灵活运用德摩根定律可以大大简化否定的逻辑操作。

  • !(X & Y) 等同于 !X | !Y

  • !(X | Y) 等同于 !X & !Y

比如 !(X & !(Y | Z)) 可以简化成 !X | !!(Y|Z),更进一步成!X | Y | Z

subset()是专门用来对数据框取子集的速记函数。使用subset()可以免掉重复输入数据框的名字从而节省代码。在非标准评估一章,你会学习subset()的工作原理。

subset(mtcars, gear == 5)
subset(mtcars, gear == 5 & cyl == 4)

逻辑运算 vs. 集合运算 (逻辑型 & ×××取子集)

认识逻辑运算(逻辑型取子集)和集合运算(整型取子集)本质上的相同点非常有用,而使用集合运算更为高效:

  • 你想知道第一个(或最后一个)TRUE

  • 你有很多的FALSE却比较少的TRUE;使用集合运算更快更节省内存。

which()可以帮助你将逻辑型转换为×××表示。在基础R中没有which()的逆操作,但是我们可以很容易的编写一个:

x <- sample(10) < 4which(x)

unwhich <- function(x, n) {
  out <- rep_len(FALSE, n)
  out[x] <- TRUE
  out
}
unwhich(which(x), 10)

我们创建两个逻辑型向量和对应的整型向量来探索一下逻辑运算和集合运算之间的关系。

(x1 <- 1:10 %% 2 == 0)
(x2 <- which(x1))
(y1 <- 1:10 %% 5 == 0)
(y2 <- which(y1))# X & Y <-> intersect(x, y)x1 & y1
intersect(x2, y2)# X | Y <-> union(x, y)x1 | y1
union(x2, y2)# X & !Y <-> setdiff(x, y)x1 & !y1
setdiff(x2, y2)# xor(X, Y) <-> setdiff(union(x, y), intersect(x, y))xor(x1, y1)
setdiff(union(x2, y2), intersect(x2, y2))

刚开始学习取子集的一个常见错误是使用x[which(y)]而不是x[y]。这里的which()没有什么意义:它将逻辑型转换为×××索引,可是结果确实完全一样的。同时注意x[-which(y)]等同于x[!y]:当y全是FALSE时,which(y)会返回integer(0),那么-integer(0)依然是integer(0),因此你会得到空值而不是所有的值。因此,除非你确实需要(比如提取第一个或最后一个TRUE值),尽量避免将逻辑型取子集转换为整型取子集。

练习

  1. 如何随机的打乱一个数据框的列?(这在随机深林方法中是非常重要的一步)你又如何同时将数据框的行和列打乱? 

  2. 如何从一个数据框中随机的提取一个m行的子集?

  3. 如何使数据框的列按字符顺序排列?

参考答案 

  1. 正整数索引提取特定位置的元素,而负整数索引去除特定位置的元素;逻辑型索引保留对应位置为TRUE的元素;字符串索引筛选和名字匹配的元素。

  2. [用来取子列表,并且总是返回列表;如果使用长度为1的×××索引,它将返回长度为1的一个列表。[[提取列表中的某个元素。$是一个便捷的速记符,x$y等同于x[["y"]]

  3. 在对一个矩阵、数组或者数据框取子集时,如果你想要保留原有的数据维度,使用drop = FALSE。在某个函数中取子集,最好总是设置drop = FALSE

  4. 如果x是一个矩阵,x[] <- 0会将每一个元素替换为0,保留原有的行数和列数。x <- 0则将整个矩阵替换为0。

  5. 一个带有名字的向量可以被用来作为一个简单的查询表: c(x = 1, y = 2, z = 3)[c("y", "z", "x")]