Дедупликация таблицы на основе значений в 2 столбцах + нечеткое сопоставление

У меня есть файл CSV, экспортированный из Zotero, с метаданными записей моей библиотеки. Я знаю, что он содержит довольно много дубликатов, но избавиться от них не так просто:

  • Не все элементы с похожими названиями на самом деле являются дубликатами, например.

    | Year |            Author             |    Title     |
    +------+-------------------------------+--------------+
    | 2016 | Jones, Erik                   | Book Reviews |
    | 2016 | Hassner, Pierre; Jones, Erik  | Book Reviews |
    | 2010 | Adams, Laura L.; Gagnon, Chip | Book Reviews |
    
  • Не все элементы, которые на самом деле похожи, имеют 100% идентичные строки метаданных, например.

    |    Author     |                     Title                     |
    +---------------+-----------------------------------------------+
    | Tichý, Lukáš; | Can Iran Reduce EU Dependence on Russian Gas? |
    | Tichy, L.;    | "can iran reduce eu dependence onrussian gas" |
    

Это крайний случай (различия обычно не такие большие), но, как видите, предварительная очистка точно не решит эту проблему; поэтому идея состоит в том, чтобы исключить строки, содержащие похожие значения в двух и более столбцах, например, "Автор" и "Заголовок".

Что я пробовал/просматривал до сих пор:

  • OpenRefine - почти не знаком с ним, поэтому ничего толкового придумать и найти не смог.
  • Расширение нечеткого поиска Excel – на самом деле не работает путь мне нужен.
  • Python — опять же, я плохо разбираюсь в языке; и я не смог найти подходящих решений/руководств.
  • Р: опробовал несколько идей:

Сначала используйте agrep в цикле for для столбца «Автор», чтобы получить индексы строк с дубликатами; затем сделайте то же самое для столбца «Заголовок»; а затем сравните векторы и выполните дедупликацию строк, в которых значения совпадают. Излишне говорить, что я не мог двигаться дальше шага 1:

titles <- unlist(corpus$"Title")
for (i in 1:length(titles)){
  Title_dupe_temp <- agrep(titles[i], titles[i+1:length(titles)], 
                           max.distance = 1, ignore.case = TRUE, fixed = FALSE)
  Title_dupes[i] <- paste(i, Title_dupe_temp, sep = " ")
}

В результате получается (почти) полная тарабарщина; плюс я получаю предупреждающие сообщения:

In Title_dupes[i] <- paste(i, Title_dupe_temp, sep = " ") :
  number of items to replace is not a multiple of replacement length

Я также прочитал документацию fuzzywuzzyR, но не найти любые функции, которые могут помочь.

Наконец, я попробовал пакет RecordLinkage. Тем не менее, я не мог пройти мимо основ. Документация довольно обширна и не содержит подробностей во всех аспектах; руководств мало, а те, которые я нашел (например, это) использовать примеры наборов данных с готовыми векторами идентичности, поэтому я не мог понять, как воспроизвести это на моих данных.

Так что на данный момент мне все равно, делать ли это в OpenRefine/R/Py/SQL/что угодно, просто сделать это любым способом.


person yys    schedule 26.02.2019    source источник


Ответы (2)


Решение I: использование цикла и библиотеки stringdist:

library(stringdist)
    zotero<-data.frame(
      Year=c(2016,2016,2010,2010,2010,2010),
      Author=c("Jones, Erik","Hassner, Pierre;","Adams, Laura L.;","Tichý, Lukáš;","Tichý, Lukáš;","Tichy, L.;"),
      Title=c("Book Reviews","Book Reviews","Book Reviews","Can Iran Reduce EU Dependence on Russian Gas?","Can Iran Reduce EU Dependence on Russian Gas?","can iran reduce eu dependence onrussian gas")
    )

    zotero$onestring<-paste0(zotero$Year,zotero$Author,zotero$Title)
    zotero<-zotero[order(zotero[,1],zotero[,2]),]

    atot<-NULL
    for (i in 2:dim(zotero)[1]){
      a<-stringdist(zotero$onestring[i-1],zotero$onestring[i])/(nchar(zotero$onestring[i-1])+nchar(zotero$onestring[i]))
      atot<-rbind(atot,a)
    }

    zotero<-cbind(zotero,threshold=c(1,atot))
    zotero[zotero$threshold>0.15,]

Решение II: может быть быстрее вычислить это с помощью матрицы, чем с помощью цикла: сначала я создаю фрейм данных на основе вашей выборки данных, во-вторых, я удаляю символы, отличные от UTF, в-третьих, я использую библиотеку stringdist для вычисления матрицы расстояний. Вы можете легко преобразовать их в проценты сходства.

zotero<-data.frame(
  Year=c(2016,2016,2010,2010,2010,2010),
  Author=c("Jones, Erik","Hassner, Pierre;","Adams, Laura L.;","Tichý, Lukáš;","Tichý, Lukáš;","Tichy, L.;"),
  Title=c("Book Reviews","Book Reviews","Book Reviews","Can Iran Reduce EU Dependence on Russian Gas?","Can Iran Reduce EU Dependence on Russian Gas?","can iran reduce eu dependence onrussian gas")
)

zotero$onestring<-paste0(zotero$Year,zotero$Author,zotero$Title)

Encoding(zotero$onestring) <- "UTF-8"
zotero$onestring<-iconv(zotero$onestring, "UTF-8", "UTF-8",sub='')

library(stringdist)
stringdistmatrix(zotero$onestring)

Результат:

> stringdistmatrix(zotero$onestring)
   1  2  3  4  5
2 11            
3 13 14         
4 47 45 44      
5 47 45 44  0   
6 47 45 42 13 13
person Nakx    schedule 26.02.2019
comment
Спасибо за ваш ответ! Боюсь, однако, что это не оптимальное решение для меня; У меня десятки тысяч строк данных, так что это автоматизация/скорость › точность. И хотя должно быть возможно выполнить автоматическое слияние на основе stringdist, я все же надеюсь на более простой способ. - person yys; 27.02.2019
comment
Кстати, я нашел одну из ошибок в своем цикле; подгруппировал вектор таким образом, что agrep всегда давал индекс элемента, следующего за совпадающим; подмножество вектора символов, такого как titles[-i] (частично), исправляет это, но все еще слишком проблематично во многих отношениях. - person yys; 27.02.2019
comment
Хорошо, я добавил решение с циклом, дайте мне знать, что вы думаете. Я думаю, что stringdist все еще впереди. Это проще, чем с матрицей. - person Nakx; 28.02.2019
comment
Большое спасибо! К сожалению, я слишком занят другими вещами, чтобы полностью проверить это прямо сейчас, но я сделаю это в ближайшие день или два (и, конечно, оставлю комментарий). - person yys; 28.02.2019
comment
Прошу прощения, что долго не отвечал на песню. Поэтому я хотел бы еще раз поблагодарить вас за подробный пример: он действительно работает очень хорошо и быстро на больших наборах данных. Тем не менее, не могли бы вы объяснить, почему вы предпочитаете преобразовывать в проценты сходства (это то, что делает деление расстояния между строками на (nchar(zotero$onestring[i-1])+nchar(zotero$onestring[i]), верно?). Кажется, что прямое использование расстояния между строками ничем не отличается; так это просто для удобства? - person yys; 05.03.2019
comment
Может быть, я ошибаюсь, но я думал, что более короткие строки всегда будут иметь более короткие расстояния между символами? Вероятно, это проблема только в том случае, если ваш порог действительно высок, поэтому вы можете просто удалить это, если оно работает для вас без него. - person Nakx; 06.03.2019
comment
Это имеет смысл, и есть некоторая разница в количестве дедуплицированных строк, хотя я думал, что это из-за минимального порога, который я установил. проверю внимательнее. - person yys; 06.03.2019

У меня был аналогичный подход к @Nakx, и мне нравится матричное решение. Однако вы также можете попытаться очистить больше, используя gsub и iconv, и использовать sapply для сопоставления (индексация значения наилучшего совпадения, которое не является самим собой..0). Что-то вроде этого:

    > library(RecordLinkage)
> 
> zotero<-data.frame(
+   Year=c(2016,2016,2010,2010,2010,2010),
+   Author=c("Jones, Erik","Hassner, Pierre;","Adams, Laura L.;","Tichý, Lukáš;","Tichý, Lukáš;","Tichy, L.;"),
+   Title=c("Book Reviews","Book Reviews","Book Reviews","Can Iran Reduce EU Dependence on Russian Gas?","Can Iran Reduce EU Dependence on Russian Gas?","can iran reduce eu dependence onrussian gas")
+ )
> 
> # Converting the special characters
> zotero$Author_new <- iconv(zotero$Author, from = '', to = "ASCII//TRANSLIT")
> zotero$Author_new <- tolower(zotero$Author_new)
> zotero$Author_new <- gsub("[[:punct:]]", "", zotero$Author_new)
> 
> # Removing punctuation making it lowercase
> zotero$Title_new <- gsub("[[:punct:]]", "", zotero$Title)
> zotero$Title_new <- tolower(zotero$Title_new)
> 
> # Removing exact duplicates
> dups <- duplicated(zotero[,c("Title_new", "Author_new", "Year")])
> zotero <- zotero[!dups,]
> zotero
  Year           Author                                         Title     Author_new
1 2016      Jones, Erik                                  Book Reviews     jones erik
2 2016 Hassner, Pierre;                                  Book Reviews hassner pierre
3 2010 Adams, Laura L.;                                  Book Reviews  adams laura l
4 2010    Tichý, Lukáš; Can Iran Reduce EU Dependence on Russian Gas?    tichy lukas
6 2010       Tichy, L.;   can iran reduce eu dependence onrussian gas        tichy l
                                     Title_new Title_dist Author_dist
1                                 book reviews          0           9
2                                 book reviews          0           9
3                                 book reviews          0           9
4 can iran reduce eu dependence on russian gas          0           0
6  can iran reduce eu dependence onrussian gas          1           4
>
> # Creating a distance measure for your title, author, and year
> zotero$Title_dist <- sapply(zotero$Title_new, function(x) sort(levenshteinDist(x, zotero$Title_new))[2])
> zotero$Author_dist <- sapply(zotero$Author_new, function(x) sort(levenshteinDist(x, zotero$Author_new))[2])
>
> # Filter here

И оттуда вы можете использовать переменные расстояния для создания критериев и фильтрации. Например, вы можете чувствовать себя комфортно при удалении, если для статьи существует расстояние от автора, равное 2, и расстояние от заголовка, равное 5.

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

> library(dplyr)
> zotero <- zotero %>%
+   group_by(Year) %>%
+   filter(!between(Title_dist, 1, 5) | 
+          !between(Author_dist, 1, 5))
> zotero
# A tibble: 4 x 7
# Groups:   Year [2]
   Year Author       Title                     Author_new   Title_new                   Title_dist Author_dist
  <dbl> <fct>        <fct>                     <chr>        <chr>                            <int>       <int>
1  2016 Jones, Erik  Book Reviews              jones erik   book reviews                         0           9
2  2016 Hassner, Pi~ Book Reviews              hassner pie~ book reviews                         0           9
3  2010 Adams, Laur~ Book Reviews              adams laura~ book reviews                         0           9
4  2010 Tichý, Luká~ Can Iran Reduce EU Depen~ tichy lukas  can iran reduce eu depende~          0           0
person Andrew    schedule 28.02.2019
comment
Извините за задержку с ответом и еще раз спасибо! Что касается предварительного форматирования - здесь вы определенно правы; это то, что я всегда делаю при работе с текстовыми данными, просто пропустил эту часть в своем посте. Что касается расстояния - levenshteinDist кажется более тяжелым, чем stringdist (обработка заголовков в таблице ~ 4500 строк заняла около 10 минут), но работает нормально. Мне также очень нравится идея работать с полями автора и заголовка отдельно. - person yys; 05.03.2019
comment
Совершенно верно, это решение было определенно больше для точности, чем для скорости - я только что увидел ваш первый комментарий к другому решению. Кроме того, если вы проводите метаанализ или какой-то систематический обзор литературы, у Zotero, вероятно, есть встроенный в программное обеспечение способ выявления дубликатов. Я знаю, что EndNote делает. Удачи!!! - person Andrew; 05.03.2019
comment
В Zotero есть встроенный инструмент дедупликации — он довольно удобен и точен, но требует, чтобы пользователь делал все вручную, запись за записью, поэтому в моем случае это не было хорошим решением. - person yys; 06.03.2019