Подтвердить что ты не робот

Работать с dplyr

Я работаю над большим фреймворком данных в R из 2,3 миллиона записей, которые содержат транзакции пользователей в местах со стартовым и остановившимся временами. Моя цель - создать новый фреймворк данных, который содержит количество времени, подключенного для каждого пользователя/каждого места. Позвольте этому почасовому подключению.

Транзакция может отличаться от 8 минут до 48 часов, поэтому целевой кадр данных будет составлять около 100 миллионов записей и будет расти каждый месяц.

В приведенном ниже коде показано, как разрабатывается окончательный формат данных, хотя общий код намного сложнее. Запуск всего кода занимает ~ 9 часов на процессоре Intel (R) Xeon (R) E5-2630 v3 @2,40 ГГц, 16-ядерном 128 ГБ оперативной памяти.

library(dplyr)

numsessions<-1000000
startdate <-as.POSIXlt(runif(numsessions,1,365*60*60)*24,origin="2015-1-1")

df.Sessions<-data.frame(userID = round(runif(numsessions,1,500)),
           postalcode = round(runif(numsessions,1,100)),
           daynr = format(startdate,"%w"),
              start =startdate ,
              end=   startdate + runif(1,1,60*60*10)
           )


dfhourly.connected <-df.Sessions %>% rowwise %>% do(data.frame(userID=.$userID,
                                          hourlydate=as.Date(seq(.$start,.$end,by=60*60)),
                                          hournr=format(seq(.$start,.$end,by=60*60),"%H")
                                          )
                               )

Мы хотим распараллелить эту процедуру над (некоторыми) из 16 ядер, чтобы ускорить процедуру. Первой попыткой было использование пакета multidplyr. Разделение производится на основе daynr

df.hourlyconnected<-df.Sessions %>% 
                      partition(daynr,cluster=init_cluster(6)) %>%
                      rowwise %>% do(data.frame(userID=.$userID,
                            hourlydate=as.Date(seq(.$start,.$end,by=60*60)),
                            hournr=format(seq(.$start,.$end,by=60*60),"%H")
                              )
                            ) %>% collect()

Теперь для функции rowwise требуется, чтобы в качестве входных данных вместо раздела использовался блок данных.

Мои вопросы

  • Есть ли способ обхода для расчета rollise для разделов на ядро?

  • Кто-нибудь получил предложение выполнить этот расчет с помощью другого R-пакета и методов?

4b9b3361

Ответ 1

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


R - это векторизованный язык, поэтому операции по строке являются одной из самых дорогостоящих операций; Особенно, если вы оцениваете множество функций, методы отправки, конвертируете классы и создаете новый набор данных, пока вы на нем.

Следовательно, первым шагом является сокращение операций "by". Посмотрев на свой код, кажется, что вы увеличиваете размер вашего набора данных в соответствии с userID, start и end - все остальные операции могут появляться после слов (и, следовательно, быть векторизованными). Кроме того, запуск seq (который не очень эффективная функция сам по себе) дважды подряд не добавляет ничего. Наконец, вызов явно seq.POSIXt в классе POSIXt избавит вас от накладных расходов на отправку метода.

Я не уверен, как это сделать эффективно с помощью dplyr, потому что mutate не может справиться с этим, а функция do (IIRC) всегда доказывала, что сама по себе очень неэффективна. Следовательно, попробуйте пакет data.table, который может легко справиться с этой задачей.

library(data.table) 
res <- setDT(df.Sessions)[, seq.POSIXt(start, end, by = 3600), by = .(userID, start, end)] 

Снова обратите внимание, что я минимизировал операции "по строке" на один вызов функции, избегая при этом методов отправки


Теперь, когда мы подготовили набор данных, нам больше не нужны никакие операции с помощью строки, теперь все можно будет векторизовать.

Хотя, векторизация - это не конец истории. Нам также необходимо учитывать преобразования классов, диспетчеризацию методов и т.д. Например, мы можем создать как hourlydate, так и hournr, используя либо различные функции класса Date, либо используя format или, возможно, даже substr. Компромисс, который нужно принять во внимание, заключается в том, что, например, substr будет самым быстрым, но результатом будет вектор character, а не Date один - вам решать, предпочитаете ли вы скорость или качество конечного продукта. Иногда вы можете выиграть оба, но сначала вы должны проверить свои варианты. Позволяет сравнить 3 различных векторизованных способа вычисления переменной hournr

library(microbenchmark)
set.seed(123)
N <- 1e5
test <- as.POSIXlt(runif(N, 1, 1e5), origin = "1900-01-01")

microbenchmark("format" = format(test, "%H"),
               "substr" = substr(test, 12L, 13L),
               "data.table::hour" = hour(test))

# Unit: microseconds
#             expr        min         lq        mean    median        uq       max neval cld
#           format 273874.784 274587.880 282486.6262 275301.78 286573.71 384505.88   100  b 
#           substr 486545.261 503713.314 529191.1582 514249.91 528172.32 667254.27   100   c
# data.table::hour      5.121      7.681     23.9746     27.84     33.44     55.36   100 a  

data.table::hour является явным победителем как по скорости, так и по качеству (результаты представлены в целочисленном векторе, а не в символе), одновременно улучшая скорость вашего предыдущего решения с коэффициентом ~ x12,000 (и я даже не тестировал его против реализации вашей строки).

Теперь попробуйте 3 разных способа для data.table::hour

microbenchmark("as.Date" = as.Date(test), 
               "substr" = substr(test, 1L, 10L),
               "data.table::as.IDate" = as.IDate(test))

# Unit: milliseconds
#                 expr       min        lq      mean    median        uq       max neval cld
#              as.Date  19.56285  20.09563  23.77035  20.63049  21.16888  50.04565   100  a 
#               substr 492.61257 508.98049 525.09147 515.58955 525.20586 663.96895   100   b
# data.table::as.IDate  19.91964  20.44250  27.50989  21.34551  31.79939 145.65133   100  a 

Похоже, что первая и третья опции практически одинаковы по скорости, в то время как я предпочитаю as.IDate из-за режима хранения integer.


Теперь, когда мы знаем, где и эффективность и качество лежат, мы могли бы просто закончить задачу, запустив

res[, `:=`(hourlydate = as.IDate(V1), hournr = hour(V1))]

(Затем вы можете легко удалить ненужные столбцы, используя аналогичный синтаксис res[, yourcolname := NULL], который я оставлю вам)


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

В качестве побочного примечания, если вы хотите продолжить изучение синтаксиса/особенностей data.table, здесь хорошо читайте

https://github.com/Rdatatable/data.table/wiki/Getting-started