#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. Спасибо, Аллан, это все еще намного аккуратнее, чем я мог себе представить. Однако, к сожалению, я только что понял, что в приведенном мной примере отсутствует некоторая дополнительная сложность, а именно, начальная буква последовательности не обязательно является первой; пожалуйста, смотрите Мое обновление.