1
votes

Comment comparer deux chaînes mot par mot dans R

J'ai un jeu de données, appelons-le "ORIGINALE", composé de plusieurs lignes différentes pour seulement deux colonnes, la première appelée "DESCRIPTION" et la seconde "CODICE". La colonne de description a les bonnes informations tandis que la colonne codice, qui est la clé, est presque toujours vide, donc j'essaye de rechercher le codice correspondant dans un autre ensemble de données, appelons-le "REFERENCE". J'utilise la description de la colonne, qui est en langage naturel, et j'essaie de la faire correspondre avec la description du deuxième ensemble de données. Je dois faire correspondre mot par mot car il peut y avoir un ordre différent de mots, de synonymes ou d'abréviations. Ensuite, je calcule le score de similarité pour ne garder que le meilleur match et accepter ceux qui dépassent un certain score. Y a-t-il un moyen de l'améliorer? Je travaille avec environ 300 000 lignes et, même si je sais que cela prendra toujours du temps, il pourrait peut-être y avoir un moyen de le rendre encore légèrement plus rapide.

algoritmo <- function(ORIGINALE, REFERENCE) {
   split1 <- strsplit(x$DESCRIPTION, " ")
   split2 <- strsplit(y$DESCRIPTION, " ")
   risultato <- vector()
   distanza <- vector()
      for(i in 1:NROW(split1)) {
      best_dist <- -5
      closest_match <- -5
        for(j in 1:NROW(split2)) {
          dist <- stringsim(as.character(split1[i]), as.character(split2[j]))
            if (dist > best_dist) {
              closest_match <- y$DESCRIPTION[j]
              best_dist <- dist 
            } 
        } 
      distanza <- append(distanza, best_dist)    
      risultato <- append(risultato, closest_match)
      }
    confronto <<- tibble(x$DESCRIPTION, risultato, distanza)
  }

match <- subset.data.frame(confronto, confronto$distanza >= "0.6")
missing <- subset.data.frame(confronto, confronto$distanza <"0.6")
ORIGINALE <- data.frame(DESCRIPTION = c("mr peter 123 rose street 3b LA"," 4c flower str jenny jane Chicago", "washington miss sarah 430f name strt"), CODICE = (NA, NA, NA))
REFERENE <- dataframe (DESCRIPTION = c("sarah brown name street 430f washington", "peter green 123 rose street 3b LA", "jenny jane flower street 4c Chicago"), CODICE = c("135tg67","aw56", "83776250"))

r

0 commentaires

3 Réponses :


0
votes

Bonne question. les boucles for sont lentes en R:

                            DESCRIPTION CODICE                           DESCRIPTION.y CODICE.y N
1:     4c flower str jenny jane Chicago     NA     jenny jane flower street 4c Chicago 83776250 5
2:       mr peter 123 rose street 3b LA     NA       peter green 123 rose street 3b LA     aw56 6
3: washington miss sarah 430f name strt     NA sarah brown name street 430f washington  135tg67 4

Pour un R rapide, vous devez vectoriser votre algorithme. Je ne suis plus aussi à l'aise avec data.frame , je vais donc utiliser son successeur, data.table .

library(data.table)
ORIGINALE = data.table(DESCRIPTION = c("mr peter 123 rose street 3b LA"," 4c flower str jenny jane Chicago", "washington miss sarah 430f name strt"), CODICE = c(NA, NA, NA))
REFERENCE = data.table(DESCRIPTION = c("sarah brown name street 430f washington", "peter green 123 rose street 3b LA", "jenny jane flower street 4c Chicago"), CODICE = c("135tg67","aw56", "83776250"))

# split DESCRIPTION to make tables that have one word per row
ORIGINALE_WORDS = ORIGINALE[,.(word=unlist(strsplit(DESCRIPTION,' ',fixed=T))),.(DESCRIPTION,CODICE)]
REFERENCE_WORDS = REFERENCE[,.(word=unlist(strsplit(DESCRIPTION,' ',fixed=T))),.(DESCRIPTION,CODICE)]

# remove empty words introduced by extra spaces in your DESCRIPTIONS
ORIGINALE_WORDS = ORIGINALE_WORDS[word!='']
REFERENCE_WORDS = REFERENCE_WORDS[word!='']

# merge the tables by word
merged = merge(ORIGINALE_WORDS,REFERENCE_WORDS,by='word',all=F,allow.cartesian=T)

# count matching words for each combination of ORIGINALE DESCRIPTION and REFERENCE DESCRIPTION and CODICE
counts = merged[,.N,.(DESCRIPTION.x,DESCRIPTION.y,CODICE.y)]

# keep only the highest N CODICE.y for each DESCRIPTION.x
topcounts = merged[order(-N)][!duplicated(DESCRIPTION.x)]

# merge the counts back to ORIGINALE
result = merge(ORIGINALE,topcounts,by.x='DESCRIPTION',by.y='DESCRIPTION.x',all.x=T,all.y=F)

Voici le résultat:

for(i in 1:NROW(split1)) {
for(j in 1:NROW(split2)) {

PS: Il existe des moyens plus économes en mémoire pour le faire, et ce code pourrait provoquer un plantage de votre machine en raison d'un -memory ou aller lentement en raison du besoin de mémoire virtuelle, mais sinon, cela devrait être plus rapide que les boucles for.


1 commentaires

Merci beaucoup pour votre suggestion. Malheureusement, je ne suis pas en mesure de l'implémenter en raison d'un problème de mémoire, chaque ligne de l'ensemble de données d'origine compte environ dix mots et plus et la référence a environ un million de lignes avec plus ou moins le même nombre de mots. Je vais peut-être essayer de supprimer les mots récurrents qui n'apportent pas d'informations pertinentes (par exemple "rue") et une machine plus puissante. Mais belle idée qui apporterait certainement une solution complète!



0
votes

Et pour:

library(stringdist)
library(dplyr)
library(tidyr) 

data_o <- ORIGINALE %>% mutate(desc_o = DESCRIPTION) %>% select(desc_o)
data_r <- REFERENE %>% mutate(desc_r = DESCRIPTION) %>% select(desc_r)
data <- crossing(data_o,data_r)
data %>% mutate(dist= stringsim(as.character(desc_o),as.character(desc_r))) %>%
         group_by(desc_o) %>% 
         filter(dist==max(dist))

  desc_o                                 desc_r                                   dist
  <chr>                                  <chr>                                   <dbl>
1 " 4c flower str jenny jane Chicago"    jenny jane flower street 4c Chicago     0.486
2 "mr peter 123 rose street 3b LA"       peter green 123 rose street 3b LA       0.758
3 "washington miss sarah 430f name strt" sarah brown name street 430f washington 0.385


1 commentaires

J'ai déjà essayé quelque chose comme ça mais, même si cela fonctionne, ne me donne pas des résultats de qualité. Mon seuil d'acceptabilité doit être assez élevé car je préfère rejeter quelque chose de bien plutôt que d'accepter quelque chose de mal, mais sans vérifier chaque mot, je perdrais beaucoup de correspondances possibles, juste dans l'exemple 2 sur 3. Merci quand même !



0
votes

La bibliothèque R tm (text mining) peut vous aider ici:

library(tm)
library(proxy) # for computing cosine similarity
library(data.table)

ORIGINALE = data.table(DESCRIPTION = c("mr peter 123 rose street 3b LA"," 4c flower str jenny jane Chicago", "washington miss sarah 430f name strt"), CODICE = c(NA, NA, NA))
REFERENCE = data.table(DESCRIPTION = c("sarah brown name street 430f washington", "peter green 123 rose street 3b LA", "jenny jane flower street 4c Chicago"), CODICE = c("135tg67","aw56", "83776250"))

# combine ORIGINALE and REFERENCE into one data.table
both = rbind(ORIGINALE,REFERENCE)

# create "doc_id" and "text" columns (required by tm)
both[,doc_id:=1:.N]
names(both)[1] = 'text'

# convert to tm corpus
corpus = SimpleCorpus(DataframeSource(both))

# convert to a tm document term matrix
dtm = DocumentTermMatrix(corpus)

# convert to a regular matrix
dtm = as.matrix(dtm)

# look at it (t() transpose for readability)
t(dtm)
            Docs
Terms        1 2 3 4 5 6
  123        1 0 0 0 1 0
  peter      1 0 0 0 1 0
  rose       1 0 0 0 1 0
  street     1 0 0 1 1 1
  chicago    0 1 0 0 0 1
  flower     0 1 0 0 0 1
  jane       0 1 0 0 0 1
  jenny      0 1 0 0 0 1
  str        0 1 0 0 0 0
  430f       0 0 1 1 0 0
  miss       0 0 1 0 0 0
  name       0 0 1 1 0 0
  sarah      0 0 1 1 0 0
  strt       0 0 1 0 0 0
  washington 0 0 1 1 0 0
  brown      0 0 0 1 0 0
  green      0 0 0 0 1 0

# compute similarity between each combination of documents 1:3 and documents 4:6
similarity = proxy::dist(dtm[1:3,], dtm[4:6,], method="cosine")

# result:
ORIGINALE               REFERENCE document
 document              4         5         6
        1      0.7958759 0.1055728 0.7763932   <-- difference (smaller = more similar)
        2      1.0000000 1.0000000 0.2000000
        3      0.3333333 1.0000000 1.0000000

# make a table of which REFERENCE document is most similar
most_similar = rbindlist(
  apply(
    similarity,1,function(x){
      data.table(i=which.min(x),distance=min(x))
    }
  )
)

# result:
   i  distance
1: 2 0.1055728
2: 3 0.2000000
3: 1 0.3333333
# rows 1, 2, 3 or rows of ORIGINALE; i: 2 3 1 are rows of REFERENCE

# add the results back to ORIGINALE
ORIGINALE1 = cbind(ORIGINALE,most_similar)
REFERENCE[,i:=1:.N]
ORIGINALE2 = merge(ORIGINALE1,REFERENCE,by='i',all.x=T,all.y=F)

# result:
   i                        DESCRIPTION.x CODICE.x  distance                           DESCRIPTION.y CODICE.y
1: 1 washington miss sarah 430f name strt       NA 0.3333333 sarah brown name street 430f washington  135tg67
2: 2       mr peter 123 rose street 3b LA       NA 0.1055728       peter green 123 rose street 3b LA     aw56
3: 3     4c flower str jenny jane Chicago       NA 0.2000000     jenny jane flower street 4c Chicago 83776250

# now the documents are in a different order than in ORIGINALE2.
# this is caused by merging by i (=REFERENCE document row).
# if order is important, then add these two lines around the merge line:
ORIGINALE1[,ORIGINALE_i:=1:.N]
ORIGINALE2 = merge(...
ORIGINALE2 = ORIGINALE2[order(ORIGINALE_i)]


0 commentaires