第2章 基本对象

第2章 基本对象

学习R语言编程的第一步是熟悉基础的R对象和它们的性质。在本章中,你将学到以下内容。

  • 原子向量(atomic vector)的创建和构建子集(subsetting vector),例如数值向量(numeric vector)、字符向量(character vector)和逻辑向量(logical vector),以及矩阵(matrice)、数组(array)、列表(list)和数据框(data frame)。
  • 定义和使用函数。

“万物皆对象,万事乃函数。”——John Chambers

例如,在统计分析中,我们经常将一组数据输入到一个线性回归模型中来获得回归系数。

尽管在R中有不同类型的R对象,但在实际工作中,我们仅需提供一个包含数据集的数据框,将其代入一个线性回归模型中,获得一个包含回归结果的信息列表,最后从这一列表中提取出另一种类型的对象——数值向量,来展示回归系数。

每项工作都涉及各种各样的对象,每个对象都有不同的用途和性质。其中非常重要的一点是,要理解一个基础对象在解决现实问题的过程中是如何运作的,尤其是如何用更优雅更简练的代码,以更少的步骤来实现它。更为重要的是,越是深入地了解对象的行为,越能让你有更多的时间用在解决实际问题上,而不是花费大量时间调试编程中的小问题。

在接下来的小节中,我们将看到多种多样的R对象,它们展示了不同类型的数据结构,并且能够使数据集的分析和可视化变得简单易行。你将对这些对象如何工作,以及它们之间如何互动有一个基本的了解。

2.1 向量

一个向量是由一组相同类型的原始值构成的序列。它可以是一组数字、一组逻辑值、一组文本或者是其他类型的值。它是所有R对象的基础数据结构之一。

在R中,有多种类型的向量,区别在于它们存储的元素类型互不相同。在接下来的小节中,我们将会看到最常使用的向量类型,包括数值向量、逻辑向量和字符向量。

2.1.1 数值向量

一个数值向量就是由数值组成的向量。单个数值(标量数值)就是最简单的数值向量。举一个例子:

1.5
## [1] 1.5

数值向量是最常用的数据类型,也几乎是所有数据分析的基础。在其他流行的编程语言中,存在一些标量类型,例如整型、双精度型和字符串型。这些标量是构成如向量等其他容器类型的基础。然而,在R中并没有对于标量类型的正式定义。标量数值只不过是数值向量的特例,并且仅仅特殊之处在其长度为1。

当我们创建一个值的时候,会很自然地想到如何存储它以备后续使用。为了存储这个值,我们可以使用<-将这个值赋给一个“符号”。换句话说,我们为这个值(1.5)创建了一个名为x的变量:

x <- 1.5

这个值被分配给了符号x,之后我们可以用x来表示它:

x
## [1] 1.5

创建一个数值向量有许多种方法。我们可以调用numeric( )来创建一个由0组成的指定长度的向量:

numeric (10)
## [1] 0 0 0 0 0 0 0 0 0 0

我们也可以使用c( )把多个向量组合成一个向量。最简单的情况是,组合多个单元素向量构成一个多元素向量:

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

我们还可以将多个单元素向量和多元素向量连接起来构成一个向量,使其和之前创建的向量具有相同的元素:

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

如果想要创建一系列连续的整数值,运算符“:”能够轻松地实现这一点。

1:5
## [1] 1 2 3 4 5

准确地说,以上代码产生的是整数值向量而不是数值向量。在很多情况下,它们之间的区别并不重要。我们之后将会讨论这个问题。

产生一个数值序列,更通用的方法是使用seq( )。例如,以下代码产生了一个数值向量,它由从1开始,到10结束,步长为2的序列组成:

seq(1, 10, 2)
## [1] 1 3 5 7 9

seq( )这样的函数有很多参数。我们可以通过提供其所有的参数来调用这个函数,但在大多数时候并不必要。大部分函数对其部分参数提供了合理的默认值,这使得我们能够更加轻松地调用它们。在现在这种情况下,我们只需指定想要更改的参数即可。

例如,可以通过指定length.out这个参数来创建另外一个从3开始,长度为10的数值向量:

seq(3,  length.out = 10)
## [1]  3 4 5 6 7 8 9 10 11 12

如上所示的函数调用过程,只修改了参数length.out的取值,其他参数的值保持默认值。

尽管我们可以使用多种方法定义数值向量,但是必须始终小心地使用“:”运算符,举一个例子:

1 + 1:5
## [1] 2 3 4 5 6

从结果来看,1 + 1:5并不意味着一个从2~5的序列,而是一个从2~6的序列。这是因为“:”比“+”具有更高的优先级,这使得首先产生的是1:5,接着才对每一个值加1,这才产生了你在结果中所看到的序列。我们之后会讨论关于运算符优先级的问题。

2.1.2 逻辑向量

与数值向量不同,一个逻辑向量储存一组TRUEFALSE值。它们基本上以“是”或“否”来表示对一组逻辑问题的回答。

最简单的逻辑向量是TRUE或者FALSE本身:

TRUE
## [1] TRUE

获得一个逻辑向量更一般的方法是询问关于R对象的逻辑性问题。例如,我们可以询问R,1是否大于2:

1 > 2
## [1] FALSE

如果回答“是”,就返回TRUE;如果回答是“否”,就返回FALSE。有时,写TRUE或者FALSE太繁琐了,我们可以使用TRUE的缩写TFALSE的缩写F。如果我们想同时进行多个比较,可以直接在问题中使用数值向量:

c(1, 2) > 2
## [1] FALSE FALSE

R 会将这一表达式理解为在c(1, 2)和2之间依次进行元素比较。换句话说,这实际上等价于c(1 > 2, 2 > 2)

我们可以比较两个多元素数值向量,只要较长向量的长度是较短向量长度的整数倍:

c(1, 2) > c(2, 1)
## [1] FALSE TRUE

上述代码等价于c(1 > 2, 2 > 1)。为了验证两个不同长度的向量是如何比较的,请看下面的例子:

c(2, 3) > c(1, 2, -1, 3)
## [1] TRUE TRUE TRUE FALSE

这个结果可能会使你有些困扰。以上代码的运算机制是不断地循环较短的向量并进行比较,等价于c(2 > 1, 3 > 2, 2 > −1, 3 > 3)。更明确地说,较短的向量将会不断地循环直到和较长向量中的元素全部完成比较。

在R中,定义了一些二元逻辑运算符,例如“==”表示相等,“>”表示大于,“>=”表示大于或等于,“<”表示小于,“<=”表示小于或等于。此外,R还提供了一些其他运算符,例如用%in%判断运算符左侧向量的每一个元素是否都包含在运算符右侧的向量中:

1 %in% c(1, 2, 3)
## [1] TRUE
c(1, 4) %in% c(1, 2, 3)
## [1] TRUE FALSE

你可能注意到了所有等式运算符都执行了循环,但是%in%并没有。相反,它总是通过迭代左侧向量的单个元素,在上述例子中,就像c(1 %in% c(1, 2, 3), 4 %in% c(1, 2, 3))这样完成运算。

2.1.3 字符向量

一个字符向量是由字符串组成的向量。这里的一个字符不是指着文学意义上的单独的字母或者符号,而是一个类似this is a string这样的字符串。双引号和单引号都可以用来生成字符向量,例如:

"hello, world!"
## [1] "hello, world!"
'hello, world!'
## [1] "hello, world!"

我们也可以使用组合函数c( )来创建一个多元素的字符向量:

c("Hello", "World")
## [1] "Hello" "World"

使用==来判断两个向量中处于对等位置的值是否相等,这同样适用于字符向量:

c("Hello", "World") == c('Hello', 'World')
## [1] TRUE TRUE

因为单引号 ' 和双引号 " 都可以用来生成字符串并且不影响其含义,所以上述两个字符向量相等:

c("Hello", "World") == "Hello, World"
## [1] FALSE FALSE

上述表达式产生了两个FALSE,是因为HelloWorld都不等于Hello, World。两种引号之间的唯一区别是,当生成一个包含引号的字符串时,它们的行为是不同的。

如果你想要在双引号内部嵌套双引号时,需要用反斜杠(\)来转义内部的双引号,类似使用“生成一个包含了其本身的字符串(一个单元素的字符向量),你需要在字符串内部输入\ 来转义”,用以防止编译时将字符串内部的”当作字符串的末引号。

接下来的例子展示了引号的转义。我们使用函数cat( )来生成指定文本:

cat("Is \"You\" a Chinese name?")
## Is "You" a Chinese name?

如果你感觉这不易于阅读,也可以使用 ’ 来生成该字符串,这样可以变得更简单:

cat('Is "You" a Chinese name?')
## Is "You" a Chinese name?

也就是说,双引号内部可以嵌套单引号,同样,单引号内部也可以嵌套双引号,即:"允许'存在于没有转义过的字符串中,'也允许"存在于没有转义过的字符串中。

现在我们掌握了关于生成数值向量、逻辑向量和字符向量的基本知识。实际上,在R中也有复数向量(complex vector)和原向量(raw vector)。复数向量是由复数组成的向量,例如c(1 + 2i, 2 + 3i)。原向量主要存储用十六进制格式表示的原始二进制数据。这两种类型的向量很少使用,但是它们与之前介绍过的3种类型的向量(整数型、逻辑型、字符型)具有很多相同的性质。

在下一节中,我们将学习到几种访问向量子集的方法。通过对向量取子集,你将理解不同类型的向量间是如何相互联系的。

2.1.4 构建向量子集

如果想访问一些特定元素或者向量的一个子集,使用向量子集是一个不错的方法。在这一节中,我们将展示几种不同的构建向量子集的方法。

首先,生成一个简单的数值向量并且赋值给v1

v1 c(1, 2, 3, 4)

接下来的每一行都是用来获取v1的特定子集。

例如,提取第2个元素:

v1[2]
## [1] 2

也可以提取第2~4个元素:

v1[2:4]
## [1] 2 3 4

还可以获取除第3个以外的其他所有元素:

v1[-3] 
## [1] 1 2 4

这个模式很清晰,我们可以在向量后面的方括号中放入任何一个数值向量获取相应的子集:

a <- c(1, 3)
v1[a]
## [1] 1 3

上述所有例子都是通过位置信息来构造子集,也就是说,通过指定元素的位置来得到一个向量的子集。方括号中使用负数将排除相应位置的元素。需要注意的一点是,在方括号中不能同时使用正数和负数:

v1[c(1, 2, -3)]
## Error in v1[c(1, 2, -3)]: 只有负下标里才能有零

如果使用向量范围之外的位置来获取子向量会发生什么呢?接下来的例子尝试获取向量v1的子集,范围是v1的第3~6个(不存在)元素:

v1[3:6]
## [1]  3  4 NA NA

正如我们所看到的一样,在不存在元素的位置用NA替代了缺失值。在现实世界的数据中,缺失值是普遍存在的。一方面,对所有包含NA的数据进行数值运算的结果都是NA,而不是其他不确定的结果;另一方面,因为直接假设数据中不存在缺失值并不恰当,所以我们需要付出额外的工作来处理数据。

另一种构造子集的方法是使用逻辑向量。我们可以输入与要获取向量子集的向量具有相等长度的逻辑向量,以此决定每一个元素是否要被获取:

v1[c(TRUE, FALSE, TRUE, FALSE)]
## [1] 1 3 

除此之外,我们也可以给向量中特定的子集重新赋值(覆盖原值):

v1[2] <- 0

在这种情况下,v1变成:

v1
## [1] 1 0 3 4

也可以同时覆盖处于不同位置的多个元素:

v1[2:4] <- c(0,1,3)

现在,v1变成:

v1
## [1] 1 0 1 3

同样,重新赋值时,逻辑选择也是适用的:

v1[c(TRUE, FALSE, TRUE, FALSE)] <- c(3, 2)

正如你所期待的,此时v1变成:

v1
## [1] 3 0 2 3

一个常用的技巧是可以通过逻辑标准来选择元素。例如,以下的代码挑选出v1中所有不大于2的元素:

v1[v1 <= 2]
## [1] 0 2

这种方法也适用于更复杂的选择标准。下面的例子挑选出v1中所有满足x2x+1\geqslant0的元素:

v1[v1 ^ 2 - v1 + 1 >= 0]
## [1] 3 0 2 3

以下代码用0替代所有满足x <= 2的元素:

v1[v1 <= 2] <- 0

像你期待的一样,此时v1变成:

v1
## [1] 3 0 0 3

如果我们对一个并不存在的元素重新赋值,向量将自动用NA填充未被指定的位置,当作缺失值处理:

v1[10] <- 8
v1
##  [1]  3  0  0  3 NA NA NA NA NA  8

2.1.5 命名向量

命名向量是一种不同于数值向量或逻辑向量的特定向量类型。它指的是该向量中的每一个元素都有相应的名称。我们可以在创建向量的同时对其命名:

x <- c(a = 1, b = 2, c = 3)
x
## a b c 
## 1 2 3

这样就可以通过单值字符向量来访问其中的元素:

x["a"]
## a 
## 1

也可以通过一个字符向量来获取多个元素:

x[c("a", "c")]
## a c 
## 1 3

如果字符向量中含有重复元素,则会选出相应的重复元素。

x[c("a", "a", "c")]
## a a c 
## 1 1 3

除此之外,其余所有的能够用于向量中的操作也适用于命名向量。我们可以通过names( )获取向量中的元素名称:

names(x)
## [1] "a" "b" "c"

向量中的名称不是固定的,可以通过对向量赋予不同的字符向量来更改元素名称:

names(x) <- c("x", "y", "z")
x["z"]
## z 
## 3

当不需要元素名称时,也可以用NULL来移除原有的名称。NULL表示一个未定义值的特殊对象:

names(x) <- NULL
x
## [1] 1 2 3

你可能会比较好奇,如果访问一个不存在的名称时会发生什么呢?我们对原始的x进行测试:

x <- c(a = 1, b = 2, c = 3)
x["d"]
## <NA> 
## NA

直觉上,访问一个不存在的元素应该会报错,但结果却返回一个无名的缺失值:

names(x["d"])
## [1] NA

如果你提供的字符向量中,只有部分名称存在,其余不存在时,返回的向量长度与选择向量(该字符向量)保持一致:

x[c("a", "d")]
## a <NA> 
## 1 NA

2.1.6 提取向量元素

[ ]能够创建一个向量子集,[[ ]]可以提取向量中的元素。我们可以将一个向量比作10盒糖果,你可以使用[ ]获取其中的3盒糖果,使用[[ ]]打开盒子并从中取出一颗糖果。

对于简单的向量,使用[ ][[ ]]取出一个元素会产生相同的结果。但在某些情况下,它们会返回不同的结果。例如,对于命名向量,创建一个子集与提取一个元素将会产生不同的结果:

x <- c(a = 1, b = 2, c = 3)
x["a"]
## a 
## 1
x[["a"]]
## [1] 1

糖果盒的比喻比较易于理解。x["a"]使你得到标签为"a"的糖果盒,而 x[["a"]]则使你得到标签为"a"的糖果盒里面的糖果。

由于[[ ]]只能用于提取出一个元素,因此不适用于提取多个元素的情况。

x[[c(1, 2)]]
## Error in x[[c(1, 2)]]: attempt to select more than one element

此外,[[ ]]也不能用于负整数,因为负整数意味着提取除特定位置之外的所有元素。

x[[-1]]
## Error in x[[-1]]: attempt to select more than one element

至此,我们知道了使用含有不存在的位置或名称来创建向量子集时将会产生缺失值。但当我们使用[[ ]]提取一个位置超出范围或者对应名称不存在的元素时,该命令将会无法运行并产生错误信息:

x[["d"]]
## Error in x[["d"]]: 下标出界

对很多初学者来说,代码中同时使用[[ ]][ ]可能会感到混乱,并且容易造成误用。此时,你只需记得糖果盒的比喻即可。

2.1.7 识别向量类型

有时我们需要在处理向量之前辨别向量的类型。class( )函数用于判断任意R对象的类型:

class(c(1, 2, 3))
## [1] "numeric"
class(c(TRUE, TRUE, FALSE))
## [1] "logical"
class(c("Hello", "World"))
## [1] "character"

如果我们需要确认某一对象是否为某个特定类型的向量,可以用is.numeric( )is.logical( )is.character( )以及其他类似函数进行判断:

is.numeric(c(1, 2, 3))
## [1] TRUE
is.numeric(c(TRUE, TRUE, FALSE))
## [1] FALSE
is.numeric(c("Hello", "World"))
## [1] FALSE

2.1.8 转换向量类型

不同类型的向量可以被强制转换为一种特定类型的向量。例如,有些数据是数值字符串,如1和20。如果不进行转换处理,就无法对其进行数值运算。幸运的是,这两个字符串可以转换为数值向量。转换后,R就能够将它们识别为数值数据而不是字符串,这样我们就能对其进行数值运算了。

在演示典型的向量类型转换之前,先创建一个字符向量:

strings <- c("1", "2", "3")
class(strings)
## [1] "character"

正如前面所提到的,字符串不能够直接进行数值运算:

strings + 10  
## Error in strings + 10: 二进列运算符中有非数值参数

我们可以用as.numeric( )将字符向量转换为数值向量:

numbers <- as.numeric(strings)
numbers
## [1] 1 2 3
class(numbers)
## [1] "numeric"

现在就能够对数字进行数值运算了:

numbers + 10
## [1] 11 12 13

is.*函数(例如is.numeric( )is.logical( )以及is.character( ))用来检验给定对象的类型,as.*函数用来转换向量的类型:

as.numeric(c("1", "2", "3", "a"))
## Warning: 强制改变过程中产生了NA
## [1]  1  2  3 NA
as.logical(c(-1, 0, 1, 2))
## [1]  TRUE FALSE  TRUE  TRUE
as.character(c(1, 2, 3))
## [1] "1" "2" "3"
as.character(c(TRUE, FALSE))
## [1] "TRUE"  "FALSE"

这似乎意味着所有类型的向量都能转换为其他任意类型。然而,事实上,向量类型转换需要遵循一系列规则。

上述代码块的第一行试图将一个字符向量转换为数值向量,这和我们之前的示例一样。显然,最后一个元素不能被转换为数字,因此相应位置产生了缺失值。除了最后一个元素,其他都完成了转换。

将数值向量转换为逻辑向量的规则是:只有0转换为FALSE,其他所有非零数字均转换为TRUE

所有数据都可以表达成字符形式,因此每种类型的向量均可转换成字符向量。然而,如果数值向量或逻辑向量被强制转换成字符向量,那么除非再被转换回来,否则转换后的向量不能直接与其他数值向量或逻辑向量进行算术运算。这就是上文所述,以下代码无法运行的原因:

c(2, 3) + as.character(c(1, 2)) 
## Error in c(2, 3) + as.character(c(1, 2)): 二进列运算符中有非数值参数

从上面的例子可知,尽管R没有对数据类型强加严格的规则,但是这并不意味着R足够聪明到可以自动并且精确地执行你想做的事情。在大多数情况下,最好事先确认参与运算的向量类型是正确设定的;否则,可能会发生意想不到的错误。换言之,只有使用正确类型的数据对象,才能进行正确的数学运算。

2.1.9 数值向量的算术运算符

数值向量的算术运算很简单,主要遵循两个原则:对相应位置的元素进行计算,并自动循环利用较短的向量(循环补齐功能)。下面的例子展示了运算符对数值向量的作用方式:

c(1, 2, 3, 4) + 2
## [1] 3 4 5 6
c(1, 2, 3) -c(2, 3, 4)
## [1] -1 -1 -1
c(1, 2, 3) * c(2, 3, 4)
## [1]  2  6 12
c(1, 2, 3) / c(2, 3, 4)
## [1] 0.5000000 0.6666667 0.7500000
c(1, 2, 3) ^ 2
## [1] 1 4 9
c(1, 2, 3) ^ c(2, 3, 4)
## [1]  1  8 81
c(1, 2, 3, 14) %% 2
## [1] 1 0 1 0

虽然向量元素可以有名称,但并不会对其进行运算。只有左侧向量的元素名称会被保留下来,右侧向量的名称会被忽略:

c(a = 1, b = 2, c = 3) + c(b = 2, c = 3, d = 4)
## a b c 
## 3 5 7
c(a = 1, b = 2, 3) + c(b = 2, c = 3, d = 4)
## a b   
## 3 5 7

通过以上内容,我们已经了解了数值向量、逻辑向量和字符向量的一些基本性质。向量是最常用的数据结构,同时也是构建其他各种有用对象的基本成分。例如矩阵,它主要应用于统计学和计量经济学理论的公式化简洁表述,并且在表示二维数据和求解线性系统方面有着良好的应用。下一节将会介绍如何在R中创建矩阵,以及它是如何植根于向量的。

2.2 矩阵

矩阵是一个用两个维度表示和访问的向量。因此,适用于向量的性质和方法大多也适用于矩阵。例如,每一种向量(例如数值向量或逻辑向量)都有对应的矩阵形式,即数值矩阵(numeric matrice)、逻辑矩阵(logical matrice)等。

2.2.1 创建一个矩阵

我们可以调用matrix( )函数将一个向量变成矩阵,方法是设定矩阵的其中一个维度。

matrix(c(1, 2, 3, 2, 3, 4, 3, 4, 5), ncol = 3)
##      [,1] [,2] [,3]
## [1,]   1    2    3
## [2,]   2    3    4
## [3,]   3    4    5

设定ncol = 3意味着我们提供的向量应该被当作一个列数为3 的矩阵(行数自动也为3)。你可能觉得原来的向量不如它的矩阵形式直观。为了让代码对用户更加友好,我们可以按行书写向量:

matrix(c(1, 2, 3, 4, 5, 6, 7, 8, 9), nrow = 3, byrow = FALSE)
##     [,1] [,2] [,3]
## [1,]  1    4    7
## [2,]  2    5    8
## [3,]  3    6    9
matrix(c(1, 2, 3, 4, 5, 6, 7, 8, 9), nrow = 3, byrow = TRUE)
##      [,1] [,2] [,3]
## [1,]   1    2    3
## [2,]   4    5    6
## [3,]   7    8    9

我们可能经常需要创建一个对角矩阵,使用diag( )函数是最便捷的方式:

diag(1, nrow = 5)
##      [,1] [,2] [,3] [,4] [,5]
## [1,]   1    0    0    0    0
## [2,]   0    1    0    0    0
## [3,]   0    0    1    0    0
## [4,]   0    0    0    1    0
## [5,]   0    0    0    0    1

2.2.2 为行和列命名

在默认情况下,创建矩阵时不会自动分配行名和列名。当不同的行列有不同的含义时,为其命名就显得必要且直观。在创建矩阵时就可以为行和列命名:

matrix(c(1, 2, 3, 4, 5, 6, 7, 8, 9), nrow = 3, byrow = TRUE, dimnames
= list(c("r1", "r2", "r3"), c("c1", "c2", "c3")))
##    c1 c2 c3
## r1  1  2  3
## r2  4  5  6
## r3  7  8  9

也可以在矩阵创建后,再对其行和列命名:

m1 <- matrix(c(1, 2, 3, 4, 5, 6, 7, 8, 9), ncol = 3)
rownames(m1) <- c("r1", "r2", "r3")
colnames(m1) <- c("c1", "c2", "c3")

这里我们遇到了两个新对象:一个列表和一种函数,比如rownames(x)<-。我们将在本章的后续内容中进行讨论。

2.2.3 构建矩阵子集

和处理向量一样,我们不仅需要创建矩阵,也需要从中抽取数据,这称为构建矩阵子集(matrix subsetting)

矩阵是用两个维度表示和访问的向量,可以用一个二维存取器(accessor)[ , ]来访问,这类似于构建向量子集时用的一维存取器[ ]

我们可以为每个维度提供一个向量来确定一个矩阵的子集。方括号中的第1个参数是行选择器(row selector),第2个是列选择器(column selector)。与构建向量子集一样,可以在两个维度中使用数值向量、逻辑向量和字符向量。

以下代码展示了构建矩阵子集的多种方式:

m1
##    c1 c2 c3
## r1  1  4  7
## r2  2  5  8
## r3  3  6  9

提取位于第1行第2列的单个元素:

m1[1, 2]
## [1] 4

也可以通过设定位置范围来构建子集:

m1[1:2, 2:3]
##    c2 c3
## r1  4  7
## r2  5  8

若一个维度的参数空缺,则该维度的所有值都会被选出来:

m1[1,]
## c1 c2 c3 
##  1  4  7
m1[,2]
## r1 r2 r3 
##  4  5  6
m1[1:2,]
##    c1 c2 c3
## r1  1  4  7
## r2  2  5  8
m1[, 2:3]
##    c2 c3
## r1  4  7
## r2  5  8
## r3  6  9

负数表示在构建矩阵子集时可排除该位置,这和向量中的用法完全一致:

m1[-1,]
##    c1 c2 c3
## r2  2  5  8
## r3  3  6  9
m1[,-2]
##    c1 c3
## r1  1  7
## r2  2  8
## r3  3  9

注意到矩阵有行名和列名,我们可以使用字符向量来构建子集:

m1[c("r1", "r3"), c("c1", "c3")]
##    c1 c3
## r1  1  7
## r3  3  9

需要注意的是,矩阵是一个用两个维度表示和访问的向量,但它本质上仍然是一个向量。因此,向量的一维存取器也可以用来构建矩阵子集:

m1[1]
## [1] 1
m1[9]
## [1] 9
m1[3:7]
## [1] 3 4 5 6 7

因为一个向量只能包含相同类型的元素,矩阵也是如此。所以它们的操作方式非常相似。如果输入一个不等式,它会返回另一个大小相同的逻辑矩阵。

m1 > 3
##       c1   c2   c3
## r1 FALSE TRUE TRUE
## r2 FALSE TRUE TRUE
## r3 FALSE TRUE TRUE

我们可以使用一个大小相同的逻辑矩阵来构建子集,就好像它是一个向量一样:

m1[m1 > 3]
## [1] 4 5 6 7 8 9

2.2.4 矩阵运算符的使用

所有适用于向量的算术运算符也适用于矩阵,就如同它们也是向量一样。这些运算符在元素上进行运算,除了一些矩阵专用的运算符,例如矩阵乘法%*%

m1 + m1
##    c1 c2 c3
## r1  2  8 14
## r2  4 10 16
## r3  6 12 18
m1 - 2 * m1
##    c1 c2 c3
## r1 -1 -4 -7
## r2 -2 -5 -8
## r3 -3 -6 -9
m1 * m1
##    c1 c2 c3
## r1  1 16 49
## r2  4 25 64
## r3  9 36 81
m1 / m1
##    c1 c2 c3
## r1 1  1  1
## r2 1  1  1
## r3 1  1  1
m1 ^ 2
##    c1 c2 c3
## r1  1 16 49
## r2  4 25 64
## r3  9 36 81
m1 %*% m1
##     c1 c2  c3
## r1  30 66 102
## r2  36 81 126
## r3  42 96 150

可以使用t( )函数对矩阵进行转置:

t(m1)
##    r1 r2 r3
## c1  1  2  3
## c2  4  5  6
## c3  7  8  9

很多情况下,向量和矩阵就够用了。然而,一些特定的问题需要使用更高维的数据结构。下一节将简要介绍数组(array),你将会看到这些数据结构有相似的性质。

2.3 数组

数组是矩阵向更高维度的自然推广。具体来说,数组就是一个维度更高(通常情况下大于2)、可访问的向量。如果你对向量和矩阵已经很熟悉,就不会对数组的操作方式感到诧异了。

2.3.1 创建一个数组

我们可以提供一个向量,然后调用array( )函数来创建一个数组,指定数组的不同维度,有时也可以给出不同维度的行名和列名。

假设数据是0~9这10个整数,需要将其分配到3个维度中,其中第1维长度为1,第2维长度为5,第3维长度为2:

a1 <- array(c(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), dim = c(1, 5, 2))
a1
## , , 1
## 
##     [,1] [,2] [,3] [,4] [,5]
## [1,]  0    1    2    3    4
## 
## , , 2
## 
##     [,1] [,2] [,3] [,4] [,5]
## [1,]  5    6    7    8    9

显然,同样可以通过指定位置访问数组中的元素。

此外,还可以在创建数组时对每个维度进行命名:

a1 <- array(c(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), dim = c(1, 5, 2), dimnames
= list(c("r1"), c("c1", "c2", "c3", "c4", "c5"), c("k1", "k2")))
a1
## , , k1
## 
##    c1 c2 c3 c4 c5
## r1  0  1  2  3  4
## 
## , , k2
## 
##    c1 c2 c3 c4 c5
## r1  5  6  7  8  9

若存在已经创建的数组,可以提供一个包含若干个字符向量的列表,用dimnames(x)<-对数组的各个维度命名:

a0 <- array(c(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10), dim = c(1, 5, 2))
dimnames(a0) <- list(c("r1"), c("c1", "c2", "c3", "c4", "c5"), c("k1",
"k2"))
a0
## , , k1
## 
##    c1 c2 c3 c4 c5
## r1  0  1  2  3  4
## 
## , , k2
## 
##    c1 c2 c3 c4 c5
## r1  5  6  7  8  9

2.3.2 构建数组子集

从数组中提取子集的原理与矩阵完全相同。我们通过给每个维度提供一个向量来提取数组子集。

a1[1,,]
##    k1 k2
## c1  0  5
## c2  1  6
## c3  2  7
## c4  3  8
## c5  4  9
a1[, 2,]
## k1 k2 
##  1  6
a1[,,1]
## c1 c2 c3 c4 c5 
##  0  1  2  3  4
a1[1,1,1]
## [1] 0
a1[1,2:4,1:2]
##    k1 k2
## c2  1  6
## c3  2  7
## c4  3  8
a1[c("r1"),c("c1", "c3"),"k1"]
## c1 c3 
## 0  2

细心的读者可能会发现,原子向量、矩阵和数组的性质和操作方式几乎完全相同。最基本的共同特征就是它们都属于同质数据类(homogeneous data types),即所存储的一定是相同类型的元素。但是在R中也存在异质数据类(heterogeneous data types),即可以存储不同类型的元素,这大大提高了存储的灵活性,但同时也降低了存储效率和运行效率。

2.4 列表

列表(list)是一个广义的向量,它可以包含其他类型的对象,甚至可以包括其他列表。

列表的灵活性使得它非常有用。举个例子,用R拟合一个线性模型,其结果本质上就是一个列表,其中包含了线性回归的详细结果,如线性回归系数(数值向量)、残差(数值向量)、QR分解(包含一个矩阵和其他对象的列表)等。

因为这些结果全都被打包到一个列表中,我们可以很方便地提取所需信息,而无需每次调用不同的函数。

2.4.1 创建一个列表

顾名思义,我们可以用list( )创建一个列表。不同类型的对象可以被装进同一个列表中。例如,以下代码创建了一个列表,包含3个成分:一个单元素的数值向量、一个两元素的逻辑向量和一个长度为 3 的字符向量:

l0 <- list(1, c(TRUE, FALSE), c("a", "b", "c"))
l0
## [[1]]
## [1] 1
## 
## [[2]]
## [1]  TRUE FALSE
## 
## [[3]]
## [1] "a" "b" "c"

可以用命名参数(named arguments)为列表中的每个成分指定名称:

l1 <- list(x = 1, y = c(TRUE, FALSE), z = c("a", "b", "c"))
l1
## $x
## [1] 1
## 
## $y
## [1]  TRUE FALSE
## 
## $z
## [1]  "a" "b" "c"

2.4.2 从列表中提取元素

有许多方法可以提取列表中的元素。最常用的方法是使用美元符号$,通过成分的名称来提取列表元素的值。

l1 <- list(x = 1, y = c(TRUE, FALSE), z = c("a", "b", "c"), m = NULL)
l1$x
## [1] 1
l1$y
## [1]  TRUE FALSE
l1$z
## [1] "a" "b" "c"
l1$m
## NULL

注意,如果访问一个不存在的成分m,将会返回NULL

或者,我们可以在双层方括号中输入一个数字n,来提取列表中第n个成分的值。比如,通过以下方法取出列表l1中第2个成分的值:

l1[[2]]
## [1] TRUE FALSE

也可以在双层方括号中输入一个列表成分的名称,以取出对应元素的值,效果类似于美元符号:

l1[["y"]]
## [1] TRUE FALSE

使用双层方括号来提取列表中的值可能会更加灵活,因为在计算之前我们有时可能不知道需要取出哪个元素:

member <- "z" # 你可以随时改变想要取出的成分
l1[[member]]
## [1] "a" "b" "c"

这里在括号中使用了一个可即时赋值的单元素字符向量。但是为什么我们要用双层方括号呢?为什么不再使用单层方括号了呢?

2.4.3 构建列表子集

许多场合下,我们需要从列表中提取多个元素。由这些元素组成的列表构成了原列表的一个子集。

构建一个列表子集,我们可以用单层方括号,就像提取向量和矩阵中元素一样。我们可以取出列表中的一些元素,然后放到一个新的列表中。

这里方括号的用法与其在向量中的用法非常相似。我们可以用字符向量表示成分名称,用数值向量表示成分位置,或用逻辑向量指定选择标准,来取出列表元素

l1["x"]
## $x
## [1] 1
l1[c("x", "y")]
## $x
## [1] 1
## 
## $y
## [1]  TRUE FALSE
l1[1]
## $x
## [1] 1
l1[c(1, 2)]
## $x
## [1] 1
## 
## $y
## [1]  TRUE FALSE
l1[c(TRUE, FALSE, TRUE)]
## $x
## [1] 1
## 
## $z
## [1] "a" "b" "c"

总而言之,我们可以说,[[用来提取向量或列表中的一个元素,而[用来提取一个向量或列表的子集。向量的子集是一个向量。同样的,列表的子集也是一个列表。

2.4.4 命名列表

无论在列表创建之初是否有为各列表成分命名,我们总能通过一个用于命名的向量,为这些列表成分命名或重命名。

names(l1) <- c("A", "B", "C")
l1
## $A
## [1] 1
## 
## $B
## [1]  TRUE FALSE
## 
## $C
## [1]  "a" "b" "c"

若想移除它们的名称,可以将 l1 的名称赋值为NULL:

names(l1) <- NULL
l1
## [[1]]
## [1] 1
## 
## [[2]]
## [1] TRUE FALSE
## 
## [[3]]
## [1] "a" "b" "c"

一旦移除了列表成分的名称,就不能再通过名称来访问列表成分,但是仍可以使用位置索引和逻辑准则访问。

2.4.5 赋值

在列表中赋值和给向量赋值一样直观:

l1 <- list(x = 1, y = c(TRUE, FALSE), z = c("a", "b", "c"))
l1$x <- 0

如果给一个不存在的成分赋值,列表会自动地在对应名称或位置下增加一个新成分:

l1$m <- 4
l1
## $x
## [1] 0
## 
## $y
## [1] TRUE FALSE
## 
## $z
## [1] "a" "b" "c"
## 
## $m
## [1] 4

也可以同时给多个列表成分赋值:

l1[c("y", "z")] <- list(y = "new value for y", z = c(1, 2))
l1
## $x
## [1] 0
## 
## $y
## [1] "new value for y"
## 
## $z
## [1] 1 2
## 
## $m
## [1] 4

如果想要移除列表中的某些成分,只需赋值为NULL

l1$x <- NULL
l1
## $y
## [1]  "new value for y"
## 
## $z
## [1]  1 2
## 
## $m
## [1]  4

还可以同时移除列表中的多个成分:

l1[c("z", "m")] <- NULL
l1
## $y
## [1] "new value for y"

2.4.6 其他函数

R 中有许多和列表相关的函数。例如,如果我们不能确定一个对象是否是列表,可以调用is.list( )进行判断:

l2 <- list(a = c(1, 2, 3), b = c("x", "y", "z", "w"))
is.list(l2)
## [1] TRUE
is.list(l2$a)
## [1] FALSE

这里的l2是一个列表,但l2$a是一个数值向量而不是列表。

我们也可以调用as.list( )函数将一个向量转换成一个列表:

l3 <- as.list(c(a = 1, b = 2, c = 3))
l3
## $a
## [1] 1
## 
## $b
## [1] 2
## 
## $c
## [1] 3

通过调用unlist( ),可以很容易地将一个列表强制转换成一个向量。该函数基本上对所有列表成分进行了转换,并把它们存储在一个类型兼容的向量中:

l4 <- list(a = 1, b = 2, c = 3)
unlist(l4)
## a b c 
## 1 2 3

如果我们对一个混合了数值和文本的列表进行去列表化(unlist),则每个成分都会被转换为其所能转换成的最近类型(closest type):

l4 <- list(a = 1, b = 2, c = "hello")
unlist(l4)
## a b c 
## "1" "2" "hello"

这里的l4$al4$b都是数字,可以被转换为一个字符;但是,l4$c是一个字符向量,无法被转换为数值。因此,能够兼容这些元素的最近类型就是字符向量。

2.5 数据框

数据框是指有若干行和列的数据集。它与矩阵类似,但并不要求每列都是相同的类型。这与最常见的数据形式是一致的:每行或每条记录由不同类型的列来描述。

表2-1充分展示了数据框的特点。

表2-1

姓名

性别

年龄

专业

Ken

Male

24

Finance

Ashley

Female

25

Statistics

Jennifer

Female

23

Computer Science

2.5.1 创建一个数据框

我们可以调用data.frame( )函数,对每一列提供相应类型的列向量来创建一个数据框。

persons <- data.frame(Name = c("Ken", "Ashley", "Jennifer"),
  Gender = c("Male", "Female", "Female"),
  Age = c(24, 25, 23),
  Major = c("Finance", "Statistics", "Computer Science"))
persons
##   Name     Gender  Age  Major
## 1 Ken      Male    24   Finance
## 2 Ashley   Female  25   Statistics
## 3 Jennifer Female  23   Computer Science

注意到,数据框的创建方式与列表完全一致。本质上讲,数据框就是一个列表,该列表的每个成分都是一个向量,并且长度相同,以表格的形式展现。

除了根据原始数据创建数据框,我们也可以对一个列表直接调用data.frame( )或者as.data.frame( )将其转换为数据框:

l1 <- list(x = c(1, 2, 3), y = c("a", "b", "c"))
data.frame(l1)
## x y
## 1 1 a
## 2 2 b
## 3 3 c
as.data.frame(l1)
##   x y
## 1 1 a
## 2 2 b
## 3 3 c

也可以用同样的方式将矩阵转换为数据框:

m1 <- matrix(c(1, 2, 3, 4, 5, 6, 7, 8, 9), nrow = 3, byrow = FALSE)
data.frame(m1)
##   X1 X2 X3
## 1  1  4  7
## 2  2  5  8
## 3  3  6  9
as.data.frame(m1)
##   V1 V2 V3
## 1  1  4  7
## 2  2  5  8
## 3  3  6  9

注意到,这种转换会自动给新数据框赋予列名。实际上,可以验证,如果矩阵已经有了列名或者行名,那么它们在转换中会被保留下来。

2.5.2 对行和列命名

数据框既是列表的特例,也是矩阵的推广,因此访问这两类对象的方式都适用于数据框。

df1 <- data.frame(id = 1:5, x = c(0, 2, 1, -1, -3), y = c(0.5, 0.2, 0.1,0.5, 0.9))
df1
##   id  x  y
## 1  1  0 0.5
## 2  2  2 0.2
## 3  3  1 0.1
## 4  4 -1 0.5
## 5  5 -3 0.9

与矩阵类似,我们也可以对数据框的行和列重命名:

colnames(df1) <- c("id", "level", "score")
rownames(df1) <- letters[1:5]
df1
##   id level score
## a  1    0    0.5
## b  2    2    0.2
## c  3    1    0.1
## d  4   -1    0.5
## e  5   -3    0.9

2.5.3 构建数据框子集

因为数据框是由列向量组成、有着矩阵形式的列表,所以我们可以用两种操作方式来访问数据框的元素和子集。

1.以列表形式构建数据框子集

如果把数据框看作是由向量组成的列表,我们可以沿用列表的操作方式来提取元素或构建子集。

例如,可以使用$按列名来提取某一列的值,或者用[[符号按照位置提取。

df1$id
## [1] 1 2 3 4 5
df1[[1]]
## [1] 1 2 3 4 5

以列表形式构建子集完全适用于数据框,同时也会生成一个新的数据框。提取子集的操作符([)允许我们用数值向量表示列的位置,用字符向量表示列名,或用逻辑向量指定TRUEFALSE的选择标准,来取出相应的列。

df1[1]
##   id
## a  1
## b  2
## c  3
## d  4
## e  5
df1[1:2]
##  id level
## a 1     0
## b 2     2
## c 3     1
## d 4    -1
## e 5    -3
df1["level"]
##  level
## a    0
## b    2
## c    1
## d   -1
## e   -3
df1[c("id", "score")]
##   id score
## a  1   0.5
## b  2   0.2
## c  3   0.1
## d  4   0.5
## e  5   0.9
df1[c(TRUE, FALSE, TRUE)]
##   id score
## a  1   0.5
## b  2   0.2
## c  3   0.1
## d  4   0.5
## e  5   0.9

2.以矩阵形式构建数据框子集

不过,以列表形式操作并不支持行选择。与此相反,以矩阵形式操作更加灵活。如果我们将数据框看作矩阵,其二维形式的存取器可以很容易地获取一个子集的元素,同时支持列选择和行选择。

换句话说,我们可以使用 [行,列] 指定行或列来提取数据框子集,[ , ] 内可以是数值向量、字符向量或者逻辑向量。

例如,选择指定的列:

df1[, "level"]
## [1] 0 2 1 -1 -3
df1[, c("id","level")]
##  id level
## a 1     0
## b 2     2
## c 3     1
## d 4    -1
## e 5    -3
df1[, 1:2]
##  id level
## a 1     0
## b 2     2
## c 3     1
## d 4    -1
## e 5    -3

或者,选择指定的行:

df1[1:4,]
##   id level score
## a  1     0   0.5
## b  2     2   0.2
## c  3     1   0.1
## d  4    -1   0.5
df1[c("c","e"),]
##   id level score
## c  3     1   0.1
## e  5    -3   0.9

也可以同时选择指定的行和列:

df1[1:4, "id"]
## [1] 1 2 3 4
df1[1:3, c("id", "score")]
##   id score
## a  1   0.5
## b  2   0.2
## c  3   0.1

以矩阵形式操作会自动简化结果,也就是说,如果只提取一列,那么结果将不再是数据框形式,而仅仅返回这一列的值。即便结果只有单列,我们也可以结合使用两种操作方式来保留数据框的形式:

df1[1:4,]["id"]
##   id
## a  1
## b  2
## c  3
## d  4

这里,第1组方括号以矩阵形式提取数据框的前4行和所有的列。第2组方括号再以列表形式提取列名为id的这一列,结果即表现为数据框的形式。

另一种方法是通过设定drop = FALSE避免简化结果:

df1[1:4, "id", drop = FALSE]
##   id
## a  1
## b  2
## c  3
## d  4

如果想要数据框的子集总是保留数据框的形式,你可以设置drop = FALSE;否则,一些特殊情况(例如只选择提取一列)可能会导致意想不到的结果,你觉得将会得到一个数据框,但却返回一个向量。

3.筛选数据

以下代码按照score >= 0.5筛选df1的行,并选择idlevel两列:

df1$score >= 0.5
## [1] TRUE FALSE FALSE  TRUE  TRUE
df1[df1$score >= 0.5, c("id", "level")]
##   id level
## a  1     0
## d  4    -1
## e  5    -3

以下代码按照行名必须在ad或者e中的准则来筛选df1的行,并选择idscore两列:

rownames(df1) %in% c("a", "d", "e")
## [1] TRUE FALSE FALSE  TRUE  TRUE
df1[rownames(df1) %in% c("a", "d", "e"), c("id", "score")]
##   id score
## a  1   0.5
## d  4   0.5
## e  5   0.9

以上两个例子都以矩阵形式对数据框进行操作,根据逻辑向量选择行,字符向量选择列。

2.5.4 赋值

处理列表和矩阵的两种方法都可以用来为一个数据框子集赋值。

1.以列表方式赋值

我们可以同时使用$和<-对列表中的成分重新赋值。

df1$score <- c(0.6, 0.3, 0.2, 0.4, 0.8)
df1
##   id level score
## a  1     0   0.6
## b  2     2   0.3
## c  3     1   0.2
## d  4    -1   0.4
## e  5    -3   0.8

此外,[也是适用的,而且它允许在一个语句中进行多重修改,但是使用[[每次只能修改一列。

df1["score"] <- c(0.8, 0.5, 0.2, 0.4, 0.8)
df1
##   id level score
## a  1     0   0.8
## b  2     2   0.5
## c  3     1   0.2
## d  4    -1   0.4
## e  5    -3   0.8
df1[["score"]] <- c(0.4, 0.5, 0.2, 0.8, 0.4)
df1
##   id level score
## a  1     0   0.4
## b  2     2   0.5
## c  3     1   0.2
## d  4    -1   0.8
## e  5    -3   0.4
df1[c("level", "score")] <- list(level = c(1, 2, 1, 0, 0), score = c(0.1,
0.2, 0.3, 0.4, 0.5))
df1
##   id level score
## a  1     1   0.1
## b  2     2   0.2
## c  3     1   0.3
## d  4     0   0.4
## e  5     0   0.5

2.以矩阵方式赋值

当我们以列表方式对数据框进行赋值时,会遇到与构建子集时同样的问题,即只能访问列。若需要更加灵活地进行赋值操作,可以以矩阵方式进行。

df1[1:3, "level"] <- c(-1, 0, 1)
df1
##   id level score
## a  1    -1   0.1
## b  2     0   0.2
## c  3     1   0.3
## d  4     0   0.4
## e  5     0   0.5
df1[1:2, c("level", "score")] <- list(level = c(0, 0), score = c(0.9, 1.0))
df1
##   id level score
## a  1     0   0.9
## b  2     0   1.0
## c  3     1   0.3
## d  4     0   0.4
## e  5     0   0.5

2.5.5 因子

值得注意的是,在默认情况下,数据框会以更有效地利用内存的方式来存储数据。但有时,这种存储方式会导致意想不到的问题。

例如,当我们用一个字符向量作为创建数据框的列时,R 会默认将其转换成因子,相同值只存储一次,以免重复存储占用过多内存。因子本质上是一个带有水平(level)属性的整数向量,其中“水平”是指我们事前确定的可能取值的有限集合。

我们可以通过对已经创建的数据框persons调用str( )来说明:

str(persons)
## 'data.frame': 3 obs. of  4 variables:
## $ Name  : Factor w/ 3 levels "Ashley","Jennifer",..: 3 1 2
## $ Gender: Factor w/ 2 levels "Female","Male": 2 1 1
## $ Age   : num  24 25 23
## $ Major : Factor w/ 3 levels "Computer Science",..: 2 3 1

正如我们所看到的,姓名(Name)、性别(Gender)和专业(Major)不是字符向量而是因子。因为性别(Gender)只可能是女(Female)或男(Male),所以它被表示为一个因子是合理的。显然使用两个整数表示这两种水平要比即使取值重复也全部保存下来的字符向量更加有效。

然而,如果某些列并不限于几个可能的取值,使用因子可能会引起问题。例如,我们想在persons中设置一个名字。

persons[1, "Name"] <- "John"
## Warning in `[<-.factor`(`*tmp*`, iseq, value = "John"): invalid factor 
## level, NA generated
persons
##       Name  Gender Age             Major
## 1     <NA>    Male  24           Finance
## 2   Ashley  Female  25        Statistics
## 3 Jennifer  Female  23  Computer Science

此时,出现了一个警告。之所以出现警告,是因为在最初设定Name这一列时,相应的水平集合中并没有John这个词。因此我们无法给第一个人赋予一个不存在的名字。将任意一个Gender设置成Unknown时也会出现同样的情况,原因是一样的。当我们定义一个数据框时,用一个字符向量作为数据框的一列,该列将默认被转换为因子,其取值只能从由对应字符向量的唯一值构成的水平集合中选取。

有时候这种情况令人非常恼火。特别是在内存已经很便宜的今天,这种设定真没有多大帮助。避免这种情况最简单的方式是在使用data.frame()创建一个数据框时,设置 stringsAsFactors = FALSE

persons <- data.frame(Name = c("Ken", "Ashley", "Jennifer"),
  Gender = factor(c("Male", "Female", "Female")),
  Age = c(24, 25, 23),
  Major = c("Finance", "Statistics", "Computer Science"),
  stringsAsFactors = FALSE)
str(persons)

## 'data.frame':  3 obs. of  4 variables:
## $ Name  : chr  "Ken" "Ashley" "Jennifer"
## $ Gender: Factor w/ 2 levels "Female","Male": 2 1 1
## $ Age   : num  24 25 23
## $ Major : chr  "Finance" "Statistics" "Computer Science"

如果我们真的想让一个因子对象发挥它的作用,可以直接在特定的列上调用factor( )函数,就像Gender那一列。

2.5.6 数据框中的实用函数

对一个数据框而言,有很多实用的函数,这里我们只介绍几个最常用的。

summary( )函数作用在数据框上,将生成一个汇总表来显示每一列的情况:

summary(persons)
 
## Name Gender Age Major
## Length:3 Female:2 Min. :23.0 Length:3
## Class :character Male :1 1st Qu.:23.5 Class :character
## Mode :character Median :24.0 Mode :character
## Mean :24.0 
## 3rd Qu.:24.5 
## Max. :25.0

对于因子Gender,汇总了取每一个值或每一个水平的行数。对于一个数值向量,返回重要的分位数。对于其他类型的列,则显示列的长度、类型和模式。另一个常见的需求是将多个数据框按行或按列进行合并。要实现这个目的,我们可以使用rbind( )cbind( ),正如函数名一样,它们分别表示按行合并和按列合并。

如果想向数据框中添加一些行,例如,在这个例子中,要添加一个人的新记录,我们可以用rbind( )

rbind(persons, data.frame(Name = "John", Gender = "Male", Age = 25, Major 
= "Statistics"))

##       Name Gender Age             Major
## 1      Ken   Male  24           Finance
## 2   Ashley Female  25        Statistics
## 3 Jennifer Female  23  Computer Science
## 4     John   Male  25        Statistics

如果想向数据框中添加一些列,例如,添加两个新列表示每个人是否已注册和其手头的项目数量,可以使用cbind( )

cbind(persons, Registered = c(TRUE, TRUE, FALSE), Projects = c(3, 2, 3))

##       Name  Gender  Age             Major   Registered Projects
## 1      Ken    Male   24           Finance         TRUE        3
## 2   Ashley  Female   25        Statistics         TRUE        2
## 3 Jennifer  Female   23  Computer Science        FALSE        3

请注意,rbind( )cbind( )不会修改原始数据,而是生成一个添加了行或列的新数据框。

另一个实用函数是expand.grid( ),它会生成一个包含所有列值组合的数据框:

expand.grid(type = c("A", "B"), class = c("M", "L", "XL"))

##   type class
## 1    A     M
## 2    B     M
## 3    A     L
## 4    B     L
## 5    A    XL
## 6    B    XL

还有很多可以用于数据框的实用函数,我们将在第7章继续讨论这些函数。

2.5.7 在硬盘上读写数据

现实中,数据通常存储在文件中。R 提供了许多函数以便从文件中读取一个表格或将一个数据框写入文件。如果一个文件储存了一个表格,通常它都会被很好地组织起来,即按照一定规则将行和列有序地排列。大多数情况下,我们不必逐个字节地读取文件,而是调用read.table( )read.csv( )等函数。

CSV(逗号分隔值,Comma-Separated Vlues)是目前最受欢迎的软件通用数据格式。CSV通常是这样组织数据的:不同列之间的值用逗号分隔开,首行默认作为表头。例如,在CSV格式中是这样存储 persons 的:

Name,Gender,Age,Major
Ken,Male,24,Finance
Ashley,Female,25,Statistics
Jennifer,Female,23,Computer Science

将数据读入 R 环境中,我们只需要调用read.csv(file),这里的file是文件所在的路径。为了保证数据文件能被 R 找到,最好直接将数据文件夹放入默认工作目录中,调用getwd( )找到该目录。我们会在下一章更详细地讨论它。

read.csv("data/persons.csv")

##       Name  Gender Age             Major
## 1      Ken    Male  24           Finance
## 2   Ashley  Female  25        Statistics
## 3 Jennifer  Female  23  Computer Science

如果需要将数据框保存成一个CSV文件,可以调用write.csv(file)并调整其他参数:

write.csv(persons, "data/persons.csv", row.names = FALSE, quote = FALSE)

参数row.names = FALSE避免存储不必要的行名,参数quote = FALSE避免对输出中的文本加引号,这两种做法在大多数情况下都是非必要的。

R 中还有很多内置函数和扩展包可以用来读写不同格式的数据,我们将在之后的章节中继续讨论这个内容。

2.6 函数

函数是一个可以调用的对象。本质上讲,它是一个具有内在逻辑的机制,输入一组值(形参或实参),并依据其逻辑返回一个值。

在前面的章节中,我们遇到过一些R的内置函数。例如,在is.numeric( )函数中输入任意一个 R 对象,会返回一个判断该对象是否为数值向量的逻辑值。类似的还有is.function( )函数,它可以判断一个给定的R对象是否为函数。

事实上,在R环境中,我们所使用的一切都是对象,所做的一切都是函数,而且,也许会令你惊讶的是,所有的函数都是对象。甚至连<-+也都是带有两个参数的函数。尽管它们被称为二元运算符,其本质上也都是函数。

当我们做一些简单的、交互式的数据分析时,通常不必自己编写函数,R 的内置函数和几千个扩展包提供的函数已经够用了。

但是,如果你需要在数据操作或分析中重复某些逻辑或过程,那么 R 中内置的或扩展包中的函数可能无法充分满足你的需求。因为它们不是为了满足特定的任务需求或特定格式的数据集编写的。此时,你就需要针对特定需求自己创建函数了。

2.6.1 创建函数

在 R 中创建函数很容易。例如我们定义一个名为add的函数,将xy相加:

add <- function(x, y){
  x + y
}

上述函数的语法(x, y)指定了函数的参数。换句话说,此函数需要两个名为xy的参数。{ x + y }是函数体,包含了一系列由xy及其他可用符号表示的表达式。除非在函数内部调用return( ),一般情况下最后一个表达式的值即为函数的返回值。最后,此函数被命名为add,之后我们就可以使用add来调用这个函数了。

创建一个简单或者稍微复杂的函数,与将一个向量赋值给一个变量并无本质差异。在R中,函数就像另一个对象。若要查看对象add的内容,在控制台中输入add即可。

add

## function(x, y){
## x + y
## }

2.6.2 调用函数

就像在数学中一样,函数一旦定义了,就可以被调用。调用语法为:“函数名(参数1, 参数2, ...)”。请看如下示例:

add(2, 3)

## [1] 5

调用函数的过程非常清晰。就像示例中调用函数,R 首先会在环境中查找是否存在一个名为 add 的函数,然后它会明白 add 是我们刚才创建的函数。之后 R 会创建一个局部环境,并在该局部环境中将2赋值给 x,将3赋值给 y。下一步,将给定的参数值代入函数体中的表达式并进行计算。最终,函数返回表达式的值,结果为5。

2.6.3 动态类型

因为 R 中的函数不是强类型的,所以它可以非常灵活。换句话说,在调用函数之前,输入对象的类型是不固定的。即使函数的最初设计是针对标量运算,当将函数“+”作用到向量上时,它也会自动拓展以适用于向量运算。例如,我们可以运行以下代码,而不必对函数做任何其他修改:

add(c(2, 3), 4)

## [1] 6 7

上面这个例子没有真正展示出动态类型的灵活性,因为在 R 中标量也是一个向量(长度为1)。举一个更具有代表性的例子:

add(as.Date("2014-06-01"), 1)

## [1] "2014-06-02"

无需检查输入类型,函数便可以将两个参数代入表达式中进行运算。其中,as.Date( )创建了一个Date对象,用来表示日期。这里没有对“add”函数进行任何更改,它就可以完美地作用于对象Date。只有在两个参数上“+”没有被很好地定义时,函数才会失效。

add(list(a = 1), list(a = 2))

## Error in x + y: 二进列运算符中有非数值参数

2.6.4 泛化函数

函数是用于解决一些特定问题的特定逻辑或者过程的集合的一种合理抽象,开发人员通常希望函数具有一般性,以适用于各种各样的场景。这样就可以轻松地使用它来解决相似的问题,而无需为每个问题编写过多的专用函数。

泛化是使一个函数具有更广泛的适用性。在弱类型的编程语言(例如 R )中泛化函数非常方便的,但是当它被不正确地执行时,也会出现错误。

为了使 add( )函数更加通用,以便可以处理各种原始代数运算,我们可以定义另一个名为calc 的函数。这个新函数包含3个参数:xytype,其中 xy 是两个向量,type 接收一个字符向量,它表示用户想要进行哪一种代数运算。

以下代码使用控制流(flow control执行该函数。我们稍后会讲解控制流的具体使用。虽然这里初次遇到,但应该很容易理解。在这段代码中,type的取值决定了使用何种表达式。

calc <- function(x, y, type){
  if (type == "add"){
    x + y
  } else if (type == "minus"){
    x - y
  } else if (type == "multiply"){
    x * y
  } else if (type == "divide"){
    x / y
  }else {
    stop("Unknown type of operation")
  }
}

一旦函数被定义,我们便可以通过提供适当的参数来调用它:

calc(2, 3, "minus")
## [1] –1

函数自动适用于数值向量:

calc(c(2, 5), c(3, 6), "divide")
## [1] 0.6666667 0.8333333

因为之前“+”已经被良好定义,所以也可以泛化calc( )函数以适用于非数值向量:

calc(as.Date("2014-06-01"), 3, "add")
## [1] "2014-06-04"

如果提供一些无效参数:

calc(1, 2, "what")
## Error in calc(1, 2, "what"): Unknown type of operation

在这种情况下,没有满足的条件,因此最后else代码块中的表达式被执行。stop( )函数被调用,输出错误信息并立即终止运算。

看起来函数运行良好,也考虑了包括无效参数的所有情况。然而事实并非如此:

calc(1, 2, c("add", "minus"))
## Warning in if (type == "add") {: 条件的长度大于一,因此只能用其第一元素
## [1] 3

这里,我们没有考虑传递多元素向量给type的情况。问题在于:当两个多元素向量比较时,也会返回一个多元素逻辑向量,这会使得 if 的判断条件含糊不清。考虑一下,if(c(TRUE, FALSE))意味着什么呢?

为了彻底地避免这种模棱两可的情况,我们需要细化函数使错误能够更加明晰,反映更多信息。进一步地,我们只需检查向量的长度是否为 1:

calc <- function(x, y, type){
  if (length(type > 1L)) stop("Only a single type is accepted")
  if (type == "add"){
    x + y
  } else if (type == "minus"){
    x - y
  } else if (type == "multiply"){
    x * y
  } else if (type == "divide"){
    x / y
  }else {
    stop("Unknown type of operation")
  }
}

重试上述会报错的代码,我们可以查看预先检查参数后,函数如何处理异常:

calc(1, 2, c("add", "minue"))
## Error in calc(1, 2, c("add", "minue")): Only a single type is accepted

2.6.5 函数参数的默认值

一些函数可以非常灵活,因为它们能够接受各种各样的输入值,满足广泛的需求。但是,很多情况下,更多的灵活性意味着增加更多的参数。

如果使用一个非常灵活的函数,每次需要指定几十个参数,那么查看代码时肯定会觉得一片混乱。在这种情况下,给参数设定合理默认值,将会极大地简化调用函数的代码。

使用arg = value给一个参数设定默认值,这将使其成为一个可选参数。下面这个例子创建了一个带有可选参数的函数:

increase <- function(x, y = 1){
  x + y
}

调用新函数increase( )时,只需提供x的取值,y会自动取值为1,除非另有明确指定。

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

R 中许多函数都包含多个参数,其中一些被赋予了默认值。有时,设定参数默认值是一件棘手的事情,因为它高度依赖于大多数用户的使用意图。

2.7 小结

在本章中,我们学习了数值向量、逻辑向量和字符向量的基本性质。这些向量属于同质数据类,即只能存储相同类型的元素。与之相比,列表和数据框更加灵活,因为它们可以存储不同类型的元素。还学习了如何从这些数据结构中提取子集和元素。最后,了解了关于创建和调用函数的相关内容。

理解了游戏规则之后,还需要熟悉操作环境。在下一章中,我们将介绍一些有关管理工作环境的基本但重要的内容,向你展示一些管理工作目录、环境和扩展包库的一般做法。


 第1个成分只包含1个元素:1;第2个成分包含2个元素:TRUE和FALSE;第3个成分包含3个元素:"a" "b" 和 "c"。使用[[ ]]提取成分,使用[ ]提取成分的元素,例如l0[[2]][2] 提取第2个成分的第2个元素FALSE。

 使用[ ]提取成分时,返回列表的子集,还是一个列表;使用[[ ]]提取成分时,返回对应成分的元素。

目录

相关技术

推荐用户