14  Соединение данных

Выше: анимированный пример левого соединения (источник изображения)

На данной странице описываются способы “соединения”, “поиска соответствия”, “связывания” и “объединения”, а также другие способы объединения датафреймов.

Очень редко эпидемиологический анализ или работа не требуют использования нескольких источников данных и объединения наборов данных. Возможно, вам будет необходимо связать лабораторные данные с клиническими исходами пациента, либо связать данные о мобильности от Google с трендами инфекционных заболеваний, либо даже набор данных на одном этапе анализа связать с тем же, но преобразованным набором данных.

На этой странице мы демонстрируем код для:

14.1 Подготовка

Загрузка пакетов

Данный фрагмент кода показывает загрузку пакетов, необходимых для анализа. В данном руководстве мы фокусируемся на использовании p_load() из пакета pacman, которая устанавливает пакет, если необходимо, и загружает его для использования. Вы можете также загрузить установленные пакеты с помощью library() из базового R. См. страницу Основы R для получения дополнительной информации о пакетах R.

pacman::p_load(
  rio,            # импорт и экспорт
  here,           # пути к файлам 
  tidyverse,      # управление данными и визуализация
  RecordLinkage,  # вероятностные совпадения
  fastLink        # вероятностные совпадения
)

Импорт данных

Для начала мы импортируем чистый построчный список случаев из имитации эпидемии Эболы. Если вы хотите выполнять действия параллельно, кликните, чтобы скачать “чистый” построчный список (как .rds файл). Импортируйте данные с помощью функции import() из пакета rio (она работает с разными типами файлов, такими как .xlsx, .csv, .rds - см. страницу Импорт и экспорт для получения детальной информации).

# импорт построчного списка случаев 
linelist <- import("linelist_cleaned.rds")

Первые 50 строк построчного списка отображены ниже.

Примеры наборов данных

В разделе соединения ниже, мы используем следующие наборы данных:

  1. “уменьшенную” версию построчного списка случаев linelist, содержащую только столбцы case_id, date_onset и hospital, и только первые 10 строк
  2. отдельный датафрейм под названием hosp_info, который содержит более детальную информацию о каждой больнице

В разделе вероятностного сопоставления мы будем использовать два разных маленьких набора данных. Код для создания этих наборов данных представлен в соответствующем разделе.

“Уменьшенный” построчный список linelist

Ниже представлен уменьшенный построчный список, который содержит лишь 10 строк и только столбцы case_id, date_onset и hospital.

linelist_mini <- linelist %>%                 # начинаем с оригинального построчного списка
  select(case_id, date_onset, hospital) %>%   # выбираем столбцы
  head(10)                                    # берем только верхние 10 строк

Датафрейм с информацией о больницах

Ниже представлен код для создания отдельного датафрейма с дополнительной информацией о семи больницах (прикрепленное население, уровень доступной помощи). Обратите внимание, что название “Military Hospital” относится к двум разным больницам - одной на первичном уровне, обслуживающей 10000 жителей, а другой - на вторичном уровне, обслуживающей 50280 жителей.

# Создаем датафрейм с информацией о больницах
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")
)

Вот этот датафрейм:

Предварительная вычистка

Традиционные соединения (невероятностные) чувствительны к регистру и требуют точного соответствия знаков между значениями в двух датафреймах. Чтобы продемонстрировать некоторые шаги вычистки, которые вам могут потребоваться до соединения, мы вычистим и унифицируем наборы данных 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(
      # критерии                         # новое значение
      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
      )
    )

Названия больниц, которые появляются в обоих датафреймах, унифицированы. Есть две больницы в 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" (второй датафрейм)
joined_data <- left_join(df1, df2, by = c("ID" = "identifier"))

Если у столбцов, по которым вы группируете (by), в обоих датафреймах одинаковое название, вы можете указать это одно название в кавычках.

# Соединение на основе общих значений в столбце "ID" в обоих датафреймах
joined_data <- left_join(df1, df2, by = "ID")

Если вы соединяете датафреймы на основе общих значений между несколькими полями, укажите эти поля в векторе c(). Этот пример соединяет строки, если значения в трех столбцах каждого набора данных абсолютно совпадают.

# Соединение на основе имени, фамилии и возраста
joined_data <- left_join(df1, df2, by = c("name" = "firstname", "surname" = "lastname", "Age" = "age"))

Команды соединения можно также выполнять в рамках цепочки канала. Это будет модифицировать тот датафрейм, который передан по каналу.

В примере ниже по каналу передается датафрейм 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 .

Загрузка пакетов

pacman::p_load(
  tidyverse,      # манипуляции с данными и визуализация
  fastLink        # сопоставление записей
  )

Вот два коротких набора данных для примера, которые мы используем, чтобы продемонстрировать поиск вероятностных совпадений (cases и test_results):

Вот код, который был использован для создания наборов данных:

# создаем наборы данных

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"
  )

В наборе данных 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%.

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 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”), представляющие собой наилучшие совпадения. Если номер строки из датафрейма отсутствует, это значит, что не было найдено совпадения в другом датафрейме в рамках указанного порога совпадения.

# печать совпадений
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

Обратите внимание:

  • Совпадения были найдены несмотря на небольшие различия в написании имен и датах рождения:
    • “Tony B. Smith” сопоставлен с “Anthony B Smith”
    • “Maria Rodriguez” сопоставлена с “Marialisa Rodrigues”
    • “Betty Chase” сопоставлена с “Elizabeth Chase”
    • “Olivier Laurent De Bordeaux” сопоставлена с “Oliver Laurent De Bordow” (игнорируется отсутствующая дата рождения)
  • По одной строке из cases (для “Blessing Adebayo”, строка 9) не было хороших соответствий в results, поэтому ее нет в my_matches.

Соединение на основе вероятностных совпадений

Чтобы использовать эти совпадения для присоединения results к cases, одна из стратегий будет:

  1. Использовать левое соединение left_join(), чтобы присоединить my_matches к cases (сопоставление имен строк в cases с “inds.a” в my_matches)
  2. Затем использовать еще одно левое соединение left_join(), чтобы присоединить results к cases (сопоставление нового полученного “inds.b” в cases с rownames в results)

Перед соединениями мы должны вычистить три датафрейма:

  • И в dfA, и в dfB номера строк (“rowname”) должны быть конвертированы в полноценный столбец.
  • Оба столбца в my_matches конвертируются в текстовый класс, чтобы их можно было соединять с текстовыми rownames
# Вычистка данных до соединения
#############################

# конвертируем имена строк случаев cases (rownames) в столбец 
cases_clean <- cases %>% rownames_to_column()

# конвертируем имена строк результатов test_results (rownames) в столбец
results_clean <- results %>% rownames_to_column()  

# конвертируем все столбцы в наборе данных совпадений в текстовый класс, чтобы их можно было присоединить к rownames
matches_clean <- my_matches %>%
  mutate(across(everything(), as.character))



# Присоединяем совпадения к dfA, затем добавляем dfB
###################################
# столбец "inds.b" добавляется к dfA
complete <- left_join(cases_clean, matches_clean, by = c("rowname" = "inds.a"))

# столбец(столбцы) из dfB добавляются 
complete <- left_join(complete, results_clean, by = c("inds.b" = "rowname"))

При выполнении указанного выше кода полученный в результате датафрейм complete будет содержать все столбцы и из cases, и из results. Ко многим будут подставлены суффиксы “.x” и “.y”, поскольку иначе дублировались бы имена столбцов.

Альтернативно, чтобы получить только “оригинальные” 9 записей в cases с новым столбцом(ами) из results, используйте select() для results до соединений, чтобы там содержались только имена строк (rownames) и столбцы, которые вы хотите добавить к cases (например, столбец result).

cases_clean <- cases %>% rownames_to_column()

results_clean <- results %>%
  rownames_to_column() %>% 
  select(rowname, result)    # выберите только определенные столбцы 

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"))

Если вы хотите выбрать подмножество данных из любого набора данных, чтобы оно включало только совпадающие строки, вы можете использовать код ниже:

cases_matched <- cases[my_matches$inds.a,]  # строки cases, которые совпали со строкой в results
results_matched <- results[my_matches$inds.b,]  # Строки в results, которые совпали со строкой в cases

Либо, чтобы увидеть только те строки, по которым нет совпадения:

cases_not_matched <- cases[!rownames(cases) %in% my_matches$inds.a,]  # Строки в cases, по которым НЕТ совпадений со строкой в results
results_not_matched <- results[!rownames(results) %in% my_matches$inds.b,]  # Строки в results, по которым НЕТ совпадений со строкой в cases

Вероятностная дедупликация

Поиск вероятностных совпадений можно использовать и для дедупликации набора данных. См. страницу по дедупликации, где описаны другие методы дедупликации.

Здесь мы начинаем с набора данных cases, но сейчас назовем его cases_dup, поскольку в нем 2 дополнительных строки, которые могут быть дубликатами предыдущих строк: See “Tony” with “Anthony”, and “Marialisa Rodrigues” with “Maria Rodriguez”.

Выполняем fastLink() как и ранее, но сравниваем датафрейм cases_dup с самим собой, если два датафрейма оказываются идентичными, функция предполагает, что вы хотите провести дедупликацию. Обратите внимание, что мы не уточняе stringdist.match = или numeric.match =, как мы это делали ранее.

## Выполняем fastLink на том же наборе данных
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 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()
cases_dedupe <- getMatches(
  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 и все вероятные дубликаты в ней
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 Связывание и упорядочивание

Еще один метод объединения двух датафреймов - это их “связывание”. Можете представить это, как “добавление” строк или столбцов.

В данном разделе мы также обсудим, как “упорядочивать” порядок строк одного датафрейма относительно порядка в другом датафрейме. Эта тема обсуждается ниже в разделе Связывание столбцов.

Связывание строк

Чтобы связать строки одного датафрейма с нижней частью другого датафрейма, используйте bind_rows() из dplyr. Это очень инклюзивная функция, поэтому любой столбец, присутствующий в одном из датафреймов, будет включен в результат. Несколько замечаний:

  • В отличие от базовой версии R row.bind(), функция bind_rows() из dplyr не требует, чтобы порядок столбцов был одинаковым в обоих датафреймах. Главное, чтобы названия столбцов были написано одинаково, тогда она их правильно упорядочит.
  • Опционально вы можете задать аргумент .id =. Укажите текстовое имя столбца. Это создаст новый столбец, который будет использован для определения, из какого датафрейма изначально взяли каждую строку.
  • Вы можете использовать bind_rows() для объекта list аналогично структурированных датафреймов для объединения их в один датафрейм. См. пример на странице [Итерации, циклы и списки], где показан импорт нескольких построчных списков с помощью purrr.

Одним из частых примеров связывания строк является связывание строки “итого” с описательной таблицей, используя функцию summarise() из dplyr. Ниже мы создаем таблицу с подсчетом случаев и медианными значениями CT по больнице со строкой итого.

Функция summarise() используется для данных, сгруппированных по больнице, чтобы выдать сводный датафрейм по больнице. Но функция summarise() не создает автоматически строку “итого”, поэтому мы создаем ее, сведя данные еще раз, но в этот раз не группируя данные по больнице. Это создает второй датафрейм всего с одной строкой. Затем мы можем связать эти датафрейма вместе, чтобы получить итоговую таблицу.

См. другие подобные примеры на страницах [Описательные таблицы] и [Таблицы для презентации].

# Создаем основную таблицу
###################
hosp_summary <- linelist %>% 
  group_by(hospital) %>%                        # группируем данные по больнице
  summarise(                                    # создаем новые сводные столбцы по интересующим индикаторам
    cases = n(),                                  # количество строк на группу больница-исход     
    ct_value_med = median(ct_blood, na.rm=T))     # медианное значение CT на группу

Вот датафрейм hosp_summary:

Создаем датафрейм с “итоговой” статистикой (не сгруппированной по больницам). Это выдаст нам только одну строку.

# создаем итого
###############
totals <- linelist %>% 
  summarise(
    cases = n(),                               # количество строк для всего набора данных     
    ct_value_med = median(ct_blood, na.rm=T))  # медианное CT для всего набора данных

Ниже вы видите этот датафрейм totals. Обратите внимание, что в нем только два столбца. Эти столбцы также есть в hosp_summary, но в hosp_summary есть один столбец, которого нет в totals (hospital).

Теперь мы связываем строки с помощью bind_rows().

# связываем датафреймы
combined <- bind_rows(hosp_summary, totals)

Теперь мы можем увидеть результат. Обратите внимание, как в последней строке пустое значение NA заполняется для столбца hospital, который не присутствовал в hosp_summary. Как объяснялось на странице [Таблицы для презентации], вы можете “заполнить” эту ячейку “итогом” используя replace_na().

Связывание столбцов

Есть похожая функция в dplyr bind_cols(), которую вы можете использовать, чтобы соединить два датафрейма боком. Обратите внимание, что строки сопоставляются друг с другом по позиции (в отличие от соединения выше) - например, 12я строка в каждом датафрейме будет выровнена.

Например, мы связываем несколько суммарных таблиц. Чтобы это сделать, мы также демонстрируем, как изменить порядок строк в одном датафрейме, чтобы он соответствовал порядку в другом датафрейме, с помощью match().

Здесь мы определяем case_info как сводный датафрейм построчного списка случаев, по больницам, с количеством случаев и количеством смертей.

# Информация о случаях
case_info <- linelist %>% 
  group_by(hospital) %>% 
  summarise(
    cases = n(),
    deaths = sum(outcome == "Death", na.rm=T)
  )

И давайте представим, что у нас есть другой датафрейм, contact_fu, в котором содержится информация о проценте расследованных контактов, по которым был сделан мониторинг, опять же по больницам.

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%")
)

Обратите внимание, что больницы одинаковые, но в разном порядке в каждом датафрейме. Самым простым решением было бы использовать 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_aligned <- contact_fu[match(case_info$hospital, contact_fu$hospital),]

Теперь мы можем связать столбцы датафрейма с правильным порядком строк. Обратите внимание. что по некоторым столбцам имеется дублирование, и они потребуют вычистки с помощью 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