R语言编程:基于tidyverse
上QQ阅读APP看书,第一时间看更新

1.3 数据结构:列表、数据框、因子

1.3.1 列表

列表(list)可以包含不同类型的对象,甚至可以包括其他列表。列表的灵活性使得它非常有用。

例如,用R拟合一个线性回归模型,其返回结果就是一个列表,其中包含了线性回归的详细结果,如线性回归系数(数值向量)、残差(数值向量)、QR分解(包含一个矩阵和其他对象的列表)等。因为这些结果全都被打包到一个列表中,就可以很方便地提取所需信息,而无须每次调用不同的函数。

列表最大的好处就是能够将多个不同类型的对象打包到一起,以便可以根据位置和名字访问它们。

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”

可以在创建列表时,为列表的每个成分指定名字:

l1 = list(A = 1, B = c(TRUE, FALSE), C = c(“a”, “b”, “c”))
l1
## $A
## [1] 1
##
## $B
## [1]  TRUE FALSE
##
## $C
## [1] “a” “b” “c”

也可以创建列表后再对列表成分命名或修改名字:

names(l1) = NULL      # 移除列表成分的名字
names(l1) = c(“x”,”y”,”z”)

2.从列表中提取成分的内容

提取列表中某成分下的内容,最常用的方法是用$,通过成分名字来提取该成分下的内容:

l1$y
l1$m                 # 访问不存在的成分m, 将会返回NULL

也可以用[[n]]来提取列表第n个成分的内容,n也可以换成成分的名字:

l1[[2]]              # 同l1[[“y”]]

[[]]提取列表中某个成分的内容则更加灵活,可用在函数调用中,通过参数来传递成分的名字或索引:

p = “y”              #想要提取其内容的成分名字
l1[[p]]

3.提取列表子集

R语言也经常需要从列表中提取多个成分及其内容,由这些成分组成的列表构成了原列表的一个子集。

就像提取向量和矩阵的子集一样,提取一个列表子集是用[],可以取出列表中的一些成分,作为一个新的列表。

[]中可以用字符向量表示成分名字,用数值向量表示成分位置,或用逻辑向量指定是否选择来取出列表成分。

l1[“x”]                    # 同l1[1]
l1[c(“x”, “z”)]            # 同l1[c(1, 3)], l1[c(TRUE, FALSE, TRUE)]

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

总之,[]用于提取对象的子集,类型仍是该对象;[[]]用于提取对象的内容(即下一级元素)。

4.为列表的成分赋值

即先访问(提取)到列表的成分,再赋以相应的值。注意,若给一个不存在的成分赋值,列表会自动地在对应名称或位置下增加一个新成分。

l1$x = 0   # 将列表的成分x赋值为0

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

l1[c(“x”, “y”)] = list(x = “new value for y”, y = c(3, 1))

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

l1[c(“z”, “m”)] = NULL

5.列表函数

用函数as.list()可将向量转换成列表:

l2 = as.list(c(a = 1, b = 2))
l2
## $a
## [1] 1
##
## $b
## [1] 2

通过去列表化函数unlist()可将一个列表打破成分界线,强制转换成一个向量[6]


[6]  若列表的成分具有不同类型,则自动向下兼容到同一类型。

unlist(l2)
## a b
## 1 2

为了方便操作列表,tidyverse系列中的purrr包提供了一系列列表相关的函数,建议读者查阅并使用。

pluck():同[[]]提取列表中的元素。

keep():保留满足条件的元素。

discard():删除满足条件的元素。

compact():删除列表中的空元素。

append():在列表末尾增加元素。

flatten():摊平列表(只摊平一层)。

1.3.2 数据框(数据表)

R语言中用于统计分析的样本数据,都是按数据框类型操作的。

数据框是指有若干行和列的数据集,它与矩阵类似,但并不要求所有列都是相同的类型。本质上讲,数据框就是一个列表,它的每个成分都是一个向量,并且长度相同,以表格的形式展现。总之,数据框是由列向量组成、有着矩阵形式的列表

数据框与常见的数据表是一致的:每一列代表一个变量属性,每一行代表一条样本数据。以表1.1所示的数据表为例。

表1.1 数据表示例

R中自带的数据框是data.frame,建议改用更现代的数据框:tibble[7]


[7]  读者若习惯用R自带的data.frame,只需要换个名字,将tibble改为data.frame即可。

Hadley在tibble包中引入了一种tibble数据框,以代替data.frame,而且tidyverse包都是基于tibble数据框的。

tibble对比data.frame的优势如下所示。

tibble()data.frame()做的更少:不改变输入变量的类型(注:R 4.0.0之前默认将字符串转化为因子),不会改变变量名,不会创建行名。

tibble对象的列名可以是R中的“非法名”:非字母开头、包含空格,但定义和使用变量时都需要用反引号`括起来。

tibble在输出时不自动显示所有行,避免数据框较大时显示出很多内容。

[]选取列子集时,即使只选取一列,返回结果仍是tibble,而不自动简化为向量。

1.创建数据框

tibble()根据若干列向量创建tibble

library(tidyverse)        # 或tibble
persons = tibble(
Name = c(«Ken», «Ashley», «Jennifer»),
Gender = c(«Male», «Female», «Female»),
Age = c(24, 25, 23),
Major = c(«Finance», «Statistics», «Computer Science»))
persons
## # A tibble: 3 x 4
##   Name     Gender   Age Major
##   <chr>    <chr>  <dbl> <chr>
## 1 Ken      Male      24 Finance
## 2 Ashley   Female    25 Statistics
## 3 Jennifer Female    23 Computer Science

tribble()通过按行录入数据的方式创建tibble

tribble(
~Name, ~Gender, ~Age, ~Major,
«Ken», «Male», 24, «Finance»,
«Ashley», «Female», 25, «Statistics»,
«Jennifer», «Female», 23, «Computer Science»)

as_tibble()可将data.framematrix这种各成分等长度的list转换为tibble

将不等长的列表转化为数据框:

a = list(A = c(1, 3, 4), B = letters[1:4])
a
## $A
## [1] 1 3 4
##
## $B
## [1] “a” “b” “c” “d”
# lengths()获取list中每个元的长度
map_dfc(a, `length<-`, max(lengths(a)))   # map循环参阅1.6.2节
## # A tibble: 4 x 2
##       A B
##   <dbl> <chr>
## 1     1 a
## 2     3 b
## 3     4 c
## 4    NA d

数据框既是列表的特例,也是广义的矩阵,因此访问这两类对象的方式都适用于数据框。例如与矩阵类似,对数据框的各列重命名,代码如下:

df = tibble(id = 1:4,
level = c(0, 2, 1, -1),
score = c(0.5, 0.2, 0.1, 0.5))
names(df) = c(“id”, “x”, “y”)
df
## # A tibble: 4 x 3
##      id     x     y
##   <int> <dbl> <dbl>
## 1     1     0   0.5
## 2     2     2   0.2
## 3     3     1   0.1
## 4     4    -1   0.5

2.提取数据框的元素、子集

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

(1)以列表方式提取数据框的元素、子集

若把数据框看作由向量组成的列表,则可以沿用列表的操作方式来提取元素或构建子集。例如,可以用 $ 按列名来提取某一列的值,或者用[[]]按照位置或列名提取。

例如,提取列名为x的值,并得到向量:

df$x                  # 同df[[“x”]], df[[2]]
## [1]  0  2  1 -1

以列表形式构建子集完全适用于数据框,同时也会生成一个新的数据框。提取子集的操作符[]允许用数值向量表示列的位置,用字符向量表示列名,或用逻辑向量指定是否选择。

例如,提取数据框的一列或多列,可以得到子数据框:

df[1]                 # 提取第1列, 同df[“id”]
## # A tibble: 4 x 1
##      id
##   <int>
## 1     1
## 2     2
## 3     3
## 4     4
df[1:2]               # 同df[c(“id”,”x”)], df[c(TRUE,TRUE,FALSE)]
## # A tibble: 4 x 2
##      id     x
##   <int> <dbl>
## 1     1     0
## 2     2     2
## 3     3     1
## 4     4    -1
(2)以矩阵方式提取数据框的元素、子集

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

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

若行选择器为空,则只选择列(所有行):

df[, “x”]
## # A tibble: 4 x 1
##       x
##   <dbl>
## 1     0
## 2     2
## 3     1
## 4    -1
df[, c(“x”,”y”)]   # 同df[,2:3]
## # A tibble: 4 x 2
##       x     y
##   <dbl> <dbl>
## 1     0   0.5
## 2     2   0.2
## 3     1   0.1
## 4    -1   0.5

若列选择器为空,则只选择行(所有列):

df[c(1,3),]
## # A tibble: 2 x 3
##      id     x     y
##   <int> <dbl> <dbl>
## 1     1     0   0.5
## 2     3     1   0.1

同时选择行和列:

df[1:3, c(“id”,”y”)]
## # A tibble: 3 x 2
##      id     y
##   <int> <dbl>
## 1     1   0.5
## 2     2   0.2
## 3     3   0.1

根据条件筛选数据。例如用y >= 0.5筛选df的行,并选择idy两列:

df[df$y >= 0.5, c(“id”,”y”)]
## # A tibble: 2 x 2
##      id     y
##   <int> <dbl>
## 1     1   0.5
## 2     4   0.5

按列名属于集合 {x, y, w} 来筛选df的列,并选择前两行:

ind = names(df) %in% c(“x”,”y”,”w”)
df[1:2, ind]
## # A tibble: 2 x 2
##       x     y
##   <dbl> <dbl>
## 1     0   0.5
## 2     2   0.2

3.给数据框赋值

给数据框赋值就是选择要赋值的位置,再准备好同样大小且格式匹配的数据,赋值给那些位置即可,同样有列表方式和矩阵方式。

(1)以列表方式给数据框赋值

$[[ ]]对数据框的某列赋值

df$y = c(0.6,0.3,0.2,0.4)   # 同df[[“y”]] = c(0.6,0.3,0.2,0.4)

利用现有列,创建(计算)新列:

df$z = df$x + df$y
df
## # A tibble: 4 x 4
##      id     x     y     z
##   <int> <dbl> <dbl> <dbl>
## 1     1     0   0.5   0.5
## 2     2     2   0.2   2.2
## 3     3     1   0.1   1.1
## 4     4    -1   0.5  -0.5
df$z = as.character(df$z)   # 转换列的类型
df
## # A tibble: 4 x 4
##      id     x     y z
##   <int> <dbl> <dbl> <chr>
## 1     1     0   0.5 0.5
## 2     2     2   0.2 2.2
## 3     3     1   0.1 1.1
## 4     4    -1   0.5 -0.5

[]可以对数据框的一列或多列进行赋值:

df[“y”] = c(0.8,0.5,0.2,0.4)
df[c(“x”, “y”)] = list(c(1,2,1,0), c(0.1,0.2,0.3,0.4))
(2)以矩阵方式给数据框赋值

以列表方式对数据框进行赋值时,也是只能访问列。若需要更加灵活地进行赋值操作,可以通过矩阵方式进行。

df[1:3,”y”] = c(-1,0,1)
df[1:2,c(“x”,”y”)] = list(c(0,0), c(0.9,1.0))

4.一些有用的函数

把函数str()glimpse()作用在R对象上,可以显示该对象的结构:

str(persons)
## tibble [3 x 4] (S3: tbl_df/tbl/data.frame)
##  $ Name  : chr [1:3] “Ken” “Ashley” “Jennifer”
##  $ Gender: chr [1:3] “Male” “Female” “Female”
##  $ Age   : num [1:3] 24 25 23
##  $ Major : chr [1:3] “Finance” “Statistics” “Computer Science”

summary()作用在数据框或列表上,将生成各列或各成分的汇总信息:

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

我们经常需要将多个数据框(或矩阵)按行或按列进行合并。用函数rbind()增加行(样本数据),要求宽度(列数)相同;用cbind()函数增加列(属性变量),要求高度(行数)相同。

例如,向persons数据框中添加一个新记录:

rbind(persons,
tibble(Name = «John», Gender = «Male»,
Age = 25, Major = «Statistics»))
## # A tibble: 4 x 4
##   Name     Gender   Age Major
##   <chr>    <chr>  <dbl> <chr>
## 1 Ken      Male      24 Finance
## 2 Ashley   Female    25 Statistics
## 3 Jennifer Female    23 Computer Science
## 4 John     Male      25 Statistics

persons数据框中添加两个新列,分别表示每个人是否已注册及其手头的项目数量:

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

1.3.3 因子

数据(变量)可划分为:定量数据(数值型)、定性数据(分类型),定性数据又分为名义型(无好坏顺序之分,如性别)、有序型(有好坏顺序之分,如疗效)。

R提供了因子(factor)这一数据结构(容器),专门用来存放名义型和有序型的分类变量。因子本质上是一个带有水平(level)属性的整数向量,其中“水平”是指事前确定可能取值的有限集合。例如,性别有两个水平属性:男、女。

直接用字符向量也可以表示分类变量,但它只有字母顺序,不能规定想要的顺序,也不能表达有序分类变量。所以,有必要把字符型的分类变量转化为因子型,这更便于对其做后续描述汇总、可视化、建模等。

1.创建与使用因子

函数factor()用来创建因子,基本格式为:

factor(x, levels, labels, ordered, ...)

x:为创建因子的数据向量。

levels:指定因子的各水平值,默认为x中不重复的所有值。

labels:设置各水平名称(前缀),与水平名称一一对应。

ordered:设置是否对因子水平排序,默认FALSE为无序因子,TRUE为有序因子。

该函数还包含参数exclude:指定有哪些水平是不需要的(设为NA);nmax用于设定水平数的上限。

若不指定参数levels,则因子水平默认按字母顺序。

比如,现有6个人的按等级划分的成绩数据,先以字符向量创建,并对其排序:

x = c(“优”, “中”, “良”, “优”, “良”, “良”)     # 字符向量
x
## [1] “优” “中” “良” “优” “良” “良”
sort(x)                                       # 排序是按字母顺序
## [1] “良” “良” “良” “优” “优” “中”

它的顺序只能是字母顺序,如果想规定顺序:中、良、优,正确的做法就是创建因子,用levels指定想要的顺序:

x1 = factor(x, levels = c(“中”, “良”, “优”))  # 转化因子型
x1
## [1]优 中 良 优 良 良
## Levels: 中 良 优
as.numeric(x1)                                # x的存储形式: 整数向量
## [1] 3 1 2 3 2 2

注意,不能直接将因子数据当字符型操作,需要用as.character()转化。

转化为因子型后,数据向量显示出来(外在表现)与原来是一样的,但这些数据的内在存储已经变了。因子的内在存储与外在表现如图1.8所示。因子型是以整数向量存储的,将各水平值按照规定的顺序分别对应到整数,将原向量的各个值分别用相应的整数存储,输出和使用的时候再换回对应的水平值。整数是有顺序的,这样就相当于在不改变原数据的前提下规定了顺序,同时也节省了存储空间。

图1.8 因子的内在存储与外在表现

注意,标签(labels)是因子水平(levels)的别名。

变成因子型后,无论是排序、统计频数、绘图等,都有了顺序:

sort(x1)
## [1]中 良 良 良 优 优
## Levels: 中 良 优
table(x1)
## x1
## 中 良 优
##  1  3  2
ggplot(tibble(x1), aes(x1)) +
geom_bar()

所生成的条形图结果如图1.9所示,x轴的条形顺序是想要的中、良、优。

图1.9 用因子控制条形顺序

levels()函数可以访问或修改因子的水平值,这也将改变数据的外在表现:

levels(x1) = c(“Fair”, “Good”, “Excellent”)    # 修改因子水平
x1
## [1] Excellent Fair      Good      Excellent Good      Good
## Levels: Fair Good Excellent

有时候你可能更希望让水平的顺序与其在数据集中首次出现的次序相匹配,这时可以设置参数levels = unique(x)

转化为因子型的另一个好处是,可以“识错”:因子数据只认识出现在水平值中的值,对于未出现在水平值中的值将被识别为NA

很多人将因子固有的顺序与有序因子混淆,二者不是一回事:上述反复提到的顺序,可称为因子固有的顺序,正是有了它,才能方便地按想要的顺序进行排序、统计频数、绘图等;而无序因子与有序因子,是与变量本身的数据类型相对应的,名义型(无顺序好坏之分的分类变量)用无序因子存放,有序型(有顺序好坏之分的分类变量)用有序因子存放,该区分是用于不同类型的数据,建模时适用不同的模型。

示例的成绩数据是有好坏之分的,因此创建为有序因子:

x2 = factor(x, levels = c(“中”, “良”, “优”), ordered = TRUE)
x2
## [1]优 中 良 优 良 良
## Levels: 中 < 良 < 优

如果对x2做排序、统计频数、绘图,你会发现与使用无序因子时没有任何区别,它们的区别体现在对其建模时所适用的模型不同。

2.有用函数

函数table()可以统计因子各水平的出现次数(频数),也可以统计向量中每个不同元素的出现次数,其返回结果为命名向量。

table(x)
## x
## 良 优 中
##  3  2  1

函数cut(),用来做连续变量离散化:将数值向量切分为若干区间段,并返回因子。基本格式为:

cut(x, breaks, labels, ...)

x:要切分的数值向量。

breaks:切分的界限值构成的向量,或表示切分段数的整数。

该函数还包含参数right用于设置区间段是否左开右闭,include.lowest用于设置是否包含下界,ordered_result用于设置是否对结果因子排序。

Age = c(23,15,36,47,65,53)
cut(Age, breaks = c(0,18,45,100),
labels = c(«Young»,»Middle»,»Old»))
## [1] Middle Young  Middle Old    Old    Old
## Levels: Young Middle Old

函数gl()用来生成有规律的水平值组合因子。对于多因素试验设计,用该函数可以生成多个因素完全组合,基本格式为:

gl(n, k, length, labels, ordered, ...)

n:为因子水平个数。

k:为同一因子水平连续重复次数。

length:为总的元素个数,默认为n*k,若不够则自动重复。

labels:设置因子水平值。

ordered:设置是否为有序,默认为FALSE

tibble(
Sex = gl(2, 3, length = 12, labels = c(«男”,”女”)),
Class = gl(3, 2, length = 12, labels = c(«甲”,”乙”,”丙”)),
Score = gl(4, 3, length = 12, labels = c(«优”,”良”,”中”, “及格”)))
## # A tibble: 12 x 3
##   Sex   Class Score
##   <fct> <fct> <fct>
## 1男    甲    优
## 2男    甲    优
## 3男    乙    优
## 4女    乙    良
## 5女    丙    良
## 6女    丙    良
## # ... with 6 more rows

3.forcats包

tidyverse系列中的forcats包是专门为处理因子型数据而设计的,forcats包提供了一系列操作因子的方便函数。

as_factor():转化为因子,默认按水平值的出现顺序。

fct_count():计算因子各水平频数、占比,可按频数排序。

fct_c():合并多个因子的水平。

改变因子水平的顺序。

fct_relevel():手动对水平值重新排序。

fct_infreq():按高频优先排序。

fct_inorder():按水平值出现的顺序排序。

fct_rev():将顺序反转。

fct_reorder():根据其他变量或函数结果排序(绘图时有用)。

修改水平。

fct_recode():对水平值逐个重编码。

fct_collapse():按自定义方式合并水平。

fct_lump_*():将多个频数小的水平合并为其他。

fct_other():将保留之外或丢弃的水平合并为其他。

增加或删除水平。

fct_drop():删除若干水平。

fct_expand:增加若干水平。

fct_explicit_na():为NA设置水平。

读者需要明白这样一个基本逻辑:操作因子是操作一个向量,该向量更多的时候是以数据框的一列的形式存在的。我们来演示一下更常用的操作数据框中的因子列的方法,这会涉及数据操作和绘图的语法,这部分知识在第2~3章才会讲到。你只需要知道大意并理解因子操作部分即可。

mpg列是汽车数据集,class列是分类变量车型,先统计各种车型的频数,共有7类;对该列做因子合并,合并为5类+Other类,再统计频数,这里将频数少的类合并为Other类:

count(mpg, class)
## # A tibble: 7 x 2
##   class          n
##   <chr>      <int>
## 1 2seater        5
## 2 compact       47
## 3 midsize       41
## 4 minivan       11
## 5 pickup        33
## 6 subcompact    35
## # ... with 1 more row
mpg1 = mpg %>%
mutate(class = fct_lump(class, n = 5))
count(mpg1, class)
## # A tibble: 6 x 2
##   class          n
##   <fct>      <int>
## 1 compact       47
## 2 midsize       41
## 3 pickup        33
## 4 subcompact    35
## 5 suv           62
## 6 Other         16

若直接对class各类绘制条形图,是按水平顺序,此时频数会参差不齐;改用根据频数多少进行排序,则条形图变得整齐易读,对比效果见图1.10。

p1 = ggplot(mpg, aes(class)) +
geom_bar() +
theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1))
p2 = ggplot(mpg, aes(fct_infreq(class))) +
geom_bar() +
theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1))
library(patchwork)
p1 | p2

图1.10 按频数排序的条形图