Учитывая произвольный список имен столбцов в 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)
Обновление 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.