резкое преобразование данных прогнозирования с использованием data.table в R

Я ищу более эффективный способ изменения данных. Табличные данные в R.

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

Я получаю правильный ответ, который мне нужен, однако считаю, что методология очень неэлегантна /(un-data.table). Поэтому я ищу сообщество SO, чтобы узнать, есть ли более элегантное решение.

Ниже приведены настройки данных, а также две попытки получить желаемый ответ.

# load libraries
require(data.table)
require(lubridate)


# set up data assumptions
id_vec <- letters
len_id_vec <- length(id_vec)
num_orig_dates <- 7
set.seed(123)


# create original data frame
orig <- data.table(ID=rep(id_vec,each=num_orig_dates),
                   date=rep(c(Sys.Date() %m+% months(0: (num_orig_dates-1))),times=len_id_vec),
                   most_recent_bal=unlist(lapply(round(runif(len_id_vec)*100),function(y){
                     y*cumprod(1+rnorm(num_orig_dates,0.001,0.002))})))


# add 24 months ahead predictions of balances using a random walk from the original dates
nrow_orig <- nrow(orig)

for(i in seq(24)){
  orig[,paste0('pred',i,'_bal'):=most_recent_bal*(1+rnorm(nrow_orig,0.001,0.003))]
  orig[,paste0('pred',i,'_date'):=date %m+% months(i)]
}


# First attempt
t0 <- Sys.time()
tmp1 <- rbindlist(lapply(unique(orig$ID),function(x){
  orig1 <- orig[ID==x,]

  bal_cols <- c('most_recent_bal',paste0('pred',seq(24),'_bal'))
  date_cols <- c('date',paste0('pred',seq(24),'_date'))

  # Go through each original date to realign predicted date and predicted balance
  date_vec <- orig1$date
  tmp <- rbindlist(lapply(date_vec,function(y){

    tmp <- data.table(dates=as.Date(as.vector(t(orig1[date==y,date_cols,with=FALSE]))),
                      bals=as.vector(t(orig1[date==y,bal_cols,with=FALSE])))
    tmp[,type:='prediction']
    tmp[,date_prediction_run:=y]

    # collect historical information too for plotting perposes.
    tmp1 <- orig1[date<=y,c('date','most_recent_bal'),with=FALSE]
    if(nrow(tmp1)!=0){

      setnames(tmp1,c('date','most_recent_bal'),c('dates','bals'))
      tmp1[,type:='history']
      tmp1[,date_prediction_run:=y]

      tmp <- rbind(tmp,tmp1)

    }

    tmp
  }))
  tmp[,ID:=x]
}))
t1 <- Sys.time()
t1-t0 #Time difference of 1.117216 secs

# Second Attempt: a slightly more data.table way which is faster but still very inelegant....
t2 <- Sys.time()
bal_cols <- c('most_recent_bal',paste0('pred',seq(24),'_bal'))
date_cols <- c('date',paste0('pred',seq(24),'_date'))
tmp1a <- rbindlist(lapply(unique(orig$ID),function(x){
  tmp <- cbind(melt(orig[ID==x,c('date',bal_cols),with=FALSE],
                    measure.vars = bal_cols,value.name='bals')[,-('variable'),with=FALSE],
               data.table(dates=melt(orig[ID==x,date_cols,with=FALSE],
                                     measure.vars = date_cols)[,value]))
  setnames(tmp,'date','date_prediction_run')
  tmp[,type:='prediction']

  tmp1 <- orig[ID==x,orig[ID==x & date<=.BY,c('date','most_recent_bal'),with=FALSE],by=date]
  setnames(tmp1,c('date_prediction_run','dates','bals'))
  tmp1[,type:='history']
  setcolorder(tmp1,colnames(tmp1)[match(colnames(tmp),colnames(tmp1))])
  tmp <- rbind(tmp,tmp1)
  tmp[,ID:=x]
  tmp
}))
t3 <- Sys.time()
t3-t2 # Time difference of 0.2309799 secs

Ответы

Ответ 1

Использование data.table в запросе OP.

Во-первых, просто data.table как создавать data.table решение data.table

т.е. разрушать то, что мы делаем, и только для этого первого прохода быть читаемым. NB После этого ниже (в кратком обновлении) я немного оптимизирую решение, потянув все вместе, например, путем объединения шагов, цепочки, назначения на месте и т.д. Более оптимизированное решение, как и следовало ожидать, будет гораздо менее читабельны, не понимая поэтапно представленного здесь сначала с целью показать людям, изучающим data.table, как они могут прийти к решению.

# First Pass = Step-by-step (not optimized) just first work out a solution 

library(data.table)

# Transform prediction data from 'orig' data.table into long format
# i.e. by melting pred#_bal and pred#_date columns
pred_data <- 
  data.table::melt( orig, 
                    measure = patterns("pred[0-9]+_bal", "pred[0-9]+_date"),  
                    value.name =     c("bals",           "date_prediction_run"))

pred_data[, type := "prediction"]  # add the 'type' column to pred_data (all are type="prediction")

# select desired columns in order
pred_data <- pred_data[, .( dates=date, bals, type, date_prediction_run, ID)] 


# Collect historical information from the most_recent_bal column, 
# which the OP wants for plotting purposes

graph_data <- 
  orig[ orig, 
        .(ID, dates=date, bals=most_recent_bal, date_prediction_run=x.date),
        on=.(ID, date>=date)]

graph_data[, type := "history"]  # these are all type="history" 

# final output, combining the prediction data and the graph data:
output <- rbindlist(list(pred_data, graph_data), use.names=TRUE)

ОБНОВЛЕНИЕ 3 = ВАЖНОЕ ПРИМЕЧАНИЕ: Код ниже не делает ничего, чтобы улучшить скорость!

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

Сначала пройдите оптимизацию, объединив несколько шагов и цепочку [не быстрее!]
library(data.table)

# Transform prediction data into long format
# by melting pred#_bal and pred#_date columns
pred_data <- 
  data.table::melt( orig[, type := "prediction"],  #add the type column to orig, before melting 
                    measure = patterns("pred[0-9]+_bal", "pred[0-9]+_date"),  
                    value.name =     c("bals",           "date_prediction_run")
                  )[, .( dates=date, bals, type, date_prediction_run, ID)] # chain, to select desired columns in order


# FINAL RESULT:  rbindlist pred_data to historic data
pred_data <- 
  rbindlist( list( pred_data, orig[ orig[, type := "history"],  
                                    .(dates=date, bals=most_recent_bal, type, date_prediction_run=x.date, ID),
                                    on=.(ID, date>=date)]
                 ), 
             use.names=TRUE)

Продолжение UPDATE 3:

Тестирование таймингов с использованием очень удобного пакета microbenchmark:

Unit: milliseconds
                expr         min          lq        mean      median          uq         max neval
 h.l.m_first_attempt 1140.017957 1190.818176 1249.499493 1248.977454 1299.497679 1427.632140   100
h.l.m_second_attempt  231.380930  239.513223  254.702865  249.735005  262.516276  375.762675   100
  krads_step.by.step    2.855509    2.985509    3.289648    3.059481    3.269429    6.568006   100
     krads_optimized    2.909343    3.073837    3.555803    3.150584    3.554100   12.521439   100
Результаты тестов показывают, что решения data.table представляют собой огромные улучшения времени от решения OP. Отлично, что было предложено: мы показали, насколько data.table быстрый data.table может быть, но я надеюсь, что он также может быть простым и понятным! Однако не пропустите следующий урок:

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

Ответ. Если вы посмотрите внимательно, все те же шаги появляются в обоих моих решениях. В моем "оптимизированном" решении, да, мы цепляемся, и вы можете сначала подумать о том, чтобы делать меньше заданий, чем "шаг за шагом" буквально излагает. Но, поскольку результаты тестов должны сказать вам, мы НЕ сделали меньше заданий! Т.е. в каждой точке, где мы используем [] для "объединения" другой операции, это буквально эквивалентно присвоению вашему первоначальному DT с помощью <-.

Если вы можете обернуть свою голову вокруг, вы будете на пути к лучшему программированию: вы можете с уверенностью пропустить шаг "цепочки" и вместо этого использовать <- чтобы указать шаг за шагом (более читаемый, более простой для отладки и более поддерживаемое) решение!

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

NB Если вы хотите использовать microbenchmark в своем собственном коде, все, что я сделал, это:

library(microbenchmark)
mbm <- microbenchmark(
  h.l.m_first_attempt = {
    # Pasted in h.l.m first solution, here
  },

  h.l.m_second_attempt = {
    # Pasted in h.l.m second solution, here
  },

  krads_step.by.step = {
    # Pasted in my first solution, here
  },

  krads_optimized = {
    # Pasted in my second solution, here
  },
  times = 100L
)
mbm

Если вам нужен график, выполните следующие действия:

library(ggplot2)
autoplot(mbm)

Ответ 2

Я попробовал это с помощью dplyr и reshape2 для этого, и я чувствую, что он немного более изящный (не apply что технически для циклов). Он также сокращает примерно 0,04 секунды времени выполнения.

t0 = Sys.time()

# Extract predicted values in long form
trial_bal = reshape2::melt(orig, id.vars = c("ID", "date"), measure.vars = 
c(colnames(orig)[grep("pred[0-9]{1,}_bal", colnames(orig))]))
colnames(trial_bal) = c("ID", "date_prediction_run", "type", "balance")
trial_bal$type = gsub("_bal", "", trial_bal$type)

trial_date = reshape2::melt(orig, id.vars = c("ID", "date"), measure.vars = 
c(colnames(orig)[grep("pred[0-9]{1,}_date", colnames(orig))]))
colnames(trial_date) = c("ID", "date_prediction_run", "type", "dates")
trial_date$type = gsub("_date", "", trial_date$type)

trial = merge.data.frame(trial_date, trial_bal, by = c("ID", "date_prediction_run", "type"))
trial$type = "prediction"
trial = trial %>% select(dates, balance, type, date_prediction_run, ID)

# Extract historical values in long form
temp = orig[, c("ID", "date", "most_recent_bal")]
temp = merge(temp[, c("ID", "date")], temp, by = "ID", allow.cartesian = TRUE)
temp = temp[temp$date.x >= temp$date.y, ]
temp$type = "history"
temp = temp %>% select(dates = date.y, balance = most_recent_bal, type, 
date_prediction_run = date.x, ID)

# Combine prediction and history
trial = rbind(trial, temp)
trial = trial %>% arrange(ID, date_prediction_run, desc(type), dates)

t1 = Sys.time()
t1 - t0 #Time difference of 0.1900001 secs

Это имеет 182 строки меньше, чем количество строк, которые у вас есть, потому что у вас есть dates = date_prediction_run два раза - один под prediction type и один в history.