Удалить часть строки на основе перекрывающихся шаблонов - PullRequest
7 голосов
/ 28 января 2020

У меня есть следующие данные:

dat <- data.frame(x               = c("this is my example text", "and here is my other text example", "my other text is short"),
                  some_other_cols = c(1, 2, 2))

Кроме того, у меня есть следующий вектор шаблонов:

my_patterns <- c("my example", "is my", "my other text")

Что я хочу добиться, это удалить любой текст my_patterns это происходит в dat$x.

Я попробовал решение ниже, но проблема в том, что как только я удаляю первый шаблон из текста (здесь: «мой пример»), мое решение не может чтобы обнаружить появление второго (здесь: «это мой») или третьего паттерна.

Неправильное решение:

library(tidyverse)
my_patterns_c <- str_c(my_patterns, collapse = "|")

dat_new <- dat %>%
  mutate(short_x = str_replace_all(x, pattern = my_patterns_c, replacement = ""))

Я думаю, я мог бы сделать что-то , подобно циклу по всем шаблонам, собирать позиции строк в dat $ x, которые соответствуют моим шаблонам, затем объединять их в диапазон и удалять этот диапазон из текста. Например, я добавляю столбцы к своему dat фрейму данных, например start_pattern_1 и end_pattern_1 и так далее. Таким образом, для первого ряда 1 я получаю 9 (начало) и 18 (конец) для первого шаблона, 6/10 для второго шаблона. Затем мне нужно проверить, перекрывается ли какая-либо позиция end с какой-либо позицией start (здесь начало 9 и конец 10), объединить их в диапазон 6-18 и удалить этот диапазон из текста.

Проблема в том, что у меня потенциально есть много новых начальных / конечных столбцов (может быть несколько сотен шаблонов в моем случае), и если мне нужно попарно сравнить перекрывающиеся диапазоны, мой компьютер, вероятно, будет иметь sh.

. Мне интересно, как я мог бы заставить его работать или как мне лучше всего подойти к этому решению. Может быть (и я надеюсь, что) есть лучшее / более элегантное / простое решение.

Желаемый вывод dat будет:

x                                    some_other_cols    short_x
this is my example text              1                  this text
and here is my other text example    2                  and here example
my other text is short               2                  is short

Цените вашу помощь ! Спасибо.

Ответы [ 2 ]

6 голосов
/ 28 января 2020

Здесь есть два важных момента:

  1. Шаблоны для удаления из строки могут перекрываться
  2. Может быть несколько не перекрывающихся шаблонов для удаления из строки

Приведенное ниже решение пытается решить обе проблемы, используя мои любимые инструменты

library(data.table)
setDT(dat)[, rn := .I] # add row numbers to join on later

library(stringr)
library(magrittr) # piping used to improve readability

pos <- 
  # find start and end positions for each pattern
  lapply(my_patterns, function(pat) str_locate_all(dat$x, pat) %>% 
           lapply(as.data.table) %>% 
           rbindlist(idcol = "rn")) %>% 
  rbindlist() %>% 
  # collapse overlapping positions
  setorder(rn, start, end) %>% 
  .[, grp := cumsum(cummax(shift(end, fill = 0)) < start), by = rn] %>% 
  .[, .(start = min(start), end = max(end)), by = .(rn, grp)]

Теперь pos стало:

    rn grp start end
 1:  1   1     6  18
 2:  2   1    10  25
 3:  3   1     1  13
 4:  5   1     6  10
 5:  5   2    24  28
 6:  6   1     1  13
 7:  6   2    15  27
 8:  7   1     3   7
 9:  8   1     1  10
10:  8   2    12  16
11:  8   3    22  34
12:  9   1     1  10
13:  9   2    19  31
# remove patterns from strings from back to front
dat[, short_x := x]
for (g in rev(seq_len(max(pos$grp)))) {
  # update join 
  dat[pos[grp == g], on = .(rn), short_x := `str_sub<-`(short_x, start, end, value = "")]
}
dat[, rn := NULL][   #remove row number
  , short_x := str_squish(short_x)][]   # remove whitespace 
                                             x some_other_cols                          short_x
1:                     this is my example text               1                        this text
2:           and here is my other text example               2                 and here example
3:                      my other text is short               2                         is short
4:                            yet another text               4                 yet another text
5: this is my text where 'is my' appears twice               5 this text where '' appears twice
6:                 my other text is my example               6                                 
7:                                 This myself               7                           Thself
8:          my example is my not my other text               8                              not
9:             my example is not my other text               9                           is not

Код для свертывания перекрывающихся позиций изменен с этот ответ .

Промежуточный результат

lapply(my_patterns, function(pat) str_locate_all(dat$x, pat) %>% 
           lapply(as.data.table) %>% 
           rbindlist(idcol = "rn"))
[[1]]
   rn start end
1:  1     9  18
2:  6    18  27
3:  8     1  10
4:  9     1  10

[[2]]
   rn start end
1:  1     6  10
2:  2    10  14
3:  5     6  10
4:  5    24  28
5:  6    15  19
6:  7     3   7
7:  8    12  16

[[3]]
   rn start end
1:  2    13  25
2:  3     1  13
3:  6     1  13
4:  8    22  34
5:  9    19  31

показывает, что шаблоны 1 и 2 перекрываются в строке 1, а шаблоны 2 и 3 перекрываются в строке 2. Строки 5, 8 и 9 имеют неперекрывающиеся шаблоны. Строка 7 показывает, что шаблоны извлекаются независимо от границ слов.

РЕДАКТИРОВАТЬ: dplyr версия

ОП имеет упомянутое , которое он / она имеет " успешно избежал data.table до сих пор". Поэтому мне пришлось добавить dplyr версию:

library(dplyr)
library(stringr)

pos <- 
  # find start end end positions for each pattern
  lapply(my_patterns, function(pat) str_locate_all(dat$x, pat) %>% 
           lapply(as_tibble) %>% 
           bind_rows(.id = "rn")) %>% 
  bind_rows() %>% 
  # collapse overlapping positions
  arrange(rn, start, end) %>% 
  group_by(rn) %>% 
  mutate(grp = cumsum(cummax(lag(end, default = 0)) < start)) %>% 
  group_by(rn, grp) %>% 
  summarize(start = min(start), end = max(end))
# remove patterns from strings from back to front
dat <- dat %>%
  mutate(rn = row_number() %>% as.character(),
         short_x = x %>% as.character())
for (g in rev(seq_len(max(pos$grp)))) {
  dat <- dat %>% 
    left_join(pos %>% filter(grp == g), by = "rn") %>% 
    mutate(short_x = ifelse(is.na(grp), short_x, `str_sub<-`(short_x, start, end, value = ""))) %>% 
    select(-grp, -start, -end)
}
# remove row number
dat %>% 
  select(-rn) %>% 
  mutate(short_x = str_squish(short_x))
                                            x some_other_cols                          short_x
1                     this is my example text               1                        this text
2           and here is my other text example               2                 and here example
3                      my other text is short               2                         is short
4                            yet another text               4                 yet another text
5 this is my text where 'is my' appears twice               5 this text where '' appears twice
6                 my other text is my example               6                                 
7                              This is myself               7                        This self
8          my example is my not my other text               8                              not
9             my example is not my other text               9                           is not

Алгоритм по сути тот же. Однако здесь есть две проблемы, где dplyr отличается от data.table:

  • dplyr требует явного приведения от factor до character
  • нет update join доступен в dplyr, поэтому for l oop стал более многословным, чем аналог data.table (возможно, кто-то знает причудливую функцию purrr или map- уменьшить трюк до совершенного sh то же самое?)

РЕДАКТИРОВАТЬ 2

Существуют некоторые исправления ошибок и улучшения вышеуказанных кодов:

  1. Свертывающиеся позиции были исправлены, чтобы работать также для некоторого крайнего случая, который я добавил к dat.
  2. seq() заменено на seq_len().
  3. str_squish() уменьшает количество повторяющихся пробелов внутри строки и удаляет пробелы из начала и конца строки.

Данные

Я добавил несколько вариантов использования для тестирования для неперекрывающихся рисунков и полного удаления, например:

dat <- data.frame(
  x = c(
    "this is my example text",
    "and here is my other text example",
    "my other text is short",
    "yet another text",
    "this is my text where 'is my' appears twice",
    "my other text is my example",
    "This myself",
    "my example is my not my other text",
    "my example is not my other text"
  ),
  some_other_cols = c(1, 2, 2, 4, 5, 6, 7, 8, 9)
)
my_patterns <- c("my example", "is my", "my other text")
6 голосов
/ 28 января 2020

Новая опция с str_locate_all, упомянутая Уве в комментарии под вопросом, которая значительно упрощает код:

library(stringr)
# Create function to remove matching part of text
# First argument is text, second argument is a list of start and length 
remove_matching_parts <- function(text, positions) {
  if (nrow(positions) == 0) return(text)
  ret <- strsplit(text,"")[[1]]
  lapply(1:nrow(positions), function(x) { ret[ positions[x,1]:positions[x,2] ] <<- NA } )
  paste0(ret[!is.na(ret)],separator="",collapse="")
}

# Loop over the data to apply the pattern
# row = length of vector, columns = length of pattern
matches <- lapply(dat$x, function(x) {
  do.call(rbind,str_locate_all(x, my_patterns)) # transform the list output of str_locate in a table of start/end
})

# Avoid growing a vector in a for loop, create it beforehand, it will be the same length as teh vector we work against
dat$result <- vector("character",length(dat$x))
# Loop on each value to remove the matching parts
for (i in 1:length(dat$x)) {
 dat$result[i] <- remove_matching_parts(as.character(dat$x[i]),matches[[i]])
}

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

> gsub("(is )?my (other text|example)?","",dat$x)
[1] "this  text"        "and here  example" " is short" 

Идея состоит в том, чтобы создать шаблон с необязательными частями (? после скобок для группировки.

Итак, мы имеем примерно:

  • (is )? <= необязательный «is», за которым следует пробел </li>
  • my <= буквальный «my», за которым следует пробел </li>
  • (other text|example)? <= дополнительный текст после «мой», «другой текст» или (<code>|) «пример»

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

# Given datas
dat <- data.frame(x               = c("this is my example text", "and here is my other text example", "my other text is short","yet another text"),
                some_other_cols = c(1, 2, 2, 4))

my_patterns <- c("my example", "is my", "my other text")

# Create function to remove matching part of text
# First argument is text, second argument is a list of start and length 
remove_matching_parts <- function(text, positions) {
  ret <- strsplit(text,"")[[1]]
  lapply(positions, function(x) { ifelse(is.na(x),,ret[ x[1]:x[2] ] <<- NA ) } )
  paste0(ret[!is.na(ret)],separator="",collapse="")
}

# Create the matches between a vector and a pattern
# First argument is the pattern to match, second is the vector of charcaters
match_pat_to_vector <- function(pattern,vector) {
  sapply(regexec(pattern,vector), 
         function(x) {
           if(x>-1) { 
             c(start=as.numeric(x), end=as.numeric(x+attr(x,"match.length")) ) # Create a start/end vector from the index and length of the match
           }
         })
}

# Loop over the patterns to create a dataframe of matches
# row = length of vector, columns = length of pattern
matches <- sapply(my_patterns,match_pat_to_vector,vector=dat$x)

# Avoid growing a vector in a for loop, create it beforehand, it will be the same length as teh vector we work against
dat$result <- vector("character",length(dat$x))
# Loop on each value to remove the matching parts
for (i in 1:length(dat$x)) {
 dat$result[i] <- remove_matching_parts(as.character(dat$x[i]),matches[i,])
}

Результат после запуска:

> dat
                                  x some_other_cols           result
1           this is my example text               1        this text
2 and here is my other text example               2 and here example
3            my other text is short               2         is short
4                  yet another text               4 yet another text
...