Как сопоставить строки нечеткого соответствия из двух наборов данных?
Я работаю над способом объединения двух наборов данных на основе несовершенной строки, такой как имя компании. В прошлом мне приходилось составлять два очень грязных списка, в одном были имена и финансовая информация, в другом-имена и адреса. Не имели уникальные идентификаторы матч! ПРЕДПОЛОЖИМ, ЧТО ОЧИСТКА УЖЕ ПРИМЕНЕНА И, ВОЗМОЖНО, ЕСТЬ ОПЕЧАТКИ И ВСТАВКИ.
до сих пор AGREP-самый близкий инструмент, который я нашел, который может работать. Я могу использовать расстояния Левенштейна в пакет AGREP, который измеряет количество удалений, вставок и подстановок между двумя строками. AGREP вернет строку с наименьшим расстоянием (наиболее похожим).
однако у меня возникли проблемы с поворотом этой команды из одного значения, чтобы применить ее ко всему фрейму данных. Я грубо использовал цикл for для повторения функции AGREP, но должен быть более простой способ.
следующий код:
a<-data.frame(name=c('Ace Co','Bayes', 'asd', 'Bcy', 'Baes', 'Bays'),price=c(10,13,2,1,15,1))
b<-data.frame(name=c('Ace Co.','Bayes Inc.','asdf'),qty=c(9,99,10))
for (i in 1:6){
a$x[i] = agrep(a$name[i], b$name, value = TRUE, max = list(del = 0.2, ins = 0.3, sub = 0.4))
a$Y[i] = agrep(a$name[i], b$name, value = FALSE, max = list(del = 0.2, ins = 0.3, sub = 0.4))
}
6 ответов
решение зависит от желаемой мощности вашего соответствия a
до b
. Если это один к одному, вы получите три ближайших матча выше. Если будет много к одному, вы получите шесть.
один к одному случай (требуется алгоритм назначения):
когда мне пришлось это сделать, прежде чем я рассматриваю его как проблему назначения с матрицей расстояний и эвристикой назначения (жадное назначение, используемое ниже). Если вы хотите "оптимальное" решение тебе лучше с optim
.
не знаком с AGREP, но вот пример использования stringdist
для матрицы расстояний.
library(stringdist)
d <- expand.grid(a$name,b$name) # Distance matrix in long form
names(d) <- c("a_name","b_name")
d$dist <- stringdist(d$a_name,d$b_name, method="jw") # String edit distance (use your favorite function here)
# Greedy assignment heuristic (Your favorite heuristic here)
greedyAssign <- function(a,b,d){
x <- numeric(length(a)) # assgn variable: 0 for unassigned but assignable,
# 1 for already assigned, -1 for unassigned and unassignable
while(any(x==0)){
min_d <- min(d[x==0]) # identify closest pair, arbitrarily selecting 1st if multiple pairs
a_sel <- a[d==min_d & x==0][1]
b_sel <- b[d==min_d & a == a_sel & x==0][1]
x[a==a_sel & b == b_sel] <- 1
x[x==0 & (a==a_sel|b==b_sel)] <- -1
}
cbind(a=a[x==1],b=b[x==1],d=d[x==1])
}
data.frame(greedyAssign(as.character(d$a_name),as.character(d$b_name),d$dist))
производит назначение:
a b d
1 Ace Co Ace Co. 0.04762
2 Bayes Bayes Inc. 0.16667
3 asd asdf 0.08333
Я уверен, что есть гораздо более элегантный способ сделать жадную эвристику задания, но выше работает для меня.
много-к-одному случаю (не проблема назначения):
do.call(rbind, unname(by(d, d$a_name, function(x) x[x$dist == min(x$dist),])))
производит результат:
a_name b_name dist
1 Ace Co Ace Co. 0.04762
11 Baes Bayes Inc. 0.20000
8 Bayes Bayes Inc. 0.16667
12 Bays Bayes Inc. 0.20000
10 Bcy Bayes Inc. 0.37778
15 asd asdf 0.08333
Edit: использовать method="jw"
для получения желаемых результатов. См.help("stringdist-package")
Я не уверен, что это полезное направление для вас, Джон Эндрюс, но оно дает вам другой инструмент (из RecordLinkage
package) и может помочь.
install.packages("ipred")
install.packages("evd")
install.packages("RSQLite")
install.packages("ff")
install.packages("ffbase")
install.packages("ada")
install.packages("~/RecordLinkage_0.4-1.tar.gz", repos = NULL, type = "source")
require(RecordLinkage) # it is not on CRAN so you must load source from Github, and there are 7 dependent packages, as per above
compareJW <- function(string, vec, cutoff) {
require(RecordLinkage)
jarowinkler(string, vec) > cutoff
}
a<-data.frame(name=c('Ace Co','Bayes', 'asd', 'Bcy', 'Baes', 'Bays'),price=c(10,13,2,1,15,1))
b<-data.frame(name=c('Ace Co.','Bayes Inc.','asdf'),qty=c(9,99,10))
a$name <- as.character(a$name)
b$name <- as.character(b$name)
test <- compareJW(string = a$name, vec = b$name, cutoff = 0.8) # pick your level of cutoff, of course
data.frame(name = a$name, price = a$price, test = test)
> data.frame(name = a$name, price = a$price, test = test)
name price test
1 Ace Co 10 TRUE
2 Bayes 13 TRUE
3 asd 2 TRUE
4 Bcy 1 FALSE
5 Baes 15 TRUE
6 Bays 1 FALSE
согласен с вышеприведенным ответом"не знаком с AGREP, но вот пример использования stringdist для вашей матрицы расстояний. " но добавьте функцию подписи, как показано ниже из объединение наборов данных на основе частично совпадающих элементов данных будет более точным, так как расчет LV основан на положение/добавление/удаление
##Here's where the algorithm starts...
##I'm going to generate a signature from country names to reduce some of the minor differences between strings
##In this case, convert all characters to lower case, sort the words alphabetically, and then concatenate them with no spaces.
##So for example, United Kingdom would become kingdomunited
##We might also remove stopwords such as 'the' and 'of'.
signature=function(x){
sig=paste(sort(unlist(strsplit(tolower(x)," "))),collapse='')
return(sig)
}
Я использую lapply
для тех случаях:
yournewvector: lapply(yourvector$yourvariable, agrep, yourothervector$yourothervariable, max.distance=0.01),
тогда написать его как csv не так просто:
write.csv(matrix(yournewvector, ncol=1), file="yournewvector.csv", row.names=FALSE)
вот решение, использующее fuzzyjoin
пакета. Он использует dplyr
-подобный синтаксис и stringdist
как один из возможных типов нечеткого соответствия.
As предложил по C8H10N4O2, в stringdist
method= " jw " создает лучшие совпадения для вашего примера.
As предложил dgrtwo, разработчик fuzzyjoin, я использовал большой max_dist, а затем использовал dplyr::group_by
и dplyr::top_n
чтобы получить только лучший матч с минимумом расстояние.
a <- data.frame(name = c('Ace Co', 'Bayes', 'asd', 'Bcy', 'Baes', 'Bays'),
price = c(10, 13, 2, 1, 15, 1))
b <- data.frame(name = c('Ace Co.', 'Bayes Inc.', 'asdf'),
qty = c(9, 99, 10))
library(fuzzyjoin)
library(dplyr)
stringdist_join(a, b,
by = "name",
mode = "left",
ignore_case = FALSE,
method = "jw",
max_dist = 99,
distance_col = "dist"
) %>%
group_by(name.x) %>%
top_n(1, -dist)
#> # A tibble: 6 x 5
#> # Groups: name.x [6]
#> name.x price name.y qty dist
#> <fctr> <dbl> <fctr> <dbl> <dbl>
#> 1 Ace Co 10 Ace Co. 9 0.04761905
#> 2 Bayes 13 Bayes Inc. 99 0.16666667
#> 3 asd 2 asdf 10 0.08333333
#> 4 Bcy 1 Bayes Inc. 99 0.37777778
#> 5 Baes 15 Bayes Inc. 99 0.20000000
#> 6 Bays 1 Bayes Inc. 99 0.20000000
вот что я использовал для получения количества раз, когда компания появляется в списке, хотя названия компаний являются неточными совпадениями,
шаг.1 установить пакет акустика
шаг.2 создайте новый столбец под названием "soundexcodes" в "mylistofcompanynames"
шаг.3 используйте функцию soundex для возврата кодов soundex названий компаний в "soundexcodes"
шаг.4 скопируйте названия компаний и соответствующий код soundex в новый файл (2 столбца под названием "companynames" и "soundexcode") называется "companysoundexcodestrainingfile"
шаг.5 удалить дубликаты soundexcodes в "companysoundexcodestrainingfile"
шаг.6 пройдите через список оставшихся названий компаний и измените имена, как вы хотите, чтобы он появился в вашей оригинальной компании
пример: Amazon Inc A625 может быть Amazon A625 Accenture Limited A455 может быть Accenture a455
шаг.6 выполните left_join или (простой vlookup) между companysoundexcodestrainingfile$soundexcodes и mylistofcompanynames$soundexcodes с помощью "soundexcodes"
шаг.7 результат должен иметь исходный список с новым столбцом под названием " co.y", который имеет название компании так, как вы оставили его в файле обучения.
шаг.8 Сортировать " co.y " и проверьте, если большинство названия компаний совпадают правильно, если это так, замените старые названия компаний новыми, заданными vlookup кода soundex.