本节主要对表数据(data.frame
)进行详解,包括增、删、查、改等单表操作,以及表之间数据关联的操作,并包含一些向量化计算的内容。
此外还会涉及到空间数据中坐标系的转换问题。
为了方便,称 data.frame
类型的数据,统称为“表数据”,这里“表”指数据类型,而不是文件格式。
上节已经讲到,用列表示的一个或更多的数据种类,每行包含一个唯一的数据实体。 因此,对表数据的操作将从行、列两个角度进行展开。 我们将单个表数据的操作归为“创建、增加、查询、修改、删除”五个类型。
创建一个表,是使用 data.frame()
函数进行的。创建表时,需要指定这个表里面有哪些列,每一列有哪些数据。例如我们使用如下语句创建一个表
demo.suspect <- data.frame(
name = c("张三", "李四", "王五"),
age = c(20, 26, 30),
gender = c("M", "F", "M"),
height = c(1.78, 1.65, 1.70),
weight = c(80, 45, 60),
shoe.size = c(43,38,41)
)
demo.suspect
函数 data.frame()
具有如下参数:
参数名 | 类型 | 含义 | 默认值 |
---|---|---|---|
... | 变长参数 | 每一列的值。可以指定参数名,也可以不指定。如果指定参数名,则使用参数名作为列名。 | |
row.names | integer/character/vector(integer/character) | 指定行名 | NULL |
check.rows | logical | 检查列名是否符合语法、是否存在重复等问题 | FALSE |
需要指出的是:
xxxxxxxxxx
data.frame(
name = c("张三", "李四", "王五"),
age = c(20, 26, 3),
gender = "M"
)
xxxxxxxxxx
data.frame() # 空表数据
查询一行或者一列,可以使用与查看矩阵相同的方法,即使用 []
运算符。
可以像矩阵一样,在 []
中使用整数、序列、整型向量、逻辑型向量或者省略行列索引其中之一来获取数据。
xxxxxxxxxx
demo.suspect[c(1,2), c(2,3)]
对于具有列名或行名的矩阵或表数据,可以通过指定其行名、列名来获取数据
xxxxxxxxxx
demo.suspect[, c("name", "height", "weight")]
如果只取某一列的所有值,可以使用运算符 $
获取这一列的值,例如
xxxxxxxxxx
demo.suspect$gender
还可以使用运算符 [[ ]]
,中间既可以填写列索引,也可以填写列名,例如
xxxxxxxxxx
demo.suspect[["gender"]]
demo.suspect[[3]]
以上只是对数据简单的查找。能否像数据库一样,给数据中指定一些条件,筛选出符合条件的数据呢?答案是可以的,而且有多种方式可以做到。这里介绍三种。
第一种是直接使用逻辑判断表达式,得到一组逻辑类型的向量,根据该向量获取对应位置上的元素。例如提取 demo.suspect
中 gender
列为 M
的行,可以这样操作
xxxxxxxxxx
demo.suspect[demo.suspect$gender == 'M',]
第二种是稍微有些多此一举,使用 which()
函数。这个函数接收一个逻辑类型的向量作为参数(如果不是向量,也会被转换成向量),返回这些向量中值为 TRUE 的元素的位置。例如
xxxxxxxxxx
which(demo.suspect$gender == 'M')
demo.suspect[which(demo.suspect$gender == 'M'),]
第三种是使用一个强大的 dplyr
包,该包提供了 select
和 filter
两个函数,可以大大简化对数据的查找。
其中,select
函数偏向于获取列,filter
函数偏向于获取行,基本对应了 SQL 语句中 select
和 where
两个子句,但提供了更多的功能。
以下是一些 select
函数的示例
xxxxxxxxxx
select(demo.suspect, name) # 获取 name 列
xxxxxxxxxx
select(demo.suspect, c("height", "weight", "shoe.size"))
xxxxxxxxxx
select(demo.suspect, height:shoe.size) # 获取 height 到 shoe.size 等列
xxxxxxxxxx
select(demo.suspect, ends_with("ght")) # 获取以 "ght" 结尾的列
starts_with 和 ends_with 是
dplyr
提供的判断字符串开头和结尾的两个函数,
可以看到,该函数可以接收多种类型的参数:字符串、字符串向量、:
表示的序列,以及一些函数返回值。
以下是一些 filter
函数的使用示例
xxxxxxxxxx
filter(demo.suspect, name == "张三")
xxxxxxxxxx
filter(demo.suspect, gender == "M" & age > 20)
xxxxxxxxxx
filter(demo.suspect, gender == "M", age > 20)
xxxxxxxxxx
filter(demo.suspect, between(age, 20, 30))
xxxxxxxxxx
filter(demo.suspect, gender == "M", ((height * 100 - 80) * 0.7) < weight)
可见,该函数还是主要接收一个逻辑类型的向量作为筛选依据,可以看作是对第一种数据检索方式的简化。
该函数也可以接收多个条件,多个条件之间是使用 &
运算符连接的。
该函数还可以在列之间进行比较,也可以对列做一些运算后比较。
该函数还支持一些函数,用于方便进行数据检索:
between(x, L, U)
判断变量 的元素是否落在 的区间内near(x, c)
对于浮点数 来说,直接使用 x == c
来判断 和 是否相等,会存在一些问题。该函数是用来判断 和 是否在浮点误差内相等。is.na(x)
判断变量 是否是 NA
当然这两个函数可以进行嵌套
xxxxxxxxxx
select(filter(demo.suspect, shoe.size < 42), "name":"gender")
在 R 语言中,还可以对数据进行分组查询,相当于 SQL 中的 group by
子句。实现这一操作有几种方式。
一种是使用 dplyr
包中提供的 group_by()
函数,该函数给数据指定一个分组依据,但不对数据进行分组处理。使用 summarise()
或 summarize()
函数进行分组处理。
使用一次 group_by()
函数,数据中增加一层分组条件;使用一次 summarise()
函数,数据中减少一次分组条件。
xxxxxxxxxx
summarise(group_by(demo.suspect, gender), height.mean = mean(height), weight.mean = mean(weight), married.num = length(which(is.married)))
这里用到了
mean()
和length()
两个函数,其作用分别是计算平均数和向量长度。
这种方式就非常类似于 SQL 语句的方式,使用起来也非常方便。
另一种是使用 R 自带的 aggregate
函数,对数据进行聚合。例如
xxxxxxxxxx
aggregate(demo.suspect[, c("height", "weight", "shoe.size", "BMI")], by = list(demo.suspect$gender), FUN = mean)
该函数具有如下参数:
参数名 | 类型 | 含义 | 默认值 |
---|---|---|---|
x | data.frame | 要处理的数据 | |
by | list | 分组依据 | |
FUN | function | 聚合函数。将每一列传入到函数的第一个参数中。 | |
... | logical | 聚合函数的额外参数 |
这里涉及到两个类型 list
和 function
,即列表类型和函数类型。
列表类型,有点类似于其他编程语言的字典类型(C++ 中对应 Map 类型),是以键值对方式存在的数据。 其中值必须有,但键名不一定必须有。当键名有时,可以比较类似于字典;当键名没有时,比较类似于任意列表。 因此该类型的使用非常灵活,可以和 JSON 数据对应。
创建列表类型变量的方式和创建表数据的方式很像,使用如下方式
xxxxxxxxxx
list(
name = "张三", gender = "Male", age = 30,
children = list(
list(name = "张军", gender = "Male", age = 3),
list(name = "张灵", gender = "Female", age = 2)
),
interests = c("体育", "音乐")
)
事实上,data.frame
类型只是一个特殊的 list
类型,特殊在不同键的值具有相同的长度。
在 aggregrate
函数中,如果 by
参数传入的列表只有几个键值对,那么就按照这几个条件进行分组。多个条件分组的示例如下
xxxxxxxxxx
aggregate(demo.suspect[, c("height", "weight", "shoe.size", "BMI")], by = list(demo.suspect$gender, demo.suspect$is.married), FUN = mean)
R 中,函数也是一个变量,即函数类型的变量(有点 JavaScript 那味了),但是 ()
运算符只能作用于函数类型的变量上。
创建函数类型的变量使用 function
关键字,将最后一条运行的语句的结果作为返回值。例如,创建一个计算二次函数值的变量
xxxxxxxxxx
quadratic <- function(x, a = 1, b = 0, c = 0) {
(a * x^2 + b * x + c)
}
quadratic(1)
quadratic(1, 1, 2)
quadratic(1, 1, 2, 1)
R 语言中的函数无需使用 return
指明返回值, return()
也是一个函数,但其只起到控制函数流程的作用。
R 语言中也提供了丰富的函数,用于进行简单的统计计算,包括 mean()
, sd()
, var()
, min()
, max()
, median()
, length()
, range()
, quantile()
, fivenum()
等。
这些函数可以直接使用。
xxxxxxxxxx
mean(1:5) # 平均数
sd(1:5) # 标准差
var(1:5) # 方差
min(1:5) # 最小值
max(1:5) # 最大值
median(1:5) # 中位数
length(1:5) # 长度
range(1:5) # 极值
quantile(1:10) # 四分位数
fivenum(1:10) # 四分位数(与上一种算法不同)
在之前可以看到,我们调用 mean
作为统计函数的时候,如果遇到字符串或逻辑值,其统计结果是有问题的。这时就需要我们使用自定义的函数,对不同类类型的列进行处理
xxxxxxxxxx
aggregate(demo.suspect, by = list(demo.suspect$gender), FUN = function(x) {
if (class(x) == "character") {
length(x)
} else if (class(x) == "logical") {
length(which(x))
} else {
mean(x)
}
})
这里使用到了 if
else
关键字进行函数流程的控制。
if
关键字后面使用 ()
包含一个逻辑表达式表示条件,当该条件成立时执行 {}
中的语句;不成立时,如果有 else
则执行 else
后 {}
的语句。
。
与其它语言类似,R 语言中也有 for
while
两种循环语句。这里顺便介绍一下。
for
语句的用法是
xxxxxxxxxx
for (i in demo.suspect$height) {
print(i * 100)
}
这里的
print()
函数将变量进行输出。
while
语句的用法是
xxxxxxxxxx
i <- 2
while (i > 0.0001) {
i <- i / 2
}
i
需要指出的是,R 语言执行 for
while
循环的效率比较低,因此不建议大量使用循环语句,推荐使用向量化计算方法,即将循环对象作为一个向量进行整体计算。
如果一定要对每一个值单独进行处理,在 R 中也有更高效的方法。
对数据增加一列,可以使用 $
运算符或者 [[]]
运算符,只要这一列的名字没有在数据中出现,直接赋值即可。新增的列出现在最后面。例如
xxxxxxxxxx
demo.suspect0 <- demo.suspect # 备份一下
xxxxxxxxxx
demo.suspect$married <- c(T, T, F)
demo.suspect
xxxxxxxxxx
demo.suspect[["children.count"]] <- c(2, 1, 0)
demo.suspect
我们也可以将一些计算值加入到列中,例如计算 BMI 指数
xxxxxxxxxx
demo.suspect$BMI <- demo.suspect$weight / demo.suspect$height^2
demo.suspect
以上这些操作,都可以使用一个函数 transform()
完成,该函数的用法类似于 select
filter
两个函数,在后面可以直接指定新列的值
xxxxxxxxxx
transform(demo.suspect0, BMI = weight / height^2, married = c(T, T, F), children.count = c(2, 1, 0))
增加列的操作是比较简单的。对于增加行的操作,由于不同列的数据类型不同,因此比较复杂。
如果有行名,可以使用 [行名,列名]
的方式,指定一个新行名的索引,填入数据即可。
建议分别赋值,可保证列不改变。
xxxxxxxxxx
row.names(demo.suspect0) <- demo.suspect0$name
demo.suspect0["赵六", "name"] <- "赵六"
demo.suspect0["赵六", "age"] <- 29
demo.suspect0
如果没有行名,可以使用函数 nrow()
计算以下当前的行数 ,使用 [r+1,列名]
的方式填入数据。
还可以使用 rbind()
函数增加一行,使用方法如下
xxxxxxxxxx
demo.suspect <- rbind(demo.suspect, data.frame(
name = "赵六",
age = 19,
gender = "F",
height = 1.6,
weight = 43,
shoe.size = 37,
BMI = 43 / 1.6^2,
married = F,
children.count = 0
))
demo.suspect
但该函数效率较低,应尽量避免使用。同样地,R 中也有一个 cbind
函数,其用法和 rbind 类似,只是用于增加列,但该函数效率也比较低,因此不推荐使用。
该部分所说的改,包括对名、类型、值的修改。
修改表数据列名和行名的方法和矩阵是类似的,都是使用 row.names()
和 col.names()
两个函数进行的。值得注意的是,不一定一次要全部修改。例如
xxxxxxxxxx
colnames(demo.suspect)[7] <- "is.married"
demo.suspect
修改类型,特指修改列的类型,方法还是使用 as 族函数,通过对一列进行整体操作,例如
xxxxxxxxxx
demo.suspect$age <- as.integer(demo.suspect$age)
demo.suspect$children.count <- as.integer(demo.suspect$children.count)
demo.suspect
修改值的方法和修改类型的方法基本一样,例如如果给 age 整体增 1,可以使用以下方式
xxxxxxxxxx
demo.suspect$age <- demo.suspect$age + 1
demo.suspect
当然也可以对某一个值单独进行赋值,例如将 王五
的 is.married
列的值修改为 T
xxxxxxxxxx
demo.suspect[demo.suspect$name == "王五", "is.married"] <- T
demo.suspect
与增操作类似,一般不对行进行整体的改操作。
删除一行或者一列的方法,等价于选择另外一些行或者列。例如如果要删除 children.count
这一列,或者删除 children.count
这一列的值小于 1 的,只需要选择其他列,即可。
而如果列比较多,要删的列只有一两个,进行查询操作并不是很方便。此时可以使用负整数索引进行删除。例如
xxxxxxxxxx
demo.suspect[-c(4),-c(8)]
此外,如果知道列名, transform
函数也可以用于删除列,只需要将该列的值设置为 NULL
即可
xxxxxxxxxx
transform(demo.suspect, is.married = NULL)
很多时候,不同部分的数据是分别收集的,会形成多个表数据。对于这种情况,在数据处理时,就需要将两张表进行连接。
使用原生 R 的语句可以做到这一点,但是会比较麻烦。这里还是主要介绍 dplyr
包中提供的方法。
先创建一个新的数据集,使用的数据是 R 中自带的,主要是关于美国各州的一些情况。此外还需要用到一个自带的表数据 USArrests
。
xxxxxxxxxx
states.info <- data.frame(name = state.name, abb = state.abb, state.center, area = state.area)
states.arrests <- USArrests[4:20,]
states.arrests$name <- rownames(states.arrests)
head(states.info)
head(states.arrests)
由于
data.frame
是一个特殊的list
,创建data.frame
时也可以直接传入list
数据,该list
中的每一个值的要求,和对其他字段的要求相同。
下面我们需要将这两个表进行关联。如果两个数据是一一对应的,可以直接使用 cbind()
函数。
但这与这种数据,显然不是,就需要使用进行连接操作,使用到的函数主要有四种:
inner_join()
:只保留两者完全匹配到的记录left_join()
:保留左边所有记录right_join()
:保留右边所有记录full_join()
:保留两边所有记录这几个函数接受的参数是一样的。
xxxxxxxxxx
inner_join(states.info, states.arrests, by = c("name"))
xxxxxxxxxx
left_join(states.info, states.arrests, by = c("name" = "name"))
这些函数中,前两个参数显然就是要连接的两张表,第一个参数是左表,第二个参数是右表。参数 by
就是连接条件。
该参数可以有多种形式:
c
函数创建一组键值对,键表示第一张表的连接字段,值表示第二张表的连接字段。NULL
,函数在两张表中寻找同名字段进行连接。连接后的表包含了两张表的所有字段,如果字段有重名,那么会在字段后面加上后缀,默认是 .x
.y
,但是也可以通过 suffix
参数进行控制。这里具体就不再进行演示了。
之前提到,R 语言中执行循环的效率是比较低的,提高效率的方法就是向量化计算,即以向量为单位进行计算。
如果向量之间各元素的操作相同,且比较简单,那么可以直接使用向量运算来进行,这是最为推荐的方式。
但是如果向量计算的操作比较复杂,这时就需要用到 apply
族函数了。这些函数主要包括以下几个:
apply
针对矩阵进行计算lapply
针对列表进行计算sapply
针对列表进行计算,结果会进行自动化简vapply
针对向量进行计算,例如,我们现在有一个 的距离矩阵(这里使用 dist
函数生成,也可以从外部导入),我们需要找到每一列中排名第 20 的距离值。就可以使用 apply 函数进行计算。
xxxxxxxxxx
distance.mat <- as.matrix(dist(states.info[, c("x", "y")]))
dim(distance.mat)
apply(X = distance.mat, MARGIN = 2, FUN = function(d) {
sort(d)[20]
})
该函数具有如下参数:
参数名 | 类型 | 含义 | 默认值 |
---|---|---|---|
X | matrix | 要处理的数据 | |
MARGIN | integer | 处理的维度。取 1 时,表示对每一行进行处理。取 2 时,表示对每一列进行处理。 | |
FUN | function | 执行函数。根据 MARGIN 将每一行或每一列作为第一个参数传入。 | |
... | logical | 执行函数的额外参数。 |
函数在返回时,每一次执行函数的结果进行组合。如果执行的结果是数量,则组合成一个向量。如果执行的结果是向量,则作为一列组合成一个矩阵。
在第一节中,我们读取了一个表数据,但是有些字段应该是浮点数,系统判断成了字符串类型;有些是整数,系统默认为浮点数。
对于这种情况,我们就可以使用 lapply
对其进行转换
xxxxxxxxxx
demo.table <- read.csv("data/LNHP03.csv")
demo.table[,c("X", "Y")] <- lapply(demo.table[,c("X", "Y")], as.numeric)
demo.table[,c("PURCHASE", "FLOORSZ")] <- lapply(demo.table[,c("PURCHASE", "FLOORSZ")], as.integer)
head(demo.table)
如果我们要取 PURCHASE
UNEMPLOY
PROF
三列中各自最大的 6 个值,可以使用如下方法
xxxxxxxxxx
lapply(demo.table[, c("PURCHASE", "FLOORSZ", "PROF")], function (x) {
head(sort(x, decreasing = T))
})
可见 lapply
处理的是列表,返回的也是列表。可以使用 sapply
对输出结果进行自动简化,生成更易于观察的结果
xxxxxxxxxx
sapply(demo.table[, c("PURCHASE", "FLOORSZ", "PROF")], function (x) {
head(sort(x, decreasing = T))
})
对于 vapply
这里就不再过多介绍了,其效果和 lapply
类似,只是接收一个向量。
对于空间数据的操作,可以分成两类:
@data
插槽中的值。坐标系转换,在 ArcGIS 中称作“投影”,就是把空间数据在一个坐标系下的坐标转换为另一种坐标系下的坐标。常用的有几种坐标:
这一操作主要使用 rgdal
包中的 spTransform
进行实现。
xxxxxxxxxx
library(rgdal)
# 读取数据
demo.shp <- readOGR("data/LNHP03.shp")
head(demo.shp@coords)
# 投影转换
demo.shp.wgs84 <- spTransform(demo.shp, CRS("+proj=longlat +datum=WGS84 +no_defs"))
head(demo.shp.wgs84@coords)
该函数接收两个参数,第一个参数是要转换的 Spatial*DataFrame
数据;第二个参数是转换到的坐标系,是 CRS()
函数返回的对象。
函数 CRS()
接收一个 Proj4 格式的字符串,不同坐标系对应的 Proj4 字符串可在 EPSG 官网上进行查询。
关于其他空间操作,详见《R 语言空间数据处理与分析实践教程》。