14 Соединение данных
Выше: анимированный пример левого соединения (источник изображения)
На данной странице описываются способы “соединения”, “поиска соответствия”, “связывания” и “объединения”, а также другие способы объединения датафреймов.
Очень редко эпидемиологический анализ или работа не требуют использования нескольких источников данных и объединения наборов данных. Возможно, вам будет необходимо связать лабораторные данные с клиническими исходами пациента, либо связать данные о мобильности от Google с трендами инфекционных заболеваний, либо даже набор данных на одном этапе анализа связать с тем же, но преобразованным набором данных.
На этой странице мы демонстрируем код для:
- Проведения соединений двух датафреймов таким образом, чтобы строки были сопоставлены по общим значениям в столбцах идентификаторах
- Соединения двух датафреймов на основе вероятностных (правдоподобных) соответствий между значениями
- Расширения датафрейма путем прямого связывания или (“добавления”) строк или столбцов из другого датафрейма
14.1 Подготовка
Загрузка пакетов
Данный фрагмент кода показывает загрузку пакетов, необходимых для анализа. В данном руководстве мы фокусируемся на использовании p_load()
из пакета pacman, которая устанавливает пакет, если необходимо, и загружает его для использования. Вы можете также загрузить установленные пакеты с помощью library()
из базового R. См. страницу Основы R для получения дополнительной информации о пакетах R.
::p_load(
pacman# импорт и экспорт
rio, # пути к файлам
here, # управление данными и визуализация
tidyverse, # вероятностные совпадения
RecordLinkage, # вероятностные совпадения
fastLink )
Импорт данных
Для начала мы импортируем чистый построчный список случаев из имитации эпидемии Эболы. Если вы хотите выполнять действия параллельно, кликните, чтобы скачать “чистый” построчный список (как .rds файл). Импортируйте данные с помощью функции import()
из пакета rio (она работает с разными типами файлов, такими как .xlsx, .csv, .rds - см. страницу Импорт и экспорт для получения детальной информации).
# импорт построчного списка случаев
<- import("linelist_cleaned.rds") linelist
Первые 50 строк построчного списка отображены ниже.
Примеры наборов данных
В разделе соединения ниже, мы используем следующие наборы данных:
- “уменьшенную” версию построчного списка случаев
linelist
, содержащую только столбцыcase_id
,date_onset
иhospital
, и только первые 10 строк
- отдельный датафрейм под названием
hosp_info
, который содержит более детальную информацию о каждой больнице
В разделе вероятностного сопоставления мы будем использовать два разных маленьких набора данных. Код для создания этих наборов данных представлен в соответствующем разделе.
“Уменьшенный” построчный список linelist
Ниже представлен уменьшенный построчный список, который содержит лишь 10 строк и только столбцы case_id
, date_onset
и hospital
.
<- linelist %>% # начинаем с оригинального построчного списка
linelist_mini select(case_id, date_onset, hospital) %>% # выбираем столбцы
head(10) # берем только верхние 10 строк
Датафрейм с информацией о больницах
Ниже представлен код для создания отдельного датафрейма с дополнительной информацией о семи больницах (прикрепленное население, уровень доступной помощи). Обратите внимание, что название “Military Hospital” относится к двум разным больницам - одной на первичном уровне, обслуживающей 10000 жителей, а другой - на вторичном уровне, обслуживающей 50280 жителей.
# Создаем датафрейм с информацией о больницах
= 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")
)
Вот этот датафрейм:
Предварительная вычистка
Традиционные соединения (невероятностные) чувствительны к регистру и требуют точного соответствия знаков между значениями в двух датафреймах. Чтобы продемонстрировать некоторые шаги вычистки, которые вам могут потребоваться до соединения, мы вычистим и унифицируем наборы данных linelist_mini
и hosp_info
.
Выявление различий
Нам нужно, чтобы значения в столбце hosp_name
в датафрейме hosp_info
соответствовали значениям столбца hospital
в датафрейме linelist_mini
.
Здесь представлены значения в датафрейме linelist_mini
, напечатанные с помощью функции базового R unique()
:
unique(linelist_mini$hospital)
[1] "Other"
[2] "Missing"
[3] "St. Mark's Maternity Hospital (SMMH)"
[4] "Port Hospital"
[5] "Military Hospital"
а здесь значения в датафрейме hosp_info
:
unique(hosp_info$hosp_name)
[1] "central hospital" "military" "port" "St. Mark's"
[5] "ignace" "sisters"
Вы видите, что хотя некоторые больницы существуют в обоих датафреймах, существует множество расхождений в написании.
Унификация значений
Мы начинаем с вычистки значений в датафрейме hosp_info
. Как объяснялось на странице Вычистка данных и ключевые функции, мы можем перекодировать значения с помощью логических критериев, используя функцию из dplyr case_when()
. Для четырех больниц, которые существуют в обоих датафреймах, мы меняем значения, чтобы они совпадали со значениями в linelist_mini
. Для других больниц мы оставим значения как есть (TRUE ~ hosp_name
).
ВНИМАНИЕ: Как правило, при вычистке следует создавать новый столбец (например, hosp_name_clean
), но для облегчения демонстрации мы покажем модификацию старого столбца
<- hosp_info %>%
hosp_info mutate(
hosp_name = case_when(
# критерии # новое значение
== "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
) )
Названия больниц, которые появляются в обоих датафреймах, унифицированы. Есть две больницы в hosp_info
, которых нет в linelist_mini
- мы с этим разберемся позже при присоединении.
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"
Перед присоединением часто легче всего конвертировать столбец во все строчные или все заглавные буквы. Если вам нужно конвертировать все значения в столбце в ЗАГЛАВНЫЕ или строчные буквы, используйте mutate()
и оберните столбец в одну из следующих функций из пакета stringr, как показано на странице Текст и последовательности.
str_to_upper()
str_to_upper()
str_to_title()
14.2 Соединения в dplyr
Пакет dplyr предлагает ряд разных функций соединения. dplyr включен в пакет tidyverse. Эти функции присоединения описаны ниже с простыми примерами их применения.
Благодарим https://github.com/gadenbuie за информативные gif!
Общий синтаксис
Команды присоединения можно выполнять как отдельные команды для соединения двух датафреймов в один новый объект, либо их можно использовать в рамках цепочки канала (%>%
), чтобы объединить один датафрейм с другим по мере его вычистки или другой модификации.
В примере ниже используется функция left_join()
в качестве отдельной команды для создания нового объединенного датафрейма joined_data
. Входными данными являются датафреймы 1 и 2 (df1
и df2
). Первый указанный датафрейм - базовый датафрейм, а второй указанный датафрейм присоединяется к нему.
Третий аргумент by =
- это аргумент, в котором вы указываете столбцы в каждом датафрейме, которые будут использоваться для сопоставления строк в двух датафреймах. Если имена этих столбцов различаются, задайте их в рамках вектора c()
, как показано ниже, где строки сопоставляются на основе общих значений между столбцом ID
в df1
и столбцом identifier
в df2
.
# Соединение на основе общих значений между столбцом "ID" (первый датафрейм) и столбцом "identifier" (второй датафрейм)
<- left_join(df1, df2, by = c("ID" = "identifier")) joined_data
Если у столбцов, по которым вы группируете (by
), в обоих датафреймах одинаковое название, вы можете указать это одно название в кавычках.
# Соединение на основе общих значений в столбце "ID" в обоих датафреймах
<- left_join(df1, df2, by = "ID") joined_data
Если вы соединяете датафреймы на основе общих значений между несколькими полями, укажите эти поля в векторе c()
. Этот пример соединяет строки, если значения в трех столбцах каждого набора данных абсолютно совпадают.
# Соединение на основе имени, фамилии и возраста
<- left_join(df1, df2, by = c("name" = "firstname", "surname" = "lastname", "Age" = "age")) joined_data
Команды соединения можно также выполнять в рамках цепочки канала. Это будет модифицировать тот датафрейм, который передан по каналу.
В примере ниже по каналу передается датафрейм df1
, df2
к нему присоединяется, таким образом модифицируется и переопределяетсяdf
.
<- df1 %>%
df1 filter(date_onset < as.Date("2020-03-05")) %>% # прочая вычистка
left_join(df2, by = c("ID" = "identifier")) # присоединение df2 к df1
ВНИМАНИЕ: Соединения чувствительны к регистру! Поэтому полезно конвертировать все значения в строчные или заглавные до соединения. См. страницу по Тексту и последовательности.
Левое и правое соединение
Левое или правое соединение обычно используется, чтобы добавить информацию к датафрейму - новая информация добавляется только к тем строкам, которые уже существовали в базовом датафрейме. Это частые соединения в эпидемиологической работе, так как они используются, чтобы добавить информацию из одного набора данных к другому.
При использовании этих соединений очень важен письменный порядок датафреймов в команде*.
- В левом соединении базовый датафрейм записывается первым
- В правом соединении базовый датафрейм записывается вторым
Все строки базового датафрейма сохраняются Информация в другом (вторичном) датафрейме присоединяется к базовому датафрейму, только если есть совпадение по столбцу идентификатору(ам). Кроме того:
- Строки во вторичном датафрейме, по которым нет совпадения, отбрасываются.
- Если существует много базовых строк, которые совпадают с одной строкой во вторичном датафрейме (много-к-одному), вторичная информация добавляется к каждой совпадающей базовой строке.
- Если базовая строка совпадает с несколькими строками во вторичном наборе данных (одна-ко-многим), даются все комбинации, то есть могут быть добавлены новые строки к выданному в результате датафрейму!
Анимированные примеры левого и правого соединения (источник изображения)
Пример
Ниже представлены выходные данные левого соединения left_join()
списка hosp_info
(вторичный датафрейм, см.здесь) к linelist_mini
(базовый датафрейм, см.здесь). В оригинальном linelist_mini
было nrow(linelist_mini)
строк. Отображен модифицированный linelist_mini
. Обратите внимание на следующее:
- К левой стороне
linelist_mini
были добавлены два новых столбца,catchment_pop
иlevel
- Все оригинальные строки базового датафрейма
linelist_mini
сохранены
- Любые оригинальные строки в
linelist_mini
для “Military Hospital” продублированы, посколько он совпадает с двумя строками во вторичном датафрейме, поэтому выдаются обе комбинации
- Столбец идентификатора, по которому идет соединение, во вторичном наборе данных (
hosp_name
) исчез, поскольку он повторяет столбец идентификатор в основном наборе данных (hospital
)
- Когда строка базового датафрейма не совпала ни с одной строкой вторичного датафрейма (например, когда больница
hospital
указана как “Other” (Прочее) или “Missing” (Отсутствует)),NA
(пропуск) заполняет столбцы из вторичного датафрейма
- Строки во вторичном датафрейме без совпадения по базовому датафрейму (больницы “sisters” и “ignace”) были отброшены.
%>%
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.
“Какое соединение мне использовать: левое или правое?”
Чтобы ответить на указанный выше вопрос, спросите себя: “какой датафрейм должен сохранить все строки?” - и используйте его как датафрейм. Левое соединение сохраняет все строки первого датафрейма, записанного в команде, а правое соединение сохраняет все строки второго датафрейма.
Две команды, указанные ниже, добиваются одних и тех же выходных данных - 10 строк hosp_info
присоединяются к базовому списку linelist_mini
, но они используют разные соединения. Результатом является то, что порядок столбцов будет отличаться в зависимости от того, будет ли добавлена информация hosp_info
справа (в левом соединении), либо добавлена слева (в правом соединении). Порядок строк может также измениться соответствующим образом. Но с этими последствиями можно потом работать, используя select()
для переупорядочивания столбцов или arrange()
для сортировки строк.
# Две команды ниже приводят к появлению тех же самых данных, но с разным порядком строк и столбцов
left_join(linelist_mini, hosp_info, by = c("hospital" = "hosp_name"))
right_join(hosp_info, linelist_mini, by = c("hosp_name" = "hospital"))
Вот результат присоединения hosp_info
к linelist_mini
через левое соединение (новые столбцы добавлены справа)
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.
Вот результат присоединения hosp_info
к linelist_mini
через правое соединение (новые столбцы добавлены слева)
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.
Также примите во внимание, применяете ли вы соединение в рамках цепочки канала (%>%
). Если набор данных в канале - базовый набор, вы, скорее всего, будете использовать левое соединение, чтобы добавить к нему данные.
Полное соединение
Полное соединение - наиболее инклюзивное из соединений - оно выдает все строки из обоих датафреймов.
Если есть какие-то строки в одном датафрейме и их нет в другом (не найдено совпадение), датафрейм их включит и станет длиннее. Для заполнения созданных пробелов будут использоваться отсутствующие значения NA
. По мере соединения обращайте внимание на количество строк и столбцов, чтобы решить проблему чувствительности к регистру и полных совпадений символов.
“Базовый” датафрейм - тот, который записан первым в команде. Изменение этого порядка не повлияет на то, какие записи будут выданы после соединения, но может повлиять на порядок столбцов, порядок строк, полученный в результате, а также на то, какие столбцы-идентификаторы будут сохранены.
Анимированный пример полного соединения (источник изображения)
Пример
Ниже представлен пример результата полного присоединения full_join()
списка hosp_info
(изначально nrow(hosp_info)
, см.здесь) к списку linelist_mini
(изначально nrow(linelist_mini)
, см.здесь). Обратите внимание на нижеследующее:
- Сохранены все строки базового датафрейма (
linelist_mini
)
- Строки во вторичном датафрейме, которые не совпадали с базовым, сохранены (“ignace” и “sisters”), а значения по соответствующим столбцам базового датафрейма
case_id
иonset
заполнены как отсутствующие значения
- Аналогично, строки базового набора данных, которые не совпали с вторичным (“Other” и “Missing”) сохранены, а вторичные столбцы
catchment_pop
иlevel
заполнены как отсутствующие значения
- В случае совпадений один-ко-многим и много-к-одному (например, строки для “Military Hospital”), выданы все возможные комбинации (что удлинняет итоговый датафрейм)
- Сохраняется только один столбец-идентификатор из базового датафрейма (
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.
Внутреннее соединение
Внутреннее соединение - наиболее ограничивающее из соединений - оно выдает только строки с совпадениями по обоим датафреймам.
Это означает, что количество строк в базовом датафрейме может даже сократиться. Изменение того, какой датафрейм будет “базовым” (первым записанным в функции) не повлияет на то, какие строки будут выданы, но повлияет на порядок столбцов, порядок строк, а также то, какие столбцы-идентификаторы будут сохранены.
Анимированный пример внутреннего соединения (источник изображения)
Пример
Ниже представлены выходные данные внутреннего соединения inner_join()
списка linelist_mini
(базовый) с hosp_info
(вторичный). Обратите внимание на нижеследующее:
- Базовые строки без совпадений во вторичных данных удаляются (строки, где больница
hospital
“Missing” (отсутствует) или указана как “Other” (прочие))
- Аналогично, строки из вторичного датафрейма, по которым нет соответствия в базовом, удаляются (строки, где название больницы
hosp_name
указано как “sisters” или “ignace”)
- Сохраняется только столбец-идентификатор из базового датафрейма (
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.
Полусоединение
Полусоединение - это “фильтрующее соединение”, которое использует еще один набор данных не для добавления строк или столбцов, а для проведения фильтрации.
Полусоединение сохраняет все наблюдения в базовом датафрейме, по которым есть совпадения во вторичном датафрейме (но не добавляет новые столбцы и не дублирует строки, чтобы создать множественные совпадения). Более детально о таких “фильтрующих” соединениях читайте тут.
Анимированный пример полусоединения (источник изображения)
В качестве примера, код ниже выдает строки из датафрейма hosp_info
, которые имеют совпадения с linelist_mini
на основе названия больницы.
%>%
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
Анти-соединение
Анти-соединение - это еще одно “фильтрующее соединение”, которое выдает строки в базовом наборе данных, которые не имеют совпадений во вторичном датафрейме.
Более детально о таких “фильтрующих” соединениях читайте тут.
Частый сценарий применения анти-соединения - выявление записей, не представленных в другом датафрейме, выявление проблем с написанием при соединении (просмотр записей, которые должны были совпасть), а также рассмотрение записей, которые были исключены после другого соединения.
Как и при right_join()
и left_join()
, важен базовый датафрейм (указанный первым). Выдаваемые строки будут только из базового набора данных. Обратите внимание в gif ниже, что строка во вторичном датафрейме (фиолетовая строка 4) не выдается, хотя по ней и нет совпадения в базовом датафрейме.
Анимированный пример анти-соединения (источник изображения)
Простой пример anti_join()
В качестве простого примера давайте найдем больницы hosp_info
, в которых нет случаев, присутствующих в linelist_mini
. Мы указываем hosp_info
первым, как базовый датафрейм. Будут выданы больницы, которые не присутствуют в linelist_mini
.
%>%
hosp_info anti_join(linelist_mini, by = c("hosp_name" = "hospital"))
Сложный пример anti_join()
В качестве еще одного примера допустим, мы провели внутреннее соединение inner_join()
для linelist_mini
и hosp_info
. Это нам выдаст подмножество оригинальных записей из linelist_mini
, так как некоторые не присутствуют в 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.
Чтобы рассмотреть записи linelist_mini
, которые были исключены в ходе внутреннего соединения, мы можем выполнить анти-соединение с теми же настройками (linelist_mini
в качестве базового).
%>%
linelist_mini anti_join(hosp_info, by = c("hospital" = "hosp_name"))
Чтобы увидеть записи hosp_info
, которые были исключены при внутреннем соединении, мы могли бы также выполнить анти-соединение с hosp_info
в качестве базового датафрейма.
14.3 Вероятностные совпадения
Если у вас нет общего уникального идентификатора между наборами данных, по которому вы можете сделать соединение, рассмотрите возможность применения вероятностных алгоритмов поиска совпадений. Они найдут совпадения между записями на основе схожести (например, сходства Джара—Винклера по расстоянию последовательности, либо по числовому расстоянию). Ниже приведен простой пример использования пакета fastLink .
Загрузка пакетов
::p_load(
pacman# манипуляции с данными и визуализация
tidyverse, # сопоставление записей
fastLink )
Вот два коротких набора данных для примера, которые мы используем, чтобы продемонстрировать поиск вероятностных совпадений (cases
и test_results
):
Вот код, который был использован для создания наборов данных:
# создаем наборы данных
<- 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"
)
В наборе данных cases
есть 9 записей пациентов, которые ожидают результатов теста.
Набор данных test_results
содержит 14 записей и содержит столбец с результатом result
, который мы хотим добавить к записям в списке случаев cases
на основе вероятностного поиска совпадений записей.
Вероятностные совпадения
Функция fastLink()
из пакета fastLink может использоваться для применения алгоритмов поиска соответствий. Ниже представлена базовая информация. Более детальную информацию вы можете получить, введя в консоль ?fastLink
.
- Задайте два датафрейма для сравнения в аргументы
dfA =
иdfB =
- В
varnames =
задайте все названия столбцов, которые будут использоваться для сопоставления. Они должны существовать и вdfA
, и вdfB
.
- В
stringdist.match =
задайте столбцы изvarnames
, которые будут оцениваться по “расстоянию” последовательности.
- В
numeric.match =
задайте столбцы изvarnames
, которые будут оцениваться по числовому расстоянию.
- Отсутствующие значения игнорируются
- По умолчанию, каждая строка в любом из датафреймов определяется как совпадение максимум к одной строке в другом датафрейме. Если вы хотите увидеть все оцененные совпадения, установите
dedupe.matches = FALSE
. Дедупликация проводится с помощью решения Винклера для линейной задачи о назначениях.
Совет: разделите один столбец даты на три отдельных числовых столбца, используя day()
, month()
и year()
из lubridate
Порог по умолчанию для совпадений составляет 0.94 (threshold.match =
), но вы можете его повысить или понизить. Если вы определеяете порог, учтите, что более высокий порог может дать вам больше ложно-отрицательных результатов (строк, которые не совпали, но должны были совпасть), а более низкий порог даст вам больше ложно-положительных совпадений.
Ниже данные сопоставляются по расстоянию последовательности в столбцах имя и район, а также по числовому расстоянию для года, месяца и дня рождения. Установлен порог совпадения с вероятностью 95%.
<- 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.
Просмотр совпадений
Мы определили объект, выдаваемый функцией fastLink()
как fl_output
. Он в классе list
и на самом деле внутри себя содержит несколько датафреймов, детализирующих результаты поиска совпадений. Один из этих датафреймов - это датафрейм matches
, который содержит наиболее вероятные совпадения по cases
(случаям) и results
(результатам). Вы можете оценить этот датафрейм “совпадений” с помощью fl_output$matches
. Ниже он сохраняется как my_matches
для удобства дальнейшего доступа.
Когда печатается my_matches
, вы видите два вектора столбцов: пары номеров строк/индексы (также называемые “rownames” - имена строк) в cases
(“inds.a”) и в results
(“inds.b”), представляющие собой наилучшие совпадения. Если номер строки из датафрейма отсутствует, это значит, что не было найдено совпадения в другом датафрейме в рамках указанного порога совпадения.
# печать совпадений
<- 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
Обратите внимание:
- Совпадения были найдены несмотря на небольшие различия в написании имен и датах рождения:
- “Tony B. Smith” сопоставлен с “Anthony B Smith”
- “Maria Rodriguez” сопоставлена с “Marialisa Rodrigues”
- “Betty Chase” сопоставлена с “Elizabeth Chase”
- “Olivier Laurent De Bordeaux” сопоставлена с “Oliver Laurent De Bordow” (игнорируется отсутствующая дата рождения)
- “Tony B. Smith” сопоставлен с “Anthony B Smith”
- По одной строке из
cases
(для “Blessing Adebayo”, строка 9) не было хороших соответствий вresults
, поэтому ее нет вmy_matches
.
Соединение на основе вероятностных совпадений
Чтобы использовать эти совпадения для присоединения results
к cases
, одна из стратегий будет:
- Использовать левое соединение
left_join()
, чтобы присоединитьmy_matches
кcases
(сопоставление имен строк вcases
с “inds.a” вmy_matches
)
- Затем использовать еще одно левое соединение
left_join()
, чтобы присоединитьresults
кcases
(сопоставление нового полученного “inds.b” вcases
с rownames вresults
)
Перед соединениями мы должны вычистить три датафрейма:
- И в
dfA
, и вdfB
номера строк (“rowname”) должны быть конвертированы в полноценный столбец.
- Оба столбца в
my_matches
конвертируются в текстовый класс, чтобы их можно было соединять с текстовыми rownames
# Вычистка данных до соединения
#############################
# конвертируем имена строк случаев cases (rownames) в столбец
<- cases %>% rownames_to_column()
cases_clean
# конвертируем имена строк результатов test_results (rownames) в столбец
<- results %>% rownames_to_column()
results_clean
# конвертируем все столбцы в наборе данных совпадений в текстовый класс, чтобы их можно было присоединить к rownames
<- my_matches %>%
matches_clean mutate(across(everything(), as.character))
# Присоединяем совпадения к dfA, затем добавляем dfB
###################################
# столбец "inds.b" добавляется к dfA
<- left_join(cases_clean, matches_clean, by = c("rowname" = "inds.a"))
complete
# столбец(столбцы) из dfB добавляются
<- left_join(complete, results_clean, by = c("inds.b" = "rowname")) complete
При выполнении указанного выше кода полученный в результате датафрейм complete
будет содержать все столбцы и из cases
, и из results
. Ко многим будут подставлены суффиксы “.x” и “.y”, поскольку иначе дублировались бы имена столбцов.
Альтернативно, чтобы получить только “оригинальные” 9 записей в cases
с новым столбцом(ами) из results
, используйте select()
для results
до соединений, чтобы там содержались только имена строк (rownames) и столбцы, которые вы хотите добавить к cases
(например, столбец result
).
<- cases %>% rownames_to_column()
cases_clean
<- results %>%
results_clean rownames_to_column() %>%
select(rowname, result) # выберите только определенные столбцы
<- 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
Если вы хотите выбрать подмножество данных из любого набора данных, чтобы оно включало только совпадающие строки, вы можете использовать код ниже:
<- cases[my_matches$inds.a,] # строки cases, которые совпали со строкой в results
cases_matched <- results[my_matches$inds.b,] # Строки в results, которые совпали со строкой в cases results_matched
Либо, чтобы увидеть только те строки, по которым нет совпадения:
<- cases[!rownames(cases) %in% my_matches$inds.a,] # Строки в cases, по которым НЕТ совпадений со строкой в results
cases_not_matched <- results[!rownames(results) %in% my_matches$inds.b,] # Строки в results, по которым НЕТ совпадений со строкой в cases results_not_matched
Вероятностная дедупликация
Поиск вероятностных совпадений можно использовать и для дедупликации набора данных. См. страницу по дедупликации, где описаны другие методы дедупликации.
Здесь мы начинаем с набора данных cases
, но сейчас назовем его cases_dup
, поскольку в нем 2 дополнительных строки, которые могут быть дубликатами предыдущих строк: See “Tony” with “Anthony”, and “Marialisa Rodrigues” with “Maria Rodriguez”.
Выполняем fastLink()
как и ранее, но сравниваем датафрейм cases_dup
с самим собой, если два датафрейма оказываются идентичными, функция предполагает, что вы хотите провести дедупликацию. Обратите внимание, что мы не уточняе stringdist.match =
или numeric.match =
, как мы это делали ранее.
## Выполняем fastLink на том же наборе данных
<- 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.
Теперь вы можете рассмотреть потенциальные дубликаты с помощью getMatches()
. Укажите датафрейм и в dfA =
, и в dfB =
, а также задайте выходной результат функции fastLink()
как fl.out =
. fl.out
должен быть классом fastLink.dedupe
, иными словами, результатом fastLink()
.
## Выполняем getMatches()
<- getMatches(
cases_dedupe dfA = cases_dup,
dfB = cases_dup,
fl.out = dedupe_output)
Посмотрите на самый правый столбец, который указывает дублирующиеся ID - последние две строки определены, как вероятные дубликаты строк 2 и 3.
Чтобы получить номера тех строк, которые, вероятно, являются дубликатами, вы можете посчитать количество строк на уникальное значение в столбце dedupe.ids
, а затем отфильтровать, чтобы сохранить только те, где более одной строки. В данном случае, это оставит строки 2 и 3.
%>%
cases_dedupe count(dedupe.ids) %>%
filter(n > 1)
dedupe.ids n
1 2 2
2 3 2
Чтобы просмотреть полные строки вероятных дубликатов, укажите номер строки в следующую команду:
# отображает строку 2 и все вероятные дубликаты в ней
$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 Связывание и упорядочивание
Еще один метод объединения двух датафреймов - это их “связывание”. Можете представить это, как “добавление” строк или столбцов.
В данном разделе мы также обсудим, как “упорядочивать” порядок строк одного датафрейма относительно порядка в другом датафрейме. Эта тема обсуждается ниже в разделе Связывание столбцов.
Связывание строк
Чтобы связать строки одного датафрейма с нижней частью другого датафрейма, используйте bind_rows()
из dplyr. Это очень инклюзивная функция, поэтому любой столбец, присутствующий в одном из датафреймов, будет включен в результат. Несколько замечаний:
- В отличие от базовой версии R
row.bind()
, функцияbind_rows()
из dplyr не требует, чтобы порядок столбцов был одинаковым в обоих датафреймах. Главное, чтобы названия столбцов были написано одинаково, тогда она их правильно упорядочит.
- Опционально вы можете задать аргумент
.id =
. Укажите текстовое имя столбца. Это создаст новый столбец, который будет использован для определения, из какого датафрейма изначально взяли каждую строку.
- Вы можете использовать
bind_rows()
для объектаlist
аналогично структурированных датафреймов для объединения их в один датафрейм. См. пример на странице [Итерации, циклы и списки], где показан импорт нескольких построчных списков с помощью purrr.
Одним из частых примеров связывания строк является связывание строки “итого” с описательной таблицей, используя функцию summarise()
из dplyr. Ниже мы создаем таблицу с подсчетом случаев и медианными значениями CT по больнице со строкой итого.
Функция summarise()
используется для данных, сгруппированных по больнице, чтобы выдать сводный датафрейм по больнице. Но функция summarise()
не создает автоматически строку “итого”, поэтому мы создаем ее, сведя данные еще раз, но в этот раз не группируя данные по больнице. Это создает второй датафрейм всего с одной строкой. Затем мы можем связать эти датафрейма вместе, чтобы получить итоговую таблицу.
См. другие подобные примеры на страницах [Описательные таблицы] и [Таблицы для презентации].
# Создаем основную таблицу
###################
<- linelist %>%
hosp_summary group_by(hospital) %>% # группируем данные по больнице
summarise( # создаем новые сводные столбцы по интересующим индикаторам
cases = n(), # количество строк на группу больница-исход
ct_value_med = median(ct_blood, na.rm=T)) # медианное значение CT на группу
Вот датафрейм hosp_summary
:
Создаем датафрейм с “итоговой” статистикой (не сгруппированной по больницам). Это выдаст нам только одну строку.
# создаем итого
###############
<- linelist %>%
totals summarise(
cases = n(), # количество строк для всего набора данных
ct_value_med = median(ct_blood, na.rm=T)) # медианное CT для всего набора данных
Ниже вы видите этот датафрейм totals
. Обратите внимание, что в нем только два столбца. Эти столбцы также есть в hosp_summary
, но в hosp_summary
есть один столбец, которого нет в totals
(hospital
).
Теперь мы связываем строки с помощью bind_rows()
.
# связываем датафреймы
<- bind_rows(hosp_summary, totals) combined
Теперь мы можем увидеть результат. Обратите внимание, как в последней строке пустое значение NA
заполняется для столбца hospital
, который не присутствовал в hosp_summary
. Как объяснялось на странице [Таблицы для презентации], вы можете “заполнить” эту ячейку “итогом” используя replace_na()
.
Связывание столбцов
Есть похожая функция в dplyr bind_cols()
, которую вы можете использовать, чтобы соединить два датафрейма боком. Обратите внимание, что строки сопоставляются друг с другом по позиции (в отличие от соединения выше) - например, 12я строка в каждом датафрейме будет выровнена.
Например, мы связываем несколько суммарных таблиц. Чтобы это сделать, мы также демонстрируем, как изменить порядок строк в одном датафрейме, чтобы он соответствовал порядку в другом датафрейме, с помощью match()
.
Здесь мы определяем case_info
как сводный датафрейм построчного списка случаев, по больницам, с количеством случаев и количеством смертей.
# Информация о случаях
<- linelist %>%
case_info group_by(hospital) %>%
summarise(
cases = n(),
deaths = sum(outcome == "Death", na.rm=T)
)
И давайте представим, что у нас есть другой датафрейм, contact_fu
, в котором содержится информация о проценте расследованных контактов, по которым был сделан мониторинг, опять же по больницам.
<- 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%")
)
Обратите внимание, что больницы одинаковые, но в разном порядке в каждом датафрейме. Самым простым решением было бы использовать left_join()
по столбцу hospital
, но можно использовать и bind_cols()
с одним дополнительным шагом.
Используйте match()
для унификации порядка
Поскольку порядок строк отличается, простая команда bind_cols()
приведет к неправильному сопоставлению данных. Чтобы это исправить, мы можем использовать match()
из базового R, чтобы унифицировать порядок строк датафрейма с порядком в другом датафрейме. Мы предполагаем для этого подхода, что ни в одном из датафреймов нет дубликатов.
Когда мы используем match()
, синтаксис будет выглядеть как match(TARGET ORDER VECTOR, DATA FRAME COLUMN TO CHANGE)
, где первым аргументом является желаемый порядок (либо отдельный вектор, либо, как в данном случае, столбец в датафрейме), а вторым аргументом является столбец в датафрейме в том датафрейме, который будет переупорядочен. На выходе после match()
мы получаем вектор чисел, представляющий правильный порядок позиций. Вы можете более детально об этом прочитать с помощью ?match
.
match(case_info$hospital, contact_fu$hospital)
[1] 4 2 3 6 5 1
Вы можете использовать этот числовой вектор, чтобы переупорядочить датафрейм - разместите его внутри квадратных скобок подмножества [ ]
перед запятой. Более детально можете прочитать о таком синтаксисе подмножества в базовом R на странице [Основы R]. Команда ниже создает новый датафрейм, определенный как старый датафрейм, в котором порядок строк меняется в соответствии с указанным выше числовым вектором.
<- contact_fu[match(case_info$hospital, contact_fu$hospital),] contact_fu_aligned
Теперь мы можем связать столбцы датафрейма с правильным порядком строк. Обратите внимание. что по некоторым столбцам имеется дублирование, и они потребуют вычистки с помощью rename()
. Более подробно о bind_rows()
можно прочитать тут.
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%
Альтернатива для bind_cols
в базовом R - это cbind()
, которая проводит ту же операцию.
14.5 Ресурсы
Страница tidyverse по соединениям
Страница R for Data Science по реляционным данным
Страница tidyverse по dplyr on binding
Виньетка по fastLink at the package’s Github page
Публикация, описывающая методологию fastLink
Публикация, описывающая пакет RecordLinkage