R - Ускорение приблизительного совпадения даты. idata.frame?

Я изо всех сил пытаюсь эффективно выполнить «близкое» совпадение даты между двумя фреймами данных. В этом вопросе рассматривается решение с использованием idata.frame из пакета plyr, но я был бы очень доволен и другими предлагаемыми решениями.

Вот очень упрощенная версия двух фреймов данных:

sampleticker<-data.frame(cbind(ticker=c("A","A","AA","AA"),
  date=c("2005-1-25","2005-03-30","2005-02-15","2005-04-21")))
sampleticker$date<-as.Date(sampleticker$date,format="%Y-%m-%d")

samplereport<-data.frame(cbind(ticker=c("A","A","A","AA","AA","AA"),
  rdate=c("2005-2-15","2005-03-15","2005-04-15",
  "2005-03-01","2005-04-20","2005-05-01")))
samplereport$rdate<-as.Date(samplereport$rdate,format="%Y-%m-%d")

В фактических данных sampleticker - это более 30 000 строк с 40 столбцами, а samplereport почти 300 000 строк с 25 столбцами.

Я хотел бы объединить два фрейма данных, чтобы каждая строка в sampleticker объединялась с ближайшим совпадением даты в samplereport, которое происходит ПОСЛЕ даты в sampleticker. Раньше я решал аналогичные проблемы, выполняя простое слияние в поле тикера, сортируя по возрастанию, а затем выбирая уникальные комбинации тикера и даты. Однако из-за размера этого набора данных слияние происходит очень быстро.

Насколько я могу судить, merge не допускает такого приблизительного соответствия. Я видел некоторые решения, в которых используется findInterval, но поскольку расстояние между датами будет разным, я не уверен, что могу указать интервал, который будет работать для всех строк.

Следуя другому сообщению, здесь, я написал следующий код для используйте adply в каждой строке и выполните соединение:

library(plyr)
merge<-adply(sampleticker,1,function(x){
  y<-subset(samplereport,ticker %in% x$ticker & rdate > x$date)
  y[which.min(y$rdate),]
  }))

Это работает довольно хорошо: для образцов данных я получаю следующее, что мне и нужно.

   date       ticker      rdate
 1 2005-01-25  A          2005-02-15
 2 2005-03-30  A          2005-04-15
 3 2005-02-15  AA         2005-03-01
 4 2005-04-21  AA         2005-05-01

Однако, поскольку код выполняет 30 000+ операций по разделению на подмножества, он работает очень медленно: я выполнял вышеуказанный запрос более суток, прежде чем окончательно его убил.

Я вижу здесь, что plyr 1.0 имеет структуру idata.frame, которая вызывает фрейм данных по ссылке, что значительно ускоряет операцию подмножества. Однако я не могу заставить работать следующий код:

isamplereport<-idata.frame(samplereport)
adply(sampleticker,1,function(x){
  y<-subset(isamplereport,isamplereport$ticker %in% x$ticker & 
    isamplereport$rdate > x$date)
  y[which.min(y$rdate),]
})

Я получаю ошибку

Error in list_to_dataframe(res, attr(.data, "split_labels")) : 
Results must be all atomic, or all data frames

Для меня это имеет смысл, поскольку операция возвращает idata.frame (я полагаю). Однако изменив последнюю строку на:

as.data.frame(y[which.min(y$rdate),]) 

также выдает ошибку:

Error in `[.data.frame`(x$`_data`, x$`_rows`, x$`_cols`) : 
undefined columns selected.

Обратите внимание, что вызов as.data.frame на простом старом samplereport возвращает исходный фрейм данных, как и ожидалось.

Я знаю, что idata.frame является экспериментальным, поэтому я не ожидал, что он будет работать должным образом. Однако, если у кого-то есть идея, как это исправить, я был бы признателен. С другой стороны, если бы кто-нибудь мог предложить совершенно другой подход, который работает более эффективно, это было бы фантастически.

Мэтт

ОБНОВЛЕНИЕ Data.table - правильный способ сделать это. См. ниже.


person Matt    schedule 13.02.2012    source источник


Ответы (3)


Благодаря Мэтью Доулу и его добавлению возможности отката назад и вперед в data.table, теперь это слияние стало намного проще.

ST <- data.table(sampleticker)
SR <- data.table(samplereport)
setkey(ST,ticker,date)
SR[,mergerdate:=rdate]
setkey(SR,ticker,mergerdate)
merge<-SR[ST,roll=-Inf]
setnames(merge,"mergerdate","date")

#    ticker       date      rdate
# 1:      A 2005-01-25 2005-02-15
# 2:      A 2005-03-30 2005-04-15
# 3:     AA 2005-02-15 2005-03-01
# 4:     AA 2005-04-21 2005-05-01
person Matt    schedule 06.03.2013
comment
И вы также можете ограничить количество броска, поставив отрицательное число вместо Inf. Фантастический материал! - person Matt; 07.03.2013
comment
Превосходно. Спасибо, что разместили это. - person Josh O'Brien; 07.03.2013

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

library(data.table)
ST <- data.table(sampleticker, key="ticker")
SR <- data.table(samplereport, key="ticker")
SR <- SR[with(SR, order(ticker, rdate)),] # rdates need to be in increasing order

SR[ST, list(date = date,
            rdate = rdate[match(TRUE, (rdate > date))]), ]
     ticker       date      rdate
[1,]      A 2005-01-25 2005-02-15
[2,]      A 2005-03-30 2005-04-15
[3,]     AA 2005-02-15 2005-03-01
[4,]     AA 2005-04-21 2005-05-01

Конечно, похоже, что вы действительно хотите объединить два гораздо более широких кадра data.frames. Чтобы продемонстрировать один из способов достижения этого, в приведенном ниже примере я добавляю несколько столбцов в обе таблицы data.tables, а затем показываю, как можно объединить соответствующие строки:

# Add some columns to both data.tables
ST$alpha <- letters[seq_len(nrow(ST))]
SR$n     <- seq_len(nrow(SR))
SR$ALPHA <- LETTERS[seq_len(nrow(SR))]

# Perform a merge that includes the whole rows from samplereport
# corresponding to the selected rdate
RES <- SR[ST, cbind(date, .SD[match(TRUE,(rdate>date)),-1]), ]

# Merge res (containing the selected rows from samplereport) back together
# with sampleticker
keycols <- c("ticker", "date")
setkeyv(RES, keycols)
setkeyv(ST, keycols)
ST[RES]
#      ticker       date alpha      rdate n ALPHA
# [1,]      A 2005-01-25     a 2005-02-15 1     A
# [2,]      A 2005-03-30     b 2005-04-15 3     C
# [3,]     AA 2005-02-15     c 2005-03-01 4     D
# [4,]     AA 2005-04-21     d 2005-05-01 6     F
person Josh O'Brien    schedule 13.02.2012
comment
roll=TRUE действительно создан для этого. Чтобы получить ближайший после, тогда, возможно, X[ X[Y,roll=TRUE,which=TRUE]+1 ], или поменяйте местами и сделайте Y[X,roll=TRUE]. - person Matt Dowle; 14.02.2012
comment
Но следующее требование после встречается очень редко. На практике обычно лучше mult="last" или DT[J(date,23:00),roll=TRUE]. - person Matt Dowle; 14.02.2012
comment
На самом деле я буду часто использовать следующее после требования; нередко выбирают первую запись после события, чтобы оценить реакцию на событие. Я попробую использовать roll=TRUE, чтобы добиться нужного эффекта. Мои предварительные (вероятно, неправильные) попытки дали мне обратное: samplereport, который находится ближе всего ПЕРЕД sampleticker, но изменение процедуры дает мне все записи в samplereport с кучей NA, что определенно не то, что я хочу. Мне нужно будет узнать больше о data.table, так как он выглядит очень полезным. - person Matt; 14.02.2012
comment
@Matt - Это разъяснение помогает. Поскольку вы также смотрите на roll=TRUE, я найду время, чтобы опубликовать решение, которое, как я выяснил, использует его. Пожалуйста, дайте мне знать, работает ли это с вашими данными. - person Josh O'Brien; 14.02.2012
comment
Спасибо, Джош. Я думаю, что ваше решение выше также работает; Я почти уверен, что у меня это работало, пока я не начал экспериментировать с roll=TRUE, а затем я напортачил. Сейчас я попытаюсь восстановить ваше решение. Мне определенно было бы интересно roll=TRUE решение. Это более лаконично и кажется значительно быстрее. - person Matt; 14.02.2012
comment
@Matt - Хорошо. Другой ответ уже сделан. Не уверен, удачно ли мое объяснение, и уж точно не лаконично. Если вы настроили оба метода, сообщите нам, есть ли между ними значительная разница в производительности. Ваше здоровье. - person Josh O'Brien; 14.02.2012
comment
Я принял этот ответ, потому что он казался более оптимальным, чем другое решение: оно более интуитивно понятно. Он также дает самый красивый результат без каких-либо настроек. Для людей, которые хотят использовать это решение, сообщаем, что для моих данных я должен был setkeyv(SR,c("ticker","rdate") перед выполнением RES слияния. - person Matt; 15.02.2012
comment
Однако между этими двумя решениями существует очень большая разница в производительности. По моим данным, это решение заняло около 200 секунд. Это значительное улучшение по сравнению с тем, что у меня было раньше, поэтому большое спасибо за то, что помог мне разобраться в этом. Однако решение roll=TRUE занимает 1,16 секунды, что, безусловно, подтверждает правильность подхода. - person Matt; 15.02.2012
comment
Мэтт - Спасибо, что предоставили время. Это очень полезная информация. Кроме того, @MatthewDowle (если вы хотите прокомментировать), решение roll=TRUE, которое я придумал ниже, выглядит оптимальным, или есть какое-то более простое заклинание, которое я не замечаю? Спасибо вам обоим. - person Josh O'Brien; 16.02.2012
comment
@Matt Быстрый небольшой комментарий, что setkey(SR,ticker,rdate) проще (на 8 символов меньше), чем setkeyv(SR,c("ticker","rdate")). setkeyv предназначен только для случаев, когда вам действительно нужно программно передать ключ как переменную. - person Matt Dowle; 16.02.2012

Вот решение, которое следует за замечанием Мэтью Доула о том, что это естественное место для применения аргумента data.table roll=TRUE.

Если вы собираетесь нанести его, нужно разгладить одну морщинку. roll=TRUE разработан таким образом, что если точное совпадение не найдено для последнего столбца ключа (здесь дата), значение с ближайшей предыдущей даты будет перенесено вперед . Однако вам нужно обратное (и даже если есть точное совпадение, вам все равно нужно значение со следующей доступной даты).

Первой попыткой может быть сортировка по "ticker" и по "rdate" в обратном порядке, слияние с полученным переупорядоченным SR. Это сработает, за исключением того, что data.table не хочет позволять вам выполнять сортировку в обратном порядке: ввод с помощью "rdate" переводит этот столбец в порядок возрастания. (data.table необходимо сделать это, чтобы реализовать быстрое сопоставление и соединение, для которых он был разработан).

Мое решение ниже - создать новый столбец - "rnd" для «обратной числовой даты» - в обеих таблицах data.tables, значения которых формируются путем выполнения -as.numeric(date). Это присваивает каждой дате уникальное значение. Более того, поскольку значения были умножены на -1, их сортировка по возрастанию приводит к сортировке дат по убыванию.

(Еще одна деталь: поскольку вам не нужны точные совпадения, а вместо этого всегда нужна следующая дата после текущей, я вычел 1 из rnd sampleticker, что дает желаемый эффект. Чтобы убедиться, что он выполняет свою работу правильно, Я немного отредактировал данные вашего примера, чтобы включить одно возможное точное совпадение ("2005-1-25"), которое не должно выбираться при слиянии).

# Create sample data.tables
library(data.table)

ST <- data.table(ticker = c("A","A","AA","AA"),
                 date = as.Date(c("2005-1-25","2005-03-30","2005-02-15",
                                  "2005-04-21"), format="%Y-%m-%d"),
                 alpha = letters[1:4])    

SR <- data.table(ticker = c("A","A","A","AA","AA","AA"),
                 rdate = as.Date(c("2005-1-25","2005-03-15","2005-04-15",
                                   "2005-03-01","2005-04-20","2005-05-01"), 
                                   format="%Y-%m-%d"),
                 ALPHA = LETTERS[1:6])

Имея в руках образцы данных, настройте и выполните желаемое слияние:

# Create a "reverse numerical date" column, which will uniquely
# identify date, and allow them to be sorted in reverse temporal order
ST$rnd <- -(as.numeric(ST$date) + 1)
SR$rnd <- -(as.numeric(SR$rdate))

# key (and thus sort) both data.tables by ticker and "reverse numerical date"
keycols <- c("ticker", "rnd")
setkeyv(ST, keycols)
setkeyv(SR, keycols)

# The syntax of the merge is now as simple as can be
res <- SR[ST, roll=TRUE]

# Finally, put the results back in temporal order, and pretty up the column order
setkeyv(res, c("ticker", "date"))
setcolorder(res, c("ticker", "date", "rdate", "alpha", "ALPHA", "rnd"))
res
#      ticker       date      rdate alpha ALPHA    rnd
# [1,]      A 2005-01-25 2005-03-15     a     B -12809
# [2,]      A 2005-03-30 2005-04-15     b     C -12873
# [3,]     AA 2005-02-15 2005-03-01     c     D -12830
# [4,]     AA 2005-04-21 2005-05-01     d     F -12895
person Josh O'Brien    schedule 14.02.2012
comment
Это также отличное решение, и оно работает намного быстрее, чем принятое решение. - person Matt; 15.02.2012
comment
Очень хорошо! Я не проверял это полностью, но концепцию я понял. Хорошо, ребята, вы меня убедили: [.data.table нужен новый аргумент, чтобы отбросить следующее наблюдение, не так ли. Между прочим, это простой внутренний переключатель. Варианты: revroll, rollback, rollbacktofirst, _5 _, _ 6_ или какая-то комбинация? Или, скорее, чем новый аргумент (аргументы), roll=-1|0|1 будет означать after|equal|previous, где TRUE и FALSE не нуждаются в изменении, поскольку они соответствуют предыдущему и равному соответственно. - person Matt Dowle; 16.02.2012
comment
Извините за поздний ответ. Было бы здорово, если бы вы могли добавить этот дополнительный аргумент! Для моих текущих целей достаточно добавить параметр в roll=-1. Тем не менее, я вижу, где вам может понадобиться rolltofirst или что-то в этом роде, поэтому имеет смысл добавить любую функциональность, необходимую для дублирования существующих roll функций в противоположном направлении. Спасибо за внимание! - person Matt; 27.03.2012