在上一篇推文《R语言进阶 | 广义向量与属性解析》中,我们详细介绍了R语言中的各种(广义)向量及其内在关联。那么当我们创建了一个向量后,我们如何进行数据索引以便于显示或赋值呢?
R语言中的数据索引是非常快速且强大的,熟练掌握后将有效地提高数据分析的效率。如果你要熟练掌握R中的数据索引,务必先内化以下几个概念:
-
数据索引其实包括三部分:被索引对象 (object) 、索引操作符 (operators) 和索引值 (index)。比如提取向量a的部分内容:
a[1:3]
,其中a
是被索引对象;[]
是索引操作符;1:3
是索引值。这三部分都存在不同的形式。 -
可以有六种方法(六种索引值形式)去索引原子向量(atomic vector)。
-
有三种索引操作符:
[[
、[
、and$
。 -
被索引对象可以是不同的数据类型(如向量、列表、因子、矩阵和数据框等),且不同数据类型的索引操作有所区别。
-
索引操作可以和赋值结合,以改变对象的部分内容。如
a[1:3]=c(4,5,6)
操作便重新赋值了第一至三个元素。
多元素索引
[
操作符可以用来从一个向量中选择多个元素。为了清楚阐述它的用法,先将[
应用于一维向量的操作,然后再推广到更复杂的对象和更高的维度。
Atomic vectors
上节回顾,Atomic vectors 包括: logical, numeric (integer, double, complex, raw), character
这里先介绍通过[
操作符用六种不同的索引值来对简单向量x
取子集。注意向量x
的每个元素小数部分的数字代表对应元素的顺序。
x <- c(2.1, 4.2, 3.3, 5.4)
- 正整数索引返回指定位置的元素:
x[c(3, 1)]
#> [1] 3.3 2.1
x[order(x)]
#> [1] 2.1 3.3 4.2 5.4
# Duplicate indices will duplicate values
x[c(1, 1)]
#> [1] 2.1 2.1
# Real numbers are silently truncated to integers
x[c(2.1, 2.9)]
#> [1] 4.2 4.2
- 负整数索引会删除指定位置的元素:
x[-c(3, 1)]
#> [1] 4.2 5.4
但是如果混用正负整数进行索引则会抛出错误:
x[c(-1, 2)]
#> Error in x[c(-1, 2)]: only 0's may be mixed with negative subscripts
- 用逻辑值索引会返回对应位置为TRUE的元素。这种索引是非常有用的,因为可以使用不同的表达式生成逻辑值向量进行索引。
x[c(TRUE, TRUE, FALSE, FALSE)]
#> [1] 2.1 4.2
x[x > 3]
#> [1] 4.2 3.3 5.4
对于表达式x[y]
,如果x
和y
的长度不一样会出现什么情况?如果长度不一样,x
和y
中更短的会循环补齐至一样的长度,应该尽量避免这种情况。
x[c(TRUE, FALSE)] #索引向量会循环补齐至长度为4
#> [1] 2.1 3.3
# Equivalent to
x[c(TRUE, FALSE, TRUE, FALSE)]
#> [1] 2.1 3.3
如果索引值中存在缺失值(NA
),NA
对应位置的索引结果仍为NA
。
x[c(TRUE, TRUE, NA, FALSE)]
#> [1] 2.1 4.2 NA
- 索引值为空(没有索引值)时会返回完整的原始向量。这种方法可能对一维向量用处不大,但对于矩阵、数据框和array非常有用。因为此方法和赋值联合使用可以整体改变对象内容。
x[]
#> [1] 2.1 4.2 3.3 5.4
- 零值索引会返回长度为零的向量。这在生成测试数据时非常有用。
x[0]
#> numeric(0)
- 如果被索引的向量已经被命名,那么便可以用元素名的字符串向量进行索引。
(y <- setNames(x, letters[1:4]))
#> a b c d
#> 2.1 4.2 3.3 5.4
y[c("d", "c", "a")]
#> d c a
#> 5.4 3.3 2.1
# Like integer indices, you can repeat indices
y[c("a", "a", "a")]
#> a a a
#> 2.1 2.1 2.1
# When subsetting with [, names are always matched exactly
z <- c(abc = 1, def = 2)
z[c("a", "d")]
#> <NA> <NA>
#> NA NA
当索引值为因子时,起到索引作用的是因子包含的整数值(因子是在整数向量的基础上建立起来的),而不是因子的levels值。
y[factor("b")] #此时的索引值为1,而不是字符"b"
#> a
#> 2.1
Lists
上一节讲了用[
操作符索引atomic vectors,其实[
索引列表的原理是类似的,每次索引返回的结果仍然是列表。所以这一小节就不展开叙述。
Matrices and arrays
我们可以使用三种方法去索引更高维的数据结构:
-
用多个向量
-
用单个向量
-
用一个矩阵
常用的索引matrix(二维)和array(大于二维)对象的方法是一维向量索引方法的泛化:为每个维度都提供一个一维向量进行索引,不同的向量之间用逗号间隔。
a <- matrix(1:9, nrow = 3)
colnames(a) <- c("A", "B", "C")
a[1:2, ]
#> A B C
#> [1,] 1 4 7
#> [2,] 2 5 8
a[c(TRUE, FALSE, TRUE), c("B", "A")] #两个维度对应两个向量
#> B A
#> [1,] 4 1
#> [2,] 6 3
a[0, -2]
#> A C
用[
索引矩阵的一列、一行或者某个元素,返回的结果便会自动简化为一维向量。后面会讲解如何避免这种简化行为。
a[1, ]
#> A B C
#> 1 4 7
a[1, 1]
#> A
#> 1
因为matrix和array本质上就是通过向量添加某些属性演化而来的,所以也可以用一个向量来索引matrix和array,把它们当做一维向量。但注意元素在矩阵中存储的顺序默认是按列进行的。
vals <- outer(1:5, 1:5, FUN = "paste", sep = ",")
vals
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] "1,1" "1,2" "1,3" "1,4" "1,5"
#> [2,] "2,1" "2,2" "2,3" "2,4" "2,5"
#> [3,] "3,1" "3,2" "3,3" "3,4" "3,5"
#> [4,] "4,1" "4,2" "4,3" "4,4" "4,5"
#> [5,] "5,1" "5,2" "5,3" "5,4" "5,5"
vals[c(4, 15)]
#> [1] "4,1" "5,3" #按列来数的第4和15个元素
对于更高维的array对象,我们可以用整数矩阵进行索引。矩阵中的一行对应array中的一个元素,矩阵中的每列分别对应于array中的每个维度。也就是说我们可以用两列的矩阵来提取矩阵,用三列的矩阵来提取三维的array。返回的结果是一维向量。
select <- matrix(ncol = 2, byrow = TRUE, c(
1, 1,
3, 1,
2, 4
))
vals[select]
#> [1] "1,1" "3,1" "2,4"
Data frames and tibbles
数据框同时具有列表和矩阵的特点:
-
如果用单个索引值进行索引,这时数据框就像是列表一样,索引值对应的是数据框的列,所以
df[1:2]
选择的是前两列。 -
如果用两个索引值进行索引,这时数据框就像矩阵一样,所以
df[1:3,]
选择的是前三行(所有的列)。
df <- data.frame(x = 1:3, y = 3:1, z = letters[1:3])
df[df$x == 2, ]
#> x y z
#> 2 2 2 b
df[c(1, 3), ]
#> x y z
#> 1 1 3 a
#> 3 3 1 c
# There are two ways to select columns from a data frame
# Like a list
df[c("x", "z")]
#> x z
#> 1 1 a
#> 2 2 b
#> 3 3 c
# Like a matrix
df[, c("x", "z")]
#> x z
#> 1 1 a
#> 2 2 b
#> 3 3 c
# There's an important difference if you select a single
# column: matrix subsetting simplifies by default, list
# subsetting does not.
str(df["x"])
#> 'data.frame': 3 obs. of 1 variable:
#> $ x: int 1 2 3
str(df[, "x"])
#> int [1:3] 1 2 3
用[
操作符索引tibble返回的结果仍然是tibble。
df <- tibble::tibble(x = 1:3, y = 3:1, z = letters[1:3])
str(df["x"])
#> tibble [3 × 1] (S3: tbl_df/tbl/data.frame)
#> $ x: int [1:3] 1 2 3
str(df[, "x"])
#> tibble [3 × 1] (S3: tbl_df/tbl/data.frame)
#> $ x: int [1:3] 1 2 3
Preserving dimensionality
当我们用单个数值、单个字符串、单个逻辑值(TRUE
)索引数据框或者矩阵,默认的返回结果会降维简化。如果要保留原来的维度,我们可以使用drop=FALSE
参数。
### 矩阵
a <- matrix(1:4, nrow = 2)
str(a[1, ])
#> int [1:2] 1 3
str(a[1, , drop = FALSE])
#> int [1, 1:2] 1 3
## 数据框
df <- data.frame(a = 1:2, b = 1:2)
str(df[, "a"])
#> int [1:2] 1 2
str(df[, "a", drop = FALSE])
#> 'data.frame': 2 obs. of 1 variable:
#> $ a: int 1 2
因子索引时也有drop
参数,但是它的作用和上面讲的不同。它控制索引时是否保留完整的levels值,默认值为FALSE
,即默认保留。
z <- factor(c("a", "b"))
z[1] #元素b没有被索引,但是其对应的levels值依然保留
#> [1] a
#> Levels: a b
z[1, drop = TRUE]
#> [1] a
#> Levels: a
单元素索引
接下来我们将介绍另外两个常用的索引操作符[[
和$
,它们的效果非常类似,都被用于索引单个元素值。
[[
操作符
[[
在索引列表时是非常有用的,因为[
索引列表返回的结果仍然是列表。为了便于理解,我们可以做一个比喻:如果列表是x
是一辆载着货物的火车,那么x[[5]]
便是第五节车厢的货物 ,而x[4:6]
则是指4-6节车厢。
x <- list(1:3, "a", 4:6)
当你想提取单个的元素,你有两种做法:你可以创建一辆更小的火车,或者你可以直接抽取某些特定车厢中的内容。这便是[
和[[
的区别:
当你想索引多个元素(或者零个元素),你必须创建更小的火车:
因为[[
仅仅能返回单个元素值,必须使用单个正整数或者单个字符串。如果在[[]]
中使用向量索引,相当于进行递归式索引,例如x[[c(1,2)]]
相当于x[[1]][[2]
,此时如果x
为非递归式的对象(如向量)便会抛出错误。
对于下面两种代码写法,我们更推荐第二种写法,因为[[
提取列表元素返回的并不是列表,而[
索引的结果仍然是列表:
#用[]索引
for (i in 2:length(x)) {
out[i] <- fun(x[i], out[i - 1])
}
#用[[]]索引
for (i in 2:length(x)) {
out[[i]] <- fun(x[[i]], out[[i - 1]])
}
$
x$y
的效果和x[["y"]]
是一样的。只不过$
常被用于数据框中,比如mtcars$cyl
或者diamonds$carat
。关于$
使用的一个常见错误是在$
右边使用变量,可以简单地认为$
操作符右边并不会进行变量替换:
var <- "cyl"
# Doesn't work - mtcars$var translated to mtcars[["var"]]
mtcars$var
#> NULL
# Instead use [[
mtcars[[var]]
#> [1] 6 6 4 6 8 6 8 4 4 6 6 8 8 8 8 8 8 4 4 4 4 8 8 8 8 4 4 4 8 6 8 4
尽管[
和$
两者的功能相似,但也存在用法的区别:$
操作符在索引时是从左至右进行非严格匹配:
x <- list(abc = 1)
x$a
#> [1] 1
x[["a"]]
#> NULL
为了避免这种行为,你可以进行全局设置,如果发生了非严格匹配就抛出警告。为了避免数据框索引出现非严格匹配的情况,可以使用tibble替代数据框,因为tibble不会进行非严格匹配。
options(warnPartialMatchDollar = TRUE)
x$a
#> Warning in x$a: partial match of 'a' to 'abc'
#> [1] 1
索引缺失处理
当我们用无效的(invalid)索引值通过[[
操作符进行索引时会发生什么?下面的表格展示了用空对象(NULL
或者logical()
)、离界值(out-of-bounds values, OOB)和缺失值(例如NA_integer_
等)来索引逻辑向量(其他类型的向量也类似)、列表和NULL
返回的结果值。row[[col]]
中的row
表示每行对应的被索引对象(Atomic, List和NULL
),col
表示每列对应的索引值(Zero-length和OOB等)。(int: integer; chr: character)
row[[col]] | Zero-length | OOB (int) | OOB (chr) | Missing |
---|---|---|---|---|
Atomic | Error | Error | Error | Error |
List | Error | Error | NULL | NULL |
NULL | NULL | NULL | NULL | NULL |
由于上表中展示的索引返回结果的不一致性,便产生了两个有用的函数:purrr::pluck
和purrr::chuck
。当被索引的值不存在时,pluck()
函数总是返回NULL
(或者是用户自己设置的默认值),而chuck
则总是会抛出错误。此外pluck
函数允许混用整数和字符进行索引。
x <- list(
a = list(1, 2, 3),
b = list(3, 4, 5)
)
purrr::pluck(x, "a", 1)
#> [1] 1
purrr::pluck(x, "c", 1)
#> NULL
purrr::pluck(x, "c", 1, .default = NA) #通过.default参数设置默认值
#> [1] NA
@
and slot()
在S4对象中还存在另外两个索引操作符:@
(等价于$
,但@
不支持非严格匹配)和slot
(等价于[[
)。关于这两个操作符的具体用法后期的推文将会讲解。
索引与赋值
这一小节讲解索引和赋值,索引和赋值联用可以修改对象的部分内容。其操作的基本形式为x[i] <- value
。尽量保证value
的长度等于i
的长度。
x <- 1:5
x[c(1, 2)] <- c(101, 102)
x
#> [1] 101 102 3 4 5
对于列表,可以使用x[[i]]<-NULL
来删除第i
个元素,但是如果就是想把某个元素的值重新赋值为NULL
便可以这样:x[i] <- list(NULL)
。
x <- list(a = 1, b = 2)
x[["b"]] <- NULL
str(x)
#> List of 1
#> $ a: num 1
y <- list(a = 1, b = 2)
y["b"] <- list(NULL)
str(y)
#> List of 2
#> $ a: num 1
#> $ b: NULL
不用索引值进行索引(空索引)对于赋值是非常有用的,因为这样可以保留对象的原始数据结构。对比以下两种写法,第一种情况mtcars
赋值后仍然是数据框,因为只改变了mtcars
对象的内容并没有改变对象本身,而第二种情况mtcars
直接变成了列表,因为变量mtcars
直接指向了另一个新的列表对象。
mtcars[] <- lapply(mtcars, as.integer)
is.data.frame(mtcars)
#> [1] TRUE
mtcars <- lapply(mtcars, as.integer)
is.data.frame(mtcars)
#> [1] FALSE