RecordLinkage: как соединить только лучшие совпадения и экспортировать объединенную таблицу?

Я пытаюсь использовать пакет R RecordLinkage для сопоставления элементов в списке заказов на покупку с записями в главном каталоге. Ниже приведен код R и воспроизводимый пример с использованием двух фиктивных наборов данных (DOrders и DCatalogue):

DOrders <- structure(list(Product = structure(c(1L, 2L, 7L, 3L, 4L, 5L, 
6L), .Label = c("31471 - SOFTSILK 2.0 SCREW 7mm x 20mm", "Copier paper white A4 80gsm", 
"High resilience memory foam standard  mattress", "Liston forceps bone cutting 152mm", 
"Micro reciprocating blade 25.4mm x 8.0mm x 0.38mm", "Micro reciprocating blade 39.5 x 7.0 x 0.38", 
"microaire dual tooth 18 x 90 x 0.89"), class = "factor"), Supplier = structure(c(5L, 
6L, 2L, 1L, 4L, 3L, 3L), .Label = c("KAROMED LTD", "Morgan Steer Ortho Limited", 
"ORTHOPAEDIC SOLUTIONS", "SURGICAL HOLDINGS", "T J SMITH NEPHEW LTD", 
"XEROX SOLUTIONS"), class = "factor"), UOI = structure(c(1L, 
1L, 1L, 1L, 1L, 1L, 2L), .Label = c("Each", "Pack"), class = "factor"), 
    Price = c(5.99, 6.99, 40, 230, 35, 80, 79)), .Names = c("Product", 
"Supplier", "UOI", "Price"), class = "data.frame", row.names = c(NA, 
-7L))

DCatalogue <- structure(list(Product = structure(c(7L, 3L, 4L, 5L, 6L, 2L, 
8L, 1L), .Label = c("7.0mm cann canc scr 32x80mm non sterile single use", 
"A4 80gsm white copier paper", "High resilience memory foam standard hospital mattress with stitched seams has a fully enclosing cover", 
"Liston bone cutting forceps with fluted handle straight 152mm", 
"Micro reciprocating blade 25.4mm x 8.0mm x 0.38mm", "Micro reciprocating blade 39.5mm x 7.0mm x 0.38mm", 
"microaire large osc dual tooth 18mm x 90mm x 0.89mm", "Softsilk 2.0 pkg 7x20 ster"
), class = "factor"), Supplier = structure(c(3L, 2L, 6L, 4L, 
4L, 7L, 5L, 1L), .Label = c("BIOMET MERCK LTD", "KAROMED LIMITED", 
"MORGAN STEER ORTHOPAEDICS LTD", "ORTHO SOLUTIONS", "SMITH & NEPHEW ADVANCED SURGICAL DEVICES", 
"SURGICAL HOLDINGS", "XEROX"), class = "factor"), UOI = structure(c(1L, 
1L, 1L, 2L, 2L, 1L, 1L, 1L), .Label = c("Each", "Pack"), class = "factor"), 
    RefPrice = c(38.7, 274.18, 34.96, 79.48, 81.29, 6.99, 5.99, 
    5)), .Names = c("Product", "Supplier", "UOI", "RefPrice"), class = "data.frame", row.names = c(NA, 
-8L))

В целях эксперимента DOrders имеет 7 записей, каждая из которых соответствует одной из девяти строк в эталонном наборе DCatalogue. В реальных данных не все ордера совпадут.

head(DOrders)
                                            Product                   Supplier  UOI  Price
1             31471 - SOFTSILK 2.0 SCREW 7mm x 20mm       T J SMITH NEPHEW LTD Each   5.99
2                       Copier paper white A4 80gsm            XEROX SOLUTIONS Each   6.99
3               microaire dual tooth 18 x 90 x 0.89 Morgan Steer Ortho Limited Each  40.00
4    High resilience memory foam standard  mattress                KAROMED LTD Each 230.00
5                 Liston forceps bone cutting 152mm          SURGICAL HOLDINGS Each  35.00
6 Micro reciprocating blade 25.4mm x 8.0mm x 0.38mm      ORTHOPAEDIC SOLUTIONS Each  80.00

> head(DCatalogue)
                                                                                                 Product                      Supplier  UOI RefPrice
1                                                    microaire large osc dual tooth 18mm x 90mm x 0.89mm MORGAN STEER ORTHOPAEDICS LTD Each    38.70
2 High resilience memory foam standard hospital mattress with stitched seams has a fully enclosing cover               KAROMED LIMITED Each   274.18
3                                          Liston bone cutting forceps with fluted handle straight 152mm             SURGICAL HOLDINGS Each    34.96
4                                                      Micro reciprocating blade 25.4mm x 8.0mm x 0.38mm               ORTHO SOLUTIONS Pack    79.48
5                                                      Micro reciprocating blade 39.5mm x 7.0mm x 0.38mm               ORTHO SOLUTIONS Pack    81.29
6                                                                            A4 80gsm white copier paper                         XEROX Each     6.99

Первым шагом в увязке является обеспечение соответствия элементов по единице выпуска (UOI). Это связано с тем, что набор предметов, очевидно, не совпадает с одной единицей, даже если предметы абсолютно одинаковы. Например.:

Micro reciprocating blade 25.4mm x 8.0mm x 0.38mm      ORTHOPAEDIC SOLUTIONS Each  80.00

Это тот же элемент, но он не должен совпадать с:

Micro reciprocating blade 25.4mm x 8.0mm x 0.38mm               ORTHO SOLUTIONS Pack    79.48

Следовательно, я использую блокирующий аргумент blockfld = 3, чтобы попытаться сопоставить только те записи с идентичными значениями в 3-м столбце. Также, используя exclude = 4, исключить цену из сопоставления. Это будет отличаться между Заказами и Каталогом и само по себе является основным интересом сопоставления. Сопоставление выполняется с помощью компаратора строк jarowinkler (как описано здесь) по названиям Продуктов и Поставщиков:

library(RecordLinkage)

rpairs <- compare.linkage(DOrders, DCatalogue, 
                          blockfld = 3,
                          exclude = 4,
                          strcmp = 1:2,
                          strcmpfun = jarowinkler)

Затем я вычисляю веса для каждой пары, используя Contiero et al. (2005) метод:

rpairs <- epiWeights(rpairs)
> summary(rpairs)
Weight distribution:

[0.3,0.4] (0.4,0.5] (0.5,0.6] (0.6,0.7] (0.7,0.8] (0.8,0.9]   (0.9,1] 
        1         1        19        10         3         0         4

На основе этого распределения я хочу классифицировать как совпадения только те пары с весом > 0,7.

result <- epiClassify(rpairs, 0.7)
> summary(result)
7 links detected 
0 possible links detected 
31 non-links detected 

Это насколько я понял, но с этим есть некоторые проблемы.

Во-первых, getPairs(result) показывает, что одна запись из DOrders может иметь высокий вес, соответствующий более чем одной записи в DCatalogue. Например.

Эта пара правильно подобрана с весом 0,948.

Micro reciprocating blade 39.5 x 7.0 x 0.38 ORTHOPAEDIC SOLUTIONS   Pack    79  
Micro reciprocating blade 39.5mm x 7.0mm x 0.38mm   ORTHO SOLUTIONS Pack    81.29   0.9480503

но также неправильно соответствует весу 0,928:

Micro reciprocating blade 39.5 x 7.0 x 0.38 ORTHOPAEDIC SOLUTIONS   Pack    79  
Micro reciprocating blade 25.4mm x 8.0mm x 0.38mm   ORTHO SOLUTIONS Pack    79.48   0.9283522

Очевидно, мне нужно ограничить спаривание только одним лучшим совпадением с наибольшим весом, но как это сделать?

И, наконец, конечный результат, который я ищу, — это объединенный набор данных, который содержит совпадающие записи из «Заказов» и «Каталога» в одной строке со всеми столбцами из обоих исходных наборов рядом для сравнения. getPairs выводит в неудобном формате:

> getPairs(result)
    id  Product Supplier    UOI Price   Weight
1   7   Micro reciprocating blade 39.5 x 7.0 x 0.38 ORTHOPAEDIC SOLUTIONS   Pack    79  
2   5   Micro reciprocating blade 39.5mm x 7.0mm x 0.38mm   ORTHO SOLUTIONS Pack    81.29   0.9480503
3                       
4   5   Liston forceps bone cutting 152mm   SURGICAL HOLDINGS   Each    35  
5   3   Liston bone cutting forceps with fluted handle straight 152mm   SURGICAL HOLDINGS   Each    34.96   0.9329244
...

person Mihael    schedule 02.11.2016    source источник


Ответы (1)


Прежде всего, спасибо за предоставленный воспроизводимый пример, который значительно облегчает ответы на ваши вопросы. Начну со второго вашего вопроса:

И, наконец, конечный результат, который я ищу, — это объединенный набор данных, который содержит совпадающие записи из «Заказов» и «Каталога» в одной строке со всеми столбцами из обоих исходных наборов рядом для сравнения.

С single.rows=TRUE getPairs перечисляет обе записи в одной строке. Кроме того, show="links" ограничивает вывод парами, классифицируемыми как принадлежащие друг другу (подробности см. в ?getPairs):

> matchedPairs <- getPairs(result, single.rows=TRUE, show="links")

Однако это не ставит совпадающие столбцы рядом друг с другом, но за всеми столбцами первой записи следуют все столбцы второй записи (и, наконец, совпадающий вес в качестве последнего столбца). Я показываю здесь только имена столбцов, так как вся таблица очень широкая:

> names(matchedPairs)
 [1] "id1"        "Product.1"  "Supplier.1" "UOI.1"      "Price.1"    "id2"        "Product.2"  "Supplier.2" "UOI.2"      "RefPrice.2" "Weight"    

Поэтому, если вам нужно прямое сравнение столбцов в этом формате, вам нужно переставить столбцы в соответствии с вашими потребностями.

Очевидно, мне нужно ограничить спаривание только одним лучшим совпадением с наибольшим весом, но как это сделать?

Эта функциональность не предоставляется пакетом, и я считаю, что процесс выбора однозначных назначений из результата связывания записей сам по себе требует некоторого концептуального внимания. Я никогда не углублялся в этот шаг, поэтому следующее может быть просто идеей для начала. Вы можете использовать библиотеку data.table, чтобы выбрать из каждой группы пар с одинаковым левым идентификатором ту, которая имеет максимальный вес (сравните Как выбрать строку с максимальным значением в каждой группе):

> library(data.table)
> matchedPairs <- data.table(matchedPairs)
> matchedPairs[matchedPairs[,.I[which.max(Weight)],by=id1]$V1, list(id1,id2)]
   id1 id2
1:   7   5
2:   5   3
3:   4   2
4:   2   6
5:   6   1
6:   3   1

Здесь list(id1,id2) ограничивает вывод идентификаторами записей.

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

Обновление: использование классов и методов для больших наборов данных

Для больших наборов данных можно использовать так называемые классы и методы "больших данных" (см. https://cran.r-project.org/web/packages/RecordLinkage/vignettes/BigData.pdf). Они используют структуры данных с файловой поддержкой, поэтому ограничением размера является доступное дисковое пространство. Синтаксис в основном, но не полностью идентичен. В этом примере необходимые вызовы для достижения того же результата, что и выше, будут следующими:

rpairs <- RLBigDataLinkage(DOrders, DCatalogue, 
                      blockfld = 3,
                      exclude = 4,
                      strcmp = 1:2,
                      strcmpfun = "jarowinkler")

rpairs <- epiWeights(rpairs)
result <- epiClassify(rpairs, 0.7)
matchedPairs <- getPairs(result, single.rows=TRUE, filter.link="link")
matchedPairs <- data.table(matchedPairs)
matchedPairs[matchedPairs[,.I[which.max(Weight)],by=id.1]$V1, list(id.1,id.2)]

Однако, учитывая вашу оценку размера в 2 ТБ, это все еще невозможно. Я думаю, вам придется еще больше уменьшить количество пар путем дополнительной блокировки.

Проблема в этом случае заключается в том, что пакет поддерживает только «жесткие» критерии блокировки (т. е. две записи должны точно совпадать в блокирующем поле). При связывании персональных данных (что было нашим вариантом использования при разработке пакета) компоненты дня, месяца и года даты рождения обычно могут быть объединены для блокировки таким образом, что количество пар значительно сокращается без пропуска кандидатов на совпадение. . Насколько я могу судить по примерам, дальнейшая "жесткая" блокировка для ваших данных невозможна, так как совпадающие пары имеют только схожие, но не равные значения атрибутов (кроме "единицы выдачи", которую вы уже используете для блокировка). Такой критерий, как «рассматривать только те пары, в которых сходство строк названий продуктов превышает [некоторый порог]», кажется мне наиболее подходящим. Для этого вам нужно будет расширить compare.linkage() или RLBigDataLinkage().

person Andreas Borg    schedule 03.11.2016
comment
спасибо за ваш очень полезный ответ. Однако одно замечание: для моей конкретной цели мне нужно удалить двойные сопоставления только в id1, а не в id2 (т. е. заказ может соответствовать только одной записи в главном справочном каталоге, но запись в каталоге может быть заказана несколько раз). - person Mihael; 04.11.2016
comment
Кроме того, к сожалению, в то время как код отлично работает с экспериментальными игрушечными наборами данных, он вообще не работает с реальными наборами данных. Это потому, что типичный список заказов имеет c. 150 000 записей и главный каталог 400 000 записей. R просто не хватает памяти для создания объекта пар, который должен иметь c. 30 триллионов пар. По моим приблизительным оценкам, для такого объекта потребуется около 2 ТБ памяти. Если у кого-то нет идей, кажется, что для связывания реальных данных потребуется совершенно другой подход. - person Mihael; 04.11.2016