У меня есть фрейм данных, который идентифицирует набор значений с идентификатором:
library(data.table)
dt <- data.table(
id = rep(c("a", "b", "c"), each = 2),
value1 = c(1, 1, 1, 2, 1, 1),
value2 = c(0, 3, 0, 3, 0, 3)
)
dt
#> id value1 value2
#> 1: a 1 0
#> 2: a 1 3
#> 3: b 1 0
#> 4: b 2 3
#> 5: c 1 0
#> 6: c 1 3
Как видите, идентификаторы a
и c
идентифицируют один и тот же набор значений. Итак, я хочу создать «идентификатор шаблона», который идентифицирует набор значений, связанных с идентификаторами a
и c
(примечания: идентификатор может идентифицировать более двух строк, я просто ограничил их двумя строками здесь для простоты) .
Мне удалось найти решение, используя вложенные таблицы данных и match()
:
dt <- dt[, .(data = list(.SD)), by = id]
unique_groups <- unique(dt$data)
dt[, pattern_id := match(data, unique_groups)]
dt[, data := NULL]
dt
#> id pattern_id
#> 1: a 1
#> 2: b 2
#> 3: c 1
Это делает трюк, но это не так быстро, как хотелось бы. match()
документация достаточно ясна в отношении эффективности работы со списками:
Matching for lists is potentially very slow and best avoided except in simple cases.
Как вы можете видеть, мне не нужны фактические данные шаблона в моем конечном результате, только таблица, которая связывает идентификаторы с идентификаторами шаблона. Я чувствую, что вложение данных, использование их для сопоставления, а затем удаление после этого немного расточительно, но не уверен, что есть лучший способ. Я думал о чем-то, что преобразовывало бы каждый фрейм данных в строку или, что еще лучше, о чем-то, что вообще избегало бы вложенности, но я не мог придумать ничего лучше, чем то, что у меня есть сейчас.
Я создал больший набор данных, чтобы поэкспериментировать и протестировать различные решения:
set.seed(0)
size <- 1000000
dt <- data.table(
id = rep(1:(size / 2), each = 2),
value1 = sample(1:10, size, replace = TRUE),
value2 = sample(1:10, size, replace = TRUE)
)
Каждый идентификатор всегда идет двойками, двумя a, двумя b, двумя c и т. д.?
В вашем игрушечном примере, если поменять местами строки 5 и 6, вы бы по-прежнему считали (a) и (c) эквивалентными? Я сделал это в своем решении, но вижу, что ваш подход не считает эквивалентным.
@zx8754 не обязательно. Надо было добавить это в описание, сейчас сделаю. Спасибо!
@langtang Хороший улов. Переключение строк сделало бы их не эквивалентными. Я не думал об этом до того, как вы упомянули, поэтому спасибо, что заметили, что это может быть ограничением.
Я переместил бит бенчмаркинга как ответ вики ниже.
Как насчет изменения формы пошире и использования paste0()
?
library(dplyr)
library(tidyr)
dt <- dt %>% group_by(id) %>%
mutate(inst = row_number(id)) %>%
pivot_wider(values_from = c(value1, value2),
names_from = inst) %>%
mutate(pattern_id = paste0(value1_1, value1_2, value2_1, value2_2))
Привет Андреа. Проблема (которую я добавил к вопросу, кстати) заключается в том, что каждый идентификатор не обязательно ограничен двумя строками. В моем случае идентификатор может идентифицировать набор значений с произвольным количеством строк.
Обновлено (чтобы удалить соединение):
Этот повторяет ваш подход (т.е. требует, чтобы порядок был таким же, как и значения)
unique(
dt[, pattern:=.(paste0(c(value1,value2), collapse = ",")), by=id][,.(id,pattern)]
)[,grp:=.GRP, by=pattern][,pattern:=NULL]
id grp
<char> <int>
1: a 1
2: b 2
3: c 1
Предыдущее решение:
dt[dt[, .(paste0(sort(c(value1,value2)), collapse = ",")), by=id] %>%
.[,pattern:=.GRP, by=V1] %>%
.[,V1:=NULL], on=.(id)]
Выход:
id value1 value2 pattern
<char> <num> <num> <int>
1: a 1 0 1
2: a 1 3 1
3: b 1 0 2
4: b 2 3 2
5: c 1 0 1
6: c 1 3 1
Спасибо, что присоединились. Мне очень нравится использовать paste()
для создания строки из набора значений, а затем определять группы на основе этой строки. Я добавил тест на вопрос, который касается вашего решения, и он кажется лучшим. Я немного адаптировал его, чтобы не полагаться на unique()
, но я протестировал свою адаптацию и ваше решение, и они работали в основном одинаково.
С toString
, как предлагается в сообщении об ошибке data.table
при использовании списка как by
:
Column or expression 1 of 'by' is type 'list' which is not currently supported.
As a workaround, consider converting the column to a supported type, e.g. by=sapply(list_col, toString)
dt <- dt[, .(data = list(.SD)), by = id]
dt[, pattern_id :=.GRP, by = sapply(data, toString)]
dt[,unlist(data,recursive=F),by=.(id,pattern_id)]
id pattern_id value1 value2
<char> <int> <num> <num>
1: a 1 1 0
2: a 1 1 3
3: b 2 1 0
4: b 2 2 3
5: c 1 1 0
6: c 1 1 3
Однако это медленнее, чем match
.
Предполагая, что каждый я бы повторяется дважды, "изменить форму" - преобразовать столбцы 2x2 в столбцы 1x4. Затем получите идентификатор группы, используя .GRP, сгруппировав по всем столбцам, кроме я бы:
res <- dt[, c(.SD[ 1 ], .SD[ 2 ]), by = id]
setnames(res, make.unique(colnames(res)))
res[, pattern_id := .GRP, by = res[, -1] ][, .(id, pattern_id)]
# id pattern_id
# 1: 1 1
# 2: 2 2
# 3: 3 3
# 4: 4 4
# 5: 5 5
# ---
# 499996: 499996 1010
# 499997: 499997 3175
# 499998: 499998 3996
# 499999: 499999 3653
# 500000: 500000 4217
Использование большего набора данных занимает около полсекунды.
Редактировать: другая версия, использующая трансляция, но она в 8 раз медленнее:
res <- dcast(dt, id ~ value1 + value2, length)
res[, pattern_id :=.GRP, by = res[, -1] ][, .(id, pattern_id)]
Привет, zx8754, к сожалению, идентификаторы не всегда идут по двое, как вы задали в исходном вопросе. Каждый идентификатор может использоваться для идентификации набора значений с произвольным количеством строк. В противном случае это было бы отличным решением!
Мы можем попробовать код ниже
dt[
,
q := toString(unlist(.SD)), id
][
,
pattern_id := .GRP, q
][
,
q := NULL
][]
или
dt[
,
q := toString(unlist(.SD)),
id
][
,
pattern_id := as.integer(factor(match(q, q)))
][
,
q := NULL
][]
который дает
id value1 value2 pattern_id
1: a 1 0 1
2: a 1 3 1
3: b 1 0 2
4: b 2 3 2
5: c 1 0 1
6: c 1 3 1
Отличное предложение! Я не знал о .GRP
, это здорово! Ваше решение превзошло мое, но оно немного медленнее, чем другое, опубликованное здесь, поэтому я отмечаю здесь другое как ответ. Спасибо, в любом случае!
Вот некоторые тесты с теми, которые не полагаются на то, что каждый идентификатор идентифицирует обязательно две строки, и я публикую результаты ниже.
library(data.table)
set.seed(0)
size <- 500000
dt <- data.table(
id = rep(1:(size / 2), each = 2),
value1 = sample(1:10, size, replace = TRUE),
value2 = sample(1:10, size, replace = TRUE)
)
my_solution <- function(x) {
x <- x[, .(data = list(.SD)), by = id]
unique_groups <- unique(x$data)
x[, pattern_id := match(data, unique_groups)]
x[, data := NULL]
x[]
}
langtang_solution <- function(x) {
x <- x[, .(data = paste0(value1, "|", value2, collapse = ";")), by = id]
x[, pattern_id := .GRP, by = data]
x[, data := NULL]
x[]
}
thomasiscoding_solution <- function(x) {
x <- x[, .(data = toString(unlist(.SD))), by = id]
x[, pattern_id := .GRP, by = data]
x[, data := NULL]
x[]
}
identical(my_solution(dt), langtang_solution(dt))
#> [1] TRUE
identical(my_solution(dt), thomasiscoding_solution(dt))
#> [1] TRUE
microbenchmark::microbenchmark(
my_solution(dt),
langtang_solution(dt),
thomasiscoding_solution(dt),
times = 50L
)
#> Unit: seconds
#> expr min lq mean median uq
#> my_solution(dt) 3.174106 3.566495 3.818829 3.793850 4.015176
#> langtang_solution(dt) 1.369860 1.467013 1.596558 1.529327 1.649607
#> thomasiscoding_solution(dt) 3.014511 3.154224 3.280713 3.256732 3.370015
#> max neval
#> 4.525275 50
#> 2.279064 50
#> 3.681657 50
Это очень обогащало. Я не знал о .GRP
, который в моих тестах работает очень похоже на match()
, хотя (очень немного) лучше. Лучшим ответом кажется использование paste()
для преобразования группы в строку, а затем поиск группы на основе этой строки.
Вы можете получить некоторое улучшение с
fastmatch::fmatch