A sequential, cumulative calculation
I need to make a time-series calculation, where the value calculated in each row depends on the result calculated in the previous row. I am hoping to use the convenience of data.table
. The actual problem is a hydrological model -- a cumulative water balance calculation, adding rainfall at each time step and subtracting runoff and evaporation as a function of the current water volume. The dataset includes different basins and scenarios (groups). Here I will use a simpler illustration of the problem.
A simplified example of the calculation looks like this, for each time step (row) i
:
v[i] <- a[i] + b[i] * v[i-1]
a
and b
are vectors of parameter values, and v
is the result vector. For the first row (i == 1
) the initial value of v
is taken as v0 = 0
.
First attempt
My first thought was to use shift()
in data.table
. A minimal example, including the desired result v.ans
, is
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
This doesn't work, because shift(v)
gives a copy of the original column v
, shifted by 1 row. It is unaffected by assignment to v
.
I also considered building the equation using cumsum() and cumprod(), but that won't work either.
Brute force approach
So I resort to a for loop inside a function for convenience:
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)
}
This cumulative function works fine with 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
My question
My question is, can I write this calculation in a more concise and efficient data.table
way, without having to use the for loop and/or function definition? Using set()
perhaps?
Or is there a better approach all together?
Edit: A better loop
David's Rcpp solution below inspired me to remove the ifelse()
from the for
loop:
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()
is 60% faster than vcalc()
.