R的取子集操作非常快捷灵活。掌握R中的取子集操作能让你用简洁的方式对数据进行复杂的操作,这是其他编程语言所望成莫及的。R的取子集不是那么容易学习,这之前你需要先了解几个相关的概念:
实例运用
带你了解数据分析中取子集的八种常见运用。
学习原子向量的取子集是最简单的,原子向量的取子集操作可以很容易地被引申运用到高维和其他更复杂的数据结构。这里我们将从最常用的取子集操作符
[
开始讲解。后面的
取子集操作符
一节会介绍另外两种操作符,
[[
和
$
。
以下用一个简单的向量
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[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对象是由原子向量,数组和列表构成的,因此你可以使用上面介绍的方法以及str()
的帮助来对S3对象取子集。
对于S4对象有另外的两种取子集的操作符:@
(等同于$
)和slot()
(等同于[[
)。@
相对于$
更严谨,如果对应所取位置不存在则会报错。这在面相对象指南一章中会详细介绍。
找出并修改如下代码中的错误:
mtcars[mtcars$cyl = 4, ]
mtcars[-1:4, ]
mtcars[mtcars$cyl <= 5]
mtcars[mtcars$cyl == 4 | 6, ]
为什么x <- 1:5; x[NA]
会返回五个缺失值?(提示:这和x[NA_real_]
有什么不同)
upper.tri()
返回什么值?使用它对矩阵取子集是如何操作的?我们需要其他的取子集原则来描述它么?
x <- outer(1:5, 1:5, FUN = "*")
x[upper.tri(x)]
为什么mtcars[1:20]
会报错,它和mtcars[1:20, ]
有什么不同?
自己编写一个对矩阵取对角元素的函数(要和对矩阵x
使用diag(x)
的返回值相同)。
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$y
等同于x[["y", exact = FALSE]]
。多用于对数据框取子集,比如mtcars$cyl
和diamonds$carat
。
使用$
的一个常用错误是使用一个变量替代某一列的名字:
var <- "cyl"
mtcars$var
mtcars[[var]]
$
和[[
使用上最大的不同是,$
采用不完整配对:
x <- list(abc = 1)
x[["a"]]
你可以修改全域设置,将warnPartialMatchDollar
设为TRUE
来避免这种操作。但是小心这样设置给其他导入代码(比如其他包中的代码)带来的影响。
缺失索引与出界索引
当使用的索引超出范围(OOB)时,使用[
和[[
会表现的有所不同。比如,你试图提取一个长度为四的向量的第五个元素,或者使用NA
或NULL
作为索引:
x <- 1:4
str(x[5])
str(x[NA_real_])
str(x[NULL])
下面的表格归纳了在对向量或列表使用[
和[[
时,当出现出界索引(OOB)或缺失索引时结果的差异:
使用空索引取子集搭配任务分派能保有原对象的类型和结构。比较如下两行代码。第一行中mtcars
将保持原类型为数据框,而第二行中mtcars
将成为一个列表。
mtcars[] <- lapply(mtcars, as.integer)
mtcars <- lapply(mtcars, as.integer)
对于列表,可以使用取子集+任务分派+NULL
来去除向量中的某个特定元素。如果要添加一个NULL
到一个列表,则可以使用[
和list(NULL)
:
x <- list(a = 1, b = 2)
x[["b"]] <- NULL
str(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()
做字符串取子集:
id <- match(grades, info$grade)
info[id, ]
rownames(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)), ]
df[sample(nrow(df), 3), ]
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()
和整形索引来简单地对行或者列排序:
df2 <- df[sample(nrow(df)), 3:1]
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) < 4
which(x)
unwhich <- function(x, n) {
out <- rep_len(FALSE, n)
out[x] <- TRUE
unwhich(which(x), 10)
我们创建两个逻辑型向量和对应的整型向量来探索一下逻辑运算和集合运算之间的关系。
(x1 <- 1:10 %% 2 == 0)
(x2 <- which(x1))
(y1 <- 1:10 %% 5 == 0)
(y2 <- which(y1))
x1 & y1
intersect(x2, y2)
x1 | y1
union(x2, y2)
x1 & !y1
setdiff(x2, y2)
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
值),尽量避免将逻辑型取子集转换为整型取子集。
如何随机的打乱一个数据框的列?(这在随机深林方法中是非常重要的一步)你又如何同时将数据框的行和列打乱?
如何从一个数据框中随机的提取一个m
行的子集?
如何使数据框的列按字符顺序排列?
正整数索引提取特定位置的元素,而负整数索引去除特定位置的元素;逻辑型索引保留对应位置为TRUE
的元素;字符串索引筛选和名字匹配的元素。
[
用来取子列表,并且总是返回列表;如果使用长度为1的整形索引,它将返回长度为1的一个列表。[[
提取列表中的某个元素。$
是一个便捷的速记符,x$y
等同于x[["y"]]
。
在对一个矩阵、数组或者数据框取子集时,如果你想要保留原有的数据维度,使用drop = FALSE
。在某个函数中取子集,最好总是设置drop = FALSE
。
如果x
是一个矩阵,x[] <- 0
会将每一个元素替换为0,保留原有的行数和列数。x <- 0
则将整个矩阵替换为0。
一个带有名字的向量可以被用来作为一个简单的查询表:
c(x = 1, y = 2, z = 3)[c("y", "z", "x")]