19  Nối bảng

19.1 Giới thiệu

Rất hiếm khi một phân tích dữ liệu chỉ liên quan đến một data frame duy nhất. Thông thường bạn có nhiều data frame, và bạn phải nối (join) chúng lại với nhau để trả lời các câu hỏi mà bạn quan tâm. Chương này sẽ giới thiệu cho bạn hai loại nối quan trọng:

  • Nối biến đổi (mutating join), thêm các biến mới vào một data frame từ các quan sát khớp trong một data frame khác.
  • Nối lọc (filtering join), lọc các quan sát từ một data frame dựa trên việc chúng có khớp hay không với một quan sát trong data frame khác.

Chúng ta sẽ bắt đầu bằng việc thảo luận về khóa (key), các biến được sử dụng để kết nối một cặp data frame trong một phép nối. Chúng ta củng cố lý thuyết bằng cách kiểm tra các khóa trong các tập dữ liệu từ package nycflights13, sau đó sử dụng kiến thức đó để bắt đầu nối các data frame lại với nhau. Tiếp theo chúng ta sẽ thảo luận về cách các phép nối hoạt động, tập trung vào tác động của chúng lên các row. Chúng ta sẽ kết thúc với một cuộc thảo luận về nối không đẳng thức (non-equi join), một họ các phép nối cung cấp cách khớp khóa linh hoạt hơn so với quan hệ bằng mặc định.

19.1.1 Điều kiện tiên quyết

Trong chương này, chúng ta sẽ khám phá năm tập dữ liệu liên quan từ nycflights13 sử dụng các function nối từ dplyr.

19.2 Khóa

Để hiểu các phép nối, trước tiên bạn cần hiểu cách hai bảng có thể được kết nối thông qua một cặp khóa, trong mỗi bảng. Trong phần này, bạn sẽ tìm hiểu về hai loại khóa và xem các ví dụ về cả hai trong các tập dữ liệu của package nycflights13. Bạn cũng sẽ học cách kiểm tra rằng các khóa của bạn hợp lệ, và phải làm gì khi bảng của bạn thiếu khóa.

19.2.1 Khóa chính và khóa ngoại

Mỗi phép nối đều liên quan đến một cặp khóa: một khóa chính (primary key) và một khóa ngoại (foreign key). Khóa chính là một biến hoặc tập hợp các biến xác định duy nhất mỗi quan sát. Khi cần nhiều hơn một biến, khóa được gọi là khóa phức hợp (compound key). Ví dụ, trong nycflights13:

  • airlines ghi lại hai phần dữ liệu về mỗi hãng row không: mã hãng vận chuyển và tên đầy đủ. Bạn có thể xác định một hãng row không bằng mã hãng vận chuyển hai chữ cái, làm cho carrier trở thành khóa chính.

    airlines
    #> # A tibble: 16 × 2
    #>   carrier name                    
    #>   <chr>   <chr>                   
    #> 1 9E      Endeavor Air Inc.       
    #> 2 AA      American Airlines Inc.  
    #> 3 AS      Alaska Airlines Inc.    
    #> 4 B6      JetBlue Airways         
    #> 5 DL      Delta Air Lines Inc.    
    #> 6 EV      ExpressJet Airlines Inc.
    #> # ℹ 10 more rows
  • airports ghi lại dữ liệu về mỗi sân bay. Bạn có thể xác định mỗi sân bay bằng mã sân bay ba chữ cái, làm cho faa trở thành khóa chính.

    airports
    #> # A tibble: 1,458 × 8
    #>   faa   name                            lat   lon   alt    tz dst  
    #>   <chr> <chr>                         <dbl> <dbl> <dbl> <dbl> <chr>
    #> 1 04G   Lansdowne Airport              41.1 -80.6  1044    -5 A    
    #> 2 06A   Moton Field Municipal Airport  32.5 -85.7   264    -6 A    
    #> 3 06C   Schaumburg Regional            42.0 -88.1   801    -6 A    
    #> 4 06N   Randall Airport                41.4 -74.4   523    -5 A    
    #> 5 09J   Jekyll Island Airport          31.1 -81.4    11    -5 A    
    #> 6 0A9   Elizabethton Municipal Airpo…  36.4 -82.2  1593    -5 A    
    #> # ℹ 1,452 more rows
    #> # ℹ 1 more variable: tzone <chr>
  • planes ghi lại dữ liệu về mỗi máy bay. Bạn có thể xác định một máy bay bằng số đuôi của nó, làm cho tailnum trở thành khóa chính.

    planes
    #> # A tibble: 3,322 × 9
    #>   tailnum  year type              manufacturer    model     engines
    #>   <chr>   <int> <chr>             <chr>           <chr>       <int>
    #> 1 N10156   2004 Fixed wing multi… EMBRAER         EMB-145XR       2
    #> 2 N102UW   1998 Fixed wing multi… AIRBUS INDUSTR… A320-214        2
    #> 3 N103US   1999 Fixed wing multi… AIRBUS INDUSTR… A320-214        2
    #> 4 N104UW   1999 Fixed wing multi… AIRBUS INDUSTR… A320-214        2
    #> 5 N10575   2002 Fixed wing multi… EMBRAER         EMB-145LR       2
    #> 6 N105UW   1999 Fixed wing multi… AIRBUS INDUSTR… A320-214        2
    #> # ℹ 3,316 more rows
    #> # ℹ 3 more variables: seats <int>, speed <int>, engine <chr>
  • weather ghi lại dữ liệu về thời tiết tại các sân bay xuất phát. Bạn có thể xác định mỗi quan sát bằng tổ hợp vị trí và thời gian, làm cho origintime_hour trở thành khóa chính phức hợp.

    weather
    #> # A tibble: 26,115 × 15
    #>   origin  year month   day  hour  temp  dewp humid wind_dir
    #>   <chr>  <int> <int> <int> <int> <dbl> <dbl> <dbl>    <dbl>
    #> 1 EWR     2013     1     1     1  39.0  26.1  59.4      270
    #> 2 EWR     2013     1     1     2  39.0  27.0  61.6      250
    #> 3 EWR     2013     1     1     3  39.0  28.0  64.4      240
    #> 4 EWR     2013     1     1     4  39.9  28.0  62.2      250
    #> 5 EWR     2013     1     1     5  39.0  28.0  64.4      260
    #> 6 EWR     2013     1     1     6  37.9  28.0  67.2      240
    #> # ℹ 26,109 more rows
    #> # ℹ 6 more variables: wind_speed <dbl>, wind_gust <dbl>, …

Khóa ngoại là một biến (hoặc tập hợp các biến) tương ứng với khóa chính trong một bảng khác. Ví dụ:

  • flights$tailnum là khóa ngoại tương ứng với khóa chính planes$tailnum.
  • flights$carrier là khóa ngoại tương ứng với khóa chính airlines$carrier.
  • flights$origin là khóa ngoại tương ứng với khóa chính airports$faa.
  • flights$dest là khóa ngoại tương ứng với khóa chính airports$faa.
  • flights$origin-flights$time_hour là khóa ngoại phức hợp tương ứng với khóa chính phức hợp weather$origin-weather$time_hour.

Các mối quan hệ này được tóm tắt trực quan trong Hình 19.1.

The relationships between airports, planes, flights, weather, and airlines datasets from the nycflights13 package. airports$faa connected to the flights$origin and flights$dest. planes$tailnum is connected to the flights$tailnum. weather$time_hour and weather$origin are jointly connected to flights$time_hour and flights$origin. airlines$carrier is connected to flights$carrier. There are no direct connections between airports, planes, airlines, and weather data frames.
Hình 19.1: Các kết nối giữa năm data frame trong package nycflights13. Các biến tạo thành khóa chính được tô màu xám, và được kết nối với các khóa ngoại tương ứng bằng mũi tên.

Bạn sẽ nhận thấy một đặc điểm hay trong thiết kế của các khóa này: khóa chính và khóa ngoại hầu như luôn có cùng tên, điều này, như bạn sẽ thấy ngay, sẽ giúp việc nối bảng của bạn dễ dàng hơn nhiều. Cũng đáng lưu ý mối quan hệ ngược lại: hầu hết mọi tên biến được sử dụng trong nhiều bảng đều có cùng ý nghĩa ở mỗi nơi. Chỉ có một ngoại lệ: year có nghĩa là năm khởi hành trong flights và năm sản xuất trong planes. Điều này sẽ trở nên quan trọng khi chúng ta bắt đầu thực sự nối các bảng lại với nhau.

19.2.2 Kiểm tra khóa chính

Bây giờ chúng ta đã xác định được các khóa chính trong mỗi bảng, việc xác minh rằng chúng thực sự xác định duy nhất mỗi quan sát là một thực hành tốt. Một cách để làm điều đó là count() các khóa chính và tìm các mục có n lớn hơn một. Điều này cho thấy planesweather đều ổn:

planes |>
  count(tailnum) |>
  filter(n > 1)
#> # A tibble: 0 × 2
#> # ℹ 2 variables: tailnum <chr>, n <int>

weather |>
  count(time_hour, origin) |>
  filter(n > 1)
#> # A tibble: 0 × 3
#> # ℹ 3 variables: time_hour <dttm>, origin <chr>, n <int>

Bạn cũng nên kiểm tra missing value (missing value) trong các khóa chính — nếu một giá trị bị khuyết thì nó không thể xác định một quan sát!

planes |>
  filter(is.na(tailnum))
#> # A tibble: 0 × 9
#> # ℹ 9 variables: tailnum <chr>, year <int>, type <chr>, manufacturer <chr>,
#> #   model <chr>, engines <int>, seats <int>, speed <int>, engine <chr>

weather |>
  filter(is.na(time_hour) | is.na(origin))
#> # A tibble: 0 × 15
#> # ℹ 15 variables: origin <chr>, year <int>, month <int>, day <int>,
#> #   hour <int>, temp <dbl>, dewp <dbl>, humid <dbl>, wind_dir <dbl>, …

19.2.3 Khóa thay thế

Cho đến giờ chúng ta chưa nói về khóa chính cho flights. Điều đó không quá quan trọng ở đây, vì không có data frame nào sử dụng nó làm khóa ngoại, nhưng vẫn hữu ích để xem xét vì sẽ dễ làm việc với các quan sát hơn nếu chúng ta có cách nào đó để mô tả chúng cho người khác.

Sau một chút suy nghĩ và thử nghiệm, chúng tôi xác định rằng có ba biến cùng nhau xác định duy nhất mỗi chuyến bay:

flights |>
  count(time_hour, carrier, flight) |>
  filter(n > 1)
#> # A tibble: 0 × 4
#> # ℹ 4 variables: time_hour <dttm>, carrier <chr>, flight <int>, n <int>

Việc không có giá trị trùng iterate có tự động làm cho time_hour-carrier-flight trở thành khóa chính không? Đó chắc chắn là một khởi đầu tốt, nhưng nó không đảm bảo điều đó. Ví dụ, liệu độ cao và vĩ độ có phải là khóa chính tốt cho airports không?

airports |>
  count(alt, lat) |>
  filter(n > 1)
#> # A tibble: 1 × 3
#>     alt   lat     n
#>   <dbl> <dbl> <int>
#> 1    13  40.6     2

Xác định một sân bay bằng độ cao và vĩ độ rõ ràng là một ý tưởng tồi, và nói chung không thể chỉ từ dữ liệu mà biết được liệu một tổ hợp biến có tạo thành một khóa chính tốt hay không. Nhưng đối với các chuyến bay, tổ hợp time_hour, carrier, và flight có vẻ hợp lý vì sẽ rất khó hiểu nếu một hãng row không và hành khách của họ có nhiều chuyến bay cùng số hiệu bay trên không cùng một lúc.

Tuy nhiên, chúng ta có thể sẽ tốt hơn nếu tạo một khóa thay thế (surrogate key) số đơn giản sử dụng số thứ tự row:

flights2 <- flights |>
  mutate(id = row_number(), .before = 1)
flights2
#> # A tibble: 336,776 × 20
#>      id  year month   day dep_time sched_dep_time dep_delay arr_time
#>   <int> <int> <int> <int>    <int>          <int>     <dbl>    <int>
#> 1     1  2013     1     1      517            515         2      830
#> 2     2  2013     1     1      533            529         4      850
#> 3     3  2013     1     1      542            540         2      923
#> 4     4  2013     1     1      544            545        -1     1004
#> 5     5  2013     1     1      554            600        -6      812
#> 6     6  2013     1     1      554            558        -4      740
#> # ℹ 336,770 more rows
#> # ℹ 12 more variables: sched_arr_time <int>, arr_delay <dbl>, …

Khóa thay thế có thể đặc biệt hữu ích khi giao tiếp với người khác: nói với ai đó hãy xem chuyến bay 2001 dễ hơn nhiều so với nói hãy xem UA430 khởi hành lúc 9 giờ sáng ngày 2013-01-03.

19.2.4 Bài tập

  1. Chúng ta quên vẽ mối quan hệ giữa weatherairports trong Hình 19.1. Mối quan hệ đó là gì và nó nên xuất hiện như thế nào trong sơ đồ?

  2. weather chỉ chứa thông tin cho ba sân bay xuất phát ở NYC. Nếu nó chứa bản ghi thời tiết cho tất cả các sân bay ở Mỹ, nó sẽ tạo thêm kết nối nào đến flights?

  3. Các biến year, month, day, hour, và origin gần như tạo thành một khóa phức hợp cho weather, nhưng có một giờ có các quan sát trùng iterate. Bạn có thể tìm ra điều gì đặc biệt về giờ đó không?

  4. Chúng ta biết rằng một số ngày trong năm là đặc biệt và ít người bay hơn bình thường (ví dụ, đêm Giáng sinh và ngày Giáng sinh). Bạn có thể biểu diễn dữ liệu đó dưới dạng data frame như thế nào? Khóa chính sẽ là gì? Nó sẽ kết nối với các data frame hiện có như thế nào?

  5. Vẽ một sơ đồ minh họa các kết nối giữa các data frame Batting, People, và Salaries trong package Lahman. Vẽ một sơ đồ khác cho thấy mối quan hệ giữa People, Managers, AwardsManagers. Bạn sẽ mô tả mối quan hệ giữa các data frame Batting, Pitching, và Fielding như thế nào?

19.3 Nối cơ bản

Bây giờ bạn đã hiểu cách các data frame được kết nối thông qua khóa, chúng ta có thể bắt đầu sử dụng các phép nối để hiểu rõ hơn tập dữ liệu flights. dplyr cung cấp sáu function nối: left_join(), inner_join(), right_join(), full_join(), semi_join(), và anti_join(). Tất cả đều có cùng giao diện: chúng nhận một cặp data frame (xy) và trả về một data frame. Thứ tự của các row và column trong kết quả đầu ra chủ yếu được xác định bởi x.

Trong phần này, bạn sẽ học cách sử dụng một phép nối biến đổi, left_join(), và hai phép nối lọc, semi_join()anti_join(). Trong phần tiếp theo, bạn sẽ học chính xác cách các function này hoạt động, và về các function còn lại inner_join(), right_join()full_join().

19.3.1 Nối biến đổi

Một nối biến đổi cho phép bạn kết hợp các biến từ hai data frame: nó trước tiên khớp các quan sát theo khóa của chúng, sau đó sao chép các biến từ data frame này sang data frame kia. Giống như mutate(), các function nối thêm biến vào bên phải, nên nếu tập dữ liệu của bạn có nhiều biến, bạn sẽ không thấy các biến mới. Cho các ví dụ này, chúng ta sẽ tạo ra một tập dữ liệu hẹp hơn chỉ với sáu biến để dễ quan sát hơn1:

flights2 <- flights |>
  select(year, time_hour, origin, dest, tailnum, carrier)
flights2
#> # A tibble: 336,776 × 6
#>    year time_hour           origin dest  tailnum carrier
#>   <int> <dttm>              <chr>  <chr> <chr>   <chr>  
#> 1  2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA     
#> 2  2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA     
#> 3  2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA     
#> 4  2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6     
#> 5  2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL     
#> 6  2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA     
#> # ℹ 336,770 more rows

Có bốn loại nối biến đổi, nhưng có một loại bạn sẽ dùng hầu như mọi lúc: left_join(). Nó đặc biệt vì đầu ra sẽ luôn có cùng số row với x, data frame mà bạn đang nối vào2. Công dụng chính của left_join() là bổ sung thêm siêu dữ liệu (metadata). Ví dụ, chúng ta có thể sử dụng left_join() để thêm tên đầy đủ của hãng row không vào dữ liệu flights2:

flights2 |>
  left_join(airlines)
#> Joining with `by = join_by(carrier)`
#> # A tibble: 336,776 × 7
#>    year time_hour           origin dest  tailnum carrier name                
#>   <int> <dttm>              <chr>  <chr> <chr>   <chr>   <chr>               
#> 1  2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA      United Air Lines In…
#> 2  2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA      United Air Lines In…
#> 3  2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA      American Airlines I…
#> 4  2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6      JetBlue Airways     
#> 5  2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL      Delta Air Lines Inc.
#> 6  2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA      United Air Lines In…
#> # ℹ 336,770 more rows

Hoặc chúng ta có thể tìm nhiệt độ và tốc độ gió khi mỗi máy bay khởi hành:

flights2 |>
  left_join(weather |> select(origin, time_hour, temp, wind_speed))
#> Joining with `by = join_by(time_hour, origin)`
#> # A tibble: 336,776 × 8
#>    year time_hour           origin dest  tailnum carrier  temp wind_speed
#>   <int> <dttm>              <chr>  <chr> <chr>   <chr>   <dbl>      <dbl>
#> 1  2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA       39.0       12.7
#> 2  2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA       39.9       15.0
#> 3  2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA       39.0       15.0
#> 4  2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6       39.0       15.0
#> 5  2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL       39.9       16.1
#> 6  2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA       39.0       12.7
#> # ℹ 336,770 more rows

Hoặc kích cỡ máy bay đang bay là gì:

flights2 |>
  left_join(planes |> select(tailnum, type, engines, seats))
#> Joining with `by = join_by(tailnum)`
#> # A tibble: 336,776 × 9
#>    year time_hour           origin dest  tailnum carrier type                
#>   <int> <dttm>              <chr>  <chr> <chr>   <chr>   <chr>               
#> 1  2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA      Fixed wing multi en…
#> 2  2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA      Fixed wing multi en…
#> 3  2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA      Fixed wing multi en…
#> 4  2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6      Fixed wing multi en…
#> 5  2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL      Fixed wing multi en…
#> 6  2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA      Fixed wing multi en…
#> # ℹ 336,770 more rows
#> # ℹ 2 more variables: engines <int>, seats <int>

Khi left_join() không tìm thấy khớp cho một row trong x, nó điền missing value vào các biến mới. Ví dụ, không có thông tin về máy bay với số đuôi N3ALAA nên type, engines, và seats sẽ bị khuyết:

flights2 |>
  filter(tailnum == "N3ALAA") |>
  left_join(planes |> select(tailnum, type, engines, seats))
#> Joining with `by = join_by(tailnum)`
#> # A tibble: 63 × 9
#>    year time_hour           origin dest  tailnum carrier type  engines seats
#>   <int> <dttm>              <chr>  <chr> <chr>   <chr>   <chr>   <int> <int>
#> 1  2013 2013-01-01 06:00:00 LGA    ORD   N3ALAA  AA      <NA>       NA    NA
#> 2  2013 2013-01-02 18:00:00 LGA    ORD   N3ALAA  AA      <NA>       NA    NA
#> 3  2013 2013-01-03 06:00:00 LGA    ORD   N3ALAA  AA      <NA>       NA    NA
#> 4  2013 2013-01-07 19:00:00 LGA    ORD   N3ALAA  AA      <NA>       NA    NA
#> 5  2013 2013-01-08 17:00:00 JFK    ORD   N3ALAA  AA      <NA>       NA    NA
#> 6  2013 2013-01-16 06:00:00 LGA    ORD   N3ALAA  AA      <NA>       NA    NA
#> # ℹ 57 more rows

Chúng ta sẽ quay lại vấn đề này vài lần trong phần còn lại của chương.

19.3.2 Chỉ định khóa nối

Theo mặc định, left_join() sẽ sử dụng tất cả các biến xuất hiện trong cả hai data frame làm khóa nối, gọi là phép nối tự nhiên (natural join). Đây là một phương pháp heuristic hữu ích, nhưng không phải lúc nào cũng hoạt động. Ví dụ, điều gì xảy ra nếu chúng ta thử nối flights2 với tập dữ liệu planes đầy đủ?

flights2 |>
  left_join(planes)
#> Joining with `by = join_by(year, tailnum)`
#> # A tibble: 336,776 × 13
#>    year time_hour           origin dest  tailnum carrier type  manufacturer
#>   <int> <dttm>              <chr>  <chr> <chr>   <chr>   <chr> <chr>       
#> 1  2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA      <NA>  <NA>        
#> 2  2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA      <NA>  <NA>        
#> 3  2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA      <NA>  <NA>        
#> 4  2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6      <NA>  <NA>        
#> 5  2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL      <NA>  <NA>        
#> 6  2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA      <NA>  <NA>        
#> # ℹ 336,770 more rows
#> # ℹ 5 more variables: model <chr>, engines <int>, seats <int>, …

Chúng ta nhận được rất nhiều khớp bị thiếu vì phép nối đang cố sử dụng tailnumyear làm khóa phức hợp. Cả flightsplanes đều có column year nhưng chúng có nghĩa khác nhau: flights$year là năm chuyến bay diễn ra và planes$year là năm máy bay được sản xuất. Chúng ta chỉ muốn nối theo tailnum nên cần cung cấp đặc tả rõ ràng với join_by():

flights2 |>
  left_join(planes, join_by(tailnum))
#> # A tibble: 336,776 × 14
#>   year.x time_hour           origin dest  tailnum carrier year.y
#>    <int> <dttm>              <chr>  <chr> <chr>   <chr>    <int>
#> 1   2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA        1999
#> 2   2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA        1998
#> 3   2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA        1990
#> 4   2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6        2012
#> 5   2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL        1991
#> 6   2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA        2012
#> # ℹ 336,770 more rows
#> # ℹ 7 more variables: type <chr>, manufacturer <chr>, model <chr>, …

Lưu ý rằng các biến year được phân biệt trong đầu ra bằng hậu tố (year.xyear.y), cho bạn biết biến đến từ argument x hay y. Bạn có thể ghi đè hậu tố mặc định bằng argument suffix.

join_by(tailnum) là viết tắt của join_by(tailnum == tailnum). Điều quan trọng là phải biết về dạng đầy đủ hơn này vì hai lý do. Thứ nhất, nó mô tả mối quan hệ giữa hai bảng: các khóa phải bằng nhau. Đó là lý do tại sao loại nối này thường được gọi là nối đẳng thức (equi join). Bạn sẽ tìm hiểu về nối không đẳng thức trong Phần 19.5.

Thứ hai, đó là cách bạn chỉ định các khóa nối khác nhau trong mỗi bảng. Ví dụ, có hai cách để nối bảng flight2airports: theo dest hoặc origin:

flights2 |>
  left_join(airports, join_by(dest == faa))
#> # A tibble: 336,776 × 13
#>    year time_hour           origin dest  tailnum carrier name                
#>   <int> <dttm>              <chr>  <chr> <chr>   <chr>   <chr>               
#> 1  2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA      George Bush Interco…
#> 2  2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA      George Bush Interco…
#> 3  2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA      Miami Intl          
#> 4  2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6      <NA>                
#> 5  2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL      Hartsfield Jackson …
#> 6  2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA      Chicago Ohare Intl  
#> # ℹ 336,770 more rows
#> # ℹ 6 more variables: lat <dbl>, lon <dbl>, alt <dbl>, tz <dbl>, …

flights2 |>
  left_join(airports, join_by(origin == faa))
#> # A tibble: 336,776 × 13
#>    year time_hour           origin dest  tailnum carrier name               
#>   <int> <dttm>              <chr>  <chr> <chr>   <chr>   <chr>              
#> 1  2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA      Newark Liberty Intl
#> 2  2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA      La Guardia         
#> 3  2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA      John F Kennedy Intl
#> 4  2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6      John F Kennedy Intl
#> 5  2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL      La Guardia         
#> 6  2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA      Newark Liberty Intl
#> # ℹ 336,770 more rows
#> # ℹ 6 more variables: lat <dbl>, lon <dbl>, alt <dbl>, tz <dbl>, …

Trong code cũ hơn, bạn có thể thấy một cách khác để chỉ định khóa nối, sử dụng một vector ký tự:

  • by = "x" tương ứng với join_by(x).
  • by = c("a" = "x") tương ứng với join_by(a == x).

Giờ đây khi đã có join_by(), chúng tôi ưu tiên sử dụng nó vì nó cung cấp đặc tả rõ ràng và linh hoạt hơn.

inner_join(), right_join(), full_join() có cùng giao diện với left_join(). Sự khác biệt là những row nào chúng giữ lại: nối trái giữ tất cả các row trong x, nối phải giữ tất cả các row trong y, nối đầy đủ giữ tất cả các row trong x hoặc y, và nối trong chỉ giữ các row xuất hiện trong cả xy. Chúng ta sẽ quay lại chi tiết hơn ở phần sau.

19.3.3 Nối lọc

Như bạn có thể đoán, hành động chính của một nối lọc là lọc các row. Có hai loại: nối bán phần (semi-join) và nối phản (anti-join). Nối bán phần giữ tất cả các row trong x có khớp trong y. Ví dụ, chúng ta có thể sử dụng nối bán phần để lọc tập dữ liệu airports chỉ hiển thị các sân bay xuất phát:

airports |>
  semi_join(flights2, join_by(faa == origin))
#> # A tibble: 3 × 8
#>   faa   name                  lat   lon   alt    tz dst   tzone           
#>   <chr> <chr>               <dbl> <dbl> <dbl> <dbl> <chr> <chr>           
#> 1 EWR   Newark Liberty Intl  40.7 -74.2    18    -5 A     America/New_York
#> 2 JFK   John F Kennedy Intl  40.6 -73.8    13    -5 A     America/New_York
#> 3 LGA   La Guardia           40.8 -73.9    22    -5 A     America/New_York

Hoặc chỉ các điểm đến:

airports |>
  semi_join(flights2, join_by(faa == dest))
#> # A tibble: 101 × 8
#>   faa   name                     lat    lon   alt    tz dst   tzone          
#>   <chr> <chr>                  <dbl>  <dbl> <dbl> <dbl> <chr> <chr>          
#> 1 ABQ   Albuquerque Internati…  35.0 -107.   5355    -7 A     America/Denver 
#> 2 ACK   Nantucket Mem           41.3  -70.1    48    -5 A     America/New_Yo…
#> 3 ALB   Albany Intl             42.7  -73.8   285    -5 A     America/New_Yo…
#> 4 ANC   Ted Stevens Anchorage…  61.2 -150.    152    -9 A     America/Anchor…
#> 5 ATL   Hartsfield Jackson At…  33.6  -84.4  1026    -5 A     America/New_Yo…
#> 6 AUS   Austin Bergstrom Intl   30.2  -97.7   542    -6 A     America/Chicago
#> # ℹ 95 more rows

Nối phản thì ngược lại: chúng trả về tất cả các row trong x không có khớp trong y. Chúng hữu ích để tìm missing value ngầm (implicit) trong dữ liệu, theme của Phần 18.3. Giá trị khuyết ngầm không hiển thị dưới dạng NA mà chỉ tồn tại như một sự vắng mặt. Ví dụ, chúng ta có thể tìm các row bị thiếu trong airports bằng cách tìm các chuyến bay không có sân bay đích khớp:

flights2 |>
  anti_join(airports, join_by(dest == faa)) |>
  distinct(dest)
#> # A tibble: 4 × 1
#>   dest 
#>   <chr>
#> 1 BQN  
#> 2 SJU  
#> 3 STT  
#> 4 PSE

Hoặc chúng ta có thể tìm tailnum nào bị thiếu trong planes:

flights2 |>
  anti_join(planes, join_by(tailnum)) |>
  distinct(tailnum)
#> # A tibble: 722 × 1
#>   tailnum
#>   <chr>  
#> 1 N3ALAA 
#> 2 N3DUAA 
#> 3 N542MQ 
#> 4 N730MQ 
#> 5 N9EAMQ 
#> 6 N532UA 
#> # ℹ 716 more rows

19.3.4 Bài tập

  1. Tìm 48 giờ (trong suốt cả năm) có độ trễ tệ nhất. Đối chiếu với dữ liệu weather. Bạn có thể thấy quy luật nào không?

  2. Hãy tưởng tượng bạn đã tìm 10 điểm đến phổ biến nhất bằng đoạn code này:

    top_dest <- flights2 |>
      count(dest, sort = TRUE) |>
      head(10)

    Bạn có thể tìm tất cả chuyến bay đến các điểm đến đó bằng cách nào?

  3. Mỗi chuyến bay khởi hành có dữ liệu thời tiết tương ứng cho giờ đó không?

  4. Các số đuôi không có bản ghi khớp trong planes có điểm gì chung? (Gợi ý: một biến giải thích ~90% vấn đề.)

  5. Thêm một column vào planes liệt kê mọi carrier đã bay máy bay đó. Bạn có thể mong đợi rằng có một mối quan hệ ngầm giữa máy bay và hãng row không, vì mỗi máy bay được bay bởi một hãng duy nhất. Xác nhận hoặc bác bỏ giả thuyết này bằng các công cụ bạn đã học trong các chương trước.

  6. Thêm vĩ độ và kinh độ của sân bay xuất phát sân bay đích vào flights. Đổi tên các column trước hay sau phép nối dễ hơn?

  7. Tính độ trễ trung bình theo điểm đến, sau đó nối với data frame airports để bạn có thể hiển thị phân bố không gian của các độ trễ. Đây là một cách dễ dàng để vẽ bản đồ nước Mỹ:

    airports |>
      semi_join(flights, join_by(faa == dest)) |>
      ggplot(aes(x = lon, y = lat)) +
        borders("state") +
        geom_point() +
        coord_quickmap()

    Bạn có thể muốn sử dụng size hoặc color của các điểm để hiển thị độ trễ trung bình cho mỗi sân bay.

  8. Điều gì đã xảy ra vào ngày 13 tháng 6 năm 2013? Vẽ bản đồ các độ trễ, và sau đó sử dụng Google để đối chiếu với thời tiết.

19.4 Phép nối hoạt động như thế nào?

Bây giờ bạn đã sử dụng phép nối vài lần, đã đến lúc tìm hiểu thêm về cách chúng hoạt động, tập trung vào cách mỗi row trong x khớp với các row trong y. Chúng ta sẽ bắt đầu bằng cách giới thiệu một biểu diễn trực quan của các phép nối, sử dụng các tibble đơn giản được định nghĩa bên dưới và hiển thị trong Hình 19.2. Trong các ví dụ này, chúng ta sẽ sử dụng một khóa đơn gọi là key và một column giá trị đơn (val_xval_y), nhưng các ý tưởng đều khái quát hóa được cho nhiều khóa và nhiều giá trị.

x <- tribble(
  ~key, ~val_x,
     1, "x1",
     2, "x2",
     3, "x3"
)
y <- tribble(
  ~key, ~val_y,
     1, "y1",
     2, "y2",
     4, "y3"
)
x and y are two data frames with 2 columns and 3 rows, with contents as described in the text. The values of the keys are colored: 1 is green, 2 is purple, 3 is orange, and 4 is yellow.
Hình 19.2: Biểu diễn đồ họa của hai bảng đơn giản. Các column key được tô màu mapping màu nền theo giá trị khóa. Các column màu xám biểu diễn các column “giá trị” được mang theo.

Hình 19.3 giới thiệu nền tảng cho biểu diễn trực quan của chúng ta. Nó cho thấy tất cả các khớp tiềm năng giữa xy dưới dạng giao điểm giữa các đường vẽ từ mỗi row của x và mỗi row của y. Các row và column trong đầu ra chủ yếu được xác định bởi x, nên bảng x nằm ngang và căn chỉnh với đầu ra.

x and y are placed at right-angles, with horizonal lines extending from x and vertical lines extending from y. There are 3 rows in x and 3 rows in y, which leads to nine intersections representing nine potential matches.
Hình 19.3: Để hiểu cách phép nối hoạt động, hãy hình dung mọi khớp có thể. Ở đây chúng ta thể hiện điều đó bằng một lưới các đường kết nối.

Để mô tả một loại nối cụ thể, chúng ta đánh dấu các khớp bằng chấm tròn. Các khớp xác định các row trong đầu ra, một data frame mới chứa khóa, các giá trị x, và các giá trị y. Ví dụ, Hình 19.4 hiển thị một nối trong (inner join), trong đó các row được giữ lại nếu và chỉ nếu các khóa bằng nhau.

x and y are placed at right-angles with lines forming a grid of potential matches. Keys 1 and 2 appear in both x and y, so we get a match, indicated by a dot. Each dot corresponds to a row in the output, so the resulting joined data frame has two rows.
Hình 19.4: Một nối trong khớp mỗi row trong x với row trong y có cùng giá trị key. Mỗi khớp trở thành một row trong đầu ra.

Chúng ta có thể áp dụng các nguyên tắc tương tự để giải thích các nối ngoài (outer join), giữ các quan sát xuất hiện trong ít nhất một data frame. Các phép nối này hoạt động bằng cách thêm một quan sát “ảo” bổ sung vào mỗi data frame. Quan sát này có một khóa khớp nếu không có khóa nào khác khớp, và các giá trị được điền bằng NA. Có ba loại nối ngoài:

  • Một nối trái (left join) giữ tất cả các quan sát trong x, Hình 19.5. Mỗi row của x được bảo toàn trong đầu ra vì nó có thể quay về khớp với một row NA trong y.

    Compared to the previous diagram showing an inner join, the y table gets a new virtual row containing NA that will match any row in x that didn't otherwise match. This means that the output now has three rows. For key = 3, which matches this virtual row, val_y takes value NA.
    Hình 19.5: Biểu diễn trực quan của nối trái, trong đó mỗi row trong x đều xuất hiện trong đầu ra.
  • Một nối phải (right join) giữ tất cả các quan sát trong y, Hình 19.6. Mỗi row của y được bảo toàn trong đầu ra vì nó có thể quay về khớp với một row NA trong x. Đầu ra vẫn khớp với x nhiều nhất có thể; các row thừa từ y được thêm vào cuối.

    Compared to the previous diagram showing an left join, the x table now gains a virtual row so that every row in y gets a match in x. val_x contains NA for the row in y that didn't match x.
    Hình 19.6: Biểu diễn trực quan của nối phải, trong đó mỗi row của y đều xuất hiện trong đầu ra.
  • Một nối đầy đủ (full join) giữ tất cả các quan sát xuất hiện trong x hoặc y, Hình 19.7. Mỗi row của xy đều có trong đầu ra vì cả xy đều có một row NA dự phòng. Một lần nữa, đầu ra bắt đầu với tất cả các row từ x, tiếp theo là các row y chưa khớp còn lại.

    Now both x and y have a virtual row that always matches. The result has 4 rows: keys 1, 2, 3, and 4 with all values from val_x and val_y, however key 2, val_y and key 4, val_x are NAs since those keys don't have a match in the other data frames.
    Hình 19.7: Biểu diễn trực quan của nối đầy đủ, trong đó mỗi row trong xy đều xuất hiện trong đầu ra.

Một cách khác để thể hiện sự khác biệt giữa các loại nối ngoài là bằng biểu đồ Venn, như trong Hình 19.8. Tuy nhiên, đây không phải là một biểu diễn tuyệt vời vì dù nó có thể giúp bạn nhớ những row nào được giữ lại, nhưng nó không minh họa được điều gì đang xảy ra với các column.

Venn diagrams for inner, full, left, and right joins. Each join represented with two intersecting circles representing data frames x and y, with x on the right and y on the left. Shading indicates the result of the join.
Hình 19.8: Biểu đồ Venn cho thấy sự khác biệt giữa nối trong, nối trái, nối phải, và nối đầy đủ.

Các phép nối được trình bày ở đây là cái gọi là nối đẳng thức, trong đó các row khớp nếu các khóa bằng nhau. Nối đẳng thức là loại nối phổ biến nhất, nên chúng ta thường bỏ qua tiền tố đẳng thức, và chỉ nói “nối trong” thay vì “nối đẳng thức trong”. Chúng ta sẽ quay lại nối không đẳng thức trong Phần 19.5.

19.4.1 Khớp hàng

Cho đến giờ chúng ta đã khám phá điều gì xảy ra khi một row trong x khớp với không hoặc một row trong y. Điều gì xảy ra nếu nó khớp với nhiều hơn một row? Để hiểu chuyện gì đang xảy ra, trước tiên hãy thu hẹp tập trung vào inner_join() và sau đó vẽ một hình minh họa, Hình 19.9.

A join diagram where x has key values 1, 2, and 3, and y has key values 1, 2, 2. The output has three rows because key 1 matches one row, key 2 matches two rows, and key 3 matches zero rows.
Hình 19.9: Ba cách một row trong x có thể khớp. x1 khớp một row trong y, x2 khớp hai row trong y, x3 khớp không row nào trong y. Lưu ý rằng dù có ba row trong x và ba row trong đầu ra, không có sự tương ứng trực tiếp giữa các row.

Có ba kết quả có thể cho một row trong x:

  • Nếu nó không khớp gì, nó bị loại bỏ.
  • Nếu nó khớp 1 row trong y, nó được giữ lại.
  • Nếu nó khớp nhiều hơn 1 row trong y, nó được nhân bản một lần cho mỗi khớp.

Về nguyên tắc, điều này có nghĩa là không có sự tương ứng đảm bảo giữa các row trong đầu ra và các row trong x, nhưng trong thực tế, điều này hiếm khi gây ra vấn đề. Tuy nhiên, có một trường hợp đặc biệt nguy hiểm có thể gây ra sự bùng nổ tổ hợp các row. Hãy tưởng tượng nối hai bảng sau:

df1 <- tibble(key = c(1, 2, 2), val_x = c("x1", "x2", "x3"))
df2 <- tibble(key = c(1, 2, 2), val_y = c("y1", "y2", "y3"))

Trong khi row đầu tiên trong df1 chỉ khớp một row trong df2, row thứ hai và thứ ba đều khớp hai row. Điều này đôi khi được gọi là nối many-to-many (nhiều-nhiều), và sẽ khiến dplyr phát ra cảnh báo:

df1 |>
  inner_join(df2, join_by(key))
#> Warning in inner_join(df1, df2, join_by(key)): Detected an unexpected many-to-many relationship between `x` and `y`.
#> ℹ Row 2 of `x` matches multiple rows in `y`.
#> ℹ Row 2 of `y` matches multiple rows in `x`.
#> ℹ If a many-to-many relationship is expected, set `relationship =
#>   "many-to-many"` to silence this warning.
#> # A tibble: 5 × 3
#>     key val_x val_y
#>   <dbl> <chr> <chr>
#> 1     1 x1    y1   
#> 2     2 x2    y2   
#> 3     2 x2    y3   
#> 4     2 x3    y2   
#> 5     2 x3    y3

Nếu bạn đang cố ý làm điều này, bạn có thể đặt relationship = "many-to-many", như cảnh báo gợi ý.

19.4.2 Nối lọc

Số lượng khớp cũng xác định hành vi của các phép nối lọc. Nối bán phần giữ các row trong x có một hoặc nhiều khớp trong y, như trong Hình 19.10. Nối phản giữ các row trong x khớp với không row nào trong y, như trong Hình 19.11. Trong cả hai trường hợp, chỉ sự tồn tại của khớp là quan trọng; không quan trọng nó khớp bao nhiêu lần. Điều này có nghĩa là nối lọc không bao giờ nhân bản row như nối biến đổi.

A join diagram with old friends x and y. In a semi join, only the presence of a match matters so the output contains the same columns as x.
Hình 19.10: Trong nối bán phần, chỉ cần có khớp là đủ; nếu không thì các giá trị trong y không ảnh hưởng đến đầu ra.
An anti-join is the inverse of a semi-join so matches are drawn with red lines indicating that they will be dropped from the output.
Hình 19.11: Nối phản là nghịch đảo của nối bán phần, loại bỏ các row từ x có khớp trong y.

19.5 Nối không đẳng thức

Cho đến giờ bạn chỉ thấy nối đẳng thức, các phép nối trong đó các row khớp nếu khóa x bằng khóa y. Bây giờ chúng ta sẽ nới lỏng hạn chế đó và thảo luận về các cách khác để xác định liệu một cặp row có khớp hay không.

Nhưng trước khi làm điều đó, chúng ta cần xem lại một phép đơn giản hóa ở trên. Trong nối đẳng thức, các khóa xy luôn bằng nhau, nên chúng ta chỉ cần hiển thị một trong đầu ra. Chúng ta có thể yêu cầu dplyr giữ cả hai khóa với keep = TRUE, dẫn đến đoạn code bên dưới và inner_join() được vẽ lại trong Hình 19.12.

x |> inner_join(y, join_by(key == key), keep = TRUE)
#> # A tibble: 2 × 4
#>   key.x val_x key.y val_y
#>   <dbl> <chr> <dbl> <chr>
#> 1     1 x1        1 y1   
#> 2     2 x2        2 y2
A join diagram showing an inner join between x and y. The result now includes four columns: key.x, val_x, key.y, and val_y. The values of key.x and key.y are identical, which is why we usually only show one.
Hình 19.12: Một nối trong hiển thị cả khóa xy trong đầu ra.

Khi chúng ta rời khỏi nối đẳng thức, chúng ta sẽ luôn hiển thị các khóa, vì các giá trị khóa thường sẽ khác nhau. Ví dụ, thay vì chỉ khớp khi x$keyy$key bằng nhau, chúng ta có thể khớp khi x$key lớn hơn hoặc bằng y$key, dẫn đến Hình 19.13. Các function nối của dplyr hiểu sự phân biệt giữa nối đẳng thức và không đẳng thức nên sẽ luôn hiển thị cả hai khóa khi bạn thực hiện nối không đẳng thức.

A join diagram illustrating join_by(key >= key). The first row of x matches one row of y and the second and thirds rows each match two rows. This means the output has five rows containing each of the following (key.x, key.y) pairs: (1, 1), (2, 1), (2, 2), (3, 1), (3, 2).
Hình 19.13: Một nối không đẳng thức trong đó khóa x phải lớn hơn hoặc bằng khóa y. Nhiều row tạo ra nhiều khớp.

Nối không đẳng thức không phải là một thuật ngữ đặc biệt hữu ích vì nó chỉ cho bạn biết phép nối không phải là gì, chứ không phải nó là gì. dplyr giúp bằng cách xác định bốn loại nối không đẳng thức đặc biệt hữu ích:

  • Nối chéo (cross join) khớp mỗi cặp row.
  • Nối bất đẳng thức (inequality join) sử dụng <, <=, >, và >= thay vì ==.
  • Nối cuộn (rolling join) tương tự như nối bất đẳng thức nhưng chỉ tìm khớp gần nhất.
  • Nối chồng lấp (overlap join) là một loại nối bất đẳng thức đặc biệt được thiết kế để làm việc với các khoảng.

Mỗi loại được mô tả chi tiết hơn trong các phần sau.

19.5.1 Nối chéo

Một nối chéo khớp mọi thứ, như trong Hình 19.14, tạo ra tích Descartes của các row. Điều này có nghĩa là đầu ra sẽ có nrow(x) * nrow(y) row.

A join diagram showing a dot for every combination of x and y.
Hình 19.14: Một nối chéo khớp mỗi row trong x với mọi row trong y.

Nối chéo hữu ích khi tạo hoán vị. Ví dụ, đoạn code dưới đây tạo ra mọi cặp tên có thể. Vì chúng ta đang nối df với chính nó, điều này đôi khi được gọi là tự nối (self-join). Nối chéo sử dụng một function nối khác vì không có sự phân biệt giữa nối trong/trái/phải/đầy đủ khi bạn khớp mọi row.

df <- tibble(name = c("John", "Simon", "Tracy", "Max"))
df |> cross_join(df)
#> # A tibble: 16 × 2
#>   name.x name.y
#>   <chr>  <chr> 
#> 1 John   John  
#> 2 John   Simon 
#> 3 John   Tracy 
#> 4 John   Max   
#> 5 Simon  John  
#> 6 Simon  Simon 
#> # ℹ 10 more rows

19.5.2 Nối bất đẳng thức

Nối bất đẳng thức sử dụng <, <=, >=, hoặc > để hạn chế tập các khớp có thể, như trong Hình 19.13Hình 19.15.

A diagram depicting an inequality join where a data frame x is joined by a data frame y where the key of x is less than the key of y, resulting in a triangular shape in the top-left corner.
Hình 19.15: Một nối bất đẳng thức trong đó x được nối với y trên các row mà khóa của x nhỏ hơn khóa của y. Điều này tạo thành hình tam giác ở góc trên bên trái.

Nối bất đẳng thức cực kỳ tổng quát, tổng quát đến mức khó nghĩ ra các trường hợp sử dụng cụ thể có ý nghĩa. Một kỹ thuật nhỏ hữu ích là sử dụng chúng để hạn chế nối chéo sao cho thay vì tạo tất cả hoán vị, chúng ta tạo tất cả tổ hợp:

df <- tibble(id = 1:4, name = c("John", "Simon", "Tracy", "Max"))

df |> inner_join(df, join_by(id < id))
#> # A tibble: 6 × 4
#>    id.x name.x  id.y name.y
#>   <int> <chr>  <int> <chr> 
#> 1     1 John       2 Simon 
#> 2     1 John       3 Tracy 
#> 3     1 John       4 Max   
#> 4     2 Simon      3 Tracy 
#> 5     2 Simon      4 Max   
#> 6     3 Tracy      4 Max

19.5.3 Nối cuộn

Nối cuộn là một loại nối bất đẳng thức đặc biệt, thay vì lấy mọi row thỏa mãn bất đẳng thức, bạn chỉ lấy row gần nhất, như trong Hình 19.16. Bạn có thể biến bất kỳ nối bất đẳng thức nào thành nối cuộn bằng cách thêm closest(). Ví dụ join_by(closest(x <= y)) khớp y nhỏ nhất lớn hơn hoặc bằng x, và join_by(closest(x > y)) khớp y lớn nhất nhỏ hơn x.

A rolling join is a subset of an inequality join so some matches are grayed out indicating that they're not used because they're not the "closest".
Hình 19.16: Nối cuộn tương tự như nối bất đẳng thức lớn-hơn-hoặc-bằng nhưng chỉ khớp giá trị đầu tiên.

Nối cuộn đặc biệt hữu ích khi bạn có hai bảng ngày tháng không căn chỉnh hoàn hảo và bạn muốn tìm (ví dụ) ngày gần nhất trong bảng 1 đến trước (hoặc sau) một ngày nào đó trong bảng 2.

Ví dụ, hãy tưởng tượng bạn phụ trách ban tổ chức tiệc cho văn phòng. Công ty của bạn khá tiết kiệm nên thay vì tổ chức tiệc riêng, bạn chỉ tổ chức tiệc mỗi quý một lần. Các quy tắc để xác định khi nào tổ chức tiệc hơi phức tạp: tiệc luôn vào thứ Hai, bạn bỏ qua tuần đầu tiên của tháng Một vì nhiều người đang nghỉ phép, và thứ Hai đầu tiên của Q3 2022 là ngày 4 tháng 7, nên phải lùi lại một tuần. Điều đó dẫn đến các ngày tiệc sau:

parties <- tibble(
  q = 1:4,
  party = ymd(c("2022-01-10", "2022-04-04", "2022-07-11", "2022-10-03"))
)

Bây giờ hãy tưởng tượng bạn có một bảng sinh nhật nhân viên:

set.seed(123)
employees <- tibble(
  name = sample(babynames::babynames$name, 100),
  birthday = ymd("2022-01-01") + (sample(365, 100, replace = TRUE) - 1)
)
employees
#> # A tibble: 100 × 2
#>   name     birthday  
#>   <chr>    <date>    
#> 1 Kemba    2022-01-22
#> 2 Orean    2022-06-26
#> 3 Kirstyn  2022-02-11
#> 4 Amparo   2022-11-11
#> 5 Belen    2022-03-25
#> 6 Rayshaun 2022-01-11
#> # ℹ 94 more rows

Và cho mỗi nhân viên, chúng ta muốn tìm ngày tiệc cuối cùng đến trước (hoặc vào) ngày sinh nhật của họ. Chúng ta có thể biểu đạt điều đó bằng nối cuộn:

employees |>
  left_join(parties, join_by(closest(birthday >= party)))
#> # A tibble: 100 × 4
#>   name     birthday       q party     
#>   <chr>    <date>     <int> <date>    
#> 1 Kemba    2022-01-22     1 2022-01-10
#> 2 Orean    2022-06-26     2 2022-04-04
#> 3 Kirstyn  2022-02-11     1 2022-01-10
#> 4 Amparo   2022-11-11     4 2022-10-03
#> 5 Belen    2022-03-25     1 2022-01-10
#> 6 Rayshaun 2022-01-11     1 2022-01-10
#> # ℹ 94 more rows

Tuy nhiên, có một vấn đề với cách tiếp cận này: những người có sinh nhật trước ngày 10 tháng 1 không được tham gia tiệc:

employees |>
  anti_join(parties, join_by(closest(birthday >= party)))
#> # A tibble: 2 × 2
#>   name   birthday  
#>   <chr>  <date>    
#> 1 Maks   2022-01-07
#> 2 Nalani 2022-01-04

Để giải quyết vấn đề này, chúng ta cần tiếp cận bài toán theo cách khác, với nối chồng lấp.

19.5.4 Nối chồng lấp

Nối chồng lấp cung cấp ba function trợ giúp sử dụng nối bất đẳng thức để giúp làm việc với khoảng dễ hơn:

  • between(x, y_lower, y_upper) là viết tắt của x >= y_lower, x <= y_upper.
  • within(x_lower, x_upper, y_lower, y_upper) là viết tắt của x_lower >= y_lower, x_upper <= y_upper.
  • overlaps(x_lower, x_upper, y_lower, y_upper) là viết tắt của x_lower <= y_upper, x_upper >= y_lower.

Hãy tiếp tục ví dụ sinh nhật để xem bạn có thể sử dụng chúng như thế nào. Có một vấn đề với chiến lược chúng ta đã dùng ở trên: không có tiệc nào trước các sinh nhật từ ngày 1-9 tháng 1. Vì vậy, có thể tốt hơn nếu chỉ rõ khoảng ngày mà mỗi bữa tiệc bao gồm, và tạo trường hợp đặc biệt cho những sinh nhật sớm:

parties <- tibble(
  q = 1:4,
  party = ymd(c("2022-01-10", "2022-04-04", "2022-07-11", "2022-10-03")),
  start = ymd(c("2022-01-01", "2022-04-04", "2022-07-11", "2022-10-03")),
  end = ymd(c("2022-04-03", "2022-07-11", "2022-10-02", "2022-12-31"))
)
parties
#> # A tibble: 4 × 4
#>       q party      start      end       
#>   <int> <date>     <date>     <date>    
#> 1     1 2022-01-10 2022-01-01 2022-04-03
#> 2     2 2022-04-04 2022-04-04 2022-07-11
#> 3     3 2022-07-11 2022-07-11 2022-10-02
#> 4     4 2022-10-03 2022-10-03 2022-12-31

Hadley rất tệ trong việc nhập dữ liệu nên anh ấy cũng muốn kiểm tra rằng các khoảng tiệc không chồng lấp nhau. Một cách để làm điều này là sử dụng tự nối để kiểm tra xem có khoảng bắt đầu-kết thúc nào chồng lấp với khoảng khác không:

parties |>
  inner_join(parties, join_by(overlaps(start, end, start, end), q < q)) |>
  select(start.x, end.x, start.y, end.y)
#> # A tibble: 1 × 4
#>   start.x    end.x      start.y    end.y     
#>   <date>     <date>     <date>     <date>    
#> 1 2022-04-04 2022-07-11 2022-07-11 2022-10-02

Ồ, có sự chồng lấp, vậy hãy sửa vấn đề đó và tiếp tục:

parties <- tibble(
  q = 1:4,
  party = ymd(c("2022-01-10", "2022-04-04", "2022-07-11", "2022-10-03")),
  start = ymd(c("2022-01-01", "2022-04-04", "2022-07-11", "2022-10-03")),
  end = ymd(c("2022-04-03", "2022-07-10", "2022-10-02", "2022-12-31"))
)

Bây giờ chúng ta có thể ghép mỗi nhân viên với bữa tiệc của họ. Đây là một chỗ tốt để sử dụng unmatched = "error" vì chúng ta muốn nhanh chóng phát hiện nếu có nhân viên nào không được xếp tiệc.

employees |>
  inner_join(parties, join_by(between(birthday, start, end)), unmatched = "error")
#> # A tibble: 100 × 6
#>   name     birthday       q party      start      end       
#>   <chr>    <date>     <int> <date>     <date>     <date>    
#> 1 Kemba    2022-01-22     1 2022-01-10 2022-01-01 2022-04-03
#> 2 Orean    2022-06-26     2 2022-04-04 2022-04-04 2022-07-10
#> 3 Kirstyn  2022-02-11     1 2022-01-10 2022-01-01 2022-04-03
#> 4 Amparo   2022-11-11     4 2022-10-03 2022-10-03 2022-12-31
#> 5 Belen    2022-03-25     1 2022-01-10 2022-01-01 2022-04-03
#> 6 Rayshaun 2022-01-11     1 2022-01-10 2022-01-01 2022-04-03
#> # ℹ 94 more rows

19.5.5 Bài tập

  1. Bạn có thể giải thích điều gì đang xảy ra với các khóa trong phép nối đẳng thức này không? Tại sao chúng khác nhau?

    x |> full_join(y, join_by(key == key))
    #> # A tibble: 4 × 3
    #>     key val_x val_y
    #>   <dbl> <chr> <chr>
    #> 1     1 x1    y1   
    #> 2     2 x2    y2   
    #> 3     3 x3    <NA> 
    #> 4     4 <NA>  y3
    
    x |> full_join(y, join_by(key == key), keep = TRUE)
    #> # A tibble: 4 × 4
    #>   key.x val_x key.y val_y
    #>   <dbl> <chr> <dbl> <chr>
    #> 1     1 x1        1 y1   
    #> 2     2 x2        2 y2   
    #> 3     3 x3       NA <NA> 
    #> 4    NA <NA>      4 y3
  2. Khi tìm xem có khoảng tiệc nào chồng lấp với khoảng tiệc khác hay không, chúng ta đã sử dụng q < q trong join_by()? Tại sao? Điều gì xảy ra nếu bạn bỏ bất đẳng thức này?

19.6 Tóm tắt

Trong chương này, bạn đã học cách sử dụng nối biến đổi và nối lọc để kết hợp dữ liệu từ một cặp data frame. Trên đường đi, bạn đã học cách xác định khóa, và sự khác biệt giữa khóa chính và khóa ngoại. Bạn cũng hiểu cách phép nối hoạt động và cách tìm ra số row đầu ra sẽ có. Cuối cùng, bạn đã có cái nhìn thoáng qua về sức mạnh của nối không đẳng thức và thấy một vài trường hợp sử dụng thú vị.

Chương này kết thúc phần “Biến đổi” của cuốn sách, nơi trọng tâm là các công cụ bạn có thể sử dụng với từng column và tibble riêng lẻ. Bạn đã học về các function dplyr và base để làm việc với vector logic, số, và bảng hoàn chỉnh, các function stringr để làm việc với string, các function lubridate để làm việc với ngày-giờ, và các function forcats để làm việc với factor.

Trong phần tiếp theo của cuốn sách, bạn sẽ tìm hiểu thêm về cách đưa các loại dữ liệu khác nhau vào R ở dạng gọn gàng.


  1. Hãy nhớ rằng trong RStudio bạn cũng có thể sử dụng View() để tránh vấn đề này.↩︎

  2. Điều đó không đúng 100%, nhưng bạn sẽ nhận được cảnh báo mỗi khi nó không đúng.↩︎