Каков самый быстрый способ создать идентификатор для многострочных групп с data.table в R?

У меня есть фрейм данных, который идентифицирует набор значений с идентификатором:

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)
)

Вы можете получить некоторое улучшение с fastmatch::fmatch

akrun 22.03.2022 22:22

Каждый идентификатор всегда идет двойками, двумя a, двумя b, двумя c и т. д.?

zx8754 22.03.2022 22:25

В вашем игрушечном примере, если поменять местами строки 5 и 6, вы бы по-прежнему считали (a) и (c) эквивалентными? Я сделал это в своем решении, но вижу, что ваш подход не считает эквивалентным.

langtang 22.03.2022 22:47

@zx8754 не обязательно. Надо было добавить это в описание, сейчас сделаю. Спасибо!

dhersz 23.03.2022 21:23

@langtang Хороший улов. Переключение строк сделало бы их не эквивалентными. Я не думал об этом до того, как вы упомянули, поэтому спасибо, что заметили, что это может быть ограничением.

dhersz 23.03.2022 21:26

Я переместил бит бенчмаркинга как ответ вики ниже.

zx8754 24.03.2022 09:14
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
6
127
6
Перейти к ответу Данный вопрос помечен как решенный

Ответы 6

Как насчет изменения формы пошире и использования 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))

Привет Андреа. Проблема (которую я добавил к вопросу, кстати) заключается в том, что каждый идентификатор не обязательно ограничен двумя строками. В моем случае идентификатор может идентифицировать набор значений с произвольным количеством строк.

dhersz 23.03.2022 21:54
Ответ принят как подходящий

Обновлено (чтобы удалить соединение):

Этот повторяет ваш подход (т.е. требует, чтобы порядок был таким же, как и значения)

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(), но я протестировал свою адаптацию и ваше решение, и они работали в основном одинаково.

dhersz 23.03.2022 22:26

С 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, к сожалению, идентификаторы не всегда идут по двое, как вы задали в исходном вопросе. Каждый идентификатор может использоваться для идентификации набора значений с произвольным количеством строк. В противном случае это было бы отличным решением!

dhersz 23.03.2022 21:57

Мы можем попробовать код ниже

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, это здорово! Ваше решение превзошло мое, но оно немного медленнее, чем другое, опубликованное здесь, поэтому я отмечаю здесь другое как ответ. Спасибо, в любом случае!

dhersz 23.03.2022 22:27

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

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() для преобразования группы в строку, а затем поиск группы на основе этой строки.

Другие вопросы по теме