Как написать кумулятивный расчет в data.table
Последовательный, кумулятивный расчет
Мне нужно сделать подсчет временных рядов, где значение, вычисленное в каждой строке, зависит от результата, вычисленного в предыдущей строке. Я надеюсь использовать удобство data.table
. Фактической проблемой является гидрологическая модель - расчет совокупного баланса воды, добавление осадков на каждом временном шаге и вычитание стока и испарения в зависимости от текущего объема воды. В набор данных входят различные бассейны и сценарии (группы). Здесь я буду использовать более простую иллюстрацию проблемы.
Упрощенный пример расчета выглядит так: для каждого временного шага (строки) i
:
v[i] <- a[i] + b[i] * v[i-1]
a
и b
- векторы значений параметров, а v
- это вектор результата. Для первой строки (i == 1
) начальное значение v
принимается за v0 = 0
.
Первая попытка
Моя первая мысль заключалась в использовании shift()
в data.table
. Минимальным примером, включая желаемый результат v.ans
, является
library(data.table) # version 1.9.7
DT <- data.table(a = 1:4,
b = 0.1,
v.ans = c(1, 2.1, 3.21, 4.321) )
DT
# a b v.ans
# 1: 1 0.1 1.000
# 2: 2 0.1 2.100
# 3: 3 0.1 3.210
# 4: 4 0.1 4.321
DT[, v := NA] # initialize v
DT[, v := a + b * ifelse(is.na(shift(v)), 0, shift(v))][]
# a b v.ans v
# 1: 1 0.1 1.000 1
# 2: 2 0.1 2.100 2
# 3: 3 0.1 3.210 3
# 4: 4 0.1 4.321 4
Это не работает, потому что shift(v)
дает копию исходного столбца v
, сдвинутого на 1 строку. Это не зависит от назначения v
.
Я также подумал о построении уравнения с помощью cumsum() и cumprod(), но это тоже не сработает.
Подход к грубой силе
Поэтому я прибегаю к циклу for внутри функции для удобства:
vcalc <- function(a, b, v0 = 0) {
v <- rep(NA, length(a)) # initialize v
for (i in 1:length(a)) {
v[i] <- a[i] + b[i] * ifelse(i==1, v0, v[i-1])
}
return(v)
}
Эта кумулятивная функция отлично работает с data.table:
DT[, v := vcalc(a, b, 0)][]
# a b v.ans v
# 1: 1 0.1 1.000 1.000
# 2: 2 0.1 2.100 2.100
# 3: 3 0.1 3.210 3.210
# 4: 4 0.1 4.321 4.321
identical(DT$v, DT$v.ans)
# [1] TRUE
Мой вопрос
Мой вопрос: могу ли я написать этот расчет более кратким и эффективным способом data.table
, не используя определение for и/или функции? Использование set()
возможно?
Или существует лучший подход?
Изменить: лучший цикл
Решение David Rcpp ниже вдохновило меня на удаление ifelse()
из цикла for
:
vcalc2 <- function(a, b, v0 = 0) {
v <- rep(NA, length(a))
for (i in 1:length(a)) {
v0 <- v[i] <- a[i] + b[i] * v0
}
return(v)
}
vcalc2()
на 60% быстрее, чем vcalc()
.
Ответы
Ответ 1
Это может быть не 100% то, что вы ищете, так как оно не использует "data.table-way" и по-прежнему использует for-loop. Однако этот подход должен быть более быстрым (я предполагаю, что вы хотите использовать data.table и data.table-way для ускорения вашего кода). Я использую Rcpp для записи короткой функции под названием HydroFun
, которая может использоваться в R как любая другая функция (вам просто нужно сначала запустить функцию). Мое чувство кисти говорит мне, что метод data.table(если существует) довольно сложный, потому что вы не можете вычислить решение с закрытой формой (но я могу ошибаться в этой точке...).
Мой подход выглядит следующим образом:
Функция Rcpp выглядит так (в файле: hydrofun.cpp
):
#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
NumericVector HydroFun(NumericVector a, NumericVector b, double v0 = 0.0) {
// get the size of the vectors
int vecSize = a.length();
// initialize a numeric vector "v" (for the result)
NumericVector v(vecSize);
// compute v_0
v[0] = a[0] + b[0] * v0;
// loop through the vector and compute the new value
for (int i = 1; i < vecSize; ++i) {
v[i] = a[i] + b[i] * v[i - 1];
}
return v;
}
Для источника и использования функции в R вы можете:
Rcpp::sourceCpp("hydrofun.cpp")
library(data.table)
DT <- data.table(a = 1:4,
b = 0.1,
v.ans = c(1, 2.1, 3.21, 4.321))
DT[, v_ans2 := HydroFun(a, b, 0)]
DT
# a b v.ans v_ans2
# 1: 1 0.1 1.000 1.000
# 2: 2 0.1 2.100 2.100
# 3: 3 0.1 3.210 3.210
# 4: 4 0.1 4.321 4.321
Что дает результат, который вы ищете (по крайней мере, с точки зрения стоимости).
Сравнение скоростей показывает ускорение примерно в 65 раз.
library(microbenchmark)
n <- 10000
dt <- data.table(a = 1:n,
b = rnorm(n))
microbenchmark(dt[, v1 := vcalc(a, b, 0)],
dt[, v2 := HydroFun(a, b, 0)])
# Unit: microseconds
# expr min lq mean median uq max neval
# dt[, `:=`(v1, vcalc(a, b, 0))] 28369.672 30203.398 31883.9872 31651.566 32646.8780 68727.433 100
# dt[, `:=`(v2, HydroFun(a, b, 0))] 381.307 421.697 512.2957 512.717 560.8585 1496.297 100
identical(dt$v1, dt$v2)
# [1] TRUE
Помогает ли это вам каким-либо образом?
Ответ 2
Я думаю, что Reduce
вместе с accumulate = TRUE
является широко используемым методом для этих типов вычислений (см., например, рекурсивно, используя вывод как вход для функции). Это не обязательно быстрее, чем хорошо написанный цикл *, и я не знаю, как data.table
-если вы считаете, что это так, но я хочу предложить его для вашего инструментария.
DT[ , v := 0][
, v := Reduce(f = function(v, i) a[i] + b[i] * v, x = .I[-1], init = a[1], accumulate = TRUE)]
DT
# a b v.ans v
# 1: 1 0.1 1.000 1.000
# 2: 2 0.1 2.100 2.100
# 3: 3 0.1 3.210 3.210
# 4: 4 0.1 4.321 4.321
Пояснение:
Установите начальное значение v на 0
(v := 0
). Используйте Reduce
для применения функции f
к целочисленному вектору номеров строк, кроме первой строки (x = .I[-1]
). Вместо этого добавьте a[1]
в начало x
(init = a[1]
).
Reduce
затем "последовательно применяет f к элементам [...] слева направо".
Последовательные комбинации сокращения "накапливаются" (accumulate = TRUE
).
* См. здесь, где вы также можете узнать больше о Reduce
в this раздел.