R语言基本语法_数据结构

R语言数据结构

如果有Python使用经验, 可以比较容易地站在numpypandas等数据分析套件的基础上理解R语言中数据结构, 主要分为以下几种:

  1. 向量(vector, 有时也被称为atomic vector), 向量中的每个元素都必须是同一种类型(同质性), 向量是一个固定的内存区域, 不支持插入和删除元素, 这相当于生成一个新的向量, 但是做切片操作等不会改变其内存空间中的数据存储区域, 而只需要改变某一个控制字段即可; (可类比numpy中ndarray的存储结构)
  2. 矩阵(matrix), 矩阵本质上也是向量, 只不过添加了行数和列数属性而已;
  3. 数组(array), 数组是更加一般化的矩阵, 可以包含多维信息, 矩阵是一个二维的数组;
  4. 列表(list, 有时也称为rcursive vector, 递归型向量), 和向量不同的是, list支持元素具有不同的类型(异质性), 并且支持插入删除合并等一系列操作; 列表看上去和结构体类似, 每一个元素都是列表的组件; 更像是C中的结构体或者Python中的字典等等;
  5. 数据框(dataframe), 数据框也支持其中包含多种类型, 如果列表是异质的向量, 则可以理解为数据框是异质的矩阵; 但是在实际使用中, 列表往往比数据框具有更加灵活的应用, 而避免太过复杂的数据框, 对数据框正确的理解应该是: 具有相同长度的向量组件的列表; 可能比较拗口, 但是列表中的组件可以是数据框, 而数据框中的组件则一般肯定为向量以便于数据分析;
  6. 因子(factor), 因子是用数字对向量内容编码后的向量, 具有levels属性来对一系列的值或者字符串做编码;
  7. 表(table), 表常常由数据框通过table()函数得到, 具有行名、列名等属性;
  8. 类(class), R中的类来源于S语言的灵感, 以列表的形式体现, 分为S3类和S4类;

R中可以使用typeof()函数来判断向量类型, 或者使用mode()来判断向量的模式, 这里的模式比类型的范围稍大一些, 比如integer和double是两种不同类型, 但是他们都是同一种(numeric)模式;

> x <- c(1, 2)
> typeof(x)
[1] "double"
> mode(x)
[1] "numeric"

向量

创建向量

R中没有标量的概念, 任何标量都是一元向量, 由于向量的同质性, 其中的每个元素都必须是同一种类型, 通过c()函数()可以生成一个向量:

> x <- 5
> x
[1] 5
> x <- c(1, 4, 2, 8)
> x
[1] 1 4 2 8
> x <- c(x[1:2], 5, x[3:4])
> x
[1] 1 4 5 2 8

逐行解释以上结果:

上面是在R语言交互式界面中的输入输出结果, <-是R中的赋值操作符, 可以看到, 如果对一个变量赋值标量数据, 实际上仍然是以向量的形式显示, 只不过这个向量只有一个元素;

如果要对向量进行元素插入的操作, 则必须使用上述后三行的方式, 通过这种方式插入元素的操作来得到;

而和Python一样, R中的变量名称实际上只是一个指针, 这意味着上述的插入操作实际上是构成了一个新的向量, 而又将x指向新向量的内存空间; 这就意味着向量操作存在一个陷阱, 如果对一个大的向量频繁进行插入或者改变的操作, 程序性能将会大大下降;

由于向量的同质性, 如果在c()函数操作过程中传入两个类型不同的向量, 则向量的类型会往较宽的方向自动转换:

> x <- 1:5
> typeof(x)
[1] "integer"
> y <- c('a', 'b', 'c')
> typeof(y)
[1] "character"
> z <- c(x, y)
> typeof(z)
[1] "character"

向量的访问通过[]进行, 其中包含需要访问到的下标数组; 如果有Python经验的同学需要注意, R里也支持负数作为访问下标, 但是和Python中的意义不同, R中的负数下标表示舍去某个下标对应元素的含义; 同时, 应该注意到在R里面, 下标是从1开始的, 而不是0;

> x <- 1:5
> x[-1]
[1] 2 3 4 5

除了最基本的c()之外, 还可以通过以下的方式创建向量:

> 2:8
[1] 2 3 4 5 6 7 8
> 8:2
[1] 8 7 6 5 4 3 2
> seq(c(4,2,3))
[1] 1 2 3
> seq(1, 10, by=2)
[1] 1 3 5 7 9
> seq(1, 10, length.out=7)
[1]  1.0  2.5  4.0  5.5  7.0  8.5 10.0
> seq(from=12, to=30, length=10)
[1] 12 14 16 18 20 22 24 26 28 30
> rep(6, 3)
[1] 6 6 6
> rep(c(1, 2, 3), 2)
[1] 1 2 3 1 2 3
> rep(c(1, 2, 3), each=2)
[1] 1 1 2 2 3 3 
> e <- vector(length=20)

等等;

第一种方式使用:来创建一个连续的向量, 也是用来生成下标数组最常用快捷的方式(当然意味着下标数组可以用任何一种方式来生成, 如:x[rep(c(1,2), 2)]等); 用这种方式得到的向量是整形的向量(integer), 而用c()函数得到的确实double类型的向量(虽然他们的模式都是numeric);

第二种方式可以使用seq()函数创建向量, 该函数接收一个向量参数时, 会直接返回其下标数组, 这种方式是得到向量下标的最佳实践(由于R语言中下标从1开始, 如果使用length()函数等方法很容易在向量为空时发生错误); 该函数同时还支持步长和输出长度两种方式控制向量的生成;

第三种方式是对已有向量的重复扩展, 支持times和each两种方式, 分别将向量整个重复, 以及每个元素重复后组成向量等(具体看上面的例子), 当这两种方式共用时, each操作将会具有较高优先级(这是理所当然的, 否则each将失去意义);

第四种方式是为了避免对向量大小进行修改时, 会频繁对向量重新分配内存造成的性能问题, 则可以用vector()函数创建一个空的足够大的向量容器, 之后对该容器中的元素进行修改即可, 而不用多次重新分配内存;

向量常用操作

向量具有很多操作函数, 常用的有:

  • length(): 取得向量的长度(即元素个数);
  • all(): 判断向量中是否全部元素都满足条件;
  • any(): 判断向量中是否至少有一个元素满足条件;

由于向量是R中的最小存储单位, 所以在其他语言中印象中的所有标量函数(包含操作符)都可以应用于向量:

> x <- c(1, 2, 3)
> y <- c(4, 5, 6)
> x > 2
[1] FALSE FALSE  TRUE
> x + y
[1] 5 7 9
> x - y
[1] -3 -3 -3
> x * y
[1]  4 10 18
> x / y
[1] 0.25 0.40 0.50
> sqrt(x)
[1] 1.000000 1.414214 1.732051
> foo <- function(x) return(x ** 2)
> foo(x)
[1] 1 4 9

由上可见, R中的函数几乎都是向量化的(包含逻辑运算, 所以有些操作必须要借助all()any()进行), 在R中判断是否是其他语言印象中的标量, 只能通过判断向量长度是否为1进行; 这和numpy中的ufunc如出一辙, 不同的是R在语言层面上就决定所有函数的想量化, 而numpy则必须在python的限制下做出一些让步;

R中还支持向量化的三元操作符ifelse()函数, 该函数可以对向量进行快速的三元操作:

> x <- 1:10
> y <- ifelse(x %% 2 == 0, 'odd', 'even')
> y
 [1] "even" "odd"  "even" "odd"  "even" "odd"  "even" "odd"  "even" "odd"

向量化的运算非常广泛, 甚至判断相等的操作符: ==都是向量化的, 所以用来判断两个向量是否相等用==来进行得到的结果会是一个布尔型向量, 而判断两个向量是否真正相同, 还需要借助其他的函数;

> x <- 1:5
> y <- c(1, 2, 3, 4, 5)
> x
[1] 1 2 3 4 5
> y
[1] 1 2 3 4 5
> x == y
[1] TRUE TRUE TRUE TRUE TRUE
> all(x == y)
[1] TRUE
> identical(x, y)
[1] FALSE
> typeof(x)
[1] "integer"
> typeof(y)
[1] "double"

可以看到, 使用==比较时是值运算, 只比较两个值是否相等, 使用这种方式不会将向量的类型纳入考虑范围之中, 而使用identical()时, 则需要两个向量完全一样, 包括类型也要完全一致;

同时向量会在操作之前进行最简单的广播(broadcast), 以确保进行操作的两个向量具有相同的长度:

> x <- c(1, 2, 3)
> y <- 1:6
> x + y
[1] 2 4 6 5 7 9

这个特性在矩阵中也同样得到体现;

在R中应该尽可能避免循环操作, 而使用向量化操作符代替, 由于向量化操作符操作更高效的内存空间, 在执行效率上会比使用循环快很多;

特殊变量

在R中有两个特殊变量: NANULL, NA表示缺失值, 而NULL表示不存在, 我粗浅的理解是: NA表示指针指向一段未初始化的内存, 而NULL表示指针为空(没有指向具体的内存);

在R中, 许多函数都具有跳过NA的参数na.rm, 将其设置为TRUE, 则该函数在对向量进行计算时会直接跳过NA:

> x <- c(4, NA, 23, NA, 41)
> mean(x)
[1] NA
> mean(x, na.rm=TRUE)
[1] 22.66667

如上例当存在NA时, 无法进行平均值计算, 得到结果也是NA, 但是将na.rm参数设置为TRUE之后, 则可以正常计算出平均值; 注意: 这里是直接将NA排除, 所以总数也降为3而不是原始的长度5;

由于NA只是指向了一段未初始化的内存, 但是这段内存的长度等属性是在向量创建时就决定的, 所以在不同的向量中, NA具有不同的类型:

> x <- c(1, NA)
> typeof(x[2])
[1] "double"
> y <- c('a', NA)
> typeof(y[2])
[1] "character"

而NULL由于是未定义的值, 所以其没有类型, 并且向量长度为0;

向量筛选

向量除了使用数字下标访问内容之外, 还可以使用布尔型向量访问内容, 当下标为TRUE时表示访问, 而FALSE就是不访问; 而通过逻辑运算可以得到一个布尔型向量, 这种方式常用于向量筛选;

> x <- c(1:10, NA)
> x
 [1]  1  2  3  4  5  6  7  8  9 10 NA
> x > 2
 [1] FALSE FALSE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE    NA
> x[x>2]
[1]  3  4  5  6  7  8  9 10 NA

用布尔型向量的方式筛选向量的问题是, 当向量中存在NA时, 这种方式得到的结果会包含NA, 如果不想在结果中包含NA的话, 则需要用到subset()函数;

> subset(x, x>2)
[1]  3  4  5  6  7  8  9 10

向量筛选还有一个函数which(), 不同的是, 该函数只是返回向量的下标, 并且只返回确定符合要求的下标, 所以NA并不包含在其中;

> which(x>2)
[1]  3  4  5  6  7  8  9 10
> x[which(x > 2)]
[1]  3  4  5  6  7  8  9 10

which()函数可以用来找到符合要求的元素在向量中的具体位置, 当然which()得到的具体位置(下标)可以用来访问原来向量中的元素, 但是并不推荐这么做, 因为有更好的办法(直接用布尔型下标或者subset()更加符合设计的初衷);

命名向量

向量可以被指定名称, 指定名称之后, 可以用向量的名称来更直观访问一个向量;

> x <- c('2016-03-06', 'hangzhou', 'haze')
> names(x) <- c('date', 'city', 'weather')
> x
        date         city      weather 
"2016-03-06"   "hangzhou"       "haze" 
> x['city']
      city 
"hangzhou" 
> names(x) <- NULL
> x
[1] "2016-03-06" "hangzhou"   "haze"    

矩阵

创建矩阵

矩阵本质上是一个特殊的向量, 这个向量包含两个附加的属性: 行数、列数; 因此矩阵的创建依赖于向量的创建:

> m <- matrix(data=c(1,2,3:5,4), nrow=2)
> m
     [,1] [,2] [,3]
[1,]    1    3    5
[2,]    2    4    4
> m <- matrix(data=c(1,2,3:5,4), nrow=2, byrow=T)
> m
     [,1] [,2] [,3]
[1,]    1    2    3
[2,]    4    5    4

矩阵创建时, data参数提供了一个矩阵的内容, nrow或者ncol提供矩阵的形状, 在创建过程中, 可以看成将data中的数据按照指定的形状逐一输入到矩阵中;

可以看出, 矩阵默认是按照列顺序输入值的, 通过byrow参数可以将输入顺序改为行顺序, 但是不论输入顺序如何, 在存储形式上都是按照列顺序存储的(当个一个向量和矩阵运算时, 矩阵可以看成按列顺序展开的向量);

还有一种创建方式是通过向量的操作得到, 当对向量的函数输出结果为一个向量时, 结果向量的长度则是输入向量长度和输出向量长度之积, 如一个函数输出一个长度为2的向量, 则当输入向量长度为5时, 最终得到的结果向量的长度为10; 该结果向量可以作为data传入到matrix()函数中创建矩阵, 也可以通过sapply()(simplify apply)函数直接得到一个矩阵:

> square <- function(x) return(c(x, x^2))
> 
> square(2)
[1] 2 4
> square(1:5)
 [1]  1  2  3  4  5  1  4  9 16 25
> matrix(square(1:5), nrow=2, byrow=TRUE)
     [,1] [,2] [,3] [,4] [,5]
[1,]    1    2    3    4    5
[2,]    1    4    9   16   25
> sapply(1:5, square)
     [,1] [,2] [,3] [,4] [,5]
[1,]    1    2    3    4    5
[2,]    1    4    9   16   25

另外, 矩阵可以通过cbind()(column bind)、rbind()(row bind)两个函数进行创建;

> m = rbind(c(1,2), c(3,4))
> m
     [,1] [,2]
[1,]    1    2
[2,]    3    4
> m = cbind(c(1,2), c(3,4))
> m
     [,1] [,2]
[1,]    1    3
[2,]    2    4

矩阵常用操作

一旦得到一个矩阵, 则可以对其进行大量线性代数的运算, 其中包括:

  1. +, -, *, /: 各种四则运算, 当其中一个参数为向量, 则该向量会被循环补齐到和矩阵同样的大小再做运算, 注意: 矩阵存储方式是按列存储, 所以和向量的运算也是按列的顺序; 如果两个参数都为矩阵, 则必须保证矩阵的形状相同;
  2. %*%: 矩阵叉乘;
  3. t(): 矩阵转置;
  4. crossprod(): 矩阵内积;
  5. solve(): 解线性方程组;
  6. qr(): 矩阵QR分解;
  7. chol(): 矩阵Cholesky分解;
  8. det(): 矩阵行列式值;
  9. eigen(): 矩阵特征值和特征向量;
  10. diag(): 从方阵中提出对角矩阵;
  11. sweep(): 数值分析批量运算符;

除了以上线性代数运算的内容之外, 对于矩阵的一般操作如访问、赋值、筛选等等操作, 都遵循向量操作时的一般规则:

> m <- matrix(data=1:10, ncol=2)
> length(m)
[1] 10
> nrow(m)
[1] 5
> ncol(m)
[1] 2
> m
     [,1] [,2]
[1,]    1    6
[2,]    2    7
[3,]    3    8
[4,]    4    9
[5,]    5   10
> m[,2]
[1]  6  7  8  9 10
> m[3,]
[1] 3 8
> m[3,1]
[1] 3
> m[2:3,2]
[1] 7 8
> m[2:3,2] <- c(0,0)
> m
     [,1] [,2]
[1,]    1    6
[2,]    2    0
[3,]    3    0
[4,]    4    9
[5,]    5   10

上述例子说明了矩阵元素访问的方式, 同样也是通过下标, 行和列的下标值之间使用逗号分隔; 如果只用一个数值作为下标访问, 则相当于访问矩阵按照列顺序展开后的向量;

同样的, 矩阵也是不可更改的, 任何对矩阵中的元素做出修改的动作都将会重新建立一个矩阵, 从而造成性能浪费;

当矩阵访问得到的结果可以用向量形式表示时, R都会倾向于自动降维, 这在某些场合可能会造成一定的问题(比如访问得到的结果不再适用于矩阵运算), 为了防止自动降维, 可以设置drop=FALSE, 以确保得到的结果仍然是一个矩阵:

> m[2:3,2, drop=FALSE]
     [,1]
[1,]    0
[2,]    0

矩阵同样也可以使用布尔值向量来访问, 所以可以做到和向量类似的筛选操作:

> m
     [,1] [,2]
[1,]    1    6
[2,]    2    1
[3,]    3    0
[4,]    4    9
[5,]    5   10
> m[1, ] > 3
[1] FALSE  TRUE
> m[m[1,] > 3, ]
     [,1] [,2]
[1,]    2    1
[2,]    4    9
> m[m[1,] > 3]
[1]  2  4  6  0 10
> m > 3
      [,1]  [,2]
[1,] FALSE  TRUE
[2,] FALSE FALSE
[3,] FALSE FALSE
[4,]  TRUE  TRUE
[5,]  TRUE  TRUE
> m[m>3]
[1]  4  5  6  9 10

在上述操作中, 对矩阵的某一列进行逻辑操作, 得到一个布尔型向量, 该向量可以用来作为下标访问矩阵, 注意访问矩阵时, 应该使用逗号隔开的两个数值, 如果只有一个值并且没有用逗号, 则会访问矩阵按照列顺序展开的向量; 当直接对矩阵进行逻辑运算时, 得到的将是一个布尔型矩阵, 用该矩阵访问矩阵最终只能得到一个向量;

注意: 任何对向量操作的函数, 对矩阵操作时, 都会对矩阵进行按列展开为向量的操作, 如: which(), subset()等等, 特别是subset()需要注意, 不论传入的布尔型向量是按照矩阵的第几列取的, 一旦逻辑运算完毕, 该布尔型向量就和矩阵没有关系, 再用subset()操作时, 原矩阵展开为向量再进行操作;

使用apply()

apply()函数用于操作矩阵或者数组的某一维度, 该函数接收三个以上参数: 矩阵(或者数组), 维度编号(或者名称), 应用的操作函数, 函数的其他参数;

> m
     [,1] [,2]
[1,]    1    4
[2,]    2    5
[3,]    3    6
> apply(m, 1, mean)
[1] 2.5 3.5 4.5
> apply(m, 2, mean)
[1] 2 5

如上述操作中, 当维度编号为1时, 对矩阵的每一行都进行mean()运算, 当维度编号为2时, 对矩阵的每一列都进行mean()运算;

当一个函数具有多个参数时, 可以在后面添加需要的参数;

> m
     [,1] [,2]
[1,]    1    4
[2,]    2    5
[3,]    3    6
> f <- function(x, b) return (x + b)
> apply(m ,1 ,f, b=2)
     [,1] [,2] [,3]
[1,]    3    4    5
[2,]    6    7    8
> apply(m ,2 ,f, b=2)
     [,1] [,2]
[1,]    3    6
[2,]    4    7
[3,]    5    8

注意到, 结果的形状展示时, 由于矩阵是按照列存储的, 所以每一行的结果都会依次以列的顺序保存, 当对行进行运算时, 其结果矩阵和输入矩阵的形状将是互为转置的关系;

apply()函数是对循环操作的更简洁描述, 但是并不会对运行性能有很大贡献, 其主要意义在于并行运算以及分布式运算时的应用;

矩阵命名

在向量中, 可以通过names()给向量命名, 而给矩阵使用names()命名时, 就会被认为展开的向量, 不能直观反映其中的元素, 用colnames()rownames()给矩阵命名:

> m <- matrix(data=1:6, ncol=2)
> m
     [,1] [,2]
[1,]    1    4
[2,]    2    5
[3,]    3    6
> names(m) <- c('a', 'b', 'c', 'd', 'e', 'f')
> m
     [,1] [,2]
[1,]    1    4
[2,]    2    5
[3,]    3    6
attr(,"names")
[1] "a" "b" "c" "d" "e" "f"
> names(m) <- NULL
> colnames(m) <- c('a', 'b')
> rownames(m) <- c('x', 'y', 'z')
> m
  a b
x 1 4
y 2 5
z 3 6

列表

创建列表

列表也是向量, 只不过在R中, 向量默认指的是原子型向量(atomic vector), 而列表则是递归型向量(之所以称为递归型向量, 是因为列表的组件也可以是一个列表), 其长度可以延伸, 并且可以保存不同类型的元素(列表中又称为组件):

> l <- list(20160306, 'hangzhou', 'haze')
> l
[[1]]
[1] 20160306

[[2]]
[1] "hangzhou"

[[3]]
[1] "haze"

> l <- list(date=20160306, city='hangzhou', weather='haze')
> l
$date
[1] 20160306

$city
[1] "hangzhou"

$weather
[1] "haze"
> l <- vector(mode='list')
> l
list()

以上操作说明列表的常用创建方式, 当不指定组件名称时, R会使用默认的数字名称; 也可以使用vector(mode='list')的方式来创建一个空的列表;

访问列表

访问列表中的组件具有多种方式, 以上述的l为例:

> l
$date
[1] 20160306

$city
[1] "hangzhou"

$weather
[1] "haze"

> l$date
[1] 20160306
> l[['date']]
[1] 20160306
> l[[1]]
[1] 20160306

如上所示, 可以通过$date或者[['date']]或者[[1]]三种方式访问同一个组件, 注意到后面两种方式使用了双层的中括号, 这是因为如果只用一层中括号, 则仅仅只是访问到了子列表, 而非其中的组件;

> l['date']
$date
[1] 20160306

> l[1:2]
$date
[1] 20160306

$city
[1] "hangzhou"

可以任意在列表中添加新的组件, 或者将某一个组件设置为NULL以删除该组件, 注意删除一个组件后, 其之后组件的默认索引值都会改变, 同时也可以使用c()函数拼接多个列表:

> l[4:5] <- c('Sun', 'Lib')
> l
$date
[1] 20160306

$city
[1] "hangzhou"

$weather
[1] "haze"

[[4]]
[1] "Sun"

[[5]]
[1] "Lib"

> l[4] <- NULL
> l
$date
[1] 20160306

$city
[1] "hangzhou"

$weather
[1] "haze"

[[4]]
[1] "lib"

> l <- c(l[1:2], c('a', 'b')) 
> > l
$date
[1] 20160306

$city
[1] "hangzhou"

[[3]]
[1] "a"

[[4]]
[1] "b"

> l[3:4] <- NULL

列表特有的操作在于可以提取列表的标签和值, 如:

> names(l)
[1] "date" "city"
> unlist(l)
      date       city 
"20160306" "hangzhou" 

其中names()函数返回列表的标签向量, 而unlist()函数则返回列表的值向量, 同时如果这些组件具有名字的话, 返回的将是一个命名向量;

使用lapply()

和矩阵中的apply()函数类似, 列表中的lapply()函数将会对列表中每一个组件都进行相同的函数操作, 并且返回一个结果列表;

> l <- list(1:5, 10:20)
> l
[[1]]
[1] 1 2 3 4 5

[[2]]
 [1] 10 11 12 13 14 15 16 17 18 19 20

> lapply(l, mean)
[[1]]
[1] 3

[[2]]
[1] 15

递归型向量

列表之所以被称为递归型向量, 是因为其组件也可以是一个列表:

> list(t=list(x=1, y=2), z=3)
$t
$t$x
[1] 1

$t$y
[1] 2


$z
[1] 3

如上所示, 该列表的第一个元素t也是一个列表;


未完待续