Быстрая конкатенация столбцов data.table
Учитывая произвольный список имен столбцов в data.table
, я хочу объединить содержимое этих столбцов в одну строку, хранящуюся в новом столбце. Столбцы, которые мне нужны для конкатенации, не всегда одинаковы, поэтому мне нужно сгенерировать выражение, чтобы сделать это на лету.
У меня есть скрытое подозрение, что способ, которым я пользуюсь вызовом eval(parse(...))
, можно заменить чем-то более элегантным, но метод, приведенный ниже, является самым быстрым, что я смог получить до сих пор.
С 10 миллионами строк это занимает около 21,7 секунды по этим данным образца (база R paste0
занимает немного больше - 23,6 секунды). Мои фактические данные объединяют 18-20 столбцов и до 100 миллионов строк, поэтому замедление становится немного более непрактичным.
Любые идеи, чтобы получить это ускорилось?
Текущие методы
library(data.table)
library(stringi)
RowCount <- 1e7
DT <- data.table(x = "foo",
y = "bar",
a = sample.int(9, RowCount, TRUE),
b = sample.int(9, RowCount, TRUE),
c = sample.int(9, RowCount, TRUE),
d = sample.int(9, RowCount, TRUE),
e = sample.int(9, RowCount, TRUE),
f = sample.int(9, RowCount, TRUE))
## Generate an expression to paste an arbitrary list of columns together
ConcatCols <- c("x","a","b","c","d","e","f","y")
PasteStatement <- stri_c('stri_c(',stri_c(ConcatCols,collapse = ","),')')
print(PasteStatement)
дает
[1] "stri_c(x,a,b,c,d,e,f,y)"
который затем используется для конкатенации столбцов со следующим выражением:
DT[,State := eval(parse(text = PasteStatement))]
Пример вывода:
x y a b c d e f State
1: foo bar 4 8 3 6 9 2 foo483692bar
2: foo bar 8 4 8 7 8 4 foo848784bar
3: foo bar 2 6 2 4 3 5 foo262435bar
4: foo bar 2 4 2 4 9 9 foo242499bar
5: foo bar 5 9 8 7 2 7 foo598727bar
Результаты профилирования
![Данные]()
Обновление 1: fread
, fwrite
и sed
Следуя предложению @Gregor, попробовав использовать sed
для выполнения конкатенации на диске. Благодаря функции data.table с быстрыми функциями fread
и fwrite
я смог записать столбцы на диск, устранить разделители запятой, используя sed, а затем прочитать обратно в пост-обработанном выходе примерно за 18,3 секунды - не достаточно быстро, чтобы сделать переключатель, но тем не менее интересным касанием!
ConcatCols <- c("x","a","b","c","d","e","f","y")
fwrite(DT[,..ConcatCols],"/home/xxx/DT.csv")
system("sed 's/,//g' /home/xxx/DT.csv > /home/xxx/DT_Post.csv ")
Post <- fread("/home/xxx/DT_Post.csv")
DT[,State := Post[[1]]]
Разбивка 18,3 общих секунд (неспособная использовать profvis с sed
невидима для профайлера R)
-
data.table::fwrite()
- 0,5 секунды
-
sed
- 14.8 секунд
-
data.table::fread()
- 3,0 секунды
-
:=
- 0.0 секунд
Если ничего другого, это свидетельствует об обширной работе авторов data.table по оптимизации производительности для IO диска. (Я использую версию разработки 1.10.5, которая добавляет многопоточность к fread
, fwrite
была многопоточной в течение некоторого времени).
Оговорка:, если есть способ обхода файла с помощью fwrite
и пустого разделителя, как предложено @Gregor в другом комментарии ниже, тогда этот метод можно было бы правдоподобно сократить до ~ 3,5 секунды!
Обновление по этой касательной: forked data.table и прокомментировала строку, требующую разделителя больше длины 0, загадочно получилось несколько пробелов? Вызвав несколько секретов, пытающихся запутаться с внутренними элементами C
, я помещал это на лед на время. Идеальное решение не требует записи на диск и будет хранить все в памяти.
Обновление 2: sprintf
для целых конкретных случаев
Второе обновление здесь: хотя я включил строки в свой пример использования, мой фактический пример использования исключительно объединяет целочисленные значения (которые всегда можно считать ненулевыми на основе шагов очистки выше по течению).
Поскольку случай использования очень специфичен и отличается от исходного вопроса, я не буду напрямую сравнивать тайминги с ранее опубликованными. Тем не менее, один взнос состоит в том, что, хотя stringi
прекрасно обрабатывает многие форматы кодировки символов, смешанные векторные типы, не требуя их указывать, и делает кучу обработки ошибок из коробки, это добавляет некоторое время (что, вероятно, стоит того большинство случаев).
Используя базовую функцию R sprintf
и давая ей знать, что все входы будут целыми числами, мы можем сэкономить около 30% времени выполнения для 5 миллионов строк с 18 целыми столбцами для вычисления. (20,3 секунды вместо 28,9)
library(data.table)
library(stringi)
RowCount <- 5e6
DT <- data.table(x = "foo",
y = "bar",
a = sample.int(9, RowCount, TRUE),
b = sample.int(9, RowCount, TRUE),
c = sample.int(9, RowCount, TRUE),
d = sample.int(9, RowCount, TRUE),
e = sample.int(9, RowCount, TRUE),
f = sample.int(9, RowCount, TRUE))
## Generate an expression to paste an arbitrary list of columns together
ConcatCols <- list("a","b","c","d","e","f")
## Do it 3x as many times
ConcatCols <- c(ConcatCols,ConcatCols,ConcatCols)
## Using stringi::stri_c ---------------------------------------------------
stri_joinStatement <- stri_c('stri_join(',stri_c(ConcatCols,collapse = ","),', sep="", collapse=NULL, ignore_null=TRUE)')
DT[, State := eval(parse(text = stri_joinStatement))]
## Using sprintf -----------------------------------------------------------
sprintfStatement <- stri_c("sprintf('",stri_flatten(rep("%i",length(ConcatCols))),"', ",stri_c(ConcatCols,collapse = ","),")")
DT[,State_sprintf_i := eval(parse(text = sprintfStatement))]
Сгенерированные операторы выглядят следующим образом:
> cat(stri_joinStatement)
stri_join(a,b,c,d,e,f,a,b,c,d,e,f,a,b,c,d,e,f, sep="", collapse=NULL, ignore_null=TRUE)
> cat(sprintfStatement)
sprintf('%i%i%i%i%i%i%i%i%i%i%i%i%i%i%i%i%i%i', a,b,c,d,e,f,a,b,c,d,e,f,a,b,c,d,e,f)
![sprintf]()
Обновление 3: R
не должно быть медленным.
Основываясь на ответе от @Martin Modrák, я собрал пакет с одним трюком, основанный на некоторых внутренних элементах data.table
, специализированных для специализированного случая с целыми числами: fastConcat
. (Не смотрите на CRAN в ближайшее время, но вы можете использовать его на свой страх и риск, установив из github repo, msummersgill/fastConcat.)
Вероятно, это может значительно улучшить тот, кто лучше понимает C
, но на данный момент он работает в том же случае, что и в обновлении 2 в 2,5 секунды - около 8x быстрее, чем sprintf()
и 11.5xбыстрее, чем метод stringi::stri_c()
, который я использовал изначально.
Для меня это подчеркивает огромную возможность повышения производительности некоторых простейших операций в R
, таких как рудиментарное конкатенация вектор-строк с улучшенной настройкой C
. Думаю, такие люди, как @Matt Dowle, видели это годами - если бы у него было время переписать все R
, а не только data.frame.
![fastConcat]()
Ответы
Ответ 1
C на помощь!
Похищая некоторый код из data.table, мы можем написать функцию C, которая работает быстрее (и может быть распараллелирована еще быстрее).
Сначала убедитесь, что у вас есть рабочая С++ toolchain с:
library(inline)
fx <- inline::cfunction( signature(x = "integer", y = "numeric" ) , '
return ScalarReal( INTEGER(x)[0] * REAL(y)[0] ) ;
' )
fx( 2L, 5 ) #Should return 10
Затем это должно работать (при условии, что данные только целочисленные, но код может быть расширен для других типов):
library(inline)
library(data.table)
library(stringi)
header <- "
//Taken from https://github.com/Rdatatable/data.table/blob/master/src/fwrite.c
static inline void reverse(char *upp, char *low)
{
upp--;
while (upp>low) {
char tmp = *upp;
*upp = *low;
*low = tmp;
upp--;
low++;
}
}
void writeInt32(int *col, size_t row, char **pch)
{
char *ch = *pch;
int x = col[row];
if (x == INT_MIN) {
*ch++ = 'N';
*ch++ = 'A';
} else {
if (x<0) { *ch++ = '-'; x=-x; }
// Avoid log() for speed. Write backwards then reverse when we know how long.
char *low = ch;
do { *ch++ = '0'+x%10; x/=10; } while (x>0);
reverse(ch, low);
}
*pch = ch;
}
//end of copied code
"
worker_fun <- inline::cfunction( signature(x = "list", preallocated_target = "character", columns = "integer", start_row = "integer", end_row = "integer"), includes = header , "
const size_t _start_row = INTEGER(start_row)[0] - 1;
const size_t _end_row = INTEGER(end_row)[0];
const int max_out_len = 256 * 256; //max length of the final string
char buffer[max_out_len];
const size_t num_elements = _end_row - _start_row;
const size_t num_columns = LENGTH(columns);
const int * _columns = INTEGER(columns);
for(size_t i = _start_row; i < _end_row; ++i) {
char *buf_pos = buffer;
for(size_t c = 0; c < num_columns; ++c) {
if(c > 0) {
buf_pos[0] = ',';
++buf_pos;
}
writeInt32(INTEGER(VECTOR_ELT(x, _columns[c] - 1)), i, &buf_pos);
}
SET_STRING_ELT(preallocated_target,i, mkCharLen(buffer, buf_pos - buffer));
}
return preallocated_target;
" )
#Test with the same data
RowCount <- 5e6
DT <- data.table(x = "foo",
y = "bar",
a = sample.int(9, RowCount, TRUE),
b = sample.int(9, RowCount, TRUE),
c = sample.int(9, RowCount, TRUE),
d = sample.int(9, RowCount, TRUE),
e = sample.int(9, RowCount, TRUE),
f = sample.int(9, RowCount, TRUE))
## Generate an expression to paste an arbitrary list of columns together
ConcatCols <- list("a","b","c","d","e","f")
## Do it 3x as many times
ConcatCols <- c(ConcatCols,ConcatCols,ConcatCols)
ptm <- proc.time()
preallocated_target <- character(RowCount)
column_indices <- sapply(ConcatCols, FUN = function(x) { which(colnames(DT) == x )})
x <- worker_fun(DT, preallocated_target, column_indices, as.integer(1), as.integer(RowCount))
DT[, State := preallocated_target]
proc.time() - ptm
Пока ваш (целочисленный) пример работает примерно за 20 секунд на моем ПК, он работает в ~ 5 секунд и может быть легко распараллелен.
Некоторые примечания:
- Код не готов к производству - на входы функций должно быть сделано много проверок на работоспособность (особенно если проверить, что все столбцы имеют одинаковую длину, проверку типов столбцов, preallocated_target и т.д.).
- Функция помещает свой вывод в предварительно выделенный вектор символов, это нестандартный и уродливый (у R обычно нет семантики pass-by-reference), но допускается распараллеливание (см. ниже).
- Последние два параметра - это начальные и конечные строки, которые нужно обработать, еще раз, это для паралеллизации
- Функция принимает индексы столбцов, а не имена столбцов. Все столбцы должны иметь тип integer.
- За исключением входных данных .table и preallocated_target, входы должны быть целыми.
- Время компиляции для функции не включено (как вы должны ее заранее скомпилировать - возможно, даже сделать пакет)
распараллеливания
РЕДАКТИРОВАТЬ: Подход, приведенный ниже, действительно завершится неудачно из-за работы clusterExport
и R string. Параллеллизация, вероятно, также должна быть выполнена на С, подобно тому, как это достигается в таблице данных.
Поскольку вы не можете передавать встроенные функции в R-процессы, паралеллизация требует некоторой дополнительной работы. Чтобы иметь возможность использовать вышеприведенную функцию параллельно, вам либо нужно ее скомпилировать отдельно с компилятором R, либо использовать dyn.load
ИЛИ обернуть его в пакет или использовать бэкэнд для параллельной работы (у меня его нет, только для работы с файлами в UNIX).
Выполняется параллельно, тогда будет выглядеть что-то вроде (не проверено):
no_cores <- detectCores()
# Initiate cluster
cl <- makeCluster(no_cores)
#Preallocated target and prepare params
num_elements <- length(DT[[1]])
preallocated_target <- character(num_elements)
block_size <- 4096 #No of rows processed at once. Adjust for best performance
column_indices <- sapply(ConcatCols, FUN = function(x) { which(colnames(DT) == x )})
num_blocks <- ceiling(num_elements / block_size)
clusterExport(cl,
c("DT","preallocated_target","column_indices","num_elements", "block_size"))
clusterEvalQ(cl, <CODE TO LOAD THE NATIVE FUNCTION HERE>)
parLapply(cl, 1:num_blocks ,
function(block_id)
{
throw_away <-
worker_fun(DT, preallocated_target, columns,
(block_id - 1) * block_size + 1, min(num_elements, block_id * block_size - 1))
return(NULL)
})
stopCluster(cl)
Ответ 2
Я не знаю, насколько репрезентативными являются данные выборки для ваших фактических данных, но в случае ваших выборочных данных вы можете добиться существенного улучшения производительности, объединив каждую уникальную комбинацию ConcatCols один раз, а не несколько раз.
Это означает, что для выборочных данных вы будете смотреть на ~ 500 тыс. конкатенаций против 10 миллионов, если вы тоже сделаете дубликаты.
См. следующий пример кода и времени:
system.time({
setkeyv(DT, ConcatCols)
DTunique <- unique(DT[, ConcatCols, with=FALSE], by = key(DT))
DTunique[, State := do.call(paste, c(DTunique, sep = ""))]
DT[DTunique, State := i.State, on = ConcatCols]
})
# user system elapsed
# 7.448 0.462 4.618
Около половины времени тратится на часть setkey
. Если ваши данные уже введены в действие, время сокращается до чуть более 2 секунд.
setkeyv(DT, ConcatCols)
system.time({
DTunique <- unique(DT[, ConcatCols, with=FALSE], by = key(DT))
DTunique[, State := do.call(paste, c(DTunique, sep = ""))]
DT[DTunique, State := i.State, on = ConcatCols]
})
# user system elapsed
# 2.526 0.280 2.181
Ответ 3
Это использует unite
из пакета tidyr
. Может быть, не самый быстрый, но, вероятно, быстрее, чем ручной код R.
library(tidyr)
system.time(
DNew <- DT %>% unite(State, ConcatCols, sep = "", remove = FALSE)
)
# user system elapsed
# 14.974 0.183 15.343
DNew[1:10]
# State x y a b c d e f
# 1: foo211621bar foo bar 2 1 1 6 2 1
# 2: foo532735bar foo bar 5 3 2 7 3 5
# 3: foo965776bar foo bar 9 6 5 7 7 6
# 4: foo221284bar foo bar 2 2 1 2 8 4
# 5: foo485976bar foo bar 4 8 5 9 7 6
# 6: foo566778bar foo bar 5 6 6 7 7 8
# 7: foo892636bar foo bar 8 9 2 6 3 6
# 8: foo836672bar foo bar 8 3 6 6 7 2
# 9: foo963926bar foo bar 9 6 3 9 2 6
# 10: foo385216bar foo bar 3 8 5 2 1 6