Идентифицировать группы / последовательности повторяющихся значений

#r

#r

Вопрос:

С input помощью фрейма данных, как показано ниже:

    ID val
1   1   c
2   1   c
3   1   d
4   1   d
5   1   c
6   2   b
7   2   c
8   2   d
9   2   b
10  2   c
11  2   b
12  3   a
13  3   b
14  3   a
15  3   b
16  3   a
17  4   b
18  4   a
19  4   b
20  5   f
21  5   f
  

Я стремлюсь к следующему output :

    ID val idx
1   1   c   1
2   1   d   1
3   1   c   1
4   2   b   1
5   2   c   1
6   2   d   1
7   2   b   1
8   2   b   2
9   2   c   2
10  2   b   2
11  3   a   1
12  3   b   1
13  3   a   1
14  3   a   2
15  3   b   2
16  3   a   2
  

Логика заключается в следующем:

  • Столбец val состоит из букв, которые в основном являются порядковыми значениями (т. Е. Они равны as.integer(factor(val)) ).
  • Для каждого val я хотел бы определить последовательность, в которой значение превращается в более высокое (например c -> d , но оно не обязательно полностью последовательное, оно также может быть c -> e ), а затем возвращается к исходному значению (например c -> d -> c ), не обращая внимания на любые дубликаты между ними.
  • Это объясняет, почему ID 4-5 удаляются из конечного вывода.
  • В некоторых случаях начальное значение превращается в более высокое, а затем повторяет этот цикл, например, для ID 2-3. В этих случаях я хотел бы восстановить все последовательности как отдельные с разными индексами (как показано в idx столбце вывода). Это обязательно скопировало бы некоторые строки, которые находятся в начале таких циклов.

Есть идеи о кратких и быстрых способах приблизиться к этому?

Редактировать

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

Это не так. Даже с приведенным input ниже, который добавляет дополнительные значения в начале и в конце ID 1 и в конце ID 2, я бы ожидал того же output , что и выше:

    ID val
1   1   b
2   1   c
3   1   c
4   1   d
5   1   d
6   1   c
7   1   e
8   2   b
9   2   c
10  2   d
11  2   b
12  2   c
13  2   b
14  2   f
15  3   a
16  3   b
17  3   a
18  3   b
19  3   a
20  4   b
21  4   a
22  4   b
23  5   f
24  5   f
  

Буква не является начальной частью последовательности, просто появляясь первой; это последующее дублирование, которое задним числом устанавливает ее как начало. То же самое касается последней буквы: она не обязательно является частью последовательности, если она есть.

Ответ №1:

Я придумал этот подход, который, возможно, является не самым сжатым ответом code-golf-ey, но я думаю, что он несколько удобочитаем и разбивает вашу проблему на более мелкие этапы.

Хитрость заключается в том, чтобы просто идентифицировать те строки, которые являются как последней строкой в последовательности, так и первой строкой следующей последовательности.
Поскольку их необходимо дублировать, я присваиваю им два IDx значения. Затем мы можем легко unnest IDx разделить столбец и получить две строки.
На втором шаге мы в основном просто очищаем данные по указанным вами правилам.


Шаг 1:
Определите для каждой строки соответствующий IDx:
// ОБНОВЛЕНИЕ: отфильтровывает любое значение, которое не появляется повторно (если оно появляется, оно считается последовательностью), и любое значение, которое следует за блоком допустимых последовательностей

 library(dplyr)
library(purrr)
library(tidyr)

df_idx <- df %>%
  group_by(ID) %>%
  add_count(val) %>%
  filter(cumsum(n > 1) > 0) %>% # removes leading vals
  filter(rev(cumsum(rev(val == first(val))) > 0)) %>% # removes trailing vals
  select(-n) %>%
  mutate(
    is_sequence_first = case_when(
      row_number() == 1 ~ T,
      lag(val) != val amp; val == first(val) amp; lead(val) != val ~ T,
      T ~ F
    ),
    is_sequence_last = case_when(
      row_number() == n() ~ T,
      val == first(val) amp; lead(is_sequence_first) ~ T,
      lag(val) != first(val) amp; is_sequence_first ~ T,
      T ~ F
    ),
    IDx = case_when(
      is_sequence_first amp; is_sequence_last ~ map(cumsum(is_sequence_first), ~c(.x-1,.x)),
      T ~ as.list(cumsum(is_sequence_first))
    )
  ) %>%
  unnest(IDx) %>%
  ungroup()
  

Шаг 2:
Отфильтруйте дубликаты в блоке, где есть только один IDx, и исключите «недопустимые» (убывающие) последовательности:

 df_final <- df_idx %>%
  group_by(ID) %>%
  filter(
    case_when(
      max(IDx) == 1 amp; row_number() == 1 ~ T,
      max(IDx) == 1 amp; val != lag(val) ~ T,
      max(IDx) > 1 ~ T,
      T ~ F
    )
  ) %>%
  group_by(ID, IDx) %>%
  filter(
    any(val > first(val))
  ) %>%
  ungroup() %>%
  select(-is_sequence_first, -is_sequence_last)
  

Это дает результат, который вы задали в своем вопросе.

Комментарии:

1. Любое решение, которое работает здесь, получает мое одобрение. Это было непросто! Вы также используете tidyr и purrr здесь, что стоит упомянуть

2. верно, я думаю, я имел в виду написать tidyverse -подход 🙄

3. Правда, это вполне читаемо — спасибо за усилия! К сожалению, теперь я понял, что моему примеру не хватало некоторой сложности, а именно, первая буква не обязательно является начальной частью последовательности, и аналогично для последней, она не обязательно является частью последовательности, если есть цикл. Пожалуйста, посмотрите мое обновление.

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

5. Спасибо, это должно сработать. Я действительно надеялся на некоторые быстрые подходы, но, похоже, нет никакого способа отобразить все индексы того, где встречаются значения и т.д. Я оставлю вопрос открытым на сегодня, и если нет других более кратких / быстрых ответов, я приму ответ.

Ответ №2:

Это сложно. Получение val столбца на самом деле довольно просто. Сложно сопоставить идентификационные номера.

Следующее не является ни быстрым, ни красивым, но оно работает. Алгоритм, который вы описываете, кажется слишком сложным, чтобы иметь четкое решение, но я могу ошибаться…

 IDs <- split(as.integer(factor(input$val)), input$ID)

vals <- unlist(lapply(IDs, function(x) {
  y <- which(x == x[1])
  a <- lapply(seq_along(y)[-1], function(z) {
    z <- x[seq(y[z-1], y[z])]
    if(length(z) == 2 | any(z < z[1])) return(NULL)
    z <- levels(factor(input$val))[z[c(which(diff(z) != 0), length(z))]]
    names(z) <- rep(names(IDs)[sapply(IDs, identical, x)], length(z))
    z
    })
  setNames(a, seq_along(a))
  }))

df2 <- data.frame(ID = as.numeric(gsub(".*\.(\d )$", "\1", names(vals))),
                  val = vals, row.names = seq_along(vals),
                  IDx = as.numeric(gsub("^.*\.(\d )\..*$", "\1", names(vals))))

output <- `rownames<-`(do.call(rbind, lapply(split(df2, df2$ID), 
                      function(x) within(x, IDx <- as.numeric(factor(IDx))))),
             seq(nrow(df2)))
  

Давая нам:

 output
#>    ID val IDx
#> 1   1   c   1
#> 2   1   d   1
#> 3   1   c   1
#> 4   2   b   1
#> 5   2   c   1
#> 6   2   d   1
#> 7   2   b   1
#> 8   2   b   2
#> 9   2   c   2
#> 10  2   b   2
#> 11  3   a   1
#> 12  3   b   1
#> 13  3   a   1
#> 14  3   a   2
#> 15  3   b   2
#> 16  3   a   2
  

Данные

 input <- structure(list(ID = c(1L, 1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 2L, 
2L, 3L, 3L, 3L, 3L, 3L, 4L, 4L, 4L, 5L, 5L), val = c("c", "c", 
"d", "d", "c", "b", "c", "d", "b", "c", "b", "a", "b", "a", "b", 
"a", "b", "a", "b", "f", "f")), class = "data.frame", row.names = c("1", 
"2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", 
"14", "15", "16", "17", "18", "19", "20", "21"))
  

Создано 2020-10-09 пакетом reprex (версия 0.3.0)

Комментарии:

1. Спасибо, Аллан, это все еще намного аккуратнее, чем я мог себе представить. Однако, к сожалению, я только что понял, что в приведенном мной примере отсутствует некоторая дополнительная сложность, а именно, начальная буква последовательности не обязательно является первой; пожалуйста, смотрите Мое обновление.