Дублировать таблицу на основе значений в 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 - опять же, я не очень хорошо с языком; и я не мог найти какие-либо соответствующие решения / руководства.
- R: опробовал несколько идей:
Сначала используйте 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/ как угодно, просто делать это каким-либо образом.
2 ответа
У меня был похожий подход к @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
Решение 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