Stringr: извлечение всех совпадений из строк в столбце data.frame. Data.frame и вектор искомых строк очень большие (> 10k) - PullRequest
3 голосов
/ 10 июля 2020

EDIT: У меня есть фрейм данных, в котором столбец 1 имеет идентификатор в некоторых текстах, а столбец 2 имеет сам текст в виде строк. У меня есть набор из нескольких слов, и задача состоит в том, чтобы stringr подсчитать, сколько раз каждое слово встречается в текстах. Слова должны быть фиксированными, а не регулярными. Выделяются две проблемы: (1) Как предоставить вектор, содержащий несколько слов, в виде фиксированного (не регулярного) шаблона? (2) Как добавить результаты во фрейм данных? (3) Как это сделать для очень больших данных?

Предыдущий ответ пользователя @akrun ответил на пункты (1) и (2), но (3) все еще остается проблемой. Вот воспроизводимый пример.

## create a very large data.frame with the text column to be analyzed
doc_number <- c()
doc_text <- c()

for(i in 1:60000){

# generate many random strings mentioning 'proposals'
doc_number[i] <- paste0("doc_",i)
set.seed(i+3)
doc_text[i] <- paste0("This is about proposal ", "(", sample(1000:9999, 1), "/", sample(letters, 1),")",
                      " and about proposal ", "(", sample(1000:9999, 1), "/", sample(letters, 1),")")

}
docs_example_df <- data.frame(doc_number, doc_text)

head(docs_example_df) # resulting df has 'doc_text' column which mentions proposals
> head(docs_example_df)
  doc_number                                                    doc_text
1      doc_1 This is about proposal (6623/k) and about proposal (3866/c)
2      doc_2 This is about proposal (3254/k) and about proposal (2832/u)
3      doc_3 This is about proposal (7964/j) and about proposal (1940/n)
4      doc_4 This is about proposal (8582/g) and about proposal (3753/o)
5      doc_5 This is about proposal (4254/b) and about proposal (5686/l)
6      doc_6 This is about proposal (2588/f) and about proposal (9786/c)


# create a very large vector of 'proposals' I want to extract from doc_text
my_proposals <- c()

for(i in 1:20000){

  set.seed(i+8)
  my_proposals[i] <- paste0("proposal ", "(", sample(1000:9999, 1), "/", sample(letters, 1),")")

}

head(my_proposals) # long list of 'proposals' I wish to locate
> head(my_proposals)
[1] "proposal (2588/f)" "proposal (1490/i)" "proposal (2785/b)" "proposal (5545/z)" "proposal (6988/j)" "proposal (1264/i)"

В предыдущем ответе @akrun (см. Ниже) рекомендовалось несколько решений, которые работали для небольшого data.frame. Но в таких> 20k объектов функции либо заедают, либо выдают ошибку, например:

Problem with mutate() input matches. x Incorrectly nested parentheses in regexp pattern. (U_REGEX_MISMATCHED_PAREN)

Итак, вкратце, как применить очень длинный список векторов к очень длинному data.frame и сохранить извлеченный совпадает с чем-то вроде списка столбцов в data.frame? Всем спасибо

1 Ответ

3 голосов
/ 10 июля 2020

Мы могли бы paste их вместе и обернуть в regex вместо fixed. В dplyr 1.0.0 добавлено несколько функций, одна из которых across

library(dplyr) #1.0.0
library(stringr)
test_df %>%
  mutate(matches = str_extract_all(text,
                pattern = regex(str_c(keywords, collapse = "|"))))

Если нам нужен окончательный ожидаемый результат, после создания столбца list в matches, unnest, чтобы развернуть строки, получите count и измените его на «широкий» формат с помощью pivot_wider

library(tidyr)
test_df %>%
   mutate(matches = str_extract_all(test_df$text, pattern = regex(str_c(keywords, collapse = "|")))) %>% 
   unnest(c(matches)) %>% 
   count(across(doc_id:matches)) %>% 
   pivot_wider(names_from = matches, values_from = n, values_fill = list(n = 0))
# A tibble: 4 x 6
#  doc_id text                                           water alcohol gasoline   h2o
#  <chr>  <chr>                                          <int>   <int>    <int> <int>
#1 doc1   This text refers to water                          1       0        0     0
#2 doc2   This text refers to water and alcohol              1       1        0     0
#3 doc4   This text refers to gasoline and more gasoline     0       0        2     0
#4 doc5   This text refers to (h2o)                          0       0        0     1

Если у нас есть dplyr <1.0.0, вместо <code>across просто укажите имена столбцов в count

... %>%
count(doc_id, text, matches)
... %>%

Или преобразуйте имена столбцов в символы и оценить

 ... %>%
   count(!!! rlang::syms(names(.)))
... %>%

 

В указанном выше методе 'doc3' удаляется, так как совпадений не было. Если нам нужно его оставить, укажите keep_empty = TRUE в unnest

test_df %>%
    mutate(matches = str_extract_all(test_df$text, 
          pattern = regex(str_c(keywords, collapse = "|")))) %>% 
    unnest(c(matches), keep_empty = TRUE) %>% 
    count(across(doc_id:matches)) %>% 
    mutate(n = replace(n, is.na(matches), 0)) %>% 
    pivot_wider(names_from = matches, values_from = n, values_fill = list(n = 0)) %>%
    select(-`NA`)
# A tibble: 5 x 6
#  doc_id text                                           water alcohol gasoline   h2o
#  <chr>  <chr>                                          <dbl>   <dbl>    <dbl> <dbl>
#1 doc1   This text refers to water                          1       0        0     0
#2 doc2   This text refers to water and alcohol              1       1        0     0
#3 doc3   This text refers to alcoolh                        0       0        0     0
#4 doc4   This text refers to gasoline and more gasoline     0       0        2     0
#5 doc5   This text refers to (h2o)                          0       0        0     1

В дополнение к описанному выше методу более простой вариант - использовать str_count

library(purrr)
map_dfc(set_names(keywords, keywords), ~ 
      str_count(test_df$text, .x)) %>% 
   bind_cols(test_df, .)
#  doc_id                                           text water alcohol gasoline (h2o)
#1   doc1                      This text refers to water     1       0        0     0
#2   doc2          This text refers to water and alcohol     1       1        0     0
#3   doc3                    This text refers to alcoolh     0       0        0     0
#4   doc4 This text refers to gasoline and more gasoline     0       0        2     0
#5   doc5                      This text refers to (h2o)     0       0        0     1

Или используя base R

test_df[keywords] <-  lapply(keywords, function(x) 
        lengths(regmatches(test_df$text, gregexpr(x, test_df$text))))

Хотя str_extract векторизован для pattern, длина pattern будет такой же, как длина столбца, и будет выполняться соответствующее извлечение

...