使用dplyr处理关系数据
往期文章
《R数据科学》学习笔记|Note2:使用ggplot2进行数据可视化(上)
《R数据科学》学习笔记|Note3:使用ggplot2进行数据可视化(下)
《R数据科学》学习笔记|Note4:使用dplyr进行数据转换(上)
《R数据科学》学习笔记|Note5:使用dplyr进行数据转换(下)
《R数据科学》学习笔记|Note6:使用tibble实现简单数据框
文章目录
8.1 简介
只涉及一张数据表的数据分析是非常罕见的。存在于多个表中的这种数据统称为关系数据,因为重要的是数据间的关系,而不是单个数据集。 关系总是定义于两张表之间。其他所有关系都是建立在这种简单思想之上:三张或更多表之间的关系总是可以用每两个表之间关系表示出来。 要想处理关系数据,你需要能够在两张表之间进行的操作。
- 合并连接:向数据框中加入新变量,新变量的值是另一个数据框中的匹配观测。
- 筛选连接:根据是否匹配另一个数据框中的观测,筛选数据框中的观测。
- 集合操作:将观测作为集合元素来处理。
关系数据最常见于关系数据库管理系统(relational database management system,RDBMS),该系统几乎囊括了所有的现代数据库。如果之前使用过数据库,那你肯定使用过 SQL
。一般来说,dplyr
要比 SQL
更容易使用,因为前者是专门用于进行数据分析的。 在进行常用的数据分析操作时,dplyr
非常得心应手,反之,它并不擅长数据分析中不常用的那些操作。
8.2 nycflights13
我们将使用 nycflights13
包来学习关系数据。nycflights13
中包含了与 flights
相关的 4 个 tibble,我们已经在之前中使用过 flights
表了。
a'a'a'a
airports
:给出了每个机场的信息,通过faa
机场编码进行标识。
> airports
# A tibble: 1,458 x 8
faa name lat lon alt tz dst tzone
<chr> <chr> <dbl> <dbl> <dbl> <dbl> <chr> <chr>
1 04G Lansdowne Airport 41.1 -80.6 1044 -5 A America/New_~
2 06A Moton Field Municipal~ 32.5 -85.7 264 -6 A America/Chic~
3 06C Schaumburg Regional 42.0 -88.1 801 -6 A America/Chic~
4 06N Randall Airport 41.4 -74.4 523 -5 A America/New_~
5 09J Jekyll Island Airport 31.1 -81.4 11 -5 A America/New_~
6 0A9 Elizabethton Municipa~ 36.4 -82.2 1593 -5 A America/New_~
7 0G6 Williams County Airpo~ 41.5 -84.5 730 -5 A America/New_~
8 0G7 Finger Lakes Regional~ 42.9 -76.8 492 -5 A America/New_~
9 0P2 Shoestring Aviation A~ 39.8 -76.6 1000 -5 U America/New_~
10 0S9 Jefferson County Intl 48.1 -123. 108 -8 A America/Los_~
# ... with 1,448 more rows
planes
:给出了每架飞机的信息,通过tailnum
进行标识。
> planes
# A tibble: 3,322 x 9
tailnum year type manufacturer model engines seats speed engine
<chr> <int> <chr> <chr> <chr> <int> <int> <int> <chr>
1 N10156 2004 Fixed win~ EMBRAER EMB-1~ 2 55 NA Turbo~
2 N102UW 1998 Fixed win~ AIRBUS INDUST~ A320-~ 2 182 NA Turbo~
3 N103US 1999 Fixed win~ AIRBUS INDUST~ A320-~ 2 182 NA Turbo~
4 N104UW 1999 Fixed win~ AIRBUS INDUST~ A320-~ 2 182 NA Turbo~
5 N10575 2002 Fixed win~ EMBRAER EMB-1~ 2 55 NA Turbo~
6 N105UW 1999 Fixed win~ AIRBUS INDUST~ A320-~ 2 182 NA Turbo~
7 N107US 1999 Fixed win~ AIRBUS INDUST~ A320-~ 2 182 NA Turbo~
8 N108UW 1999 Fixed win~ AIRBUS INDUST~ A320-~ 2 182 NA Turbo~
9 N109UW 1999 Fixed win~ AIRBUS INDUST~ A320-~ 2 182 NA Turbo~
10 N110UW 1999 Fixed win~ AIRBUS INDUST~ A320-~ 2 182 NA Turbo~
# ... with 3,312 more rows
weather
:给出了纽约机场每小时的天气状况。
> weather
# A tibble: 26,115 x 15
origin year month day hour temp dewp humid wind_dir wind_speed
<chr> <int> <int> <int> <int> <dbl> <dbl> <dbl> <dbl> <dbl>
1 EWR 2013 1 1 1 39.0 26.1 59.4 270 10.4
2 EWR 2013 1 1 2 39.0 27.0 61.6 250 8.06
3 EWR 2013 1 1 3 39.0 28.0 64.4 240 11.5
4 EWR 2013 1 1 4 39.9 28.0 62.2 250 12.7
5 EWR 2013 1 1 5 39.0 28.0 64.4 260 12.7
6 EWR 2013 1 1 6 37.9 28.0 67.2 240 11.5
7 EWR 2013 1 1 7 39.0 28.0 64.4 240 15.0
8 EWR 2013 1 1 8 39.9 28.0 62.2 250 10.4
9 EWR 2013 1 1 9 39.9 28.0 62.2 260 15.0
10 EWR 2013 1 1 10 41 28.0 59.6 260 13.8
# ... with 26,105 more rows, and 5 more variables: wind_gust <dbl>,
# precip <dbl>, pressure <dbl>, visib <dbl>, time_hour <dttm>
展示不同数据表之间关系的一种方法是绘制图形。
这个图有点让人眼花缭乱,但跟实际工作中的一些图比起来,可以说是相当简洁了。理解这种图的关键是,记住每种关系只与两张表有关。不需要弄清楚所有的事情,只要明白你所关心的表格间的关系即可。 对于 nycflights13
包中的表来说:
flights
与planes
通过单变量tailnum
相连;flights
与airlines
通过变量carrier
相连;flights
与airports
通过两种方式相连(变量origin
和dest
);flights
与weather
通过变量origin
(位置)以及year
、month
、day
和hour
(时间)相连。
8.3 键
用于连接每对数据表的变量称为键。键是能唯一标识观测的变量(或变量集合)。简单情况下,单个变量就足以标识一个观测。例如,每架飞机都可以由 tailnum
唯一标识。其 他情况可能需要多个变量。例如,要想标识 weather
中的观测,你需要 5 个变量:year
、 month
、day
、hour
和 origin
。
键的类型有两种。
- **主键:**唯一标识其所在数据表中的观测。例如,
planes$tailnum
是一个主键,因为其可以唯一标识planes
表中的每架飞机。 - **外键:**唯一标识另一个数据表中的观测。例如,
flights$tailnum
是一个外键,因为其出现在flights
表中,并可以将每次航班与唯一一架飞机匹配。
一个变量既可以是主键,也可以是外键。例如,origin
是 weather
表主键的一部分,同时也是 airports
表的外键。 一旦识别出表的主键,最好验证一下,看看它们能否真正唯一标识每个观测。一种验证方法是对主键进行 count()
操作,然后查看是否有 n 大于 1 的记录:
planes %>%
count(tailnum) %>% #计算每个tialum的飞机个数
filter(n > 1) #筛选出个数多于 1 的
> planes %>%
+ count(tailnum) %>%
+ filter(n > 1)
# A tibble: 0 x 2
# ... with 2 variables: tailnum <chr>, n <int>
weather %>%
count(year, month, day, hour, origin) %>%
filter(n > 1)
> weather %>%
+ count(year, month, day, hour, origin) %>%
+ filter(n > 1)
# A tibble: 3 x 6
year month day hour origin n
<int> <int> <int> <int> <chr> <int>
1 2013 11 3 1 EWR 2
2 2013 11 3 1 JFK 2
3 2013 11 3 1 LGA 2
有时数据表没有明确的主键:每行都是一个观测,但没有一个变量组合能够明确地标识它。例如,flights
表中的主键是什么?你可能认为是日期加航班号或者是日期加机尾编 号,但这两种组合都不是唯一标识。
如果一张表没有主键,有时就需要使用 mutate()
函数和 row_number()
函数为表加上一个主键。这样一来, 如果你完成了一些筛选工作,并想要使用原始数据检查的话,就可以更容易地匹配观测。 这种主键称为代理键。
主键与另一张表中与之对应的外键可以构成关系。关系通常是一对多的。例如,每个航班 只有一架飞机,但每架飞机可以飞多个航班。在另一些数据中,你有时还会遇到一对一的 关系。你可以将这种关系看作一对多关系的特殊情况。你可以使用多对一关系加上一对多关系来构造多对多关系。例如,在这份数据中,航空公司与机场之间存在着多对多关系:每个航空公司可以使用多个机场,每个机场可以服务多个航空公司。
8.4 合并连接
本节将介绍用于组合两个表格的第一种工具,即合并连接。合并连接可以将两个表格中的变量组合起来,它先通过两个表格的键匹配观测,然后将一个表格中的变量复制到另一个 表格中。
和 mutate()
函数一样,连接函数也会将变量添加在表格的右侧,因此如果表格中已经有了很多变量,那么新变量就不会显示出来。为了解决这个问题,我们建立一个简化的数据集,以便更易看到示例数据集中发生的变化:
flights2 <- flights %>%
select(year:day, hour, origin, dest, tailnum, carrier)
#如果报错则用下面这个代码
flights2 <- flights %>%
dplyr::select(year:day, hour, origin, dest, tailnum, carrier)
'''
dplyr::select这是因为不同的包之间关于select方法有冲突。
library(conflicted) 可以清楚的告诉你是哪些包之间有冲突
你可以用包的名字::方法名字来解决这个问题如:dplyr::select
'''
flights2
> flights2
# A tibble: 336,776 x 8
year month day hour origin dest tailnum carrier
<int> <int> <int> <dbl> <chr> <chr> <chr> <chr>
1 2013 1 1 5 EWR IAH N14228 UA
2 2013 1 1 5 LGA IAH N24211 UA
3 2013 1 1 5 JFK MIA N619AA AA
4 2013 1 1 5 JFK BQN N804JB B6
5 2013 1 1 6 LGA ATL N668DN DL
6 2013 1 1 5 EWR ORD N39463 UA
7 2013 1 1 6 EWR FLL N516JB B6
8 2013 1 1 6 LGA IAD N829AS EV
9 2013 1 1 6 JFK MCO N593JB B6
10 2013 1 1 6 LGA ORD N3ALAA AA
# ... with 336,766 more rows
假设想要将航空公司的全名加入 flights2
数据集,可以通过 left_join()
函数组合 airlines
和 flights2
数据框:
flights2 <- flights %>%
dplyr::select(year:day, hour, origin, dest, tailnum, carrier)
> flights2 %>%
+ dplyr::select(-origin, -dest) %>%
+ left_join(airlines, by = "carrier")#通过carrier匹配
# A tibble: 336,776 x 7
year month day hour tailnum carrier name
<int> <int> <int> <dbl> <chr> <chr> <chr>
1 2013 1 1 5 N14228 UA United Air Lines Inc.
2 2013 1 1 5 N24211 UA United Air Lines Inc.
3 2013 1 1 5 N619AA AA American Airlines Inc.
4 2013 1 1 5 N804JB B6 JetBlue Airways
5 2013 1 1 6 N668DN DL Delta Air Lines Inc.
6 2013 1 1 5 N39463 UA United Air Lines Inc.
7 2013 1 1 6 N516JB B6 JetBlue Airways
8 2013 1 1 6 N829AS EV ExpressJet Airlines Inc.
9 2013 1 1 6 N593JB B6 JetBlue Airways
10 2013 1 1 6 N3ALAA AA American Airlines Inc.
# ... with 336,766 more rows
将航空公司数据连接到 flights2
的结果产生了一个新变量:name
。这就是我们将这种连接称为合并连接的原因。对于这个示例,我们可以通过 mutate()
函数和 R 的取子集操作达到同样的效果:
flights2 %>%
dplyr::select(-origin, -dest) %>%
mutate(name = airlines$name[match(carrier, airlines$carrier)])
但这种方式很难推广到需要匹配多个变量的情况,而且需要仔细阅读代码才能搞清楚操作目的。
8.4.1 理解连接
x <- tribble(
~key, ~val_x,
1, "x1",
2, "x2",
3, "x3"
)
y <- tribble(
~key, ~val_y,
1, "y1",
2, "y2",
4, "y3"
)
![2](https://gitee.com/mzbj/blog-image/raw/master/img/2.png)
有颜色的列表示作为“键”的变量:它们用于在表间匹配行。灰色列表示“值”列,是与键对应的值。在以下的示例中,虽然键和值都是一个变量,但非常容易推广到多个键变量和多个值变量的情况。
连接是将 x 中每行连接到 y 中 0 行、一行或多行的一种方法。下图表示出了所有可能的匹配,匹配就是两行之间的交集。
匹配在实际的连接操作中是用圆点表示的。圆点的数量 = 匹配的数量 = 结果中行的数量。
8.4.2 内连接
内连接是最简单的一种连接。只要两个观测的键是相等的,内连接就可以匹配它们。
> x %>%
+ inner_join(y, by = "key")
# A tibble: 2 x 3
key val_x val_y
<dbl> <chr> <chr>
1 1 x1 y1
2 2 x2 y2
8.4.3 外连接
内连接保留同时存在于两个表中的观测,外连接则保留至少存在于一个表中的观测。外连 接有 3 种类型。
- 左连接:保留 x 中的所有观测。
- 右连接:保留 y 中的所有观测 。
- 全连接:保留 x 和 y 中的所有观测。
inner_join(x, y) #内连接
left_join(x, y) #左连接
right_join(x, y) #右连接
full_join(x, y) #全连接
8.4.4 重复键
至今为止,所有图都假设键具有唯一性。但情况并非总是如此。本节说明了当键不唯一时将会发生的两种情况。
-
一张表中具有重复键。通常来说,当存在一对多关系时,如果你想要向表中添加额外信息,就会出现这种情况。
-
两张表中都有重复键。这通常意味着出现了错误,因为键在任意一张表中都不能唯一标识观测。当连接这样的重复键时,你会得到所有可能的组合,即笛卡儿积:
8.4.5 定义键列
迄今为止,两张表都是通过一个单变量来连接的,而且这个变量在两张表中具有同样的名 称。这种限制条件是通过 by = "key"
来实现的。你还可以对 by 设置其他值,以另外的方 式来连接表。
- 默认值
by = NULL
。这会使用存在于两个表中的所有变量,这种方式称为自然连接。例如, 匹配航班表和天气表时使用的就是其公共变量:year
、month
、day
、hour
和origin
。
> flights2 %>%
+ left_join(weather)
Joining, by = c("year", "month", "day", "hour", "origin")
# A tibble: 336,776 x 18
year month day hour origin dest tailnum carrier temp dewp humid
<int> <int> <int> <dbl> <chr> <chr> <chr> <chr> <dbl> <dbl> <dbl>
1 2013 1 1 5 EWR IAH N14228 UA 39.0 28.0 64.4
2 2013 1 1 5 LGA IAH N24211 UA 39.9 25.0 54.8
3 2013 1 1 5 JFK MIA N619AA AA 39.0 27.0 61.6
4 2013 1 1 5 JFK BQN N804JB B6 39.0 27.0 61.6
5 2013 1 1 6 LGA ATL N668DN DL 39.9 25.0 54.8
6 2013 1 1 5 EWR ORD N39463 UA 39.0 28.0 64.4
7 2013 1 1 6 EWR FLL N516JB B6 37.9 28.0 67.2
8 2013 1 1 6 LGA IAD N829AS EV 39.9 25.0 54.8
9 2013 1 1 6 JFK MCO N593JB B6 37.9 27.0 64.3
10 2013 1 1 6 LGA ORD N3ALAA AA 39.9 25.0 54.8
# ... with 336,766 more rows, and 7 more variables: wind_dir <dbl>,
# wind_speed <dbl>, wind_gust <dbl>, precip <dbl>, pressure <dbl>,
# visib <dbl>, time_hour <dttm>
- 字符向量
by = "x"
。这种方式与自然连接很相似,但只使用某些公共变量。例如,flights
和planes
表中都有year
变量,但是它们的意义不同,因此我们只通过tailnum
进行连接:
flights2 %>%
left_join(planes, by = "tailnum")
#注意,结果中的 year 变量(同时存在于两个输入数据框中,但并不要求相等)添加了一个后缀,以消除歧义。
-
命名字符向量
by = c("a" = "b")
。这种方式会匹配 x 表中的 a 变量和 y 表中的 b 变量。 输出结果中使用的是 x 表中的变量。例如,如果想要画出一幅地图,那么我们就需要在航班数据中加入机场数据,后者包含了每个机场的位置(
lat
和lon
)。因为每次航班都有起点机场和终点机场,所以需要指定使用哪个机场进行连接:
flights2 %>%
left_join(airports, c("dest" = "faa"))
flights2 %>%
left_join(airports, c("origin" = "faa"))
8.4.6 其他实现方式
base::merge()
函数可以实现所有 4 种合并连接操作。
8.5 筛选连接
筛选连接匹配观测的方式与合并连接相同,但前者影响的是观测,而不是变量。筛选连接有两种类型。
semi_join(x, y)
(半连接):保留 x 表中与 y 表中的观测相匹配的所有观测。anti_join(x, y)
(反连接):丢弃 x 表中与 y 表中的观测相匹配的所有观测。
半连接的图形表示如下所示。
半连接的逆操作是反连接。反连接保留 x 表中那些没有匹配 y 表的行。
反连接可以用于诊断连接中的不匹配。例如,在连接 flights
和 planes
时,你可能想知道 flights
中是否有很多行在 planes
中没有匹配记录:
flights %>%
anti_join(planes, by = "tailnum") %>%
count(tailnum, sort = TRUE)
8.6 连接中的问题
为了在使用自己的数据时可以顺畅地进行各种连接,你需 要注意以下几点。
- 首先,需要找出每个表中可以作为主键的变量。
- 确保主键中的每个变量都没有缺失值。
- 检查外键是否与另一张表的主键相匹配。
8.7 集合操作
两表之间的最后一种操作就是集合操作。通常很少使用这种操作,但如果要将一个复杂的筛选操作分解为多个简单部分时,它们还是有些用处的。所有集合操作都是作用于整行 的,比较的是每个变量的值。集合操作需要 x 和 y 具有相同的变量,并将观测按照集合来处理。
intersect(x, y)
返回既在 x 表,又在 y 表中的观测。union(x, y)
返回 x 表或 y 表中的唯一观测setdiff(x, y)
返回在 x 表,但不在 y 表中的观测。
df1 <- tribble(
~x, ~y,
1, 1,
2, 1
)
df2 <- tribble(
~x, ~y,
1, 1,
1, 2
)
# 4种可能的集合操作为:
intersect(df1, df2)
> intersect(df1, df2)
# A tibble: 1 x 2
x y
<dbl> <dbl>
1 1 1
union(df1, df2)
> union(df1, df2)
# A tibble: 3 x 2
x y
<dbl> <dbl>
1 1 1
2 2 1
3 1 2
setdiff(df1, df2)
> setdiff(df1, df2)
# A tibble: 1 x 2
x y
<dbl> <dbl>
1 2 1
setdiff(df2, df1)
> setdiff(df2, df1)
# A tibble: 1 x 2
x y
<dbl> <dbl>
1 1 2