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 và 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.
::p_load(
pacman# import and export
rio, # locate files
here, # data management and visualisation
tidyverse, # probabilistic matches
RecordLinkage, # probabilistic matches
fastLink )
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
<- import("linelist_cleaned.rds") linelist
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:
- Một phiên bản “thu nhỏ” của bộ dữ liệu
linelist
, chỉ chứa các cộtcase_id
,date_onset
,hospital
và chỉ 10 hàng đầu tiên
- 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_onset
và hospital
.
<- linelist %>% # start with original linelist
linelist_mini 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
= data.frame(
hosp_info 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_mini
và hosp_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"
[2] "Missing"
[3] "St. Mark's Maternity Hospital (SMMH)"
[4] "Port Hospital"
[5] "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"
[5] "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
== "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",
hosp_name 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"
[2] "Military Hospital"
[3] "Port Hospital"
[4] "St. Mark's Maternity Hospital (SMMH)"
[5] "ignace"
[6] "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 (df1
và df2
). 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)
<- left_join(df1, df2, by = c("ID" = "identifier")) joined_data
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
<- left_join(df1, df2, by = "ID") joined_data
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
<- left_join(df1, df2, by = c("name" = "firstname", "surname" = "lastname", "Age" = "age")) joined_data
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_pop
vàlevel
đã được thêm vào bên trái củalinelist_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"))
Warning in left_join(., hosp_info, by = c(hospital = "hosp_name")): Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 5 of `x` matches multiple rows in `y`.
ℹ Row 4 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
"many-to-many"` to silence this warning.
“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)
Warning in left_join(linelist_mini, hosp_info, by = c(hospital = "hosp_name")): Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 5 of `x` matches multiple rows in `y`.
ℹ Row 4 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
"many-to-many"` to silence this warning.
Đâ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)
Warning in right_join(hosp_info, linelist_mini, by = c(hosp_name = "hospital")): Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 4 of `x` matches multiple rows in `y`.
ℹ Row 5 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
"many-to-many"` to silence this warning.
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_id
vàonset
đ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_pop
vàlevel
đ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"))
Warning in full_join(., hosp_info, by = c(hospital = "hosp_name")): Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 5 of `x` matches multiple rows in `y`.
ℹ Row 4 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
"many-to-many"` to silence this warning.
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"))
Warning in inner_join(., hosp_info, by = c(hospital = "hosp_name")): Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 5 of `x` matches multiple rows in `y`.
ℹ Row 4 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
"many-to-many"` to silence this warning.
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()
và 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_mini
và hosp_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"))
Warning in inner_join(., hosp_info, by = c(hospital = "hosp_name")): Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 5 of `x` matches multiple rows in `y`.
ℹ Row 4 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
"many-to-many"` to silence this warning.
Để 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
::p_load(
pacman# data manipulation and visualization
tidyverse, # record matching
fastLink )
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 (cases
và test_results
):
Đây là code được sử dụng để tạo bộ dữ liệu:
# make datasets
<- tribble(
cases ~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"
)
<- tribble(
results ~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 =
và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ả haidfA
vàdfB
.
- Trong
stringdist.match =
cung cấp các cột từ những cột trongvarnames
được đánh giá trên chuỗi “distance”.
- Trong
numeric.match =
cung cấp các cột từ những cột trongvarnames
đượ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()
và 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.
<- fastLink::fastLink(
fl_output 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 8 are used.
Running the EM algorithm.
Getting the indices of estimated matches.
Parallelizing calculation using OpenMP. 1 threads out of 8 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()
là 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 cases
và results
. 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
<- fl_output$matches
my_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)
- “Tony B. Smith” khớp với “Anthony B Smith”
Một hàng trong
cases
(đối với “Blessing Adebayo”, hàng 9) không có kết quả khớp tốt trongresults
, vì vậy nó không tồn tạimy_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à:
- Sử dụng
left_join()
để nốimy_matches
vàocases
(khớp tên hàng (rowname) trongcases
với “inds.a” trongmy_matches
)
- Sau đó sử dụng
left_join()
khác để nốiresults
vàocases
(khớp với “inds.b” mới có được trongcases
với rowname trongresults
)
Trước khi nối, chúng ta nên làm sạch ba data frame:
- Cả
dfA
vàdfB
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 %>% rownames_to_column()
cases_clean
# convert test_results rownames to a column
<- results %>% rownames_to_column()
results_clean
# convert all columns in matches dataset to character, so they can be joined to the rownames
<- my_matches %>%
matches_clean mutate(across(everything(), as.character))
# Join matches to dfA, then add dfB
###################################
# column "inds.b" is added to dfA
<- left_join(cases_clean, matches_clean, by = c("rowname" = "inds.a"))
complete
# column(s) from dfB are added
<- left_join(complete, results_clean, by = c("inds.b" = "rowname")) complete
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ả cases
và results
. 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 %>% rownames_to_column()
cases_clean
<- results %>%
results_clean rownames_to_column() %>%
select(rowname, result) # select only certain columns
<- my_matches %>%
matches_clean mutate(across(everything(), as.character))
# joins
<- left_join(cases_clean, matches_clean, by = c("rowname" = "inds.a"))
complete <- left_join(complete, results_clean, by = c("inds.b" = "rowname")) complete
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[my_matches$inds.a,] # Rows in cases that matched to a row in results
cases_matched <- results[my_matches$inds.b,] # Rows in results that matched to a row in cases results_matched
Hoặc, để chỉ xem các hàng không khớp::
<- cases[!rownames(cases) %in% my_matches$inds.a,] # Rows in cases that did NOT match to a row in results
cases_not_matched <- results[!rownames(results) %in% my_matches$inds.b,] # Rows in results that did NOT match to a row in cases results_not_matched
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
<- fastLink(
dedupe_output 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 8 are used.
Running the EM algorithm.
Getting the indices of estimated matches.
Parallelizing calculation using OpenMP. 1 threads out of 8 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 =
và dfB =,
đồng thời cung cấp kết quả đầu ra của hàm fastLink()
là 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()
<- getMatches(
cases_dedupe 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
$dedupe.ids == 2,] cases_dedupe[cases_dedupe
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ụng
bind_rows()
trên mộtlist
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ả và Trình bày bảng.
# Create core table
###################
<- linelist %>%
hosp_summary 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
###############
<- linelist %>%
totals 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
<- bind_rows(hosp_summary, totals) combined
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
<- linelist %>%
case_info 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.
<- data.frame(
contact_fu 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[match(case_info$hospital, contact_fu$hospital),] contact_fu_aligned
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 × 6
hospital...1 cases deaths hospital...4 investigated per_fu
<chr> <int> <int> <chr> <chr> <chr>
1 Central Hospital 454 193 St. Mark's … 80% 60%
2 Military Hospital 896 399 Military Ho… 82% 25%
3 Missing 1469 611 Missing <NA> <NA>
4 Other 885 395 Central Hos… 78% 20%
5 Port Hospital 1762 785 Port Hospit… 64% 75%
6 St. Mark's Maternity Hospital (… 422 199 Other 55% 80%
Một hàm trong base R thay thế cho bind_cols
là cbind()
, hàm này hoạt động tương tự với bind_cols
.
14.5 Tài nguyên học liệu
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