R语言进阶 | 广义向量和属性解析

本期推文我们从广义向量出发,从属性的角度,深度解析 R 语言常用数据结构及其内在关联

逻辑梳理

广义的向量包括两种:atomic vectorList (列表),而我们常说的狭义的向量就是指 atomic vector,正如其名字所传达的意思一样,它就像原子一样能通过增加属性从而形成更复杂的数据类型。另外NULL虽然不属于向量,但它常被作为长度为零的向量。下图显示了它们的基本关系。

atomic vector又分为四种:logical, integer, double, and character。atomic vector 和列表的区别在于它们对于其元素所属的种类要求不同:atomic vector 要求所有的元素必须属于同一种类型。

List的元素可以是不同的类型

每个向量都有属性(attributes),其中names是向量最基本的属性。另外,维度属性(dim)可以让 atomic vector 转换为 matrix 或者 array 对象,有意思的是即使是List也可以通过增加dim属性转换成 list-matirx;增加class属性则会形成 S3 对象,关于 S3 对象我们后面会有推文专门详细讲解,最重要的几种 S3 对象包括:factordatetimes, data frametibble。下面两张示意图展示了向量和 S3 对象的关系。

Atomic vectors

根据元素种类的不同,atomic vector 又可主要分为四类: logical, integer, double 和 character。其中整数型(integer)和浮点数型(double)属于数字类型(numeric)的向量。其实还有另外两种类型的向量:complex 和 raw,我们平常用的不多,所以不在这里讨论。

Scalars

标量(Scalars)是和向量相对的概念,单个数字、逻辑值或者字符串就是标量,注意只有一个元素的向量仍然是向量,不要和标量搞混。

  • 逻辑值标量包括:TRUEFALSE,也可以缩写成TF

  • 浮点数型可以写成十进制(0.1234),科学计数法(1.23e4)或者十六进制(0xcafe)等。有三个特殊的浮点数类型的标量:Inf-InfNaN (缺失值,不是个数字)

  • 整数类型的写法类似于浮点数类型,但是后面必须加上字母 L,比如 1234L,1e4L 或者 0xcafeL,且不能包含小数

  • 字符串类型需要使用单引号 (‘hi’) 或者双引号 (“byte”)

Making longer vectors with c()

我们可以通过c()创建向量:

lgl_var <- c(TRUE, FALSE)
int_var <- c(1L, 6L, 10L)
dbl_var <- c(1, 2.5, 4.5)
chr_var <- c("these are", "some strings")

当向量中包含向量时,里面的向量就会被”压扁“,里面向量的元素会释放出来成为外面向量的元素,也即向量不是像列表一样的递归式(recursive)的数据结构。

c(c(1, 2), c(3, 4))
#> [1] 1 2 3 4

你可以用typeof()来查看一个向量的类型,用length()计算向量的长度。

typeof(lgl_var)
#> [1] "logical"
typeof(int_var)
#> [1] "integer"
typeof(dbl_var)
#> [1] "double"
typeof(chr_var)
#> [1] "character"

Missing values

R 语言中用NA表示缺失或者不知道的值,NA是 not applicable 的缩写。NA具有”传染性“,因为绝大部分牵涉到NA的运算结果都是NA

NA > 5
#> [1] NA
10 * NA
#> [1] NA
!NA
#> [1] NA

但其实也有一些特例存在,这些特例存在的原因很简单,因为即使把这些运算中的NA换成其他任意值得到的结果都是一样的。

NA ^ 0
#> [1] 1
NA | TRUE
#> [1] TRUE
NA & FALSE
#> [1] FALSE

当你打算用==判断符号去判断向量中哪些元素为NA时,似乎这样并不可行。为什么返回的结果都是NA呢?因为你永远不知道这个缺失值是否真的等于另一个缺失值。你可以用is.na()函数来判断向量中是否存在缺失值。

x <- c(NA, 5, NA, 10)
x == NA
#> [1] NA NA NA NA

is.na(x)
#> [1]  TRUE FALSE  TRUE FALSE

从技术上讲,其实有四种缺失值,每种 atomic vector 都对应一种缺失值:NA(logical),NA_integer_ (integer),NA_real_ (double),and NA_character_ (character)。但它们的区别根本不用管,平时可以只使用NA,运算过程中如果需要 R 会自动进行转换。

Testing and coercion

我们可以用形如is.*()的函数来判断向量所属的类型,比如你可以分别用is.logical()is.integer()is.double()is.character()四个函数来判断向量是否属于逻辑值类型、整数类型、浮点数类型和字符串类型。另外还有is.vector()is.atomic(),和 is.numeric()函数,关于他们的具体含义读者可以查看帮助文档深入了解。

前面我们讲到向量(atomic vector)要求所有的元素必须属于同一种类型,但当向量包含不同类型元素时会发生什么呢?这时不同类型的元素会强制转换成同一种类型,转换的优先级关系如下(箭头表示转换方向):logical -> integer -> double -> character。例如当向量同时包含整数和字符串时,数字会被自动转换成字符串。

c("a", 1)
#>  [1] "a" "1"

在 R 语言中,绝大多数的数学运算函数(+, log, abs, ect.)会自动将能转换为数字类型的元素转换为数字类型。

x <- c(FALSE, FALSE, TRUE)
as.numeric(x)
#> [1] 0 0 1

# Total number of TRUEs
sum(x)
#> [1] 1

# Proportion that are TRUE
mean(x)
#> [1] 0.333

当然强制转换类型也可以通过形如as.*()的函数来实现,比如as.logical()as.integer()as.double() 或者 as.character()。当强制转换不符合优先级时则会抛出错误。

as.integer(c("1", "1.5", "a"))
#> Warning: NAs introduced by coercion
#> [1]  1  1 NA

属性(Attributes)

上文讲到每个向量都有属性,向量通过添加属性可以转换成其他更复杂的数据类型。这小节将介绍几个重要的属性。

获得和设置属性

你可以认为属性就是用来给对象添加 metadata 的键值对。获取或者修改对象的单个属性可以用attr()函数,获取和设置对象的所有属性可以分别用attributrs()structure()函数。

a <- 1:3
attr(a, "x") <- "abcdef"
attr(a, "x")
#> [1] "abcdef"

attr(a, "y") <- 4:6
str(attributes(a))
#> List of 2
#>  $ x: chr "abcdef"
#>  $ y: int [1:3] 4 5 6

# Or equivalently
a <- structure(
  1:3,
  x = "abcdef",
  y = 4:6
)
str(attributes(a))
#> List of 2
#>  $ x: chr "abcdef"
#>  $ y: int [1:3] 4 5 6

其实很多属性的存在都是短暂的,在经过一定的运算之后便会消失。但是有两个属性可以长久保存的:名字(names)和维度(dim)。

attributes(a[1])  #a是上一步运算产生的对象
#> NULL
attributes(sum(a))
#> NULL

名字属性(names)

你可以用四种方法给向量命名:

# When creating it:
x <- c(a = 1, b = 2, c = 3)

# By assigning a character vector to names()
x <- 1:3
names(x) <- c("a", "b", "c")

# Inline, with setNames():
x <- setNames(1:3, c("a", "b", "c"))

#using attr() function
x <- c(1, 2, 3)
ttr(x, 'names') <- c('a', 'b', 'c')

相反也可以用以下两种方式去除names属性:x <- unname(x) or names(x) <- NULL

维度(Dimensions)属性

不管是向量还是列表都可以添加维度属性,转换成matrix或者array对象。所以要创建 matrix 或者 array 不仅可以用matrix()array()函数,还可以通过dim()函数赋予向量维度属性来创建。

# Two scalar arguments specify row and column sizes
x <- matrix(1:6, nrow = 2, ncol = 3)
x
#>      [,1] [,2] [,3]
#> [1,]    1    3    5
#> [2,]    2    4    6

# One vector argument to describe all dimensions
y <- array(1:12, c(2, 3, 2))
y
#> , , 1
#>
#>      [,1] [,2] [,3]
#> [1,]    1    3    5
#> [2,]    2    4    6
#>
#> , , 2
#>
#>      [,1] [,2] [,3]
#> [1,]    7    9   11
#> [2,]    8   10   12

# You can also modify an object in place by setting dim()
z <- 1:6
dim(z) <- c(3, 2)
z
#>      [,1] [,2]
#> [1,]    1    4
#> [2,]    2    5
#> [3,]    3    6

由于 matrix 和 array 是由向量衍生而来,所以 matrix 和 array 中也有一些和向量中功能相似的函数。

VectorMatrixArray
names()rownames(), colnames()dimnames()
length()nrow(), ncol()dim()
c()rbind(), cbind()abind::abind()
t()aperm()
is.null(dim(x))is.matrix()is.array()

没有维度属性的向量可以被认为是一维向量,但实际上用dim()函数查看会发现返回的属性值是NULL。矩阵可以只有单独的一列或者一行,array 也可以只有一个维度,这几种情况的输出是类似的,但你可以用str()函数查看它们的区别。

str(1:3)                   # 1d vector
#>  int [1:3] 1 2 3
str(matrix(1:3, ncol = 1)) # column vector
#>  int [1:3, 1] 1 2 3
str(matrix(1:3, nrow = 1)) # row vector
#>  int [1, 1:3] 1 2 3
str(array(1:3, 3))         # "array" vector
#>  int [1:3(1d)] 1 2 3

S3 atomic vectors

class属性是最重要的属性之一,添加class属性可以将向量转换成 S3 对象。常见的S3 vectors有:factorDatePOSIXctDifftimes。理解他们的关系可以参考下面的图。

  • factor:因子是基于整数向量建立起来的存储分类变量的数据类型。

  • Date:存储时间的数据类型,时间精度天数。

  • POSIXct/POSIXltPOSIX是 Portable Operating System Interface 的缩写,ct表示 calendar time,lt表示 local time。也是存储时间的数据类型,时间精度是秒数。

  • Difftimes:也是存储时间的数据类型。记录的是两个时间点之间的时间长短。

由于后面三种类型在我们数据分析中用的少之又少,所以这里并不打算展开介绍,感兴趣的读者可以阅读原文。

因子(factor)

因子是基于整数向量而衍生来的,因子存在两个属性,一个是class,其值为"factor",这个属性让它和普通的整数向量不同;另一个属性是level,其定义了因子中所有允许存在的值。

x <- factor(c("a", "b", "b", "a"))
x
#> [1] a b b a
#> Levels: a b

typeof(x)
#> [1] "integer"
attributes(x)
#> $levels
#> [1] "a" "b"
#>
#> $class
#> [1] "factor"

仔细观察下面的代码,你会发现当用table()函数计算因子中元素的个数时,允许存在(level属性包含的所有值)但因子中实际上没有的值也会被计算,当然计算的结果肯定为零。

sex_char <- c("m", "m", "m")
sex_factor <- factor(sex_char, levels = c("m", "f"))

table(sex_char)
#> sex_char
#> m
#> 3
table(sex_factor)
#> sex_factor
#> m f
#> 3 0

Ordered factors 是一类特殊的因子。它表现得像常规的因子,但level值的顺序是有意义的,比如"low","medium"和"high"等。

grade <- ordered(c("b", "b", "a", "c"), levels = c("c", "b", "a"))
grade
#> [1] b b a c
#> Levels: c < b < a

一些 base R 的函数,比如read.csv()data.frame等,他们会自动将字符串向量转换成因子,有时候如果你不想要这样做,可以使用参数stringsAsFactors = FALSE关闭这个行为。

列表(list)

列表也属于广义上的向量,但它的每个元素可以是不同的数据类型。但结合上期推文的内容,从技术上讲,列表中的每个元素实际上是同种类型,因为列表中的每个元素实际上是对对应对象的一个引用,所以每个元素肯定属于同种类型,但所指向的对象可以属于不同数据类型。

创建列表

你可以使用list()函数创建对象:

l1 <- list(
  1:3,
  "a",
  c(TRUE, FALSE, TRUE),
  c(2.3, 5.9)
)

typeof(l1)
#> [1] "list"

class(l1)
#> [1] "list"

str(l1)
#> List of 4
#>  $ : int [1:3] 1 2 3
#>  $ : chr "a"
#>  $ : logi [1:3] TRUE FALSE TRUE
#>  $ : num [1:2] 2.3 5.9

因为列表的每个元素都是某个对应对象的链接,创建一个列表并不牵涉到其他对象的复制,所以往往列表的实际大小比你预计的要小。

lobstr::obj_size(mtcars)
#> 7,208 B

l2 <- list(mtcars, mtcars, mtcars, mtcars)
lobstr::obj_size(l2)
#> 7,288 B

列表有时也被称为递归式(recursive)的向量,因为列表的元素也可以是列表,而 atomic vector 的元素只能是标量。

l3 <- list(list(list(1)))
str(l3)
#> List of 1
#>  $ :List of 1
#>   ..$ :List of 1
#>   .. ..$ : num 1

c()函数可以将多个列表合并成一个列表,当如果参数同时包含 atomic vectors 和列表时,c()函数会先将 atomic vectors 转换成列表,然后将不同的列表进行合并。下面的代码比较了c()list()函数:

l4 <- list(list(1, 2), c(3, 4))
l5 <- c(list(1, 2), c(3, 4))
str(l4)
#> List of 2
#>  $ :List of 2
#>   ..$ : num 1
#>   ..$ : num 2
#>  $ : num [1:2] 3 4
str(l5)
#> List of 4
#>  $ : num 1
#>  $ : num 2
#>  $ : num 3
#>  $ : num 4

Testing and coercion

可以用is.list()函数判断一个对象是否为列表,as.list()函数可以用于将其他可行的对象转换成列表。如果想将列表转换为 atomic vector 可以使用unlist()函数。

list(1:3)
#> [[1]]
#> [1] 1 2 3
as.list(1:3)
#> [[1]]
#> [1] 1
#>
#> [[2]]
#> [1] 2
#>
#> [[3]]
#> [1] 3

Matrices and arrays

对于 atomic vector,如果增加维度属性,可以转换为 matrix 或者 array 对象;同样对于列表,维度属性也能够用于将列表转换为 list-matrix 或者 list-array 对象。

l <- list(1:3, "a", TRUE, 1.0)
dim(l) <- c(2, 2)
l
#>      [,1]      [,2]
#> [1,] Integer,3 TRUE
#> [2,] "a"       1

l[[1, 1]]
#> [1] 1 2 3

Data frames and tibbles

前面我们介绍了四个建立在 atomic vector 基础上的 S3 对象,另外在列表基础上建立的两种 S3 对象在数据处理过程中也是非常有用的,它们分别是:数据框(data frame)和 tibble。

数据框在数据处理过程中使用得非常频繁,它存在列名属性(names)、行名属性(row.names)和class属性(data.frame)。

df1 <- data.frame(x = 1:3, y = letters[1:3])
typeof(df1)
#> [1] "list"     #data frame是有列表衍生而来

attributes(df1)
#> $names
#> [1] "x" "y"
#>
#> $class
#> [1] "data.frame"   # class属性种类
#>
#> $row.names
#> [1] 1 2 3

相比于列表,数据框有一个另外的限制:数据框的每列必须具有同样的长度,使得数据框有矩形结构。这就是为什么数据框同时具有矩阵和列表的特征

  • 数据框可以使用rownames()colnames()。对数据框使用names()函数会返回数据框的列名。

  • 数据框可以使用nrowncol函数。对数据框使用length()函数会返回数据框的列数。

数据框虽然好用,但数据框也有它的缺点,比如数据框的行名不允许出现重复,这有时候会令人头疼。所以由此衍生了很多类似的数据类型:tibble 和 data.table。这两种数据类型各有特点,由于篇幅问题我们不打算在这篇推文对他们进行比较,但我们找了一篇详细比较data.framedata.tabletibble的文章,链接放在文末,供感兴趣的读者学习。

NULL

这篇推文的最后我们介绍一个特殊的数据类型:NULL。我们可以认为它是长度为零的向量,它有特殊的类型并且没有任何属性。可以使用is.null()函数来检测NULL

typeof(NULL)
#> [1] "NULL"

length(NULL)
#> [1] 0

x <- NULL
attr(x, "y") <- 1
#> Error in attr(x, "y") <- 1: attempt to set an attribute on NULL

is.null(NULL)
#> [1] TRUE

NULL有两个常见的作用:

  • 作为一个任何类型的空向量。例如如果你使用c()函数但不包括任何参数,将会产生一个NULL。一个向量和NULL合并后并不会发生任何改变。

  • 代表一个缺失的向量。例如当一个函数的某个参数是可选的,NULL经常被用作这个参数的默认值。注意将NANULL区分开来,NA往往表示向量某个元素的缺失。

写在篇末

这是翻译学习《advanced R》系列第二篇,我们将持续向大家分享 R 语言进阶方面的底层知识,敬请保持关注~

参考

data.frame,data.table 和 tibble 的使用比较

Advanced R

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值