web-dev-qa-db-fra.com

Comment puis-je faire correspondre des chaînes de correspondance floues à partir de deux ensembles de données?

J'ai travaillé sur un moyen de joindre deux ensembles de données basés sur une chaîne imparfaite, comme le nom d'une entreprise. Dans le passé, je devais faire correspondre deux listes très sales, une liste avait des noms et des informations financières, une autre liste avait des noms et une adresse. Ni l'un ni l'autre n'avait d'identifiants uniques pour correspondre! ASSUMEZ QUE LE NETTOYAGE A DÉJÀ ÉTÉ APPLIQUÉ ET IL PEUT ÊTRE TYPOS ET INSERTIONS.

Jusqu'à présent, AGREP est l'outil le plus proche que j'ai trouvé qui pourrait fonctionner. Je peux utiliser des distances de levenshtein dans le package AGREP, qui mesurent le nombre de suppressions, insertions et substitutions entre deux chaînes. AGREP retournera la chaîne avec la plus petite distance (la plus similaire).

Cependant, j'ai eu du mal à transformer cette commande à partir d'une seule valeur pour l'appliquer à un bloc de données entier. J'ai grossièrement utilisé une boucle for pour répéter la fonction AGREP, mais il doit y avoir un moyen plus simple.

Voir le code suivant:

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))
}
23
A L

La solution dépend de la cardinalité souhaitée de votre a à b correspondant. Si c'est un contre un, vous obtiendrez les trois matchs les plus proches ci-dessus. Si c'est plusieurs contre un, vous en aurez six.

Cas individuel (nécessite un algorithme d'affectation):

Quand j'ai dû le faire avant de le traiter comme un problème d'affectation avec une matrice de distance et une heuristique d'affectation (affectation gourmande utilisée ci-dessous). Si vous voulez une solution "optimale", vous feriez mieux avec optim.

Pas familier avec AGREP mais voici un exemple utilisant stringdist pour votre matrice de distance.

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

Produit l'affectation:

       a          b       d
1 Ace Co    Ace Co. 0.04762
2  Bayes Bayes Inc. 0.16667
3    asd       asdf 0.08333

Je suis sûr qu'il existe une façon beaucoup plus élégante de faire l'heuristique d'affectation gourmande, mais ce qui précède fonctionne pour moi.

Cas plusieurs-à-un (pas un problème d'affectation):

do.call(rbind, unname(by(d, d$a_name, function(x) x[x$dist == min(x$dist),])))

Produit le résultat:

   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: utilisez method="jw" Pour produire les résultats souhaités. Voir help("stringdist-package")

17
C8H10N4O2

Voici une solution utilisant le package fuzzyjoin. Il utilise la syntaxe semblable à dplyr et stringdist comme l'un des types possibles de correspondance floue.

Comme suggéré par C8H10N4O2, la méthode stringdist = "jw" crée les meilleures correspondances pour votre exemple.

Comme suggéré par dgrtwo, le développeur de fuzzyjoin, j'ai utilisé un grand max_dist puis j'ai utilisé dplyr::group_by et dplyr::top_n pour obtenir uniquement la meilleure correspondance avec une distance minimale.

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
11
Arthur Yip

Je ne sais pas si c'est une direction utile pour vous, John Andrews, mais cela vous donne un autre outil (à partir du package RecordLinkage) et pourrait aider.

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
2
lawyeR

D'accord avec la réponse ci-dessus " Pas familier avec AGREP mais voici un exemple utilisant stringdist pour votre matrice de distance." mais ajoutez- sur la fonction de signature comme ci-dessous de Fusionner des ensembles de données basés sur des éléments de données partiellement appariés sera plus précis car le calcul de LV est basé sur position/ajout/suppression

##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)
}
1
YummyLin Yang

J'utilise lapply pour ces circonstances:

yournewvector: lapply(yourvector$yourvariable, agrep, yourothervector$yourothervariable, max.distance=0.01),

puis l'écrire en csv ce n'est pas si simple:

write.csv(matrix(yournewvector, ncol=1), file="yournewvector.csv", row.names=FALSE)
1
user3909910