Поиск образца в матрице в R

У меня есть 8 x n матрица, например

set.seed(12345)
m <- matrix(sample(1:50, 800, replace=T), ncol=8)
head(m)

     [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8]
[1,]   37   15   30    3    4   11   35   31
[2,]   44   31   45   30   24   39    1   18
[3,]   39   49    7   36   14   43   26   24
[4,]   45   31   26   33   12   47   37   15
[5,]   23   27   34   29   30   34   17    4
[6,]    9   46   39   34    8   43   42   37

Я хотел бы найти определенный шаблон в матрице, например, я хотел бы знать, где я могу найти 37, а затем в следующей строке 10 и 29, а строка после 42

Это происходит, например, в строках 57:59 указанной выше матрицы

m[57:59,]
     [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8]
[1,]  *37   35    1   30   47    9   12   39
[2,]    5   22  *10  *29   13    5   17   36
[3,]   22   43    6    2   27   35  *42   50

A (возможно, неэффективное) решение состоит в том, чтобы получить все строки, содержащие 37 с

sapply(1:nrow(m), function(x){37 %in% m[x,]})

И затем используйте несколько циклов для проверки других условий.

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

ИЗМЕНИТЬ: ответить на различные комментарии

  • Мне нужно найти EXACT-шаблон
  • Порядок в той же строке не имеет значения (если в каждой строке можно упорядочить более простые значения)
  • Строки должны быть смежными.
  • Я хочу получить (начальную) позицию всего возвращенного шаблона (т.е. если шаблон присутствует несколько раз в матрице, я хочу несколько возвращаемых значений).
  • Пользователь вводит шаблон через графический интерфейс, мне еще предстоит решить, как это сделать. Например, для поиска вышеуказанного шаблона он может написать что-то вроде

37;10,29;42

Где ; представляет новую строку, а , разделяет значения в одной строке. Аналогично мы можем искать

50,51;;75;80,81

Значение 50 и 51 в строке n, 75 в строке n + 2 и 80 и 81 в строке n + 3

Ответы

Ответ 1

Вот обобщенная функция:

PatternMatcher <- function(data, pattern, idx = NULL) {
  p <- unlist(pattern[1])
  if(is.null(idx)){
    p <- unlist(pattern[length(pattern)])
    PatternMatcher(data, rev(pattern)[-1], 
                   idx = Filter(function(n) all(p %in% intersect(data[n, ], p)),
                                1:nrow(data)))
  } else if(length(pattern) > 1) {
    PatternMatcher(data, pattern[-1], 
                   idx = Filter(function(n) all(p %in% intersect(data[n, ], p)), 
                                idx - 1))
  } else
    Filter(function(n) all(p %in% intersect(data[n, ], p)), idx - 1)
}

Это рекурсивная функция, которая уменьшает pattern на каждой итерации и проверяет только строки, которые идут сразу после тех, которые указаны в предыдущей итерации. Структура списка позволяет передавать шаблон удобным способом:

PatternMatcher(m, list(37, list(10, 29), 42))
# [1] 57
PatternMatcher(m, list(list(45, 24, 1), 7, list(45, 31), 4))
# [1] 2
PatternMatcher(m, list(1,3))
# [1] 47 48 93

Изменить: Идея функции выше кажется прекрасной: проверьте все строки для вектора pattern[[1]] и получите индексы r1, затем проверьте строки r1+1 для pattern[[2]] и получите r2 и т.д. Но на первом этапе при переходе через все строки требуется очень много времени. Конечно, каждый шаг занимает много времени, например, m <- matrix(sample(1:10, 800, replace=T), ncol=8), т.е. когда изменений индексов r1, r2 нет... Итак, вот еще один подход, здесь PatternMatcher выглядит очень похоже, но есть еще одна функция matchRow для нахождения строки, которые имеют все элементы vector.

matchRow <- function(data, vector, idx = NULL){
  if(is.null(idx)){
    matchRow(data, vector[-1], 
             as.numeric(unique(rownames(which(data == vector[1], arr.ind = TRUE)))))
  } else if(length(vector) > 0) {
    matchRow(data, vector[-1], 
             as.numeric(unique(rownames(which(data[idx, , drop = FALSE] == vector[1], arr.ind = TRUE)))))
  } else idx
}
PatternMatcher <- function(data, pattern, idx = NULL) {
  p <- pattern[[1]]
  if(is.null(idx)){
    rownames(data) <- 1:nrow(data)
    p <- pattern[[length(pattern)]]
    PatternMatcher(data, rev(pattern)[-1], idx = matchRow(data, p))
  } else if(length(pattern) > 1) {
    PatternMatcher(data, pattern[-1], idx = matchRow(data, p, idx - 1))
  } else
    matchRow(data, p, idx - 1)
}

Сравнение с предыдущей функцией:

library(rbenchmark)
bigM <- matrix(sample(1:50, 800000, replace=T), ncol=8)
benchmark(PatternMatcher(bigM, list(37, c(10, 29), 42)), 
          PatternMatcher(bigM, list(1, 3)), 
          OldPatternMatcher(bigM, list(37, list(10, 29), 42)), 
          OldPatternMatcher(bigM, list(1, 3)), 
          replications = 10,
          columns = c("test", "elapsed"))
#                                                  test elapsed
# 4                 OldPatternMatcher(bigM, list(1, 3))   61.14
# 3 OldPatternMatcher(bigM, list(37, list(10, 29), 42))   63.28
# 2                    PatternMatcher(bigM, list(1, 3))    1.58
# 1       PatternMatcher(bigM, list(37, c(10, 29), 42))    2.02

verybigM1 <- matrix(sample(1:40, 8000000, replace=T), ncol=20)
verybigM2 <- matrix(sample(1:140, 8000000, replace=T), ncol=20)
benchmark(PatternMatcher(verybigM1, list(37, c(10, 29), 42)), 
          PatternMatcher(verybigM2, list(37, c(10, 29), 42)), 
          find.combo(verybigM1, convert.gui.input("37;10,29;42")),
          find.combo(verybigM2, convert.gui.input("37;10,29;42")),          
          replications = 20,
          columns = c("test", "elapsed"))
#                                                      test elapsed
# 3 find.combo(verybigM1, convert.gui.input("37;10,29;42"))   17.55
# 4 find.combo(verybigM2, convert.gui.input("37;10,29;42"))   18.72
# 1      PatternMatcher(verybigM1, list(37, c(10, 29), 42))   15.84
# 2      PatternMatcher(verybigM2, list(37, c(10, 29), 42))   19.62

Также теперь аргумент pattern должен быть как list(37, c(10, 29), 42) вместо list(37, list(10, 29), 42). И наконец:

fastPattern <- function(data, pattern)
  PatternMatcher(data, lapply(strsplit(pattern, ";")[[1]], 
                    function(i) as.numeric(unlist(strsplit(i, split = ",")))))
fastPattern(m, "37;10,29;42")
# [1] 57
fastPattern(m, "37;;42")
# [1] 57  4
fastPattern(m, "37;;;42")
# [1] 33 56 77

Ответ 2

Это легко читается и, как мы надеемся, достаточно обобщающе для вас:

has.37 <- rowSums(m == 37) > 0
has.10 <- rowSums(m == 10) > 0
has.29 <- rowSums(m == 29) > 0
has.42 <- rowSums(m == 42) > 0

lag <- function(x, lag) c(tail(x, -lag), c(rep(FALSE, lag)))

which(has.37 & lag(has.10, 1) & lag(has.29, 1) & lag(has.42, 2))
# [1] 57

Изменить: вот обобщение, которое может использовать положительные и отрицательные задержки:

find.combo <- function(m, pattern.df) {

   lag <- function(v, i) {
      if (i == 0) v else
      if (i > 0)  c(tail(v, -i), c(rep(FALSE, i))) else
      c(rep(FALSE, -i), head(v, i))
   }

   find.one <- function(x, i) lag(rowSums(m == x) > 0, i)
   matches  <- mapply(find.one, pattern.df$value, pattern.df$lag)
   which(rowSums(matches) == ncol(matches))

}

Протестировано здесь:

pattern.df <- data.frame(value = c(40, 37, 10, 29, 42),
                         lag   = c(-1,  0,  1,  1,  2))

find.combo(m, pattern.df)
# [1] 57

Edit2: после редактирования OP, относящегося к входу GUI, вот функция, которая преобразует вход GUI в функцию pattern.df my find.combo, ожидает:

convert.gui.input <- function(string) {
   rows   <- strsplit(string, ";")[[1]]
   values <- strsplit(rows,   ",")
   data.frame(value = as.numeric(unlist(values)),
              lag = rep(seq_along(values), sapply(values, length)) - 1)
}

Протестировано здесь:

find.combo(m, convert.gui.input("37;10,29;42"))
# [1] 57

Ответ 3

Поскольку у вас есть целое число, вы можете преобразовать свою матрицу в строку и использовать регулярное выражение

ss <- paste(apply(m,1,function(x) paste(x,collapse='-')),collapse=' ')
## some funny regular expression
pattern <- '[^ \t]+[ \t]{1}[^ \t]+10[^ \t]+29[^ \t]+[ \t]{1}[^ \t]+42'
regmatches(ss,regexpr(pattern ,text=ss))
[1] "37-35-1-30-47-9-12-39 5-22-10-29-13-5-17-36 22-43-6-2-27-35-42"

 regexpr(pattern ,text=ss)
[1] 1279
attr(,"match.length")
[1] 62
attr(,"useBytes")
[1] TRUE

Чтобы увидеть это в действии, посмотрите this.

Редактировать Динамически конструировать шаблон

searchep <- '37;10,29;42'       #string given by the user
str1 <- '[^ \t]+[ \t]{1}[^ \t]+' 
str2 <- '[^ \t]'
hh <- gsub(';',str1,searchep)
pattern <- gsub(',',str2,hh)
pattern
[1] "37[^ \t]+[ \t]{1}[^ \t]+10[^ \t]29[^ \t]+[ \t]{1}[^ \t]+42"

test for searchep <- '37;10,29;;40'  ## we skip a line here 

pattern
[1] "37[^ \t]+[ \t]{1}[^ \t]+10[^ \t]29[^ \t]+[ \t]{1}[^ \t]+[^ \t]+[ \t]{1}[^ \t]+40"
regmatches(ss,regexpr(pattern ,text=ss))
"37-35-1-30-47-9-12-39 5-22-10-29-13-5-17-36 22-43-6-2-27-35-42-50 12-31-24-40"

Редактирование результатов теста

matrix.pattern <- function(searchep='37;10,29;42' ){
 str1 <- '[^ \t]+[ \t]{1}[^ \t]+' 
 str2 <- '[^ \t]+'
 hh <- gsub(';',str1,searchep)
 pattern <- gsub(',',str2,hh)
 res <- regmatches(ss,regexpr(pattern ,text=ss))
}

system.time({ss <- paste(apply(bigM,1,function(x) paste(x,collapse='-')),collapse=' ')
             matrix.pattern('37;10,29;42')})
   user  system elapsed 
   2.36    0.01    2.40 

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

system.time(matrix.pattern('37;10,29;42'))
   user  system elapsed 
   0.71    0.02    0.72 

Ответ 4

Может быть, это поможет кому-то, но что касается ввода, я думал о следующем:

PatternMatcher <- function(data, ...) {
  Selecting procedure here.
}

PatternMatcher(m, c(1, 37, 2, 10, 2, 29, 4, 42))

Вторая часть, поданная в функцию, состоит из строки, в которой она должна начинаться, после чего следует значение, а затем вторая строка и второе значение. Вы также можете сказать, например, 8-я строка после начальной строки со значением 50.

Вы можете даже расширить это, чтобы задать конкретные координаты X, Y для каждого значения (так что 3 элемента переданы функции за значение).

Ответ 5

Edit: Теперь я добавил более обобщенную функцию:

Здесь одно решение, которое дает все возможные комбинации: я получаю все позиции всех четырех чисел, затем использую expand.grid для получения всех комбинаций позиций, а затем filter the meaningless, проверяя, равна ли каждая строка матрицы равным соответствующая строка сортированной матрицы.

set.seed(12345)
m <- matrix(sample(1:50, 800, replace=T), ncol=8)
head(m)
get_grid <- function(in_mat, vec_num) {
    v.idx <- sapply(vec_num, function(idx) {
        which(apply(in_mat, 1, function(x) any(x == idx)))
    })
    out <- as.matrix(expand.grid(v.idx))
    colnames(out) <- NULL
    out
}

out <- get_grid(m, c(37, 10, 29, 42))
out.s <- t(apply(out, 1, sort))

idx <- rowSums(out == out.s)
out.f <- out[idx==4, ]

> dim(out.f)
[1] 2946    4

> head(out.f)
     [,1] [,2] [,3] [,4]
[1,]    1   22   28   36
[2,]    4   22   28   36
[3,]    6   22   28   36
[4,]    9   22   28   36
[5,]   11   22   28   36
[6,]   13   22   28   36

Это индексы строк вхождения чисел в этом порядке (37, 10, 29, 42).

Из этого вы можете проверить любую комбинацию, которую вы хотите. Например, запрошенную комбинацию можно выполнить с помощью:

cont.idx <- apply(out.f, 1, function(x) x[1] == x[2]-1 & x[2] == x[4]-1)
> out.f[cont.idx,]
[1] 57 58 58 59

Ответ 6

Здесь один из способов: sapply:

which(sapply(seq(nrow(m)-2),
             function(x)
               isTRUE(37 %in% m[x,] & 
                      which(10 == m[x+1,]) < which(29 == m[x+1,]) & 
                      42 %in% m[x+2,])))

Результат содержит все номера строк, в которых начинается последовательность:

[1] 57