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" )
有颜色的列表示作为“键”的变量:它们用于在表间匹配行。灰色列表示“值”列,是与键对应的值。在以下的示例中,虽然键和值都是一个变量,但非常容易推广到多个键变量和多个值变量的情况。
连接是将 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