R语言进阶 | 系统解析向量索引

在上一篇推文《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],如果xy的长度不一样会出现什么情况?如果长度不一样,xy中更短的会循环补齐至一样的长度,应该尽量避免这种情况。

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-lengthOOB (int)OOB (chr)Missing
AtomicErrorErrorErrorError
ListErrorErrorNULLNULL
NULLNULLNULLNULLNULL

由于上表中展示的索引返回结果的不一致性,便产生了两个有用的函数:purrr::pluckpurrr::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
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值