x <- tribble(
~key, ~val_x,
1, "x1",
2, "x2",
3, "x3"
)Data Analytics Introduction - Part 2
下面将使用 nycflights13 包来介绍关系数据处理。nycflights13 中包含了与 flights 相关的 4 组数据,关系如下图所示:

用于连接每对数据表的变量称为键。键是能唯一标识观测的变量(或变量集合)。简单情况下,单个变量就足以标识一个观测。例如,每架飞机都可以由 tailnum 唯一标识。其他情况可能需要多个变量。例如,要想标识 weather 中的观测,你需要 5 个变量:year、month、day、hour 和 origin。键的类型有两种:
planes$tailnum 是一个主键,因为其可以唯一标识 planes 表中的每架飞机。flights$tailnum 是一个外键,因为其出现在 flights 表中,并可以将每次航班与唯一一架飞机匹配。一个变量既可以是主键,也可以是外键。例如,origin 是 weather 表主键的一部分,同时也是 airports 表的外键。
主键与另一张表中与之对应的外键可以构成关系。关系通常是一对多的。例如,每个航班只有一架飞机,但每架飞机可以飞多个航班。在另一些数据中,你有时还会遇到一对一的关系。你可以将这种关系看作一对多关系的特殊情况。你可以使用多对一关系加上一对多关系来构造多对多关系。例如,在这份数据中,航空公司与机场之间存在着多对多关系:每个航空公司可以使用多个机场,每个机场可以服务多个航空公司。
合并连接可以将两个表格中的变量组合起来,它先通过两个表格的键匹配观测,然后将一个表格中的变量复制到另一个表格中。和 mutate() 函数一样,连接函数也会将变量添加在表格的右侧,因此如果表格中已经有了很多变量,那么新变量就不会显示出来。
为了帮助掌握连接的工作原理,在此介绍用图形来表示连接的一种方法。
有颜色的列表示作为“键”的变量:它们用于在表间匹配行。灰色列表示“值”列,是与键对应的值。
在以下的示例中,虽然键和值都是一个变量,但非常容易推广到多个键变量和多个值变量的情况。
连接是将 x 中每行连接到 y 中 0 行、一行或多行的一种方法。
右图表示出了所有可能的匹配,匹配就是两行之间的交集。

内连接保留同时存在于两个表中的观测,外连接则保留至少存在于一个表中的观测。外连接有 3 种类型。
x 中的所有观测y 中的所有观测x 和 y 中的所有观测这些连接会向每个表中添加额外的“虚拟”观测,这个观测拥有总是匹配的键(如果没有其他键可匹配的话),其值则用 NA 来填充。
最常用的连接是左连接:只要想从另一张表中添加数据,就可以使用左连接,因为它会保留原表中的所有观测,即使它没有匹配。

至今为止,所有图都假设键具有唯一性,但情况并非总是如此。
一张表中具有重复键。通常来说,当存在一对多关系时,如果向表中添加额外信息,就会出现这种情况。
两张表中都有重复键。这通常意味着出现了错误,因为键在任意一张表中都不能唯一标识观测。
当连接这样的重复键时,你会得到所有可能的组合,即笛卡儿积:
筛选连接匹配观测的方式与合并连接相同,但前者影响的是观测,而不是变量。筛选连接有两种类型。
semi_join(x, y):保留 x 表中与 y 表中的观测相匹配的所有观测。anti_join(x, y):丢弃 x 表中与 y 表中的观测相匹配的所有观测。半连接的图形表示如下所示。

重要的是存在匹配,匹配了哪条观测则无关紧要。这说明筛选连接不会像合并连接那样造成重复的行。

半连接的逆操作是反连接。反连接保留 x 表中那些没有匹配 y 表的行。

处理字符串会使用到 stringr 包。
str_length() 函数可以返回字符串中的字符数量:
要想组合两个或更多字符串,可以使用 str_c() 函数:
可以使用 str_sub() 函数来提取字符串的一部分。
注意,即使字符串过短,str_sub() 函数也不会出错,它将返回尽可能多的字符:
利用 str_to_lower() 函数可以将文本转换为小写,利用 str_to_upper() 函数可以将文本转换为大写,利用 str_to_title() 函数可以将文本转换为首字母大写。
大小写转换要比你想象的更复杂,因为不同的语言有不同的转换规则。你可以通过明确区域设置来选择使用哪种规则:
要想确定一个字符向量能否匹配一种模式,可以使用 str_detect() 函数。它返回一个与输入向量具有同样长度的逻辑向量:
str_detect() 函数的一种常见用法是选取出匹配某种模式的元素。你可以通过逻辑取子集方式来完成这种操作,也可以使用便捷的 str_subset() 包装器函数:
要想提取匹配的实际文本,我们可以使用 str_extract() 函数。我们将使用维基百科上的 Harvard sentences 数据集进行测试。
现在我们可以选取出包含一种颜色的句子,再从中提取出颜色,就可以知道有哪些颜色了:
[1] "blue" "blue" "red" "red" "red" "blue"
注意,str_extract() 只提取第一个匹配。这是 stringr 函数的一种通用模式,因为单个匹配可以使用更简单的数据结构。要想得到所有匹配,可以使用 str_extract_all() 函数,它会返回一个列表:
str_replace() 和 str_replace_all() 函数可以使用新字符串替换匹配内容。最简单的应用是使用固定字符串替换匹配内容:
str_split() 函数可以将字符串拆分为多个片段。例如,我们可以将句子拆分成单词:
返回列表的其他 stringr 函数一样,你可以通过设置 simplify = TRUE 返回一个矩阵:
使用 forcats 包可以处理因子,这个包提供了能够处理分类变量的工具,还包括了处理因子的大量辅助函数。
假设我们想要创建一个记录月份的变量:
使用字符串来记录月份有两个问题。
不在有效水平集合内的所有值都会自动转换为 NA:
有时你会想让因子的顺序与初始数据的顺序保持一致。在创建因子时,将水平设置为 unique(x),或者在创建因子后再对其使用 fct_inorder() 函数,就可以达到这个目的:
比修改因子水平顺序更强大的操作是修改水平的值。修改水平最常用、最强大的工具是 fct_recode() 函数,它可以对每个水平进行修改或重新编码。
# A tibble: 3 × 2
partyid n
<fct> <int>
1 Ind,near dem 2499
2 Not str democrat 3690
3 Strong democrat 3490
对水平的描述太过简单,而且不一致。我们将其修改为较为详细的排比结构:
gss_cat |>
mutate(partyid = fct_recode(partyid,
"Republican, strong" = "Strong republican",
"Republican, weak" = "Not str republican",
"Independent, near rep" = "Ind,near rep",
"Independent, near dem" = "Ind,near dem",
"Democrat, weak" = "Not str democrat",
"Democrat, strong" = "Strong democrat"
)) |> count(partyid) |> tail(3)# A tibble: 3 × 2
partyid n
<fct> <int>
1 Independent, near dem 2499
2 Democrat, weak 3690
3 Democrat, strong 3490
fct_recode() 会让没有明确提及的水平保持原样, 如果不小心修改了一个不存在的水平,那么它也会给出警告。
你可以将多个原水平赋给同一个新水平,这样就可以合并原来的分类。使用这种操作时一定要小心:如果合并了原本不同的分类,那么就会产生误导性的结果。
gss_cat |>
mutate(partyid = fct_recode(partyid,
"Republican, strong" = "Strong republican",
"Republican, weak" = "Not str republican",
"Independent, near rep" = "Ind,near rep",
"Independent, near dem" = "Ind,near dem",
"Democrat, weak" = "Not str democrat",
"Democrat, strong" = "Strong democrat",
"Other" = "No answer",
"Other" = "Don't know",
"Other" = "Other party"
)) |> count(partyid)# A tibble: 8 × 2
partyid n
<fct> <int>
1 Other 548
2 Republican, strong 2314
3 Republican, weak 3032
4 Independent, near rep 1791
5 Independent 4119
6 Independent, near dem 2499
7 Democrat, weak 3690
8 Democrat, strong 3490
如果想要合并多个水平,那么可以使用 fct_recode() 函数的变体 fct_collapse() 函数。对于每个新水平,你都可以提供一个包含原水平的向量:
# A tibble: 4 × 2
partyid n
<fct> <int>
1 other 548
2 rep 5346
3 ind 8409
4 dem 7180
ymd() 和类似的其他函数可以创建日期。要想创建日期时间型数据,可以在后面加一个下划线,以及 h、m 和 s 之中的一个或多个字母,这样就可以得到解析日期时间的函数了:
除了单个字符串,日期时间数据的各个成分还经常分布在表格的多个列中。如果想要按照这种表示方法来创建日期或时间,可以使用 make_date() 函数创建日期,使用 make_datetime() 函数创建日期时间:
# A tibble: 336,776 × 6
year month day hour minute departure
<int> <int> <int> <dbl> <dbl> <dttm>
1 2013 1 1 5 15 2013-01-01 05:15:00
2 2013 1 1 5 29 2013-01-01 05:29:00
3 2013 1 1 5 40 2013-01-01 05:40:00
4 2013 1 1 5 45 2013-01-01 05:45:00
5 2013 1 1 6 0 2013-01-01 06:00:00
6 2013 1 1 5 58 2013-01-01 05:58:00
7 2013 1 1 6 0 2013-01-01 06:00:00
8 2013 1 1 6 0 2013-01-01 06:00:00
9 2013 1 1 6 0 2013-01-01 06:00:00
10 2013 1 1 6 0 2013-01-01 06:00:00
# ℹ 336,766 more rows
有时你需要在日期时间型数据和日期型数据之间进行转换,这正是 as_datetime() 和 as_date() 函数的功能:
如果想要提取出日期中的独立成分,可以使用以下访问器函数:year()、month()、mday()(一个月中的第几天)、yday()(一年中的第几天)、wday()(一周中的第几天)、hour() 、minute() 和 second():
对于 month() 和 wday() 函数,你可以设置 label = TRUE 来返回月份名称和星期数的缩写,还可以设置 abbr = FALSE 来返回全名:
[1] Jul
12 Levels: Jan < Feb < Mar < Apr < May < Jun < Jul < Aug < Sep < ... < Dec
通过 floor_date() 、round_date() 和 ceiling_date() 函数将日期舍入到临近的一个时间单位。函数的参数都包括一个待调整的时间向量,以及时间单位名称,函数会将这个向量舍下、入上或四舍五入到这个时间单位。
通过 update() 函数创建一个新日期时间。这样也可以同时设置多个成分:
在 R 中,如果将两个日期相减,那么你会得到不同的对象:
表示时间差别的对象记录时间间隔的单位可以是秒、分钟、小时、日或周。因为这种模棱两可的对象处理起来非常困难,所以 lubridate 提供了总是使用秒为单位的另一种计时对象 - 时期:
时期总是以秒为单位来记录时间间隔。使用标准比率(1 分钟为 60 秒、1 小时为 60 分钟、1 天为 24 小时、1 周为 7 天、1 年为 365 天)将分钟、小时、日、周和年转换为秒,从而建立具有更大值的对象。
可以对时期进行加法和乘法操作:
阶段也是一种时间间隔,但它不以秒为单位;相反,它使用“人工”时间,比如日和月。这使得它们使用起来更加直观:
可以对阶段进行加法和乘法操作:
显然,dyears(1) / ddays(365) 应该返回 1,因为时期总是以秒来表示的,表示 1 年的时期就定义为相当于 365 天的秒数。
那么 years(1) / days(1) 应该返回什么呢?如果年份是 2015 年,那么结果就是 365,但如果年份是 2016 年,那么结果就是 366!没有足够的信息让 lubridate 返回一个明确的结果。lubridate 会给出一个估计值,并给出一条警告:
将函数作为参数传入另一个函数的这种做法是一种非常强大的功能,它是促使 R 成为函数式编程语言的因素之一。使用 purrr 函数代替 for 循环的目的是将常见的列表处理问题分解为独立的几个部分。
先对向量进行循环,然后对其每个元素进行一番处理,最后保存结果。这种模式太普遍了,因此 purrr 包提供了一个函数族来替你完成这种操作。每种类型的输出都有一个相应的函数:
map():用于输出列表map_lgl():用于输出逻辑型向量map_int():用于输出整型向量map_dbl():用于输出双精度型向量map_chr():用于输出字符型向量每个函数都使用一个向量作为输入,并对向量的每个元素应用一个函数,然后返回和输入向量同样长度(同样名称)的一个新向量。向量的类型由映射函数的后缀决定。
可能有些人会告诉你不要使用 for 循环,因为它们很慢。这些人完全错了!至少他们已经赶不上时代了,因为 for 循环已经有很多年都不慢了。使用 map() 函数的主要优势不是速度而是简洁:它们可以让你的代码更易编写,也更易读。
映射函数的重点在于需要执行的操作,而不是在所有元素中循环所需的跟踪记录以及保存结果。
所有 purrr 函数都是用 C 实现的。这使得它们的速度非常快,但牺牲了一些可读性。第二个参数(即 .f ,要应用的函数)可以是一个公式、一个字符向量或一个整型向量。
对于参数 .f,你可以使用几种快捷方式来减少输入量。假设你想对某个数据集中的每个分组都拟合一个线性模型。以下这个示例将 mtcars 数据集拆分成 3 个部分(按照气缸的值分类),对每个部分拟合一个线性模型:
当检查多个模型时,有时你会需要提取出像 \(R^2\) 这样的摘要统计量。要想完成这个任务,需要先运行 summary() 函数,然后提取出结果中的 r.squared。我们可以使用匿名函数的快捷方式来完成这个操作:
因为提取命名成分的这种操作非常普遍,所以 purrr 提供了一种更为简洁的快捷方式:使用字符串。
当使用映射函数重复多种操作时,某次操作失败的概率会大大增加。当这种情况发生时,你不仅会收到一条错误消息,而且不会得到任何结果。safely() 是一个修饰函数(副词),它接受一个函数(动词),对其进行修改并返回修改后的函数。这样一来,修改后的函数就不会抛出错误。相反,它总是会返回由以下两个元素组成的一个列表。
result:原始结果。如果出现错误则为 NULL。error:错误对象。如果操作成功则为 NULL。我们使用一个简单的 log 函数来进行说明:
当函数成功运行时,result 元素中包含原始结果,error 元素的值是 NULL;当函数运行失败时,result 元素的值是 NULL ,error 元素中包含错误对象。
safely() 也可以与 map() 函数共同使用:
List of 3
$ :List of 2
..$ result: num 0
..$ error : NULL
$ :List of 2
..$ result: num 2.3
..$ error : NULL
$ :List of 2
..$ result: NULL
..$ error :List of 2
.. ..$ message: chr "non-numeric argument to mathematical function"
.. ..$ call : language .f(...)
.. ..- attr(*, "class")= chr [1:3] "simpleError" "error" "condition"
如果将以上结果转换为两个列表,一个列表包含所有错误对象,另一个列表包含所有原始结果,那么处理起来就会更加容易。可以使用 purrr::transpose() 函数轻松完成这个任务:
purrr 还提供了另外两个有用的修饰函数。
safely() 类似,possibly() 函数也总是会成功返回。它比 safely() 还要简单一些,因为可以设定出现错误时返回一个默认值:quietly() 函数与 safely() 的作用基本相同,但前者的结果中不包含错误对象,而是包含输出、消息和警告:迄今为止,我们的映射函数都是对单个输入进行映射。但我们经常会有多个相关的输入需要同步迭代,这就是 map2() 和 pmap() 函数的用武之地。例如,假设你想模拟几个均值不同的随机正态分布,我们已经知道了如何使用 map() 函数来完成这个任务:
但是这种方法很难让人理解代码的本意。相反,我们应该使用 map2() 函数,它可以对两个向量进行同步迭代:
List of 3
$ : num [1:5] 4.81 3.45 7.61 5.2 4.53
$ : num [1:5] 2.33 16.7 9.76 3.39 3.94
$ : num [1:5] -6.68 5.32 -5.29 -9.33 9.93
map2() 函数可以生成以下一系列函数调用:

注意,每次调用时值发生变化的参数(这里是 mu 和 sigma)要放在映射函数(这里是 rnorm)的前面,值保持不变的参数(这里是 n )要放在映射函数的后面。
还有一种更复杂的情况:不但传给函数的参数不同,甚至函数本身也是不同的。为了处理这种情况,你可以使用 invoke_map() 函数:

第一个参数是一个函数列表或包含函数名称的字符向量。第二个参数是列表的一个列表,其中给出了要传给各个函数的不同参数。随后的参数要传给每个函数。
如果调用函数的目的是利用其副作用,而不是返回值时,那么就应该使用游走函数,而不是映射函数。通常来说,使用这个函数的目的是在屏幕上提供输出或者将文件保存到磁盘 - 重要的是操作过程,而不是返回值。以下是一个非常简单的示例:
一般来说,walk() 函数不如 walk2() 和 pwalk() 实用。 例如,如果有一个图形列表和一个文件名向量,那么你就可以使用 pwalk() 将每个文件保存到相应的磁盘位置:
walk()、 walk2() 和 pwalk() 都会隐式地返回 .x,即第一个参数。这使得它们非常适用于管道操作。
keep() 和 discard() 函数可以分别保留输入中预测值为 TRUE 和 FALSE 的元素:
'data.frame': 150 obs. of 1 variable:
$ Species: Factor w/ 3 levels "setosa","versicolor",..: 1 1 1 1 1 1 1 1 1 1 ...
'data.frame': 150 obs. of 4 variables:
$ Sepal.Length: num 5.1 4.9 4.7 4.6 5 5.4 4.6 5 4.4 4.9 ...
$ Sepal.Width : num 3.5 3 3.2 3.1 3.6 3.9 3.4 3.4 2.9 3.1 ...
$ Petal.Length: num 1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 ...
$ Petal.Width : num 0.2 0.2 0.2 0.2 0.2 0.4 0.3 0.2 0.2 0.1 ...
some() 和 every() 函数分别用来确定预测值是否对某个元素为真以及是否对所有元素为真:
head_while() 和 tail_while() 分别从向量的开头和结尾找出预测值为真的元素:
# A tibble: 2 × 4
name age sex treatment
<chr> <dbl> <chr> <chr>
1 John 30 M <NA>
2 Mary NA F A
或者你想要找出一张向量列表中的向量间的交集:
版权所有 © 范叶亮 Leo Van