#使用dplyr處理關(guān)系數(shù)據(jù)
#要想處理關(guān)系數(shù)據(jù),你需要能夠在兩張表之間進(jìn)行的操作。我們設(shè)計(jì)了三類操作來處理關(guān)系數(shù)據(jù)。
#? 合并連接:向數(shù)據(jù)框中加入新變量,新變量的值是另一個數(shù)據(jù)框中的匹配觀測。
#? 篩選連接:根據(jù)是否匹配另一個數(shù)據(jù)框中的觀測,篩選數(shù)據(jù)框中的觀測。
#? 集合操作:將觀測作為集合元素來處理。
#關(guān)系數(shù)據(jù)最常見于關(guān)系數(shù)據(jù)庫管理系統(tǒng)(relational database management system,RDBMS), 該系統(tǒng)幾乎囊括了所有的現(xiàn)代數(shù)據(jù)庫。
library(tidyverse)
library(nycflights13)
#nycflights13 中包含了與 flights 相關(guān)的 4 個tibble
#? airlines:可以根據(jù)航空公司的縮寫碼查到公司全名。
airlines
#? airports:給出了每個機(jī)場的信息,通過 faa 機(jī)場編碼進(jìn)行標(biāo)識。
airports
#? planes:給出了每架飛機(jī)的信息,通過 tailnum 進(jìn)行標(biāo)識。
planes
#? weather:給出了紐約機(jī)場每小時的天氣狀況。
weather
#展示不同數(shù)據(jù)表之間關(guān)系的一種方法是繪制圖形。
#記住每種關(guān)系只與兩張表有關(guān)。
#對于 nycflights13 包中的表來說:
#? flights 與 planes 通過單變量 tailnum 相連;
#? flights 與 airlines 通過變量 carrier 相連;
#? flights 與 airports 通過兩種方式相連(變量 origin 和 dest);
#? flights 與 weather 通過變量 origin(位置)以及 year、month、day 和 hour(時間)相連。
#鍵
#用于連接每對數(shù)據(jù)表的變量稱為鍵。
#鍵是能唯一標(biāo)識觀測的變量(或變量集合)。
#簡單情況下,單個變量就足以標(biāo)識一個觀測。
#例如,每架飛機(jī)都可以由 tailnum 唯一標(biāo)識。
#其他情況可能需要多個變量。
#例如,要想標(biāo)識 weather 中的觀測,你需要 5 個變量:year、 month、day、hour 和 origin。
#鍵的類型有兩種。
#? 主鍵:唯一標(biāo)識其所在數(shù)據(jù)表中的觀測。
#例如,planes$tailnum 是一個主鍵,因?yàn)槠淇梢晕ㄒ粯?biāo)識 planes 表中的每架飛機(jī)。
#? 外鍵:唯一標(biāo)識另一個數(shù)據(jù)表中的觀測。
#例如,flights$tailnum 是一個外鍵,因?yàn)槠涑霈F(xiàn)在 flights 表中,并可以將每次航班與唯一一架飛機(jī)匹配。
#一個變量既可以是主鍵,也可以是外鍵。
#例如,origin 是 weather 表主鍵的一部分,同時也是 airports 表的外鍵。
#一旦識別出表的主鍵,最好驗(yàn)證一下,看看它們能否真正唯一標(biāo)識每個觀測。
#一種驗(yàn)證方法是對主鍵進(jìn)行 count() 操作,然后查看是否有 n 大于 1 的記錄
planes %>%
? count(tailnum) %>%
? filter(n > 1)
weather %>%
? count(year, month, day, hour, origin) %>%
? filter(n > 1)
#有時數(shù)據(jù)表沒有明確的主鍵:每行都是一個觀測,但沒有一個變量組合能夠明確地標(biāo)識它。
#例如,flights 表中的主鍵是什么?你可能認(rèn)為是日期加航班號或者是日期加機(jī)尾編號,但這兩種組合都不是唯一標(biāo)識:
flights %>%
? count(year, month, day, flight) %>%
? filter(n > 1)
flights %>%
? count(year, month, day, tailnum) %>%
? filter(n > 1)
#如果一張表沒有 主鍵,有時就需要使用 mutate() 函數(shù)和 row_number() 函數(shù)為表加上一個主鍵。
#如果你完成了一些篩選工作,并想要使用原始數(shù)據(jù)檢查的話,就可以更容易地匹配觀測。 這種主鍵稱為代理鍵。
#主鍵與另一張表中與之對應(yīng)的外鍵可以構(gòu)成關(guān)系。
#關(guān)系通常是一對多的。例如,每個航班只有一架飛機(jī),但每架飛機(jī)可以飛多個航班。
#在另一些數(shù)據(jù)中,你有時還會遇到一對一的關(guān)系。
#你可以將這種關(guān)系看作一對多關(guān)系的特殊情況。
#你可以使用多對一關(guān)系加上一對多關(guān)系來構(gòu)造多對多關(guān)系。
#例如,在這份數(shù)據(jù)中,航空公司與機(jī)場之間存在著多對多關(guān)系:
#每個航空公司可以使用多個機(jī)場,每個機(jī)場可以服務(wù)多個航空公司。
#合并連接
#本節(jié)將介紹用于組合兩個表格的第一種工具,即合并連接。
#合并連接可以將兩個表格中的變量組合起來,它先通過兩個表格的鍵匹配觀測,然后將一個表格中的變量復(fù)制到另一個表格中。
#連接函數(shù)也會將變量添加在表格的右側(cè)
flights2 <- flights %>%
? select(year:day, hour, origin, dest, tailnum, carrier)
flights2
#假設(shè)想要將航空公司的全名加入 flights2 數(shù)據(jù)集,
#你可以通過 left_join() 函數(shù)組合airlines 和 flights2 數(shù)據(jù)框:
flights2 %>%
? select(-origin, -dest) %>%
? left_join(airlines, by = "carrier")
#將航空公司數(shù)據(jù)連接到 flights2 的結(jié)果產(chǎn)生了一個新變量:name。
#對于這個示例,我們可以通過 mutate() 函數(shù)和 R 的取子集操作達(dá)到同樣的效果:
flights2 %>%
? select(-origin, -dest) %>%
? mutate(name = airlines$name[match(carrier, airlines$carrier)])
#但這種方式很難推廣到需要匹配多個變量的情況
#理解連接
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 行、一行或多行的一種方法。
#匹配就是兩行之間的交集。
#內(nèi)連接
#內(nèi)連接是最簡單的一種連接。
#只要兩個觀測的鍵是相等的,內(nèi)連接就可以匹配它們。
#內(nèi)連接的結(jié)果是一個新數(shù)據(jù)框,其中包含鍵、x 值和 y 值。
#我們使用 by 參數(shù)告訴 dplyr 哪 個變量是鍵:
x %>%
? inner_join(y, by = "key")
#內(nèi)連接最重要的性質(zhì)是,沒有匹配的行不會包含在結(jié)果中。
#這意味著內(nèi)連接一般不適合在分析中使用,因?yàn)樘菀讈G失觀測了。
#外連接
#內(nèi)連接保留同時存在于兩個表中的觀測,
#外連接則保留至少存在于一個表中的觀測。
#外連接有 3 種類型。
#? 左連接:保留 x 中的所有觀測。left_join(x,y)
#? 右連接:保留 y 中的所有觀測.right_join(x,y)
#? 全連接:保留 x 和 y 中的所有觀測。full_join(x,y)
#這些連接會向每個表中添加額外的“虛擬”觀測,
#這個觀測擁有總是匹配的鍵(如果沒有其他鍵可匹配的話),
#其值則用 NA 來填充。
#最常用的連接是左連接:只要想從另一張表中添加數(shù)據(jù),就可以使用左連接,
#因?yàn)樗鼤A粼碇械乃杏^測,即使它沒有匹配。
#左連接應(yīng)該是你的默認(rèn)選擇,除非有足夠充分的 理由選擇其他的連接方式。
#重復(fù)鍵
#當(dāng)鍵不唯一時將會發(fā)生的兩種情況。
#? 一張表中具有重復(fù)鍵。
#通常來說,當(dāng)存在一對多關(guān)系時,如果你想要向表中添加額外信息,就會出現(xiàn)這種情況。
x <- tribble(
? ~key, ~val_x,
? 1, "x1",
? 2, "x2",
? 2, "x3",
? 1, "x4"
)
y <- tribble(
? ~key, ~val_y,
? 1, "y1",
? 2, "y2" )
left_join(x, y, by = "key")
#? 兩張表中都有重復(fù)鍵。
#這通常意味著出現(xiàn)了錯誤,因?yàn)殒I在任意一張表中都不能唯一標(biāo)識觀測。
#當(dāng)連接這樣的重復(fù)鍵時,你會得到所有可能的組合,即笛卡兒積
x <- tribble(
? ~key, ~val_x,
? 1, "x1",
? 2, "x2",
? 2, "x3",
? 3, "x4"
)
y <- tribble(
? ~key, ~val_y,
? 1, "y1",
? 2, "y2",
? 2, "y3",
? 3, "y4" )
left_join(x, y, by = "key")
#定義鍵列
#迄今為止,兩張表都是通過一個單變量來連接的,
#而且這個變量在兩張表中具有同樣的名稱。
#這種限制條件是通過by = "key"來實(shí)現(xiàn)的。
#? 默認(rèn)值by = NULL。這會使用存在于兩個表中的所有變量,這種方式稱為自然連接。
#匹配航班表和天氣表時使用的就是其公共變量:year、month、day、hour 和 origin。
flights2 %>%
? left_join(weather)
#? 字符向量by = "x"。這種方式與自然連接很相似,但只使用某些公共變量。
#例如, flights 和 planes 表中都有 year 變量,但是它們的意義不同,因此我們只通過 tailnum 進(jìn)行連接:
flights2 %>%
? left_join(planes, by = "tailnum")
#注意,結(jié)果中的 year 變量(同時存在于兩個輸入數(shù)據(jù)框中,但并不要求相等)添加了一個后綴,以消除歧義。
#? 命名字符向量by = c("a" = "b")。
#這種方式會匹配x表中的a變量和y表中的b變量。
#輸出結(jié)果中使用的是 x 表中的變量。
#因?yàn)槊看魏桨喽加衅瘘c(diǎn)機(jī)場和終點(diǎn)機(jī)場,所以需要指定使用哪個機(jī)場進(jìn)行連接:
flights2 %>%
? left_join(airports, c("dest" = "faa"))
flights2 %>%
? left_join(airports, c("origin" = "faa"))
#篩選連接
#篩選連接匹配觀測的方式與合并連接相同,但前者影響的是觀測,而不是變量。
#篩選連接有兩種類型。
#? semi_join(x, y):保留 x 表中與 y 表中的觀測相匹配的所有觀測。
#? anti_join(x, y):丟棄 x 表中與 y 表中的觀測相匹配的所有觀測。
#對數(shù)據(jù)表進(jìn)行篩選或摘要統(tǒng)計(jì)后,
#如果想要使用表中原來的行來匹配篩選或摘要結(jié)果,
#那么半連接是非常有用的。
#例如,假設(shè)你已經(jīng)找出了最受歡迎的前 10 個目的地:
top_dest <- flights %>% count(dest, sort = TRUE) %>% head(10)
top_dest
#現(xiàn)在想要找出飛往這些目的地的所有航班,你可以自己構(gòu)造一個篩選器:
flights %>%
? filter(dest %in% top_dest$dest)
#但這種方法很難擴(kuò)展到多個變量。例如,假設(shè)已經(jīng)找出了平均延誤時間最長的 10 天,那么你應(yīng)該如何使用 year、month 和 day 來構(gòu)造篩選語句,才能在 flights 中找出這 10 天的觀測?
#此時你應(yīng)該使用半連接,它可以像合并連接一樣連接兩個表,但不添加新列,而是保留 x 表中那些可以匹配 y 表的行:
flights %>%
? semi_join(top_dest)
#重要的是存在匹配,匹配了哪條觀測則無關(guān)緊要。
#這說明篩選連接不會像合并連接那樣造成重復(fù)的行。
#半連接的逆操作是反連接。反連接保留 x 表中那些沒有匹配 y 表的行。
#反連接可以用于診斷連接中的不匹配。例如,在連接 flights 和 planes 時,你可能想知道 flights 中是否有很多行在 planes 中沒有匹配記錄:
flights %>%
? anti_join(planes, by = "tailnum") %>% count(tailnum, sort = TRUE)
#連接中的問題
#(1) 首先,需要找出每個表中可以作為主鍵的變量。
#(2) 確保主鍵中的每個變量都沒有缺失值。如果有缺失值,那么這個變量就不能標(biāo)識觀測!
#(3) 檢查外鍵是否與另一張表的主鍵相匹配。
#最好的方法是使用 anti_join(),
#由于數(shù)據(jù)錄入錯誤,外鍵和主鍵不匹配的情況很常見。
#解決這種問題通常需要大量工作。
#如果鍵中確實(shí)有缺失值,那么你就要深思熟慮一下,是應(yīng)該使用內(nèi)連接還是外連接,
#此外,是否應(yīng)該丟棄那些沒有匹配記錄的行。
#注意,僅憑檢查連接前后的行數(shù)是不足以確保連接能夠順暢運(yùn)行的。
#集合操作
#兩表之間的最后一種操作就是集合操作。
#但如果你想要將一個復(fù)雜的篩選操作分解為多個簡單部分時,它們還是有些用處的。
#所有集合操作都是作用于整行的,比較的是每個變量的值。
#集合操作需要 x 和 y 具有相同的變量,并將觀測按照集合來處理。
#intersect(x, y)
#返回既在 x 表,又在 y 表中的觀測。
#union(x, y)
#返回 x 表或 y 表中的唯一觀測。
#setdiff(x, y)
#返回在 x 表,但不在 y 表中的觀測。
#給定以下簡單數(shù)據(jù):
? df1 <- tribble(
? ? ~x, ~y,
? ? 1, 1,
? ? 2, 1 )
df2 <- tribble(
? ~x, ~y,
? 1, 1,
? 1, 2 )
#4 種可能的集合操作為:
? intersect(df1, df2)
#> # A tibble: 1 × 2
#> x y
#> <dbl> <dbl>
#> 1 1 1
union(df1, df2)
#> # A tibble: 3 × 2
#> x y
#>? <dbl> <dbl>
#> 1? ? 1? ? 2
#> 2? ? 2? ? 1
#> 3? ? 1? ? 1
# 注意,我們得到了3行,而不是4行
setdiff(df1, df2)
#> # A tibble: 1 × 2 #> x y
#> <dbl> <dbl>
#> 1 2 1
setdiff(df2, df1)
#> # A tibble: 1 × 2 #> x y
#> <dbl> <dbl>
#> 1 1 2