1

[译] Advanced R by Hadley Wickham

数据结构

本章主要总结base R中最重要的数据结构。你之前很有可能已经使用到它们,或者它们中的一部分,但是可能从来没有用心思考过它们之间有什么关联。这里,我们将不会更深地去讨论单个数据结构的类型。而是,展示它们是如何由个体构成整体的。如果,你想了解更多的细节,请参照R语言官方文档。R的基础数据结构可以按照维度和同构性进行划分:

  • 维度:1维、2维或者n维

  • 同构性:某一数据结构中的数据类型是否要求一致

下面给出了5种数据分析中最常用的数据结构:

# 同构 异构
1维 原子向量 (Atomic vector) 列表 (List)
2维 矩阵 (Matrix) 数据框 (Data frame)
n维 数组 (Array) -

几乎所有的对象都是建立在这几种数据结构基础之上的。在面向对象领域指南中(OO field guide)你将会看到这些简单的基础数据结构是如何构建起更加复杂的对象的。请注意:R没有0维数据结构,或者标量。你认为是标量的单个数字或者字符串,实际上是长度为1的向量。

给定一个对象,理解它是由哪些数据结构组成的最好方法是使用str()函数。str()是structure的缩写,它输出任何R数据结构的概括描述,而且这些描述是简洁易读的。

小测试

利用这些小测试来决定你是否有必要阅读本章内容。如果你能立即想到答案,那么你可以放心地跳过本章。你可以在这里检验你的答案。

  1. 除了所包含的内容,向量(vector)的三个属性是什么?

  2. 原子向量(atomic vector)的四种常见类型是什么?两种罕见类型是什么?

  3. 属性(attributes)是什么?如何获取属性和设置属性?

  4. 列表(list)和原子向量(atomic vector)的区别是什么?矩阵(matrix)和数据框(data frame)的区别是什么?

  5. 列表(list)可以是矩阵(matirx)吗?矩阵(matrix)可以作为数据框(data frame)的一个列吗?

提纲
  • 向量(Vectors)介绍原子向量和列表,R的1维数据结构。

  • 属性(Attributes)绕了一个小小的弯路去讨论属性,R的灵活的元数据规范。这里,你将会了解到因子(factors),通过设置原子向量的属性而得到的一种重要的数据结构。

  • 矩阵和数组(Matrix and array)介绍矩阵和数组,储存2维和高维数据的数据结构。

  • 数据框(Data frames)介绍数据框,R中储存数据的最重要的数据结构。数据框结合了列表和矩阵的行为,非常适合统计数据的需求。

向量(Vectors)

R语言中最基础的数据结构是向量。向量可以分为:原子向量和列表。它们有3个共同的属性:

  • 类型,typeof(),它是什么。

  • 长度,length(),它包含多少个元素。

  • 属性,attributes(),附加的任意元数据。

它们在元素类型上有所区别:原子向量的所有元素必须是相同类型的,而列表的元素可以是不同类型的。

附注:is.vector()不能验证一个对象是否是向量。相反,只要对象是除了名称之外不含有任何属性的向量(原子向量和列表),它就返回TRUE。可以用is.atomic(x) || is.list(x)去验证一个对象是否真的是向量。

原子向量(Atomic vector)

在这,我们将详细讨论原子向量的四种常见类型:logical, integer, double (常称为 numeric), 以及character。另外,还有两个罕见的类型将不做讨论:complex和raw。
原子向量通常用c()创建,combine的缩写。

dbl_var <- c(1, 2.5, 4.5)
# 使用L后缀, 你将会得到integer型,而并非double型
int_var <- c(1L, 6L, 10L)
# 使用TRUE和FALSE (或T和F)创建逻辑向量
log_var <- c(TRUE, FALSE, T, F)
chr_var <- c("these are", "some strings")

原子向量总是水平排列的,即使你使用c()进行嵌套:

c(1, c(2, c(3, 4)))
#> [1] 1 2 3 4
# 和下面方法相同
c(1, 2, 3, 4)
#> [1] 1 2 3 4

注:上面创建的原子向量为列向量。

缺失值用NA来指定,长度为1的逻辑向量。在c()NA将被强制转换为正确的类型,或者你可以使用NA_real_ (双精度浮点型向量), NA_integer_ 以及NA_character_来创建特定类型的缺失值。

类型及测试

给定一个向量,你可以使用typeof()来确定其类型,或者使用"is"函数验证它是否是某种特定的类型:is.character(), is.double(), is.integer(), is.logical(), 或更一般的 is.atomic()

int_var <- c(1L, 6L, 10L)
typeof(int_var)
#> [1] "integer"
is.integer(int_var)
#> [1] TRUE
is.atomic(int_var)
#> [1] TRUE
dbl_var <- c(1, 2.5, 4.5)
typeof(dbl_var)
#> [1] "double"
is.double(dbl_var)
#> [1] TRUE
is.atomic(dbl_var)
#> [1] TRUE

附注:is.numeric()是对向量是否是“数值”类型一般的检验方法,它对整型(double),双精度浮点型(double)的向量都返回TRUE。它并不是针对双精度浮点型(double)向量的检验方法,我们通常称双精度浮点型(double)为“数值型”。

is.numeric(int_var)
#> [1] TRUE
is.numeric(dbl_var)
#> [1] TRUE
强制转换

原子向量中的所有元素必须是同种类型的。所以,当你尝试合并多种类型数据时,将强制转换为最灵活的那种类型。灵活度从小到大依次为:逻辑型(logical), 整型(integer), 双精度浮点型(double), 字符型(character)。

例如,合并字符型和整型输出字符型:

str(c("a", 1))
#> chr [1:2] "a" "1"

当逻辑型(logical)强制转换为整型(integer)和双精度浮点型(double)时,TRUE将会转换为1,FALSE转换为0。这在结合sum()mean()使用时将会非常有用。

x <- c(FALSE, FALSE, TRUE)
as.numeric(x)
#> [1] 0 0 1
# TRUE的总数
sum(x)
#> [1] 1
# TRUE所占的比例
mean(x)
#> [1] 0.3333333

强制转换经常自动执行。大部分数学函数(+logabs等。)将强制转换为双精度浮点型(double)或者整型(integer),大部分逻辑运算符(&|any等)将强制转换为逻辑型(logical)。如果强制过程中共有信息的丢失,你将会得到警告信息。如果转换遇到歧义,可以使用as.character(), as.double(), as.integer(), 或者 as.logical()进行明确的转换。

列表(Lists)

列表不同于原子向量,因为它们的元素可以是任何类型的,包括列表类型。使用list(),而不是c()来创建列表。

x <- list(1:3, "a", c(TRUE, FALSE, TRUE), c(2.3, 5.9))
str(x)
#> List of 4
#>  $ : int [1:3] 1 2 3
#>  $ : chr "a"
#>  $ : logi [1:3] TRUE FALSE TRUE
#>  $ : num [1:2] 2.3 5.9

列表有时被称为递归向量,因为一个列表可以包含其它表。这使它从根本上不同于原子向量。

x <- list(list(list(list())))
str(x)
#> List of 1
#>  $ :List of 1
#>   ..$ :List of 1
#>   .. ..$ : list()
is.recursive(x)
#> [1] TRUE

c()可以将多个列表合并成一个列表。如果给定一个原子向量和列表组合,c()将会在合并它们之前把原子向量强制转换为列表。比较list()c()两种结果:

x <- list(list(1, 2), c(3, 4))
y <- c(list(1, 2), c(3, 4))
x
#> [[1]]
#> [[1]][[1]]
#> [1] 1

#> [[1]][[2]]
#> [1] 2


#> [[2]]
#> [1] 3 4
str(x)
#> List of 2
#>  $ :List of 2
#>   ..$ : num 1
#>   ..$ : num 2
#>  $ : num [1:2] 3 4
y
#> [[1]]
#> [1] 1

#> [[2]]
#> [1] 2

#> [[3]]
#> [1] 3

#> [[4]]
#> [1] 4
str(y)
#> List of 4
#>  $ : num 1
#>  $ : num 2
#>  $ : num 3
#>  $ : num 4

对列表调用typeof()函数,将会输出列表。你可以使用is.list()来验证一个对象是否为列表,使用as.list()把一个对象强制转换为列表。你也可以使用unlist()将列表转换为原子向量。如果一个列表中的元素是不同数据类型的,unlist()使用与c()相同的强制规则,根据灵活度对数据进行强制转换。
列表用来创建R语言中较为复杂的数据结构。例如:数据框(data frames)和线性模型对象(由lm()产生)都是列表。

is.list(mtcars)
#> [1] TRUE
mod <- lm(mpg ~ wt, data = mtcars)
is.list(mod)
#> [1] TRUE

练习

  1. 原子向量的6种类型是什么?列表和原子向量的区别是什么?

  2. is.vector()is.numeric()is.list()is.character()的根本区别是什么?

  3. 检测你的向量强制转换规则知识的了解:尝试预测下面c()函数的强制输出结果:

       c(1, FALSE)
       c("a", 1)
       c(list(1), "a")
       c(TRUE, 1L)
  4. 为什么要用unlist()将列表转换为原子向量,而不是用as.vector()

  5. 为什么 1 == "1" 输出TRUE? -1 < FALSE 输出TRUE? "one" < 2 输出FALSE?

  6. 为什么默认的缺失值NA是逻辑向量?逻辑向量有什么特殊地方?(提示:思考一下c(FALSE, NA_character_)

属性(Attributes)

所有的对象都可以拥有任意的附加属性,用于存储与对象相关的元数据。属性可以看做为具有唯一标识符的命名列表。属性可以通过attr()单独访问,也可以通过attributes()以列表的形式一次性访问。

y <- 1:10
attr(y, "my_attribute") <- "This is a vector"
attr(y, "my_attribute")
#> [1] "This is a vector"
str(attributes(y))
#> List of 1
#>  $ my_attribute: chr "This is a vector"

structure()函数返回一个已被修改属性的新对象:

structure(1:10, my_attribute = "This is a vector")
#>  [1]  1  2  3  4  5  6  7  8  9 10
#> attr(,"my_attribute")
#> [1] "This is a vector"

默认情况下,当修改一个向量时,大多数属性都会丢失:

attributes(y[1])
#> NULL
attributes(sum(y))
#> NULL

但是,有三个重要的属性不会丢失:

  1. 名字(Names),一个字符型向量,为每一个元素赋予一个名字,将在 名字(Names)中描述。

  2. 维度(Dimensions),用于把向量转换为矩阵和数组,将在 矩阵和数组(Matrix and arrays)中描述。

  3. 类(Class),用于实现S3对象系统,将在S3中描述。
    这些属性中的任一属性都可以通过指定的函数进行访问和赋值。当访问这些属性的时候,使用names(x), dim(x), 和class(x), 而不是attr(x, "dim"), 和attr(x, "class")

名字(Names)

你可以通过三种方式为向量命名:

  1. 创建时命名:x <- c(a = 1, b = 2, c = 3)

  2. 修改现有的向量:x <- 1:3; names(x) <- c("a", "b", "c")

  3. 创建一个向量的修订副本:x <- setNames(1:3, c("a", "b", "c"))
    名字(Names)不是必须唯一的。然而,使用名字(Names)的最重要的原因是对字符取子集操作,当名字(Names)唯一时,取子集操作会更加有用。

并不是向量中的所有元素都需要拥有一个名字。如果元素的名字缺失,names()将为这些元素返回空字符串。如果所有的名字缺失,names()将返回空值NULL

y <- c(a = 1, 2, 3)
names(y)
#> [1] "a" ""  ""
z <- c(1, 2, 3)
names(z)
#> NULL

你可以使用unname(x)创建一个没有名字的向量,或者通过 names(x) <- NULL移除名字。

因子(Factors)

属性的一个重要用途是定义因子。因子是只包含预定义值的向量,用来存储分类数据。因子建立在整型向量基础之上,拥有两个属性:

  1. class():"factor",这使得因子和常规的整型向量有所差别

  2. levels(),定义了允许取值的集合

x <- factor(c("a", "b", "b", "a"))
x
#> [1] a b b a
#> Levels: a b
class(x)
#> [1] "factor"
levels(x)
#> [1] "a" "b"
# You can't use values that are not in the levels
x[2] <- "c"
#> Warning in `[<-.factor`(`*tmp*`, 2, value = "c"): invalid factor level, NA
#> generated
x
#> [1] a    <NA> b    a   
#> Levels: a b
# 注: 你不可以合并因子
c(factor("a"), factor("b"))
#> [1] 1 1

当你知道变量的所有可能取值时,即使你在数据集中看不到这些取值,因子是非常有用的。使用因子而不是字符向量可以使得没有包含这些取值的分组看起来更加醒目。

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

有时,你从文件中直接读取数据框时,那些你认为将产生数值型向量的列却变成了因子向量。这是由该列中含有非数值型的数据造成的,通常是用来标记缺失值的特殊符号:.或者-。为了纠正这种情况,可以先把因子向量强制转换为字符向量,然后再把字符向量转换为双精度浮点型向量。(这个过程后,务必检查缺失值。)当然,更好的方法是首先弄清问题产生的原因,然后及时解决;使用read.csv()na.string参数通常是一个好的解决方法。

# 在这里从"text"中读取数据,而不是从文件中读取:
z <- read.csv(text = "value\n12\n1\n.\n9")
z
#>   value
#> 1    12
#> 2     1
#> 3     .
#> 4     9
typeof(z$value)
#> [1] "integer"
as.double(z$value)
#> [1] 3 2 1 4
# 哎呦, 不对: 3 2 1 4 是因子的水平, 
# 并不是我们所读取的数据。
class(z$value)
#> [1] "factor"
# 我们现在可以对它进行处理:
as.double(as.character(z$value))
#> Warning: NAs introduced by coercion
#> [1] 12  1 NA  9
# 或者改变我们的读取方式:
z <- read.csv(text = "value\n12\n1\n.\n9", na.strings=".")
typeof(z$value)
#> [1] "integer"
class(z$value)
#> [1] "integer"
z$value
#> [1] 12  1 NA  9
# Perfect! :)

不幸的是,R语言中,大部分的数据加载函数都会自动地将字符型向量转换为因子向量。这种方法是欠佳的,因为这些函数并没有方法获取因子的所有水平,以及这些因子的最优次序。然而,我们可以通过设置参数 stringAsFactors = FALSE 阻止自动因子转换行为,然后再根据你对数据的了解,手工地将字符向量转换为因子向量。全局设置,options(stringsAsFactors = FALSE)也能控制自动因子转换行为,但不建议使用这种方法。因为修改全局设置后,当你运行其他代码(不管是包还是用source()引入代码),你会得到意想不到的结果。修改全局设置会使代码变得更难理解,因为这增加了你需要阅读的代码量,而且你必须得弄清楚这些代码起到什么样的作用。
尽管因子向量看起来像,而且经常表现的像字符向量,它们实际上是整型向量。当把它们当做字符串处理的时候要多加小心。一些字符串处理方法(如gsub()grepl())将把因子强制转换为字符串,然而其它方法(如nchar())将会抛出异常,以及其它方法(如c())将会使用它们隐藏的整型数值(因子水平)。鉴于这些原因,如果你想让因子表现出类似字符串的行为,最好的方法就是把因子显式转换为字符向量。在R的早期版本中,使用因子向量比字符串向量有节省内存的优势,但是现在内存已不再是问题了。

练习

  1. 早期使用以下代码来举例说明structure()

    structure(1:5, comment = "my attribute")
    #> [1] 1 2 3 4 5

    但是,当打印对象时,你却看不到comment属性。这是为什么呢?是属性缺失?还是有什么特殊之处?(提示:尝试使用帮助文档。)

  2. 当你修改因子水平时,发生了什么?

    f1 <- factor(letters)
    levels(f1) <- rev(levels(f1))
  3. 这段代码有什么作用?f2,f3与f1有什么区别?

    f2 <- rev(factor(letters))

    f3 <- factor(letters, levels = rev(letters))

矩阵和数组(Matrices and arrays)

正在更新.......

Hadley Wickham 是 RStudio 的首席科学家以及 Rice University 统计系的助理教授。他是著名图形可视化软件包 ggplot2 的开发者,以及其他许多被广泛使用的软件包的作者,代表作品如 plyr、reshape2 等。文章英文原著来自于 Hadley Wickham 的Advanced R
N|Solid


xiao蜗牛
85 声望20 粉丝

{name: 'Xiao蜗牛',