R: Как классифицировать строки на основе набора регулярных выражений - PullRequest
0 голосов
/ 27 мая 2019

У меня есть вектор текстовых строк, которые описывают источники изображений в книге, но не в полностью согласованном формате - они взяты из источника LaTeX и записаны в текстовый файл.

Мне нужно обработать их и собрать информацию об атрибуции источника и статус разрешения. Пример строки выглядит так:

strings <- '
GBD 2016 Alcohol Collaborators (2018)
\citet {DeBoldFriedman:2015}, permission required
Author graphic, using various public domain images
\url {http://www.histogames.com/HTML/inventaire/periodes-historiques/prehistoire.php}
\url {https://commons.wikimedia.org/wiki/File:Egyptmotionseries.jpg}
\url {http://commons.wikimedia.org}, public domain.
\citet {Plot:1685}, author image collection
Author image collection
 From \citet {Priestley:1765}, author image collection
 Author image collection
 Courtesy Library of Congress
\citet {Langren:1644}, image courtesy of the Koninklijke Bibliotheek van Belgi\"e 
Public domain
Author graphic
Author graphic
Author graphic
Author image collection
Wikipedia, https://commons.wikimedia.org/wiki/File:Tablas\_alfonsies.jpg, public domain
'

Одной из основных задач является создание переменной status, указывающей статус разрешения, путем нахождения соответствует простым регулярным выражениям в строках. Категории могут быть определены следующие шаблоны, все из которых должны соответствовать тексту строк без отношения к делу.

AUpat <- "author (image|graphic|collection)"
PDpat <- "public domain"
REQpat <- "permission required"
LIBpat <- "courtesy|by permission"

# or as a list:
pats <- list(
  'AU' = "author (image|graphic|collection)",
  'PD' = "public domain",
  'REQ'= "permission required",
  'LIB'= "courtesy|by permission"
)

Псевдокод, чтобы делать то, что я хочу (не работает):

status <- rep("", length(strings))
for(i in seq_along(strings)) {
  if      (grep(AUpat, strings[i],  ignore.case=TRUE)) status[i] <- "AU"
  else if (grep(PDpat, strings[i],  ignore.case=TRUE)) status[i] <- "PD"
  else if (grep(REQpat, strings[i], ignore.case=TRUE)) status[i] <- "REQ"
  else if (grep(LIBpat, strings[i], ignore.case=TRUE)) status[i] <- "LIB"
}

Это безобразно, если не считать того факта, что оно выдает ошибку:

Error in if (grep(AUpat, strings[i])) status[i] <- "AU" else if (grep(PDpat,  : 
  argument is of length zero

Есть ли лучший способ попробовать выполнить эту задачу, возможно, с помощью stringr или других tidyverse инструментов?

1 Ответ

1 голос
/ 27 мая 2019

Из ваших шаблонов видно, что строки могут фактически принадлежать нулю или более, а не только одному.Если это так, то первым шагом было бы определить, какими категориями (0 или более) обладает строка.

Мне не хватает вашего strings для реального тестирования, поэтому я сгенерирую несколькопростые данные и шаблоны:

strings <- c("something", "something else", "nothing", "nothing here either",
             "something or nothing", "interesting",
             "something interesting", "nothing interesting")

pats <- c(p1 = "something", p2 = "nothing", p3 = "interesting")

(Шаблоны называются для удобства, когда сами шаблоны становятся громоздкими в качестве меток.) ​​Начните с создания logical матрицы:

m <- sapply(pats, grepl, strings, ignore.case = TRUE)
m
#         p1    p2    p3
# [1,]  TRUE FALSE FALSE
# [2,]  TRUE FALSE FALSE
# [3,] FALSE  TRUE FALSE
# [4,] FALSE  TRUE FALSE
# [5,]  TRUE  TRUE FALSE
# [6,] FALSE FALSE  TRUE
# [7,]  TRUE FALSE  TRUE
# [8,] FALSE  TRUE  TRUE

Если вы должны применить только одну категорию к строке, то, предполагая, что шаблоны расположены в порядке приоритета, вы можете сделать:

apply(m, 1, function(a) head(c(which(a), NA), n = 1))
# [1] 1 1 2 2 1 3 1 2

names(pats)[ apply(m, 1, function(a) head(c(which(a), 0), n = 1)) ]
# [1] "p1" "p1" "p2" "p2" "p1" "p3" "p1" "p2"

Примечание: это создаст NA s, когда строки не содержат ни одного из шаблонов, демонстрируя:

m[8,] <- FALSE
apply(m, 1, function(a) head(c(which(a), NA), n = 1))
# [1]  1  1  2  2  1  3  1 NA

Вы должны остерегаться этого в своих предположениях.(Сейчас я сохраню m с этим изменением.)

Если вам нужно сохранить категории для каждой строки, то как сделать это зависит от того, как вы собираетесь хранитьуказанные классификации.Для этого можно начать:

str(m2 <- apply(m, 1, function(r) names(which(r))))
# List of 8
#  $ : chr "p1"
#  $ : chr "p1"
#  $ : chr "p2"
#  $ : chr "p2"
#  $ : chr [1:2] "p1" "p2"
#  $ : chr "p3"
#  $ : chr [1:2] "p1" "p3"
#  $ : chr(0) 

Это одно прямое хранилище: каждый элемент list соответствует strings и содержит ноль или более имен шаблонов.Если это для человеческого глаза, вы можете преобразовать это в набор с разделителями-запятыми:

sapply(m2, paste, collapse = ",")
# [1] "p1"    "p1"    "p2"    "p2"    "p1,p2" "p3"    "p1,p3" ""     

или для более формального хранения базы данных, как насчет

stringids <- seq_len(length(strings)) # perhaps you have something better?
d <- data.frame(
  stringid = rep(stringids, times = lengths(m2)),
  ptnmatch = unlist(m2),
  stringsAsFactors = FALSE
)
d
#   stringid ptnmatch
# 1        1       p1
# 2        2       p1
# 3        3       p2
# 4        4       p2
# 5        5       p1
# 6        5       p2
# 7        6       p3
# 8        7       p1
# 9        7       p3

Обратите вниманиекак строка 8 (которую я изменил, чтобы не было категорий) не включена, здесь это просто дизайн.Когда-то всегда можно заставить его вернуться с помощью:

misses <- setdiff(stringids, unique(d$stringid))
misses
# [1] 8

d <- rbind(d, data.frame(stringid = misses, ptnmatch = rep(NA, length(misses))),
           stringsAsFactors = FALSE)
d
#    stringid ptnmatch
# 1         1       p1
# 2         2       p1
# 3         3       p2
# 4         4       p2
# 5         5       p1
# 6         5       p2
# 7         6       p3
# 8         7       p1
# 9         7       p3
# 10        8     <NA>

...