Advanced R之数据结构
看了几本R语言语法相关的书籍,感觉都不怎么好,在实际使用过程中仍然遇到很多难以理解的问题,后来看了Hadley Wickham的Advanced R,好多问题迎刃而解,今天重温了该书的第一章即数据结构,记录下要点。干脆翻译下吧。
原文地址:http://adv-r.had.co.nz/Data-structures.html
本人水平有限,如有错误请谅解和指正,非常感谢。转载请注明出处:http://www.cnblogs.com/lizichao/p/4792373.html
数据结构
这一章概述了base R中最重要的数据结构。你很可能之前使用过其中的一些(即使不是全部),但也许你没有深入思考它们是如何关联的。在这篇简短的概述中,我不会深入讨论每种类型。相反,我将展示它们是如何组成一个整体的。如果想了解更多的细节,可以参考R的文档。
R的基础数据结构可以按照维数(1维、2维、或者多维)或者同质(所有元素都是同类型的)、非同质来划分。按照这种思路可以得到数据分析中使用最多的5种数据类型:
同质 | 非同质 | |
1维 | 原子向量 | 列表 |
2维 | 矩阵 | 数据框 |
多维 | 数组 |
几乎所有其他对象都是建立在这些类型之上。在the OO field guide中你将看到更加复杂对象是如何通过这些简单类型创建的。注意R没有0维或者标量类型。一个数或者字符串,你可能认为是标量的,但其实是长度为1的向量。
给定一个对象,想要了解其数据结构的最好方式是使用str()函数。str()是structure的缩写,调用该函数将返回对R中任意数据结构的简洁、可读的描述。
测试
做下这个小测试,以确定你是否需要阅读这一章。如果可以很快想到答案,你可以跳过这一章。可以在答案(译者注:位于本章结尾)中检验你的答案。
- 向量的3个属性是什么,不是类型?
- 原子向量的4种常见类型是哪4种?另外2中少见的类型是什么?
- 属性是什么?如何获取和设置属性?
- 列表与原子向量有何不同?矩阵与数据框有何不同?
- 列表是否可以是矩阵?矩阵是否可以作为数据框的一列?
概述
- 向量,介绍原子向量和列表,属于R中1维的数据结构。
- 属性,绕个弯儿,讨论下属性,R中灵活的元数据规范。你将学习到因子,一种通过设置原子向量创建的重要数据结构。
- 矩阵和数组,介绍矩阵和数组,它们是2维和高维数据结构。
- 数据框,介绍数据框,它是R中数据存储最重要的数据结构。数据框组合了列表和矩阵的行为,这使得数据框成为一种最适合存储统计数据的数据结构。
向量
R的基础数据结构是向量。向量有2类:原子向量和列表。它们有3个共同的属性:
- 类型,
typeof(),表明它是什么。
- 长度,
length(),它有多少元素。
- 属性,
attributes()
,附加的任意元数据。
原子向量和列表的元素不同:原子向量的所有元素必须是同类型的,而列表的元素可以是不同类型的。
注意:is.vector()
不能验证一个对象是否是向量。相反,只有当对象除了names外没有其他属性时,该函数返回TRUE
。测试一个对象是否是向量,使用is.atomic(x) || is.list(x)。
原子向量
我将详细讨论4种常见类型的原子向量:逻辑向量,整形向量,双精度(常称为数值向量),字符向量。还有2种不常见类型的向量:复数向量和raw向量,这里不讨论这2种类型的向量。
原子向量一般用c()创建,c是combine的缩写:
dbl_var <- c(1, 2.5, 4.5) # With the L suffix, you get an integer rather than a double int_var <- c(1L, 6L, 10L) # Use TRUE and FALSE (or T and F) to create logical vectors 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 # the same as c(1, 2, 3, 4) #> [1] 1 2 3 4
缺失值用NA
表示,NA是一个长度为1的逻辑向量。当
NA出现在
c()中时,它总能被强制转换为正确的类型,或者你可以创建特定类型的
NA
s,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()是对数值型向量一般的测试,对于整形向量和双精度向量,它都返回
TRUE。它不是对双精度向量的具体测试,双精度向量常被称为数值向量。
is.numeric(int_var) #> [1] TRUE is.numeric(dbl_var) #> [1] TRUE
强制转换
原子向量的所有元素都必须是同类型的,所以当你试图将不同类型组合到一起时,它们会被强制转换为最灵活的类型。最不灵活到最灵活类型的顺序是:逻辑、整形、双精度、字符。
比如,组合字符和整形到一个原子向量,会得到一个字符向量:
str(c("a", 1)) #> chr [1:2] "a" "1"
当逻辑向量强制转换为整形或者双精度向量时,TRUE变为1,
FALSE变为0。这在联合使用
sum()
和mean()时非常有用:
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.3333333
强制转换经常自动进行。绝大多数数学函数(+,
log,
abs,等
)会强制转换为双精度和整形向量,大多数逻辑操作(&,|,any,等)会强制转换为逻辑向量。如果强制转换时发生信息丢失,一般你会得到一个警告信息。如果很可能造成困惑,可以显示使用as.character(),
as.double(),
as.integer(),
或者as.logical()进行强制转换。
列表
列表不同于原子向量,因为列表的元素可以是任意类型,包括列表。使用list()创建列表:
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)) str(x) #> List of 2 #> $ :List of 2 #> ..$ : num 1 #> ..$ : num 2 #> $ : num [1:2] 3 4 str(y) #> List of 4 #> $ : num 1 #> $ : num 2 #> $ : num 3 #> $ : num 4
typeof()作用于列表时得到
list。你可以用
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
练习
- 原子向量的6种类型是?列表与原子向量有何不同?
- 为何
is.vector()
和is.numeric()从根本上不同于
is.list()和
is.character()
? - 测试你关于向量强制转换规则的知识,请尝试预测下列使用
c()
代码的输出结果:c(1, FALSE) c("a", 1) c(list(1), "a") c(TRUE, 1L)
4. 为何需要使用unlist()将列表转换为原子向量?为什么
as.vector()不好使?
5. 为什么1 == "1"为真?为什么
-1 < FALSE为真?为什么
"one" < 2为假?
6. 为什么缺失值,NA,是一个逻辑向量?逻辑向量有什么特别的?(提示:思考c(FALSE, NA_character_)
)。
属性
所有对象都可以有任意附加属性,可以用来存储对象的元数据。属性可以认为是一个命名的列表(有唯一的名称)。属性可以单独使用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
唯一不会丢失的3个属性也是最重要的属性:
- 名称,一个字符向量,给定了每个元素名称,在names中介绍。
- 维度,用来将向量转换为矩阵和数组,在matrices and arrays中介绍。
- 类,用来实现S3对象体系,在S3中介绍。
这3个属性,都有具体的访问函数,可以获取和设置对应的属性值。对于这些属性,使用names(x)
,dim(x),和
class(x),不要使用
attr(x, "names"),
attr(x, "dim"),和
attr(x, "class")。
名称
你可以使用3种方式命名向量:
- 创建向量时:
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"))。
名称不必是唯一的。但是,取字符子集,在subsetting介绍,是使用名称的最重要原因,而且当名称唯一时最为有用。
不是向量的所有元素都需要有名称。如果一些名称缺失, 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。
因子
属性的一个重要应用是定义因子。因子是只含有预定义值的向量,被用来存储分类数据。利用2个属性,因子基于整形向量实现,这两个属性是:class()
,为“factor”,该属性使得因子与常规整形向量有不同行为,另一个属性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 # NB: you can't combine factors 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.strings
参数是不错的方法。
# Reading in "text" instead of from a file here: z <- read.csv(text = "value\n12\n1\n.\n9") typeof(z$value) #> [1] "integer" as.double(z$value) #> [1] 3 2 1 4 # Oops, that's not right: 3 2 1 4 are the levels of a factor, # not the values we read in! class(z$value) #> [1] "factor" # We can fix it now: as.double(as.character(z$value)) #> Warning: NAs introduced by coercion #> [1] 12 1 NA 9 # Or change how we read it in: 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中绝大多数加载函数会将字符向量自动转换为因子。这不是最优的,因为这些函数不可能知道所有的因子水平或者最优次序。因此,设置stringsAsFactors = FALSE
参数来禁止这种情况,然后根据你对数据的了解,手动的将字符向量转换为因子。有个全局选项,options(stringsAsFactors = FALSE)
,可以用来控制自动转换为因子的情况,但是我不建议这么做。修改全局选项,可能导致其他代码出现异常(当使用其他包或者使用source()
导入代码时),而且会使得代码难以理解,因为修改全局选项会增加代码的数量,而这本来可以由一行代码实现。
因子看起来(而且经常表现的)像字符向量,但是因子其实是整形向量。将因子作为字符串对待需要非常小心。一些字符串方法(比如gsub()
和grepl()
)会将因子强制转换为字符串,而其他方法(比如nchar()
)会抛出错误,其他一些(比如c()
)会使用因子背后的整形值。因此,如果你想让因子表现的像字符串一样,通常最好的方法是显示的将因子转化为字符向量。在R的早期版本,因子比字符向量有内存上的优势,但现在情况不同了。
练习
- 之前一个说明
structure()的代码草稿:
structure(1:5, comment = "my attribute") #> [1] 1 2 3 4 5
但是当你打印结果时,并没有看到comment这个属性。为什么?难道这个属性丢失了,或者comment属性有其他什么特殊的?(思路:使用help)
2. 当修改因子的水平时会发生什么?
f1 <- factor(letters)
levels(f1) <- rev(levels(f1))
3.下列代码是干什么的?f2和f3为何与f1不同?
f2 <- rev(factor(letters))
f3 <- factor(letters, levels = rev(letters))
矩阵和数组
在原子向量上添加dim()
属性,可以使得它表现的像多维数组。一个特殊的数组是矩阵,矩阵是2维的。矩阵常被作为数学统计工具的一部分。数组要少见一些,但是也值得关注。
矩阵和数组可以通过matrix()
和array()
创建,或者赋予原子向量dim()属性:
# Two scalar arguments to specify rows and columns a <- matrix(1:6, ncol = 3, nrow = 2) # One vector argument to describe all dimensions b <- array(1:12, c(2, 3, 2)) # You can also modify an object in place by setting dim() c <- 1:6 dim(c) <- c(3, 2) c #> [,1] [,2] #> [1,] 1 4 #> [2,] 2 5 #> [3,] 3 6 dim(c) <- c(2, 3) c #> [,1] [,2] [,3] #> [1,] 1 3 5 #> [2,] 2 4 6
length()
和names()有高维的概括结果:
length()概括了矩阵的
nrow()
和ncol(),对应
数组的是dim()
。names()
概括了矩阵的rownames()
和colnames(),
对应数组的是dimnames()
,
是一个字符向量的列表。dimnames()
length(a) #> [1] 6 nrow(a) #> [1] 2 ncol(a) #> [1] 3 rownames(a) <- c("A", "B") colnames(a) <- c("a", "b", "c") a #> a b c #> A 1 3 5 #> B 2 4 6 length(b) #> [1] 12 dim(b) #> [1] 2 3 2 dimnames(b) <- list(c("one", "two"), c("a", "b", "c"), c("A", "B")) b #> , , A #> #> a b c #> one 1 3 5 #> two 2 4 6 #> #> , , B #> #> a b c #> one 7 9 11 #> two 8 10 12
c()
概括了矩阵的cbind()
和rbind()
,对应数组的是abind()
(由abind
包提供)。对矩阵转置可使用t()
;对数组更加一般化的转置可使用aperm()
。
可以使用is.matrix()
测试一个对象是否是矩阵,使用is.array()
测试一个对象是否是数组,或者查看dim()
的长度。使用as.matrix()和
as.array()可以很容易的将已有的向量转换为矩阵和数组。
向量不是唯一的1维数据结构。矩阵可以只有一行或者一列,数组也可以只有1维。它们打印结果可能相似,但是表现不同。不同之处并不是太重要,但是你需要知道存在不同,万一你调用某个函数(tapply()
是常见的一个)时得到了奇怪的输出。通常可以使用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
虽然最常见的情况是将原子向量转换为矩阵,维数属性同样可以作用于列表,使得其表现的像列表矩阵或者列表数组:
l <- list(1:3, "a", TRUE, 1.0) dim(l) <- c(2, 2) l #> [,1] [,2] #> [1,] Integer,3 TRUE #> [2,] "a" 1
这些是相对难懂的数据结构,但是将对象布局到类似网格的结构时非常有用。比如,当你运行一个时空网格模型,通过将模型存储到3维数组来保存网格结构是很自然的事情。
练习
- 将
dim()
作用于向量时返回什么? - 如果
is.matrix(x)
是TRUE
,那is.array(x)
返回什么? - 你如何描述下列代码中的3个对象?这3个对象与
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中最常见的数据存储方式,而且如果使用得当(used systematically)会使数据分析变得非常容易。在底层,数据框是一个列表,列表中的元素是长度相等的向量。也就是说数据框是2维数据结构,所以它兼有矩阵和列表的属性。也就是说数据框有names()
,colnames(),和
rownames()属性,尽管
names()
和colnames()
对数据框是同一个事情。对数据框来说,length()
得到数据框底层列表的长度,因此
与length()
ncol()
结果相同;nrow()
得到行的数量。
正如在subsetting介绍的,对于数据框,你可以像1维数据结构(数据框表现的像列表),或者2维数据结构(数据框表现的像矩阵)一样取子集。
创建
可以使用data.frame()
创建数据框,它以命名的向量作为输入:
df <- data.frame(x = 1:3, y = c("a", "b", "c")) str(df) #> 'data.frame': 3 obs. of 2 variables: #> $ x: int 1 2 3 #> $ y: Factor w/ 3 levels "a","b","c": 1 2 3
注意data.frame()
默认将字符串转换为因子。使用stringAsFactors = FALSE
禁止这种情况:
df <- data.frame( x = 1:3, y = c("a", "b", "c"), stringsAsFactors = FALSE) str(df) #> 'data.frame': 3 obs. of 2 variables: #> $ x: int 1 2 3 #> $ y: chr "a" "b" "c"
测试和强制转换
因为data.frame
是S3类,它的类型是底层用来创建它的向量:列表。测试一个对象是否是数据框,使用class(),
或者显示使用is.data.frame()
:
typeof(df) #> [1] "list" class(df) #> [1] "data.frame" is.data.frame(df) #> [1] TRUE
可以使用as.data.frame()
将对象强制转换为数据框:
- 作用于向量会创建一个一列的数据框。
- 作用于列表会为列表的每个元素创建一个列;如果列表元素的长度不同会抛出错误。
- 作用于矩阵,得到一个具有相同列数和行数的数据框。
合并数据框
使用cbind()
和rbind()
合并数据框:
cbind(df, data.frame(z = 3:1)) #> x y z #> 1 1 a 3 #> 2 2 b 2 #> 3 3 c 1 rbind(df, data.frame(x = 10, y = "z")) #> x y #> 1 1 a #> 2 2 b #> 3 3 c #> 4 10 z
当以列方式合并时,数据框的行数必须相同,但是行名称可以不同。当以行方式合并时,数据框的列数和列名称都必须匹配。使用plyr::rbind.fill()
组合列不同的情况。
一个常见的错误是使用cbind()
将向量组合为数据框。这不好使因为cbind()
会创建一个矩阵,除非cbind()
的一个实参已经是数据框了。直接使用data.frame()来将向量合并为数据框
:
bad <- data.frame(cbind(a = 1:2, b = c("a", "b"))) str(bad) #> 'data.frame': 2 obs. of 2 variables: #> $ a: Factor w/ 2 levels "1","2": 1 2 #> $ b: Factor w/ 2 levels "a","b": 1 2 good <- data.frame(a = 1:2, b = c("a", "b"), stringsAsFactors = FALSE) str(good) #> 'data.frame': 2 obs. of 2 variables: #> $ a: int 1 2 #> $ b: chr "a" "b"
cbind()
的转换规则很复杂,最好的方式是确保所有的输入有相同的类型。
特殊列
因为数据框是包含向量的列表,因此数据框的列可能是列表:
df <- data.frame(x = 1:3) df$y <- list(1:2, 1:3, 1:4) df #> x y #> 1 1 1, 2 #> 2 2 1, 2, 3 #> 3 3 1, 2, 3, 4
然而,当列表作为data.frame()
的输入时,列表会试图将其中的每个元素转换为一列,因此会失败:
data.frame(x = 1:3, y = list(1:2, 1:3, 1:4)) #> Error in data.frame(1:2, 1:3, 1:4, check.names = FALSE, stringsAsFactors = TRUE): arguments imply differing number of rows: 2, 3, 4
一种解决方法是使用I()
,I()
使得data.frame()
将列表看作一个整体:
dfl <- data.frame(x = 1:3, y = I(list(1:2, 1:3, 1:4))) str(dfl) #> 'data.frame': 3 obs. of 2 variables: #> $ x: int 1 2 3 #> $ y:List of 3 #> ..$ : int 1 2 #> ..$ : int 1 2 3 #> ..$ : int 1 2 3 4 #> ..- attr(*, "class")= chr "AsIs" dfl[2, "y"] #> [[1]] #> [1] 1 2 3
I()
添加AsIs
类作为输入,但通常可以无视。
相似的,数据框的一列也可以是矩阵或者数组,只要行数匹配即可:
dfm <- data.frame(x = 1:3, y = I(matrix(1:9, nrow = 3))) str(dfm) #> 'data.frame': 3 obs. of 2 variables: #> $ x: int 1 2 3 #> $ y: 'AsIs' int [1:3, 1:3] 1 2 3 4 5 6 7 8 9 dfm[2, "y"] #> [,1] [,2] [,3] #> [1,] 2 5 8
使用列表和数组作为列需要小心:许多函数作用于数据框时,假设数据框的列是原子向量。
练习
- 数据框有哪些属性?
as.matrix()
作用于数据框,而且该数据框的列是不同类型的,会发生什么?- 一个数据框可以有0行吗?0列哪?
答案
- 向量的3个属性是类型,长度和属性。
- 常见的4种原子向量类型是逻辑向量,整形向量,双精度向量(有时称为数值向量),字符向量。2种不常见的类型是复数向量和raw向量。
- 属性允许在任何对象上添加任意附加元数据。可以使用
attr(x, "y")
和attr(x, "y") <- value
获取和设置一个属性;或者使用attributes()
一次性获取和设置所有属性。 - 列表的元素可以是任意类型的(甚至列表);原子向量的所有元素必须是同类型的。类似的,矩阵的所有元素必须是同类型的;数据框中的不同列可以是不同类型的。
- 通过设置列表的维数得到“列表数组”。可以使用
df$x <- matrix()
将矩阵作为数据框的一列,或者在创建数据框时使用I(),就像
。data.frame(x = I(matrix()))