Эффективная репликация R data.table по группам

Я сталкиваюсь с некоторыми проблемами с выделением памяти, пытаясь реплицировать некоторые данные по группам, используя data.table и rep.

Вот некоторые примеры данных:

ob1 <- as.data.frame(cbind(c(1999),c("THE","BLACK","DOG","JUMPED","OVER","RED","FENCE"),c(4)),stringsAsFactors=FALSE)
ob2 <- as.data.frame(cbind(c(2000),c("I","WALKED","THE","BLACK","DOG"),c(3)),stringsAsFactors=FALSE)
ob3 <- as.data.frame(cbind(c(2001),c("SHE","PAINTED","THE","RED","FENCE"),c(1)),stringsAsFactors=FALSE)
ob4 <- as.data.frame(cbind(c(2002),c("THE","YELLOW","HOUSE","HAS","BLACK","DOG","AND","RED","FENCE"),c(2)),stringsAsFactors=FALSE)
sample_data <- rbind(ob1,ob2,ob3,ob4)
colnames(sample_data) <- c("yr","token","multiple")

То, что я пытаюсь сделать, это реплицировать токены (в нынешнем порядке) кратно за каждый год.

Следующий код работает и дает мне ответ, который я хочу:

good_solution1 <- ddply(sample_data, "yr", function(x) data.frame(rep(x[,2],x[1,3])))

good_solution2 <- data.table(sample_data)[, rep(token,unique(multiple)),by = "yr"]

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

Если я правильно понимаю, эти решения, по сути, выполняют rbind, который каждый раз выделяет.

У кого-нибудь есть лучшее решение?

Я посмотрел на set() для data.table, но столкнулся с проблемами, потому что хотел сохранить токены в одном и том же порядке для каждой репликации.


person Brad    schedule 24.03.2013    source источник
comment
Начиная с версии 1.9.2. (CRAN, 27 февраля 2014 г.) data.table получила новую функцию setDT() , которая принимает list или data.frame и изменяет свой тип по ссылке на data.table, без какой-либо копии. Таким образом, setDT(sample_data) вместо data.table(sample_data) может помочь сэкономить память.   -  person Uwe    schedule 03.12.2017


Ответы (2)


Один из способов:

require(data.table)
dt <- data.table(sample_data)
# multiple seems to be a character, convert to numeric
dt[, multiple := as.numeric(multiple)]
setkey(dt, "multiple")
dt[J(rep(unique(multiple), unique(multiple))), allow.cartesian=TRUE]

Все, кроме последней строки, должно быть простым. В последней строке используется подмножество с использованием ключевого столбца с помощью J(.). Для каждого значения в J(.) соответствующее значение сопоставляется с «ключевым столбцом», и возвращается совпадающее подмножество.

То есть, если вы сделаете dt[J(1)], вы получите подмножество, где multiple = 1. И если вы обратите внимание, выполнение dt[J(rep(1,2)] даст вам то же самое подмножество, но дважды. Обратите внимание, что есть разница между передачей dt[J(1,1)] и dt[J(rep(1,2)]. В первом случае значения (1,1) сопоставляются с первыми двумя ключевыми столбцами таблицы data.table соответственно, а во втором — путем сопоставления (1 и 2) с < em>first-key столбец таблицы data.table.

Итак, если мы передаем одно и то же значение столбца 2 раза в J(.), то оно дублируется дважды. Мы используем этот трюк, чтобы пройти 1 1-кратно, 2 2-кратно и т. д., и это то, что делает часть rep(.). rep(.) дает 1,2,2,3,3,3,4,4,4,4.

И если в результате соединения получается больше строк, чем max(nrow(dt), nrow(i)) (i — это вектор представления, который находится внутри J(.)), вы должны явно использовать allow.cartesian = TRUE для выполнения этого соединения (я думаю, это новая функция из data.table 1.8.8).


Редактировать. Вот некоторые результаты сравнительного анализа, которые я провел на «относительно» больших данных. Я не вижу никакого всплеска выделения памяти в обоих методах. Но мне еще предстоит найти способ отслеживать пиковое использование памяти в функции в R. Я уверен, что видел такой пост здесь, на SO, но в данный момент он ускользает от меня. Я отпишусь снова. А пока вот тестовые данные и некоторые предварительные результаты на случай, если кому-то будет интересно или захочется испытать их на себе.

# dummy data
set.seed(45)
yr <- 1900:2013
sz <- sample(10:50, length(yr), replace = TRUE)
token <- unlist(sapply(sz, function(x) do.call(paste0, data.frame(matrix(sample(letters, x*4, replace=T), ncol=4)))))
multiple <- rep(sample(500:5000, length(yr), replace=TRUE), sz)

DF <- data.frame(yr = rep(yr, sz), 
                 token = token, 
                 multiple = multiple, stringsAsFactors=FALSE)

# Arun's solution
ARUN.DT <- function(dt) {
    setkey(dt, "multiple")
    idx <- unique(dt$multiple)
    dt[J(rep(idx,idx)), allow.cartesian=TRUE]
}

# Ricardo's solution
RICARDO.DT <- function(dt) {
    setkey(dt, key="yr")
    newDT <- setkey(dt[, rep(NA, list(rows=length(token) * unique(multiple))), by=yr][, list(yr)], 'yr')
    newDT[, tokenReps := as.character(NA)]

    # Add the rep'd tokens into newDT, using recycling
    newDT[, tokenReps := dt[.(y)][, token], by=list(y=yr)]
    newDT
}

# create data.table
require(data.table)
DT <- data.table(DF)

# benchmark both versions
require(rbenchmark)
benchmark(res1 <- ARUN.DT(DT), res2 <- RICARDO.DT(DT), replications=10, order="elapsed")

#                     test replications elapsed relative user.self sys.self
# 1    res1 <- ARUN.DT(DT)           10   9.542    1.000     7.218    1.394
# 2 res2 <- RICARDO.DT(DT)           10  17.484    1.832    14.270    2.888

Но, как говорит Рикардо, может не иметь значения, если у вас закончилась память. Таким образом, в этом случае должен быть компромисс между скоростью и памятью. Что я хотел бы проверить, так это пиковую память, используемую в обоих методах, чтобы окончательно сказать, лучше ли использовать Join.

person Arun    schedule 24.03.2013
comment
Спасибо! Да, проблема с персонажем была просто в том, что я был глуп при создании этого примера данных. - person Brad; 24.03.2013
comment
Как вы думаете, этот подход будет использовать меньше памяти? Я не реализовал его для своего большого образца, но заметил, что он также использует репликацию. В чем разница между вашим решением и моим good_solution2? - person Brad; 24.03.2013
comment
Он будет подмножеством для каждого значения в J(.) и продолжит объединение. Итак, если у вас есть 10 идентификаторов со 100 строками в каждом и кратными 4, вы получите 10 * 100 * 4 = 4000 строк (по 100 строк в каждом подмножестве). - person Arun; 24.03.2013
comment
Я подозреваю, что решение Рикардо может быть вашим ответом в этом случае (после повторного прочтения вашего вопроса). Я бы просто сравнил два метода с относительно большими данными (но не слишком большими, чтобы случайно не хватить памяти и привести к сбою R-сессии, но достаточно четкими, чтобы отличить самый быстрый подход + подход с осторожностью к памяти). - person Arun; 24.03.2013
comment
Мне нравится бенчмаркинг, но я не уверен, что это будет явным признаком лучшего подхода. Я думаю, что в конечном итоге для эффективного достижения цели придется пожертвовать скоростью. - person Ricardo Saporta; 24.03.2013
comment
+1 к этому серебряному тегу data.table :-) за отличное объяснение, которое помогает мне учиться - person Simon O'Hanlon; 24.03.2013
comment
@RicardoSaporta, я подозреваю, что у вас будет быстрее, если привязка произойдет без предварительного выделения. Но поскольку data.table выдает ошибку, что имеется 66 строк (в данном случае), если вы не используете allow.cartesian=TRUE, интересно, будет ли разница в производительности. Если есть разница, мы должны увидеть ее, поскольку объект копируется каждый раз (я полагаю, что это похоже на предварительное выделение цикла for). Я постараюсь разобраться и вернуться. - person Arun; 24.03.2013
comment
@SimonO101, спасибо. Я попытаюсь сделать сравнение и посмотреть, можно ли его улучшить. - person Arun; 24.03.2013
comment
@Brad, я тестировал с data.frame dim=3482*3 с кратностью от 500 до 2000. Вывод составил 9 миллионов строк. И это работало нормально для меня. И не было разницы в памяти между Рикардо и моей (но это очень преждевременно, так как я смотрел на системный монитор, а не измерял). Но точно join быстрее (0,7 против 1,9 секунды)! Итак, было бы здорово, если бы вы могли сравнить свои данные (а также, каков размер вашего data.frame?) и отчитаться. Благодарю. - person Arun; 24.03.2013

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

  # make sure `sample_data$multiple` is an integer
  sample_data$multiple <- as.integer(sample_data$multiple)

  # create data.table
  S <- data.table(sample_data, key='yr')

  # optionally, drop original data.frame if not needed
  rm(sample_data)

  ## Allocate the memory first
  newDT <- data.table(yr = rep(sample_data$yr, sample_data$multiple), key="yr")
  newDT[, tokenReps := as.character(NA)]

  # Add the rep'd tokens into newDT, using recycling
  newDT[, tokenReps := S[.(y)][, token], by=list(y=yr)]

Два примечания:

(1) sample_data$multiple в настоящее время является символом и, таким образом, принудительно передается при передаче rep (в исходном примере). Возможно, стоит перепроверить ваши реальные данные, если это также так.

(2) Я использовал следующее, чтобы определить количество строк, необходимых в год.

S[, list(rows=length(token) * unique(multiple)), by=yr] 
person Ricardo Saporta    schedule 24.03.2013
comment
Рикардо, это характер, потому что он использует cbind и as.data.frame для создания данных. И cbind(.) создает матрицу, которая затем оборачивается as.data.frame. Поскольку для cbind нет входа data.frame, это матрица, поэтому каждое значение преобразуется в character. - person Arun; 24.03.2013
comment
Спасибо Рикардо и Арун. Рикардо, в вашем коде есть tokenReps и tokenRep, которые дают окончательный ответ и дополнительный столбец. - person Brad; 24.03.2013
comment
@RicardoSaporta, ваш первый шаг распределения: newDT <- data.table(yr = rep(sample_data$yr, sample_data$multiple), key="yr"), не так ли? - person Arun; 24.03.2013
comment
@Арун, действительно! И это немного быстрее и чище. Отредактировал мой ответ, чтобы отразить, спасибо! - person Ricardo Saporta; 24.03.2013