14 Nối dữ liệu

Ở trên: một ví dụ động về phép nối bên trái (nguồn ảnh)

Chương này mô tả các cách “nối (join)”, “khớp (match)”, “liên kết (link),”gắn (bind)” và các cách khác để kết hợp các data frame.

Việc phân tích hay quy trình làm việc dịch tễ học của bạn liên quan đến nhiều nguồn dữ liệu và sự liên kết của nhiều bộ dữ liệu với nhau là phổ biến. Có thể bạn cần kết nối dữ liệu phòng thí nghiệm với kết quả lâm sàng của bệnh nhân, hoặc dữ liệu di động của Google với các xu hướng bệnh truyền nhiễm hay thậm chí là bộ dữ liệu ở một giai đoạn phân tích với phiên bản đã biến đổi của chính nó.

Trong chương này chúng ta trình bày code nhằm:

  • Hướng dẫn nối hai data frame sao cho các hàng khớp với nhau dựa trên các giá trị chung trong cột định danh
  • Nối hai data frame dựa trên sự phù hợp (có khả năng) theo xác suất giữa các giá trị
  • Mở rộng một data frame bằng cách gắn hoặc (“thêm vào”) trực tiếp các hàng hoặc cột từ một data frame khác

14.1 Chuẩn bị

Gọi package

Đoạn code này hiển thị những package cần gọi cho các phân tích. Trong sổ tay này, chúng ta nhấn mạnh đến hàm p_load() từ pacman, hàm sẽ cài đặt package nếu cần gọi nó ra để sử dụng. Bạn cũng có thể gọi các package đã cài đặt với hàm library() từ base R. Xem chương R cơ bản để có thêm thông tin về các R package.

pacman::p_load(
  rio,            # import and export
  here,           # locate files 
  tidyverse,      # data management and visualisation
  RecordLinkage,  # probabilistic matches
  fastLink        # probabilistic matches
)

Nhập dữ liệu

Để bắt đầu, chúng ta nhập các trường hợp trong linelist đã được làm sạch từ một vụ dịch Ebola mô phỏng. Nếu bạn muốn theo dõi, bấm để tải xuống linelist “đã được làm sạch” (tệp .rds). Nhập dữ liệu với hàm import() từ package rio (hàm này xử lý nhiều loại tệp như .xlsx, .csv, .rds - xem chương Nhập xuất dữ liệu để biết thêm chi tiết).

# import case linelist 
linelist <- import("linelist_cleaned.rds")

50 hàng đầu tiên của linelist được hiển thị dưới đây.

Bộ dữ liệu mẫu

Trong phần nối dữ liệu dưới đây, chúng ta sẽ sử dụng các bộ dữ liệu sau:

  1. Một phiên bản “thu nhỏ” của bộ dữ liệu linelist, chỉ chứa các cột case_id, date_onset, hospital và chỉ 10 hàng đầu tiên
  2. Một data frame riêng biệt có tên hosp_info, chứa thêm thông tin chi tiết của từng bệnh viện

Trong phần ghép theo xác suất, chúng ta sẽ sử dụng hai bộ dữ liệu nhỏ khác nhau. Code để tạo các bộ dữ liệu này được đưa ra trong phần đó.

Dữ liệu linelist “thu nhỏ”

Dưới đây là bộ dữ liệu linelist thu nhỏ, bao gồm 10 hàng và chỉ chứa các cột case_id, date_onsethospital.

linelist_mini <- linelist %>%                 # start with original linelist
  select(case_id, date_onset, hospital) %>%   # select columns
  head(10)                                    # only take the first 10 rows

Data frame thông tin bệnh viện

Dưới đây là code để tạo một data frame riêng biệt với thông tin bổ sung của bảy bệnh viện (số lượng người dân có thể tiếp cận và mức độ chăm sóc hiện có). Lưu ý rằng tên “Bệnh viện Quân đội (Military Hospital)” thuộc về hai bệnh viện khác nhau - một bệnh viện cấp 1 phục vụ 10000 cư dân và một bệnh viện cấp hai phục vụ 40500 cư dân.

# Make the hospital information data frame
hosp_info = data.frame(
  hosp_name     = c("central hospital", "military", "military", "port", "St. Mark's", "ignace", "sisters"),
  catchment_pop = c(1950280, 40500, 10000, 50280, 12000, 5000, 4200),
  level         = c("Tertiary", "Secondary", "Primary", "Secondary", "Secondary", "Primary", "Primary")
)

Đây là data frame này:

Làm sạch trước

Các phương pháp nối truyền thống (không theo xác suất) phân biệt chữ hoa, chữ thường và yêu cầu khớp các ký tự chính xác giữa các giá trị trong hai data frame. Để minh họa một số bước làm sạch mà bạn có thể cần làm trước khi bắt đầu nối, chúng ta sẽ làm sạch và căn chỉnh bộ dữ liệu linelist_minihosp_info ngay bây giờ.

Xác định điểm khác biệt

Chúng ta cần các giá trị của cột hosp_name trong data frame hosp_info để khớp với các giá trị của cột hospital trong data frame linelist_mini.

Dưới đây là các giá trị trong data frame linelist_mini, được in bằng hàm unique()trong base R:

unique(linelist_mini$hospital)
## [1] "Other"                                "Missing"                              "St. Mark's Maternity Hospital (SMMH)"
## [4] "Port Hospital"                        "Military Hospital"

và đây là các giá trị trong data frame hosp_info:

unique(hosp_info$hosp_name)
## [1] "central hospital" "military"         "port"             "St. Mark's"       "ignace"           "sisters"

Bạn có thể thấy rằng mặc dù một số bệnh viện tồn tại trong cả hai data frame, nhưng có nhiều điểm khác biệt về chính tả.

Căn chỉnh giá trị

Chúng ta bắt đầu bằng cách làm sạch các giá trị trong data frame hosp_info. Như đã được giải thích trong chương Làm sạch số liệu và các hàm quan trọng, chúng ta có thể code lại các giá trị với tiêu chí logic bằng cách sử dụng hàm case_when() của dplyr. Đối với bốn bệnh viện tồn tại trong cả hai data frame, chúng ta thay đổi các giá trị để phù hợp với các giá trị trong linelist_mini. Các bệnh viện khác chúng ta để nguyên giá trị (TRUE ~ hosp_name).

CẨN TRỌNG: Thông thường khi làm sạch, chúng ta nên tạo một cột mới (ví dụ: hosp_name_clean), nhưng để dễ dàng giải thích, chúng ta hiển thị các thay đổi trên cột cũ

hosp_info <- hosp_info %>% 
  mutate(
    hosp_name = case_when(
      # criteria                         # new value
      hosp_name == "military"          ~ "Military Hospital",
      hosp_name == "port"              ~ "Port Hospital",
      hosp_name == "St. Mark's"        ~ "St. Mark's Maternity Hospital (SMMH)",
      hosp_name == "central hospital"  ~ "Central Hospital",
      TRUE                             ~ hosp_name
      )
    )

Tên bệnh viện xuất hiện trong cả hai data frame đều được căn chỉnh. Có hai bệnh viện trong dữ liệu hosp_info không có trong linelist_mini - chúng ta sẽ giải quyết những trường hợp này sau, trong phần nối dữ liệu.

unique(hosp_info$hosp_name)
## [1] "Central Hospital"                     "Military Hospital"                    "Port Hospital"                       
## [4] "St. Mark's Maternity Hospital (SMMH)" "ignace"                               "sisters"

Trước một phép nối, việc chuyển đổi một cột thành tất cả chữ thường hoặc tất cả chữ hoa thường dễ dàng nhất. Nếu bạn cần chuyển đổi tất cả các giá trị trong một cột thành CHỮ HOA hoặc chữ thường, hãy sử dụng mutate() và đặt cột bên trong một trong những hàm từ package stringr, như ã được trình bày trong chương Ký tự và chuỗi.

str_to_upper()
str_to_upper()
str_to_title()

14.2 Nối bằng dplyr

Package dplyr cung cấp một số hàm nối khác nhau. dplyr là một package thuộc hệ sinh thái tidyverse. Các hàm nối này được mô tả ở bên dưới, trong các trường hợp sử dụng đơn giản.

Rất cảm ơn https://github.com/gadenbuie vì những tấm ảnh động bổ ích!

Cú pháp chung

Các lệnh nối có thể được chạy dưới dạng các lệnh độc lập để nối hai data frame thành một đối tượng mới, hoặc chúng có thể được sử dụng trong một chuỗi pipe (%>%) để hợp nhất một data frame vào một data frame khác khi nó đang được làm sạch hoặc chỉnh sửa.

Trong ví dụ dưới đây, hàm left_join() được sử dụng như một lệnh độc lập để tạo một data frame joined_data mới. Các dữ liệu đầu vào là data frame 1 và 2 (df1df2). Data frame đầu tiên được liệt kê là data frame cơ sở và data frame thứ hai được liệt kê là data frame sẽ nối với data frame thứ nhất.

Đối số thứ ba by = là nơi bạn xác định các cột trong mỗi data frame mà sẽ được sử dụng để căn chỉnh các hàng trong hai data frame. Nếu tên của các cột này khác nhau, hãy đặt chúng trong một vectơ c() như được trình bày dưới đây, nơi mà các hàng được khớp trên cơ sở các giá trị chung giữa cột ID trong df1 và cột identifier trong df2.

# Join based on common values between column "ID" (first data frame) and column "identifier" (second data frame)
joined_data <- left_join(df1, df2, by = c("ID" = "identifier"))

Nếu các cột by trong cả hai data frame có cùng tên, bạn chỉ cần cung cấp tên này, đặt trong dấu ngoặc kép.

# Joint based on common values in column "ID" in both data frames
joined_data <- left_join(df1, df2, by = "ID")

Nếu bạn đang nối các data frame dựa trên các giá trị chung của nhiều trường, hãy liệt kê các trường này trong vectơ c(). Ví dụ dưới đây nối các hàng nếu các giá trị trong ba cột trong mỗi bộ dữ liệu căn chỉnh chính xác.

# join based on same first name, last name, and age
joined_data <- left_join(df1, df2, by = c("name" = "firstname", "surname" = "lastname", "Age" = "age"))

Các lệnh nối cũng có thể được chạy trong một chuỗi pipe. Điều này sẽ thực hiện sửa đổi data frame trong chuỗi pipe.

Trong ví dụ dưới đây, df1 được đưa qua các pipe, df2 được nối vào đó và vì thế df1 được chỉnh sửa và xác định lại.

df1 <- df1 %>%
  filter(date_onset < as.Date("2020-03-05")) %>% # miscellaneous cleaning 
  left_join(df2, by = c("ID" = "identifier"))    # join df2 to df1

CẨN TRỌNG: Nối dựa trên những trường hợp cụ thể! Do đó, rất hữu ích khi chuyển đổi tất cả các giá trị thành chữ thường hoặc chữ hoa trước khi nối. Xem thêm chương về ký tự/chuỗi.

Nối trái và phải

Nối trái hoặc phải thường được sử dụng để thêm thông tin vào data frame - thông tin mới chỉ được thêm vào các hàng đã tồn tại trong data frame cơ sở. Đây là những phép nối phổ biến trong hoạt động dịch tễ học vì chúng được sử dụng để thêm thông tin từ một bộ dữ liệu vào một bộ dữ liệu khác.

Khi sử dụng các phép nối này, thứ tự viết của các data frame trong lệnh rất quan trọng*.

  • Trong phép nối trái, data frame đầu tiên được viết là data frame cơ sở
  • Trong phép nối phải, data frame thứ hai được viết là data frame cơ sở

Tất cả các hàng của data frame cơ sở được giữ lại. Thông tin trong data frame (thứ cấp) khác được kết hợp với data frame cơ sở chỉ khi có sự trùng khớp của (các) cột định danh. Ngoài ra:

  • Các hàng trong data frame thứ cấp không khớp sẽ bị loại bỏ.
  • Nếu có nhiều hàng cơ sở khớp với một hàng trong data frame thứ cấp (nhiều-khớp-một), thông tin phụ sẽ được thêm vào mỗi hàng cơ sở được khớp.
  • Nếu một hàng cơ sở khớp với nhiều hàng trong data frame thứ cấp (một-khớp-nhiều), tất cả các kết hợp sẽ được đưa ra, nghĩa là các hàng mới có thể được thêm vào data frame trả về của bạn!

Sau đây là các ví dụ động về phép nối trái và phải (nguồn ảnh)

Ví dụ

Dưới đây là kết quả đầu ra của phép nối trái left_join() của bộ dữ liệu hosp_info (data frame thứ cấp, xem tại đây) vào linelist_mini (data frame cơ sở, xem tại đây). linelist_mini gốc có nrow(linelist_mini) hàng. linelist_mini đã chỉnh sửa được hiển thị. Lưu ý những điều dưới đây:

  • Hai cột mới catchment_poplevel đã được thêm vào bên trái của linelist_mini
  • Tất cả các hàng gốc của data frame cơ sở linelist_mini đều được giữ lại
  • Bất kỳ hàng gốc nào của linelist_mini cho “Military Hospital” đều bị trùng lặp vì nó khớp với hai hàng trong data frame thứ cấp, do đó cả hai sự kết hợp đều được trả về
  • Cột định danh kết hợp của bộ dữ liệu thứ cấp (hosp_name) đã biến mất vì nó thừa so với cột định danh trong bộ dữ liệu chính (hospital)
  • Khi một hàng cơ sở không khớp với bất kỳ hàng thứ cấp nào (ví dụ: khi hospital là “Other” hoặc “Missing”), NA (trống) sẽ điền vào các cột từ data frame thứ cấp
  • Các hàng trong data frame thứ cấp không khớp với data frame cơ sở (bệnh viện “sisters” và “ignace”) đã bị loại bỏ
linelist_mini %>% 
  left_join(hosp_info, by = c("hospital" = "hosp_name"))

“Tôi nên sử dụng phép nối phải hay phép nối trái?”

Để trả lời câu hỏi trên, hãy tự hỏi “data frame nào nên giữ lại tất cả các hàng của nó?” - hãy sử dụng cái này làm data frame cơ sở. Phép nối trái giữ tất cả các hàng trong data frame đầu tiên được viết trong lệnh, trong khi phép nối phải giữ tất cả các hàng trong data frame thứ hai.

Hai lệnh bên dưới đạt cùng một kết quả đầu ra - 10 hàng hosp_info được nối vào bộ dữ liệu cơ sở linelist_mini, tuy nhiên chúng sử dụng các phép nối khác nhau. Kết quả là thứ tự cột sẽ khác nhau dựa trên việc hosp_info đến từ bên phải (trong phép nối bên trái) hay đến từ bên trái (trong phép nối bên phải). Thứ tự của các hàng cũng có thể thay đổi tương ứng. Nhưng cả hai hệ quả này đều có thể được giải quyết bằng cách sử dụng select() để sắp xếp lại các cột hoặc arrange() để sắp xếp các hàng.

# The two commands below achieve the same data, but with differently ordered rows and columns
left_join(linelist_mini, hosp_info, by = c("hospital" = "hosp_name"))
right_join(hosp_info, linelist_mini, by = c("hosp_name" = "hospital"))

Đây là kết quả nối hosp_info vào linelist_mini qua phép nối trái (các cột mới đến từ bên phải)

Đây là kết quả nối hosp_info vào linelist_mini qua phép nối phải (các cột mới đến từ bên trái)

Ngoài ra, hãy xem xét liệu trường hợp-đang sử dụng của bạn có nằm trong một chuỗi pipe (%>%) hay không. Nếu bộ dữ liệu nằm trong pipe là đường cơ sở, bạn có thể sẽ sử dụng một phép nối trái để thêm dữ liệu vào đó.

Nối hoàn toàn

Nối hoàn toàn là phép nối bao hàm nhất trong tất cả các phép nối - nó trả về tất cả các hàng từ cả hai data frame.

Nếu có bất kỳ hàng nào chỉ hiện diện duy nhất trong một data frame (khi không tìm thấy hàng nào phù hợp), data frame sẽ bao gồm các hàng đó và trở nên dài hơn. Các giá trị missing NA được sử dụng để điền-vào bất kỳ khoảng trống nào đã tạo. Khi bạn nối, hãy kiểm tra số cột và số hàng cẩn thận để khắc phục lỗi về phân biệt chữ hoa với chữ thường và đảm bảo các kết quả khớp ký tự chính xác.

Data frame “cơ sở” là data frame được viết đầu tiên trong lệnh. Việc điều chỉnh data frame này sẽ không ảnh hưởng đến những bản ghi nào được trả về bởi phép nối, nhưng nó có thể ảnh hưởng đến thứ tự cột kết quả, thứ tự hàng và cột định danh nào được giữ lại.

Ví dụ động về một phép nối hoàn toàn (nguồn ảnh)

Ví dụ

Dưới đây kết quả đầu ra của phép nối hoàn toàn full_join() của hosp_info (ban đầu là nrow(hosp_info), xem tại đây) vào linelist_mini (ban đầu là nrow(linelist_mini), xem tại đây). Lưu ý những điều dưới đây:

  • Tất cả các hàng cơ sở đều được giữ nguyên (linelist_mini)
  • Các hàng trong data frame thứ cấp không khớp với data frame cơ sở được giữ lại (“ignace” và “sisters”), với các giá trị trong các cột cơ sở tương ứng case_idonset điền-vào các giá trị missing
  • Tương tự, các hàng trong data frame cơ sở không khớp với hàng trong data frame thứ cấp (“Other” và “Missing”) được giữ lại, với các cột phụ catchment_poplevel điền-vào các giá trị missing
  • Trong trường hợp khớp một-với-một hoặc nhiều-với-một (ví dụ: các hàng của “Military Hospital”), tất cả các kết hợp có thể có được trả về (kéo dài thêm data frame cuối cùng)
  • Chỉ cột định danh từ data frame cơ sở được giữ lại (hospital)
linelist_mini %>% 
  full_join(hosp_info, by = c("hospital" = "hosp_name"))

Nối bên trong

Nối bên trong là phép nối hạn chế nhất trong tất cả các phép nối - nó chỉ trả về các hàng có kết quả khớp trên cả hai data frame.
Điều này có nghĩa là số hàng trong data frame cơ sở có thể thực sự giảm xuống. Việc điều chỉnh data frame nào là “cơ sở” (được viết đầu tiên trong hàm) sẽ không ảnh hưởng đến hàng nào được trả về, nhưng nó sẽ ảnh hưởng đến thứ tự cột, thứ tự hàng và cột định danh nào được giữ lại.

Ví dụ động về một phép nối bên trong (nguồn ảnh)

Ví dụ

Dưới đây kết quả đầu ra việc nối inner_join() của linelist_mini (cơ sở) với hosp_info (thứ cấp). Lưu ý những điều dưới đây:

  • Các hàng cơ sở không khớp với dữ liệu thứ cấp sẽ bị xóa (các hàng mà hospital nhận giá trị “Missing” hoặc “Other”)
  • Tương tự, các hàng từ data frame thứ cấp không khớp trong data frame cơ sở sẽ bị xóa (các hàng mà hosp_name nhận giá trị “sisters” hoặc “ignace”)
  • Chỉ cột định danh từ data frame cơ sở được giữ lại (hospital)
linelist_mini %>% 
  inner_join(hosp_info, by = c("hospital" = "hosp_name"))

Nối một phần

Nối một phần là một “phép nối chọn lọc” mà sử dụng bộ dữ liệu khác không nhằm để thêm hàng hay cột, mà để lọc dữ liệu.

Phép nối-một phần giữ lại tất cả các quan sát trong data frame cơ sở mà có sự trùng khớp với data frame thứ cấp (nhưng không thêm cột mới cũng như không sao chép bất kỳ hàng nào cho các dữ liệu khớp). Đọc thêm về những phép nối “chọn lọc” này tại đây.

Ví dụ động về phép nối một phần (nguồn ảnh)

Như một ví dụ, code dưới đây trả về các hàng từ data frame hosp_info mà khớp với linelist_mini theo tên bệnh viện.

hosp_info %>% 
  semi_join(linelist_mini, by = c("hosp_name" = "hospital"))
##                              hosp_name catchment_pop     level
## 1                    Military Hospital         40500 Secondary
## 2                    Military Hospital         10000   Primary
## 3                        Port Hospital         50280 Secondary
## 4 St. Mark's Maternity Hospital (SMMH)         12000 Secondary

Anti join

Anti join là một “phép nối chọn lọc” khác trả về các hàng trong data frame cơ sở không khớp với data frame thứ cấp.

Đọc thêm về những phép nối “chọn lọc” này tại đây.

Các tình huống phổ biến của anti join bao gồm xác định các bản ghi không tồn tại trong một data frame khác, khắc phục lỗi chính tả trong một phép nối (xem xét các bản ghi đáng lẽ sẽ khớp) và kiểm tra các bản ghi đã bị loại trừ sau một phép nối khác.

Như với right_join()left_join(), quan trọng là data frame cơ sở (được liệt kê đầu tiên). Các hàng được trả về chỉ từ data frame cơ sở. Lưu ý trong ảnh động bên dưới, hàng trong data frame thứ cấp (hàng 4 màu tím) không được trả về mặc dù nó không khớp với hàng cơ sở.

Ví dụ động về anti join (nguồn ảnh)

Ví dụ anti_join() đơn giản

Một ví dụ đơn giản, hãy tìm bệnh viện trong hosp_info mà không tồn tại trong linelist_mini. chúng ta liệt kê hosp_info trước, như một data frame cơ sở. Các bệnh viện không có trong linelist_mini sẽ được trả về.

hosp_info %>% 
  anti_join(linelist_mini, by = c("hosp_name" = "hospital"))

Ví dụ anti_join() phức tạp

Một ví dụ khác, giả sử chúng ta đã chạy một inner_join() giữa linelist_minihosp_info. Lệnh này chỉ trả về một tập con các bản ghi của linelist_mini gốc, vì một số bản ghi không có trong hosp_info.

linelist_mini %>% 
  inner_join(hosp_info, by = c("hospital" = "hosp_name"))

Để xem lại các bản ghi linelist_mini đã bị loại trừ trong phép nối bên trong, chúng ta có thể chạy một phép nối anti-join với cùng thiết lập (linelist_mini là data frame cơ sở).

linelist_mini %>% 
  anti_join(hosp_info, by = c("hospital" = "hosp_name"))

Để xem các bản ghi hosp_info đã bị loại trừ trong phép nối bên trong, chúng ta cũng có thể chạy một phép nối anti-join với hosp_info là data frame cơ sở.

14.3 Khớp theo xác suất

Nếu bạn không có thông tin định danh duy nhất chung trên các bộ dữ liệu để nối, hãy xem xét sử dụng thuật toán khớp theo xác suất. Phương pháp này sẽ tìm thấy các bản ghi khớp với nhau dựa trên sự tương đồng (ví dụ: khoảng cách chuỗi Jaro-Winkler hoặc khoảng cách số). Dưới đây là một ví dụ đơn giản sử dụng package fastLink.

Gọi package

pacman::p_load(
  tidyverse,      # data manipulation and visualization
  fastLink        # record matching
  )

Dưới đây là hai bộ dữ liệu mẫu nhỏ mà chúng ta sẽ sử dụng để giải thích phương pháp khớp theo xác suất (casestest_results):

Đây là code được sử dụng để tạo bộ dữ liệu:

# make datasets

cases <- tribble(
  ~gender, ~first,      ~middle,     ~last,        ~yr,   ~mon, ~day, ~district,
  "M",     "Amir",      NA,          "Khan",       1989,  11,   22,   "River",
  "M",     "Anthony",   "B.",        "Smith",      1970, 09, 19,      "River", 
  "F",     "Marialisa", "Contreras", "Rodrigues",  1972, 04, 15,      "River",
  "F",     "Elizabeth", "Casteel",   "Chase",      1954, 03, 03,      "City",
  "M",     "Jose",      "Sanchez",   "Lopez",      1996, 01, 06,      "City",
  "F",     "Cassidy",   "Jones",      "Davis",     1980, 07, 20,      "City",
  "M",     "Michael",   "Murphy",     "O'Calaghan",1969, 04, 12,      "Rural", 
  "M",     "Oliver",    "Laurent",    "De Bordow" , 1971, 02, 04,     "River",
  "F",      "Blessing",  NA,          "Adebayo",   1955,  02, 14,     "Rural"
)

results <- tribble(
  ~gender,  ~first,     ~middle,     ~last,          ~yr, ~mon, ~day, ~district, ~result,
  "M",      "Amir",     NA,          "Khan",         1989, 11,   22,  "River", "positive",
  "M",      "Tony",   "B",         "Smith",          1970, 09,   19,  "River", "positive",
  "F",      "Maria",    "Contreras", "Rodriguez",    1972, 04,   15,  "Cty",   "negative",
  "F",      "Betty",    "Castel",   "Chase",        1954,  03,   30,  "City",  "positive",
  "F",      "Andrea",   NA,          "Kumaraswamy",  2001, 01,   05,  "Rural", "positive",      
  "F",      "Caroline", NA,          "Wang",         1988, 12,   11,  "Rural", "negative",
  "F",      "Trang",    NA,          "Nguyen",       1981, 06,   10,  "Rural", "positive",
  "M",      "Olivier" , "Laurent",   "De Bordeaux",  NA,   NA,   NA,  "River", "positive",
  "M",      "Mike",     "Murphy",    "O'Callaghan",  1969, 04,   12,  "Rural", "negative",
  "F",      "Cassidy",  "Jones",     "Davis",        1980, 07,   02,  "City",  "positive",
  "M",      "Mohammad", NA,          "Ali",          1942, 01,   17,  "City",  "negative",
  NA,       "Jose",     "Sanchez",   "Lopez",        1995, 01,   06,  "City",  "negative",
  "M",      "Abubakar", NA,          "Abullahi",     1960, 01,   01,  "River", "positive",
  "F",      "Maria",    "Salinas",   "Contreras",    1955, 03,   03,  "River", "positive"
  )

Bộ dữ liệu cases có 9 bản ghi của những bệnh nhân đang chờ kết quả xét nghiệm.

Bộ dữ liệu test_results có 14 bản ghi và chứa cột result, cột mà chúng ta muốn thêm vào các bản ghi trong cases dựa trên các bản ghi khớp theo xác xuất.

Khớp theo xác suất

Hàm fastLink() từ package fastLink có thể được sử dụng để áp dụng một thuật toán so khớp. Đây là thông tin cơ bản. Bạn có thể đọc chi tiết thêm bằng cách nhập ?fastLink trong console của mình.

  • Xác định hai data frame để so sánh với các đối số dfA =dfB =
  • Trong varnames = cung cấp tất cả các tên cột được sử dụng để khớp. Tất cả tên cột phải tồn tại trong cả hai dfAdfB.
  • Trong stringdist.match = cung cấp các cột từ những cột trong varnames được đánh giá trên chuỗi “distance”.
  • Trong numeric.match = cung cấp các cột từ những cột trong varnames được đánh giá trên khoảng.
  • Các giá trị missing sẽ bị bỏ qua
  • Theo mặc định, mỗi hàng từ một trong hai data frame sẽ được khớp với nhiều nhất một hàng trong data frame còn lại. Nếu bạn muốn xem tất cả các kết quả khớp được đánh giá, hãy đặt dedupe.matches = FALSE. Việc loại bỏ trùng lặp được thực hiện bằng giải pháp gán tuyến tính của Winkler.

Mẹo: tách một cột ngày thành ba cột số riêng biệt bằng cách sử dụng day(), month()year() từ package lubridate

Ngưỡng mặc định cho các kết quả khớp là 0,94 (threshold.match =) nhưng bạn có thể điều chỉnh nó cao hơn hoặc thấp hơn. Nếu bạn xác định ngưỡng, hãy cân nhắc việc ngưỡng cao hơn có thể mang lại nhiều âm tính giả hơn (các hàng không khớp sẽ thực sự khớp) và tương tự như vậy, ngưỡng thấp hơn có thể mang lại nhiều kết quả dương tính giả hơn.

Dưới đây, dữ liệu được đối sánh trên khoảng cách chuỗi trên các cột tên (name) và quận (district), cũng như khoảng cách số cho ngày (day), tháng month), năm sinh (year). Ngưỡng đối sánh với xác suất là 95% được thiết lập.

fl_output <- fastLink::fastLink(
  dfA = cases,
  dfB = results,
  varnames = c("gender", "first", "middle", "last", "yr", "mon", "day", "district"),
  stringdist.match = c("first", "middle", "last", "district"),
  numeric.match = c("yr", "mon", "day"),
  threshold.match = 0.95)
## 
## ==================== 
## fastLink(): Fast Probabilistic Record Linkage
## ==================== 
## 
## If you set return.all to FALSE, you will not be able to calculate a confusion table as a summary statistic.
## Calculating matches for each variable.
## Getting counts for parameter estimation.
##     Parallelizing calculation using OpenMP. 1 threads out of 4 are used.
## Running the EM algorithm.
## Getting the indices of estimated matches.
##     Parallelizing calculation using OpenMP. 1 threads out of 4 are used.
## Deduping the estimated matches.
## Getting the match patterns for each estimated match.

Xem lại các kết quả khớp

chúng ta đã định nghĩa đối tượng được trả về từ fastLink()fl_output. Nó thuộc nhóm list, và nó thực sự chứa một số data frame bên trong nó, mô tả chi tiết kết quả của việc so khớp. Một trong những data frame này là matches, chứa các kết quả khớp có nhiều khả năng nhất giữa casesresults. Bạn có thể truy cập data frame “khớp” này với fl_output$matches. Dưới đây, nó được lưu dưới dạng my_matches để tiện cho việc truy cập sau này.

Khi my_matches được in, bạn sẽ thấy hai vectơ cột: các cặp số/chỉ số hàng (còn được gọi là “tên hàng (rownames)”) trong cases (“inds.a”) và trong results (“inds.b”) đại diện cho các kết quả khớp phù hợp nhất. Nếu số hàng từ data frame bị thiếu, có nghĩa là không tìm thấy kết quả khớp nào trong data frame khác ở ngưỡng đối sánh đã chỉ định.

# print matches
my_matches <- fl_output$matches
my_matches
##   inds.a inds.b
## 1      1      1
## 2      2      2
## 3      3      3
## 4      4      4
## 5      8      8
## 6      7      9
## 7      6     10
## 8      5     12

Những điều cần lưu ý:

  • Các kết quả trùng khớp đã xảy ra mặc dù có sự khác biệt nhỏ về cách viết tên cũng như ngày sinh:

    • “Tony B. Smith” khớp với “Anthony B Smith”
    • “Maria Rodriguez” khớp với “Marialisa Rodrigues”
    • “Betty Chase” khớp với “Elizabeth Chase”
    • “Olivier Laurent De Bordeaux” khớp với “Oliver Laurent De Bordow” (ngày sinh missing bị bỏ qua)
  • Một hàng trong cases (đối với “Blessing Adebayo”, hàng 9) không có kết quả khớp tốt trong results, vì vậy nó không tồn tại my_matches.

Nối dựa trên việc khớp theo xác suất

Để sử dụng các kết quả khớp này nhằm nối results vào cases, chiến lược là:

  1. Sử dụng left_join() để nối my_matches vào cases (khớp tên hàng (rowname) trong cases với “inds.a” trong my_matches)
  2. Sau đó sử dụng left_join() khác để nối results vào cases (khớp với “inds.b” mới có được trong cases với rowname trong results)

Trước khi nối, chúng ta nên làm sạch ba data frame:

  • Cả dfAdfB nên có số hàng của chúng (“rowname”) được chuyển đổi thành một cột thích hợp.
  • Cả hai cột trong my_matches đều được chuyển đổi thành nhóm ký tự, vì vậy chúng có thể được nối với ký tự rownames
# Clean data prior to joining
#############################

# convert cases rownames to a column 
cases_clean <- cases %>% rownames_to_column()

# convert test_results rownames to a column
results_clean <- results %>% rownames_to_column()  

# convert all columns in matches dataset to character, so they can be joined to the rownames
matches_clean <- my_matches %>%
  mutate(across(everything(), as.character))



# Join matches to dfA, then add dfB
###################################
# column "inds.b" is added to dfA
complete <- left_join(cases_clean, matches_clean, by = c("rowname" = "inds.a"))

# column(s) from dfB are added 
complete <- left_join(complete, results_clean, by = c("inds.b" = "rowname"))

Như được trình bày bằng cách sử dụng code trên, data frame kết quả complete sẽ chứa tất cả các cột từ cả casesresults. Nhiều cột sẽ được thêm vào bằng các hậu tố “.x” và “.y”, vì nếu không, tên cột sẽ bị trùng lặp.

Ngoài ra, để chỉ lấy 9 bản ghi “gốc” trong cases với (các) cột mới từ results, hãy sử dụng select() trên results trước khi nối, để nó chỉ chứa rownames và cột mà bạn muốn thêm vào cases (ví dụ: cột result).

cases_clean <- cases %>% rownames_to_column()

results_clean <- results %>%
  rownames_to_column() %>% 
  select(rowname, result)    # select only certain columns 

matches_clean <- my_matches %>%
  mutate(across(everything(), as.character))

# joins
complete <- left_join(cases_clean, matches_clean, by = c("rowname" = "inds.a"))
complete <- left_join(complete, results_clean, by = c("inds.b" = "rowname"))

Nếu bạn chỉ muốn lấy một trong hai bộ dữ liệu cho các hàng khớp, bạn có thể sử dụng code bên dưới:

cases_matched <- cases[my_matches$inds.a,]  # Rows in cases that matched to a row in results
results_matched <- results[my_matches$inds.b,]  # Rows in results that matched to a row in cases

Hoặc, để chỉ xem các hàng không khớp::

cases_not_matched <- cases[!rownames(cases) %in% my_matches$inds.a,]  # Rows in cases that did NOT match to a row in results
results_not_matched <- results[!rownames(results) %in% my_matches$inds.b,]  # Rows in results that did NOT match to a row in cases

Loại bỏ trùng lặp theo xác suất

Khớp theo xác suất cũng có thể được sử dụng để loại bỏ trùng lặp trong một bộ dữ liệu. Xem chương về Loại bỏ trùng lặp để biết các phương pháp loại bỏ trùng lặp khác.

Ở đây chúng ta đã bắt đầu với bộ dữ liệu cases, nhưng bây giờ đang được gọi là cases_dup, vì nó có 2 hàng bổ sung mà có thể là bản trùng lặp của các hàng trước đó: Xem “Tony” với “Anthony”, và “Marialisa Rodrigues” với “Maria Rodriguez”.

Chạy fastLink() giống nhu trước, nhưng so sánh data frame cases_dup với chính nó. Khai hai data frames được cung cấp giống hệt nhau, hàm sẽ giả định rằng bạn muốn loại bỏ trùng lặp. Lưu ý rằng chúng ta không chỉ định stringdist.match = hoặc numeric.match = như chúng ta đã làm trước đây.

## Run fastLink on the same dataset
dedupe_output <- fastLink(
  dfA = cases_dup,
  dfB = cases_dup,
  varnames = c("gender", "first", "middle", "last", "yr", "mon", "day", "district")
)
## 
## ==================== 
## fastLink(): Fast Probabilistic Record Linkage
## ==================== 
## 
## If you set return.all to FALSE, you will not be able to calculate a confusion table as a summary statistic.
## dfA and dfB are identical, assuming deduplication of a single data set.
## Setting return.all to FALSE.
## 
## Calculating matches for each variable.
## Getting counts for parameter estimation.
##     Parallelizing calculation using OpenMP. 1 threads out of 4 are used.
## Running the EM algorithm.
## Getting the indices of estimated matches.
##     Parallelizing calculation using OpenMP. 1 threads out of 4 are used.
## Calculating the posterior for each pair of matched observations.
## Getting the match patterns for each estimated match.

Bây giờ, bạn có thể xem xét các bản trùng lặp có thể xảy ra với getMatches(). Cung cấp data frame dưới dạng cả dfA =dfB =, đồng thời cung cấp kết quả đầu ra của hàm fastLink()fl.out =. fl.out = phải thuộc nhóm fastLink.dedupe, hay nói cách khác, là kết quả của fastLink().

## Run getMatches()
cases_dedupe <- getMatches(
  dfA = cases_dup,
  dfB = cases_dup,
  fl.out = dedupe_output)

Xem cột ngoài cùng bên phải, cột cho biết ID trùng lặp - hai hàng cuối cùng được xác định có thể là trùng lặp ở hàng 2 và 3.

Để trả về số hàng của những hàng có khả năng trùng lặp, bạn có thể đếm số hàng trên mỗi giá trị duy nhất trong cột dedupe.ids, sau đó lọc để chỉ giữ lại những hàng có nhiều hơn một hàng. Trong trường hợp này, nó để lại hàng 2 và 3.

cases_dedupe %>% 
  count(dedupe.ids) %>% 
  filter(n > 1)
##   dedupe.ids n
## 1          2 2
## 2          3 2

Để kiểm tra toàn bộ các hàng có khả năng trùng lặp, hãy đặt số hàng trong lệnh này:

# displays row 2 and all likely duplicates of it
cases_dedupe[cases_dedupe$dedupe.ids == 2,]   
##    gender   first middle  last   yr mon day district dedupe.ids
## 2       M Anthony     B. Smith 1970   9  19    River          2
## 10      M    Tony     B. Smith 1970   9  19    River          2

14.4 Gắn vào và căn chỉnh

Một phương pháp khác để kết hợp hai data frame là “ràng buộc” chúng với nhau. Bạn cũng có thể coi đây là hàng hoặc cột được “gắn vào” hoặc “thêm”.

Phần này cũng sẽ thảo luận về cách “căn chỉnh” thứ tự các hàng của một data frame với thứ tự các hàng trong data frame khác. Chủ đề này được thảo luận dưới đây trong phần về Gắn các cột.

Gắn các hàng

Để gắn các hàng của một data frame này với phần cuối của một data frame khác, hãy sử dụng bind_rows() từ dplyr. Hàm này có tính dung nạp, vì vậy bất kỳ cột nào có trong một trong hai data frame sẽ được đưa vào kết quả đầu ra. Một số lưu ý:

  • Không giống như row.bind() của phiên bản base R, bind_rows() của dplyr không yêu cầu thứ tự của các cột phải giống nhau trong cả hai data frame. Miễn là các tên cột được viết giống nhau, nó sẽ căn chỉnh chúng một cách chính xác.
  • Bạn có thể tùy chọn xác định đối số .id =. Cung cấp một tên cột ký tự. Điều này sẽ tạo ra một cột mới dùng để xác định mỗi hàng ban đầu đến từ data frame nào.
  • Bạn có thể sử dụngbind_rows() trên một list các data frame có cấu trúc tương tự để kết hợp chúng thành một. Xem ví dụ trong chương [Lặp, vòng lặp và danh sách] về việc nhập nhiều linelist với purrr.

Một ví dụ phổ biến về row binding là gắn một hàng “tổng (total)” vào một bảng mô tả được tạo bằng hàm summarise() của dplyr. Dưới đây, chúng ta tạo một bảng đếm số trường hợp và giá trị CT trung bình theo bệnh viện với một hàng tổng.

Hàm summarise() được sử dụng trên dữ liệu đã nhóm theo bệnh viện để trả về một data frame tóm tắt theo bệnh viện. Nhưng hàm summarise() không tự động tạo ra hàng “tổng”, vì vậy chúng ta tạo ra nó bằng cách tổng hợp lại dữ liệu, nhưng với dữ liệu không bị nhóm theo bệnh viện. Điều này tạo ra một data frame thứ hai chỉ gồm một hàng. Sau đó, chúng ta có thể liên kết các data frame này với nhau để có được bảng cuối cùng.

Xem các ví dụ hoạt động khác tương tự như thế này trong chương Bảng mô tảTrình bày bảng.

# Create core table
###################
hosp_summary <- linelist %>% 
  group_by(hospital) %>%                        # Group data by hospital
  summarise(                                    # Create new summary columns of indicators of interest
    cases = n(),                                  # Number of rows per hospital-outcome group     
    ct_value_med = median(ct_blood, na.rm=T))     # median CT value per group

Đây là data frame hosp_summary:

Tạo một data frame với thống kê “tổng” (không bị nhóm theo bệnh viện). Điều này sẽ trả về chỉ một hàng.

# create totals
###############
totals <- linelist %>% 
  summarise(
    cases = n(),                               # Number of rows for whole dataset     
    ct_value_med = median(ct_blood, na.rm=T))  # Median CT for whole dataset

Và dưới đây là data frame totals. Lưu ý cách mà chỉ tạo ra hai cột. Những cột này cũng nằm trong hosp_summary, nhưng có một cột trong hosp_summary mà không nằm trong totals (hospital).

Bây giờ chúng ta có thể gắn các hàng với nhau bằng bind_rows().

# Bind data frames together
combined <- bind_rows(hosp_summary, totals)

Bây giờ chúng ta có thể xem kết quả. Xem cách mà trong hàng cuối cùng, giá trị NA trống được điền vào cột trong hospital mà không có trong hosp_summary. Như đã giải thích trong chương Trình bày bảng, bạn có thể “điền” vào ô này với “Tổng” bằng cách sử dụng replace_na().

Gắn các cột

Có một hàm dplyr tuơng tự là bind_cols(), hàm mà bạn có thể sử dụng để kết hợp hai data frame theo chiều ngang. Lưu ý rằng các hàng được khớp với nhau theo vị trí (khác với phép nối ở trên) - ví dụ: hàng thứ 12 trong mỗi data frame sẽ được căn chỉnh.

Ví dụ, chúng ta liên kết một số bảng tóm tắt với nhau. Để làm điều này, chúng ta cũng trình bày cách sắp xếp lại thứ tự của các hàng trong một data frame để khớp với thứ tự hàng trong một data frame khác, với match().

Ở đây chúng ta định nghĩa case_info là một data frame tóm tắt về các trường hợp trong linelist theo bệnh viện, với số trường hợp và số ca tử vong.

# Case information
case_info <- linelist %>% 
  group_by(hospital) %>% 
  summarise(
    cases = n(),
    deaths = sum(outcome == "Death", na.rm=T)
  )

Và giả sử rằng đây là một data frame contact_fu khác chứa thông tin về phần trăm số liên hệ bị phơi nhiễm được điều tra và “theo dõi”, lại một lần nữa bởi bệnh viện.

contact_fu <- data.frame(
  hospital = c("St. Mark's Maternity Hospital (SMMH)", "Military Hospital", "Missing", "Central Hospital", "Port Hospital", "Other"),
  investigated = c("80%", "82%", NA, "78%", "64%", "55%"),
  per_fu = c("60%", "25%", NA, "20%", "75%", "80%")
)

Lưu ý rằng các bệnh viện đều như nhau, nhưng theo thứ tự khác nhau trong mỗi data frame. Giải pháp đơn giản nhất là sử dụng left_join() trên cột hospital, nhưng bạn cũng có thể sử dụng bind_cols() với một bước bổ sung.

Sử dụng match() để sắp xếp thứ tự

Do thứ tự hàng khác nhau, một lệnh bind_cols() đơn giản sẽ dẫn đến khớp sai dữ liệu. Để khắc phục điều này, chúng ta có thể sử dụng hàm match() trong base R để căn chỉnh các hàng của data frame theo thứ tự giống với thứ tự trong data frame khác. Chúng ta giả định đối với phương pháp này rằng không có giá trị trùng lặp nào trong cả hai data frame.

Khi chúng ta sử dụng match(), với cú pháp là match(TARGET ORDER VECTOR, DATA FRAME COLUMN TO CHANGE), trong đó đối số đầu tiên là thứ tự mong muốn (hoặc là một vectơ độc lập, hoặc trong trường hợp này là một cột trong data frame), và đối số thứ hai là cột data frame trong data frame mà sẽ được sắp xếp lại. Kết quả đầu ra của match() sẽ là một vectơ số đại diện cho thứ tự vị trí chính xác. Bạn có thể đọc thêm với ?match.

match(case_info$hospital, contact_fu$hospital)
## [1] 4 2 3 6 5 1

Bạn có thể sử dụng vectơ số này để sắp xếp lại thứ tự data frame - đặt nó trong tập con của dấu ngoặc [ ] trước dấu phẩy. Đọc thêm về cú pháp tập con của dấu ngoặc base R trong chương R cơ bản. Lệnh bên dưới tạo ra một data frame mới, được định nghĩa là data frame cũ mà trong đó các hàng được sắp xếp theo thứ tự trong vectơ số ở trên.

contact_fu_aligned <- contact_fu[match(case_info$hospital, contact_fu$hospital),]

Bây giờ chúng ta có thể gắn các cột data frame với nhau, với thứ tự hàng chính xác. Lưu ý rằng một số cột bị trùng lặp và sẽ yêu cầu làm sạch bằng rename(). Đọc thêm về bind_rows() tại đây.

bind_cols(case_info, contact_fu)
## New names:
## * hospital -> hospital...1
## * hospital -> hospital...4
## # A tibble: 6 x 6
##   hospital...1                         cases deaths hospital...4                         investigated per_fu
##   <chr>                                <int>  <int> <chr>                                <chr>        <chr> 
## 1 Central Hospital                       454    193 St. Mark's Maternity Hospital (SMMH) 80%          60%   
## 2 Military Hospital                      896    399 Military Hospital                    82%          25%   
## 3 Missing                               1469    611 Missing                              <NA>         <NA>  
## 4 Other                                  885    395 Central Hospital                     78%          20%   
## 5 Port Hospital                         1762    785 Port Hospital                        64%          75%   
## 6 St. Mark's Maternity Hospital (SMMH)   422    199 Other                                55%          80%

Một hàm trong base R thay thế cho bind_colscbind(), hàm này hoạt động tương tự với bind_cols.

14.5 Tài nguyên học liệu

tidyverse page on joins

R for Data Science page on relational data

tidyverse page on dplyr về ràng buộc dữ liệu

Đặc trưng của fastLink tại trang Github package

Xuất bản mô tả phương pháp luận của fastLink

Xuất bản mô tả package RecordLinkage