数据结构
本章主要概括讲解R中的几种最重要的数据结构。你可能已经使用了其中的大部分数据结构,可是确还没弄清楚他们之间的相互关系。在本章,我不会去逐个深入讲解不同的数据结构,而是帮助你理清它们之间整体的关联性。你可以查找R文档来获得对单个数据结构的详细介绍。
R的基础数据结构可以通过维度(1维,2维或n维)和同质或异质性(数据类型是否一致)来概括。以下包括了数据分析中最常用的五种数据结构:
同质 | 异质 | |
---|---|---|
1维 | Atomic vector(原子向量) | List (列表) |
2维 | Matrix(矩阵) | Data frame (数据框) |
n维 | Array(数组) |
其他所有的数据类型几乎都是建立在这些基础的数据类型之上的。在面相对象指南一章你将会学习如何使用这些基础的数据类型来建立更复杂的数据类型。请注意R语言没有0维和标量类型。那些你会认为是标量的单个数字或者字符串,在R中实际上是一维的原子向量。
查看一个对象数据结构的最简单的方式是使用str()
函数。str()
是structure(结构)的缩写,它能对任何R数据结构提供简洁明了的描述。
测试
做做这个简单的测试看看你是否需要阅读本章内容。如果你能很快地得到答案,你可以轻松地跳过本章。本章最后提供参考答案。
- 数组除了它的内容,其他的三个特性是什么?
- 四种主要的原子向量类型?以及另外两种不常用的原子向量类型?
- 什么是属性?如何来查看和设置属性?
- 列表和原子向量有什么不同,矩阵和数据结构有什么不同?
- 一个列表可以是矩阵吗?一个数据框的一列可以是矩阵吗?
概要
向量 介绍R的一维数据结构,原子向量和列表
属性 简单的介绍下R灵活的元数据说明方式-属性,这里会介绍原子向量设置属性中的一种重要的数据结构-因子。
矩阵和数组 介绍二维和高维的数据结构,矩阵和数组。
数据框 学习R中最重要的数据结构-数据框,数据框同时包涵列表和矩阵的特性,是一种非常适合做统计分析的数据结构。
向量
向量是R中的基础数据结构。向量有两种形式:原子向量和列表。它们有如下三种共同特性:
- 类型,
typeof()
,是什么。 - 长度,
length()
,有多少个元素。 - 属性,
attributes()
, 其他任意的原数据.
他们主要的不同在于元素的类型:原子向量的所有元素必须是相同的类型,但是列表的元素可以是不一样的类型。
注意:is.vector()
并不测试一个对象是否是向量。只有当一个对象是向量并且除了名字没有其他属性时,is.vector()
的返回值才是TRUE
。请使用is.atomic(x) || is.list(x)
来测试一个对象是否为向量。
原子向量
我将详细的介绍下R中原子向量的四种常见的类型:逻辑型(logicle),整型(integer),数值型(double或则numeric)和字符串型(character)。还有两种不常用的类型:复杂型(complex)和粗糙型(raw),这里就不详细介绍了。
原子向量通常可以用c()
函数来创建,c()
表示组合(combine):
dbl_var <- c(1, 2.5, 4.5)
# 用L做后缀会的到整型而不是数值型
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)))
# 等同于如下
c(1, 2, 3, 4)
缺失值用NA
来标明,实际上NA
是一个长度为1的逻辑型向量。在 c()
函数里,NA
会始终被转换成逻辑类型。你可以使用NA_real_
(一个数值型向量),NA_integer_
和NA_character_
来创建特殊类型的NA
.
类型测试
你可以使用typeof()
来确定一个向量的类型, 或者使用"is"函数(is.character()
,is.double()
,is.integer()
,is.logical()
)来测试是否为某个具体的类型,或者使用is.atomic()
做一般测试。
int_var <- c(1L, 6L, 10L)
typeof(int_var) # "integer"
is.integer(int_var) # TRUE
is.atomic(int_var) # TRUE
dbl_var <- c(1, 2.5, 4.5)
typeof(dbl_var) # "double"
is.double(dbl_var) # TRUE
is.atomic(dbl_var) # TRUE
注意:is.numeric()
是判断一个向量是否为数字的通用测试。也就是说不管是整型还是数值型的向量,它的返回值都是TRUE
。所以is.numeric()
并不是检测数值型(double或numeric)的专用函数。
is.numeric(int_var) # TRUE
is.numeric(dbl_var) # TRUE
强制类型转换
一个原子向量的所有元素都必须是相同的类型。因此当你不同类型的元素组合在一起的时候,它们会被强制转换成最通用的类型。不同类型的通用型由低到高排列是:逻辑型(logical),整型(integer), 数值型(double), 和字符串型(character)。
例如,整形和字符串型结合在一起就被转换成字符串型:
str(c("a", 1))
当一个逻辑型被强制转换成整型或则数值型时,TRUE
会被转换成1,FALSE
会被转换成0。这在与sum()
和mean()
搭配使用的时候非常有用。
x <- c(FALSE, FALSE, TRUE)
as.numeric(x)
# Total number of TRUEs
sum(x)
# Proportion that are TRUE
mean(x)
R中很多强制类型转换是自动的。比如在使用数学运算函数(如+
,log
,abs
等)时,数据类型会被强制转换成数值型(double)或则整型(integer); 在逻辑运算比如&
,|
,any
等中,数据类型会被强制转换成逻辑型(logical)。在一些强制转换中通常如果有信息丢失,你都会的到一些警告信息。如果不确定,可以使用as.character()
,as.double()
,as.integer()
或as.logical()
来进行明确的类型转换。
列表
列表和原子向量的不同在于列表的元素可以是任意不同的类型和数据结构,包括列表。列表由list()
而不是c()
来创建。
x <- list(1:3, "a", c(TRUE, FALSE, TRUE), c(2.3, 5.9))
str(x)
列表有时候也被称为迭代向量,因为一个列表也能包括其他列表。 这是他们与原子向量有本质上的不同。
x <- list(list(list(list())))
str(x)
is.recursive(x)
c()
函数可以把多个列表组合成一个列表。c()
不仅能组合原子向量也能组合列表。在组合原子向量前,c()
会先将原子向量强制转换成列表然后再组合。比较一下list()
和c()
的不同:
x <- list(list(1, 2), c(3, 4))
y <- c(list(1, 2), c(3, 4))
str(x)
str(y)
一个列表的typeof()
返回值是list
。你可以使用is.list()
来检测一个对象是否为列表,也可以使用as.list()
将一个对象强制转换成列表。如果一个列表中的元素是不同的数据类型,使用unlist()
进行强制类型转换将和c()
函数的强制转换规则一样。
列表在R中通常被用来创建更复杂的数据结构。比如数据框和线性模型对象(lm()
函数的结果)都是列表:
is.list(mtcars) # TRUE
mod <- lm(mpg ~ wt, data = mtcars)
is.list(mod) # TRUE
练习
原子向量有哪六种类型?列表和原子向量有什么不同?
is.vector()
和is.numeric()
与is.list()
和is.character()
本质上有什么不同?预测如下代码中
c()
的结果来检测你对向量中强制类型转换规则的掌握情况:c(1, FALSE) c("a", 1) c(list(1), "a") c(TRUE, 1L)
为什么要使用
unlist()
来将一个列表转换成原子向量而不是使用as.vector()
?为什么
1 == "1"
的返回值是TRUE?为什么-1 < FALSE
返回值是TRUE?为什么"one" < 2
的返回值是FALSE?为什么表示缺失值的
NA
是一个逻辑型向量?逻辑型向量有什么特点?(提示:想一想c(FALSE, NA_character_)
)
属性
所有的对象都可以拥有附加的属性,用来存储关于对象的元数据。属性可以看作无重复的名字列表。一个对象的属性可以使用attr()
来单独查看或修改,也可以使用attributes()
列出所有属性。
y <- 1:10
attr(y, "my_attribute") <- "This is a vector"
attr(y, "my_attribute")
str(attributes(y))
structure()
函数可以用来修改一个对象的属性然后返回一个新的对象:
structure(1:10, my_attribute = "This is a vector")
当一个向量被修改后,其大部分属性会默认地被丢失:
attributes(y[1])
attributes(sum(y))
其中有三个最重要的属性是不会丢失的:
以上三种属性有专门的函数来用来查看和设置其属性值,分别是 names(x)
,dim(x)
和class(x)
,注意这里不是用attr(x, "names")
,attr(x, "dim")
和attr(x, "class")
。
名字
你可以用以下三种方式来给向量命名:
在创建时:
x <- c(a = 1, b = 2, c = 3)
。修改一个已有向量的名字属性:
x <- 1:3; names(x) <- c("a", "b", "c")
。复制一个向量并添加名称属性:
x <- setNames(1:3, c("a", "b", "c"))
。
一个向量的名字可以是重复的。然而无重复的名字属性对于R中便利的取子集操作来说非常关键。
并不是一个向量中的所有元素都需要有名字。names()
函数会对那些没有名字的向量元素返回一个空字符。如果所有的名字都缺失,那么names()
函数会返回NULL
。
y <- c(a = 1, 2, 3)
names(y)
z <- c(1, 2, 3)
names(z)
你可以使用unname(x)
得到一个去掉名字的新向量,或则用names(x) <- NULL
来去掉该对象的名字。
因子
使用名字的一大好处是可以定义因子。因子是一个仅包含预设值的向量,用来存储分类数据。因子是由整型向量添建两种属性来创建的。这两种属性分别是:class()
为“因子”,使他们和常规的整形向量区分开,以及levels()
水平,用来定义向量元素的类别。
x <- factor(c("a", "b", "b", "a"))
x
class(x)
levels(x)
# 不可以使用不在水平范围中的值
x[2] <- "c"
x
# 注意:因子不可以合并
c(factor("a"), factor("b"))
当你不知道一个数据中的所有值,但是只知道某个变量的可能值时,使用因子会非常有用。使用因子而不是字符串向量使得查看某个类别中是否有观测值变得非常方便:
sex_char <- c("m", "m", "m")
sex_factor <- factor(sex_char, levels = c("m", "f"))
table(sex_char)
table(sex_factor)
当你从一个文件中直接读取一个数据框时,你认为应该是数值型向量的某一列可能会变成了一个因子。这应该是因为该列中存在非数值的元素,通常是一些用特殊字符比如.
或者-
标记的缺失值。对于这种情况,你可以将这一列先强制转换成字符串向量,然后再强制转换成数值型向量(在转换后记得查看缺失值)。当然,更简单的方法是在读取数据的时候就解决这个问题。比如在使用read.csv()
函数时设置na.strings
参数。
# 用你的文本文件名替代下面的"text":
z <- read.csv(text = "value\n12\n1\n.\n9")
typeof(z$value)
as.double(z$value)
# 嗷嗷,这不对。3 2 1 4是这个因子的水平,不是我们读进去的数值
class(z$value)
# 我们可以这样来解决:
as.double(as.character(z$value))
# 或者我们修改我们读取文件的函数:
z <- read.csv(text = "value\n12\n1\n.\n9", na.strings=".")
typeof(z$value)
class(z$value)
z$value
# 完美!:)
不幸的是,R中很多的数据读取函数都会自动的将字符串型向量转换成因子。这不是最优的方案,因为这些函数不可能知道所有的因子水平以及他们正确的顺序。你可以在函数中声名stringsAsFactors = FALSE
来去除这种转换,然后根据你对数据的了解和需要,手动的将特定的字符串型向量转换成因子。另外,设置全局变量options(stringsAsFactors = FALSE)
也可以用来去除这种强制转换,然而我并不推荐使用。修改全局变量会对外部添加的代码(无论是使用source()
或引入其他包)造成不可预知的影响,同时它也降低了代码的可读性,因为你需要知道修改的全局变量对哪些代码产生了影响。
尽管因子在使用的时候像一个字符串类向量,然而实际上因子是整型向量。一些字符串操作函数(比如gsub()
和grepl()
)会将因子强制转换成字符串,也有一些(比如nchar()
)会报错,而像c()
则会用因子内在的整型值。因此,如果需要字符串型的操作,最好先将因子强制转换成字符串型向量。在R先前的版本中使用因子比字符串型向量节省一些内存,现在的版本中确并不是这样。
练习
之前有种如下的代码演示
structure()
的使用:structure(1:5, comment = "my attribute")
可是打印出来确并看不到comment属性。为什么?属性消失了吗,或者有一些其他的特别之处?
当你修改一个因子的水平的时候会发生什么?
f1 <- factor(letters) levels(f1) <- rev(levels(f1))
下面的代码是用来做什么的?
f2
和f3
与f1
有什么不同?f2 <- rev(factor(letters)) f3 <- factor(letters, levels = rev(letters))
矩阵和数组
给原子向量添加一个dim()
属性会将原子向量转换成多维数组。矩阵是一种特殊的二维数组。矩阵在统计数学中的使用非常普遍。数组确相对比较少用,但是也是一种很重要的数据结构。
矩阵和数组可以分别使用matrix()
和array()
来创建,或者通过使用dim()
给原子向量赋予维度来创建:
# 用两个标量参数来设置行数和列数
a <- matrix(1:6, ncol = 3, nrow = 2)
# 用一个向量来设置维度
b <- array(1:12, c(2, 3, 2))
# 也可以使用dim()将向量转换成矩阵和数组
c <- 1:8
dim(c) <- c(4, 2)
c
dim(c) <- c(2, 2, 3)
c
length()
和names()
在矩阵和数组中也有相对应的函数:
在矩阵中和
length()
相对应的函数是nrow()
和ncol()
,在数组中和length()
相对应的函数则是dim()
。在矩阵中和
names()
相对应的函数是rownames()
和colnames()
信息,在数组中和names()
相对应的函数则是dimnames()
。
length(a)
nrow(a)
ncol(a)
rownames(a) <- c("A", "B")
colnames(a) <- c("a", "b", "c")
a
length(b)
dim(b)
dimnames(b) <- list(c("one", "two"), c("a", "b", "c"), c("A", "B"))
b
矩阵中和c()
相对应的函数分别是cbind()
和rbind()
,数组中则是abind()
(由abind
包提供)。你可以使用t()
来转置一个矩阵;数组转置则用aperm()
。
你可以使用is.matrix()
和is.array()
来检测一个对象是否为矩阵或则数组,使用dim()
来检测该对象的纬度。使用as.matrix()
或as.array()
则可以将一个向量轻松地转换成矩阵或则数组。
不仅仅是有向量是一维的数据结构。你也可以构建只有一行或一列的矩阵,或者只是一维的数组。他们打印出来可能很相似,可是操作起来确大不相同。这些不同不是很重要,可是知道有这些不同对于使用一些函数(比如tapply()
)会非常有用。记住,多使用str()
来查看数据的结构。
str(1:3) # 一维向量
str(matrix(1:3, ncol = 1)) # 一列矩阵
str(matrix(1:3, nrow = 1)) # 一行矩阵
str(array(1:3, 3)) # “数组”向量
尽管通常使用dim()
来将向量转换成矩阵,使用纬度属性也可以将列表转换成矩阵列表或数组列表。
l <- list(1:3, "a", TRUE, 1.0)
dim(l) <- c(2, 2)
l
这些是一些相对隐秘的数据结构,但是如果你想将对象存放在格子一样的数据结构中,这将会很有帮助。比如你在时空格(spatio-temporal grid)模型中运算,将数据存储在三维数组中将维持数据的三维空间结构从而极大地方便了数据操控。
练习
对一个向量使用
dim()
将返回什么值?如果
is.matrix(x)
返回TRUE
,那么is.array(x)
将返回什么?如何描述如下三种对象?它们和
1:5
有什么不同?x1 <- array(1:5, c(1, 1, 5)) x2 <- array(1:5, c(1, 5, 1)) x3 <- array(1:5, c(5, 1, 1))
数据框
数据框是R中存储数据最常用的数据结构。在程序中普及使用数据框会使得数据分析更简单。其实隐藏在数据框中的是一个包含多个相同长度向量的列表。因此数据框是一个同时拥有矩阵和列表特性的二维数据结构。这意味着数据框有names()
也有colnames()
和rownames()
,尽管names()
和colnames()
是同一回事。用length()
查看一个数据框的返回值是数据框中潜在列表的长度,因此和ncol()
的返回值相同; nrow()
返回数据框的行数。
在下一章取子集中,你会学到如何像一个列表一样对数据框取子集,也会学到如何像一个矩阵一样对数据框取子集。
创建
你可以使用data.frame()
函数来创建一个数据框,其输入是一个个有名字的向量:
df <- data.frame(x = 1:3, y = c("a", "b", "c"))
str(df)
注意使用data.frame()
会默认地将字符串转换成因子,可以使用 stringAsFactors = FALSE
来取消这一转换。
df <- data.frame(
x = 1:3,
y = c("a", "b", "c"),
stringsAsFactors = FALSE)
str(df)
测试和转换
因为data.frame
是一个S3类,因此它的类型是列表。查看一个对象是否为数据框,可以使用class()
或者更准确点使用is.data.frame()
:
typeof(df)
class(df)
is.data.frame(df)
你可是使用 as.data.frame()
将一个对象强制转换成一个数据框:
一个向量会被转化成一列的数据框。
一个列表中的每一个元素会被转化成数据框中的一列,如果各元素间长度不一致,则会报错。
一个矩阵会被转换成you相同列数和行数的数据框。
合并数据框
你可以使用cbind()
和rbind()
来合并数据框:
cbind(df, data.frame(z = 3:1))
rbind(df, data.frame(x = 10, y = "z"))
如果按列来合并,那么两个数据框必须要有相同的行数,行名称会被忽略。如果按行来合并,那么他们的列数和列名称都必须相同。你也可以使用plyr::rbind.fill()
来合并拥有不同列数的数据框。
使用cbind()
合并多个向量来构建数据框是一个常见的错误。这样cbind()
实际上会返回一个矩阵,只有当其中有一个是数据框时其返回的才是一个数据框。最简单的还是直接使用data.frame()
:
bad <- data.frame(cbind(a = 1:2, b = c("a", "b")))
str(bad)
good <- data.frame(a = 1:2, b = c("a", "b"), stringsAsFactors = FALSE)
str(good)
使用cbind()
合并的转换规则比较复杂,最好使用的时候保证所有输入都是相同的类型。
特殊列
由于数据框本质是包含多个向量的列表,因此给一个数据框添加一个是列表的列也是可以的:
df <- data.frame(x = 1:3)
df$y <- list(1:2, 1:3, 1:4)
df
但是,当传递一个列表到data.frame()
函数时,data.frame()
会试图将列表中的每一个元素单独放到一列中,这时候就可能报错:
data.frame(x = 1:3, y = list(1:2, 1:3, 1:4))
一种折中的解决方案是使用I()
,这样data.frame()
会把列表看成一个单元:
dfl <- data.frame(x = 1:3, y = I(list(1:2, 1:3, 1:4)))
str(dfl)
dfl[2, "y"]
I()
函数会给它的输入添加AsIs
类,但是你可以放心的忽略这一点。
同样地,你也可以给数据框添加一个是矩阵或者向量的列,只要他们的行数和数据框的行数一致:
dfm <- data.frame(x = 1:3, y = I(matrix(1:9, nrow = 3)))
str(dfm)
dfm[2, "y"]
使用含有列表和数组作为一列的数据框时要注意:大多数对数据框的操作函数都默认数据框的每一列为原子向量。
练习
数据框拥有什么样的属性?
当对一个含有不同类型列的数据框使用
as.matrix()
时会得到什么?可以创建一个只有一行或者只有一列的数据框吗?
参考答案
向量的三种特性分别是类型,长度和属性。
属性可以给任何对象添加任意的元数据。你可以通过
attr(x, "y")
和attr(x, "y") <- value
来获取和设置一个属性;或者使用attributes()
来获得和设置所有属性。一个列表的元素可以是任意类型(甚至是列表);一个原子向量的元素必须是相同的类型。同样的,矩阵的所有元素都必须是相同的类型;在数据框中,不同的列可以是不一样的类型。
你可以通过给一个列表添加维度来创建列表数组。你可以使用
df$x <- matrix()
或者I()
(data.frame(x = I(matrix()))
)来给一个数据框添加一个矩阵列。