Матрица расстояний строк по критериям

Я написал скрипт для нечеткого сопоставления названий компаний. Я сопоставляю несколько не всегда полностью правильных названий компаний (т. Е. Могут быть небольшие орфографические ошибки или отсутствует суффикс "inc.") С набором "правильных" названий компаний и идентификаторов. Очевидно, что смысл состоит в том, чтобы правильно прикрепить идентификаторы к не всегда корректным названиям компаний.

Вот несколько сильно упрощенных версий наборов данных, которые я сопоставляю (я пока не использую zip-часть, но вернусь к ней позже):

df <- data.frame(zip = c("4760","5445", "2200"), company = c("company x", "company y", "company z"))
corpus <- data.frame(zip = c("4760","5445", "2200", "2200", "2200"), company = c("company x inc.", "company y inc.", "company z inc.", "company a inc.", "company b inc."), id = c(12121212, 23232323, 34343434, 56565656, 67676767))

df
   zip   company
1 4760 company x
2 5445 company y
3 2200 company z

corpus
   zip        company       id
1 4760 company x inc. 12121212
2 5445 company y inc. 23232323
3 2200 company z inc. 34343434
4 2200 company a inc. 56565656
5 2200 company b inc. 67676767

Затем я использую следующий фрагмент кода для создания матрицы расстояния строки

library(stringdist)
distance.method <- c("jw")

string.dist.matrix <- stringdistmatrix(tolower(corpus$company),
                                       tolower(df$company),
                                       method = distance.method,
                                       nthread = getOption("sd_num_thread"))

string.dist.matrix

          [,1]      [,2]      [,3]
[1,] 0.1190476 0.1798942 0.1798942
[2,] 0.1798942 0.1190476 0.1798942
[3,] 0.1798942 0.1798942 0.1190476
[4,] 0.1798942 0.1798942 0.1798942
[5,] 0.1798942 0.1798942 0.1798942

Затем я иду вперед и сопоставляю пары минимальной дистанции. Обычно я хочу сопоставить, может быть, 4000 компаний с корпусом в 4,5 миллиона. компании, которые требуют некоторой вычислительной мощности, чтобы не сказать больше. У меня была идея, что вместо вычисления расстояния между строками между всеми возможными парами я бы рассчитывал его только для тех, кто использует почтовый индекс. На мой взгляд, результатом будет гораздо меньшее количество вычислений и еще большая точность нечеткого сопоставления для более сложных случаев, чем те, которые я иллюстрировал здесь с моими упрощенными данными.

Короче итоговая матрица, которую я хотел бы, будет выглядеть примерно так:

     [,1]            [,2]              [,3]
[1,] 0.1190476       NA                NA
[2,] NA              0.1190476         NA
[3,] NA              NA                0.1190476
[4,] NA              NA                0.1798942
[5,] NA              NA                0.1798942

Я просто не могу найти способ сделать это. Есть идеи?

3 ответа

Решение

Подходы ниже используют dplyr и начинается с подхода Фивера joining два кадра данных, но затем продолжает производить либо кадр данных, аналогичный вашему string.dist.matrix или фрейм данных в сжатой форме "ключ-значение". Я добавил еще одну компанию в ваш df фрейм данных, чтобы включить случай нескольких компаний с одинаковым df zip,

Версия матрицы расстояний:

 df <- data.frame(zip = c("4760","5445", "2200","2200"), company = c("company x", "company y", "company z","company a"))
  corpus <- data.frame(zip = c("4760","5445", "2200", "2200", "2200"), company = c("company x inc.", "company y inc.", "company z inc.", "company a inc.", "company b inc."),
                       id = c(12121212, 23232323, 34343434, 56565656, 67676767))

    # large matrix version
    library(dplyr)
    dist_mat <- inner_join(corpus, df, by = "zip") %>%
      mutate(corpus_co=tolower(as.character(company.x)), df_co=tolower(as.character(company.y)), company.x=NULL, company.y=NULL) %>%
      group_by(zip) %>%
      do( { dist_df=data.frame(unique(.$corpus_co), 
                               stringdistmatrix(unique(.$corpus_co), unique(.$df_co), method=distance.method), stringsAsFactors=FALSE);
            colnames(dist_df) = c("corpus_co", unique(.$df_co));
            dist_df}) 

с результатом

     zip      corpus_co company z company a company x company y
  (fctr)          (chr)     (dbl)     (dbl)     (dbl)     (dbl)
1   2200 company z inc. 0.1190476 0.1798942        NA        NA
2   2200 company a inc. 0.1798942 0.1190476        NA        NA
3   2200 company b inc. 0.1798942 0.1798942        NA        NA
4   4760 company x inc.        NA        NA 0.1190476        NA
5   5445 company y inc.        NA        NA        NA 0.1190476

Тем не менее, с 4000 строк в вашем df матрица, полная матрица расстояния строки очень большая со многими NA. Более эффективная версия использует gather функция от tidyr пакет для получения результата в key value формат. При таком подходе некоторые переменные образуют уникальные ключи, которые затем имеют связанные значения. Виньетка для tidyr Пакет объясняет это более подробно. В вашем случае corpus название компании и df название компании от key и расстояние строки между их именами value, Это делается для каждого почтового индекса, поэтому полная матрица расстояний между строками никогда не сохраняется. Вам также может оказаться, что с этим проще работать для последующего анализа. Код отличается от предыдущей версии только последней строкой.

library(tidyr)
dist_keyval <- inner_join(corpus, df, by = "zip") %>%
               mutate(corpus_co=tolower(as.character(company.x)), df_co=tolower(as.character(company.y)), company.x=NULL, company.y=NULL) %>%
               group_by(zip) %>%
               do( { dist_df=data.frame(unique(.$corpus_co), 
                               stringdistmatrix(unique(.$corpus_co), unique(.$df_co), method=distance.method), stringsAsFactors=FALSE);
                     colnames(dist_df) = c("corpus_co", unique(.$df_co));
                     gather(dist_df, key=df_co, value=str_dist, -corpus_co)})

который дает результат

    zip      corpus_co     df_co  str_dist
  (fctr)          (chr)     (chr)     (dbl)
1   2200 company z inc. company z 0.1190476
2   2200 company a inc. company z 0.1798942
3   2200 company b inc. company z 0.1798942
4   2200 company z inc. company a 0.1798942
5   2200 company a inc. company a 0.1190476
6   2200 company b inc. company a 0.1798942
7   4760 company x inc. company x 0.1190476
8   5445 company y inc. company y 0.1190476

отредактированный

Код для поиска corpus_co что минимальное расстояние от каждого df_co является:

 dist_min <- dist_keyval %>% group_by(zip, df_co) %>%
                slice(which.min(str_dist))

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

final_result <- corpus %>% mutate(lower_co = tolower(as.character(company)))  %>%
            right_join(dist_min, by = c("zip", "lower_co" = "corpus_co") ) %>%
            select(c(df_co, company, id),  everything(), -lower_co)

который дает

      df_co        company       id  zip  str_dist
1 company a company a inc. 56565656 2200 0.1190476
2 company z company z inc. 34343434 2200 0.1190476
3 company x company x inc. 12121212 4760 0.1190476
4 company y company y inc. 23232323 5445 0.1190476

Последний select показывает, как переставить столбцы в определенный порядок.

У меня есть несколько идей. Если вам не нужна матрица расстояний, вы можете решить ее следующим образом. Я использовал dplyr, потому что знаю этот лучше. Вы можете разбить код на части вместо одной команды dplyr. Или используйте data.table. Это может быть даже быстрее.

Предпринятые шаги:

  1. Соедините df и корпус с внутренним соединением на молнии. Это удаляет все ненужные записи, и у вас есть названия компаний рядом друг с другом.
  2. рассчитать расстояние между названиями компаний
  3. группа по оригинальной компании
  4. фильтр на минимальном расстоянии

Эти шаги позволяют избежать использования сначала создания матрицы, а затем поиска минимального значения или установки других значений в NA.

library(stringdist)
library(dplyr)

df <- data.frame(zip = c("4760","5445", "2200"), company = c("company x", "company y", "company z"))
corpus <- data.frame(zip = c("4760","5445", "2200", "2200", "2200"), company = c("company x inc.", "company y inc.", "company z inc.", "company a inc.", "company b inc."), id = c(12121212, 23232323, 34343434, 56565656, 67676767))


distance.method <- c("jw")

combined_min_distance <- inner_join(df, corpus, by = "zip" ) %>% 
  mutate(distance = stringdist(tolower(combined$company.x),
                    tolower(combined$company.y),
                    method = distance.method,
                    nthread = getOption("sd_num_thread"))) %>% 
  group_by(company.x) %>% 
  filter(distance == min(distance))

combined_min_distance

     zip company.x      company.y       id  distance
  (fctr)    (fctr)         (fctr)    (dbl)     (dbl)
1   2200 company z company z inc. 34343434 0.1190476
2   4760 company x company x inc. 12121212 0.1190476
3   5445 company y company y inc. 23232323 0.1190476

Ты можешь использовать stringdist::amatch и избегать вычисления полной матрицы строковых данных.

df <- data.frame(zip = c("4760","5445", "2200"), company = c("company x", "company y", "company z"))
corpus <- data.frame(zip = c("4760","5445", "2200", "2200", "2200"), company = c("company x inc.", "company y inc.", "company z inc.", "company a inc.", "company b inc."), id = c(12121212, 23232323, 34343434, 56565656, 67676767))


i <- stringdist::amatch(df$company,corpus$company,maxDist=5)
merged <- data.frame(df$company,corpus$company[i])
merged

> merged
  df.company corpus.company.i.
1  company x    company x inc.
2  company y    company y inc.
3  company z    company z inc.

Еще лучше выполнить некоторую очистку строки, так что вы знаете, что расстояния вызваны только фактическими опечатками (обратите внимание на maxDist).

lookup <- gsub(" inc.$","",corpus$company)
i2 <- stringdist::amatch(df$company,lookup,maxDist=2)
merged2 <- data.frame(df$company,corpus$company[i2])
Другие вопросы по тегам