Here is another way to do this without changing or grouping, which could speed things up. If this is a small number of lines, then this will probably not be a noticeable difference.
cols<-names(pop)[-1] combs<-list() for(i in 2:length(cols)) { combs[[length(combs)+1]]<-c(cols[i-1], cols[i]) } newnames<-sapply(combs,function(x) gsub('N_surv','death',x[2])) deathpop<-copy(pop) deathpop[,(newnames):=lapply(combs,function(x) get(x[2])-get(x[1]))] deathpop[,(cols):=NULL]
I did some tests
rows<-10000000 pop <- data.table(group_id = 1:rows, N = runif(rows,3000,4000), N_surv_1 = runif(rows,3000,4000), N_surv_2 = runif(rows,3000,4000), N_surv_3 = runif(rows,3000,4000)) system.time({ cols<-names(pop)[-1] combs<-list() for(i in 2:length(cols)) { combs[[length(combs)+1]]<-c(cols[i-1], cols[i]) } newnames<-sapply(combs,function(x) gsub('N_surv','death',x[2])) deathpop<-copy(pop) deathpop[,(newnames):=lapply(combs,function(x) get(x[2])-get(x[1]))] deathpop[,(cols):=NULL]})
and he returned
user system elapsed 0.192 0.808 1.003
In contrast, I did
system.time(pop[, as.list(diff(unlist(.SD))), group_id])
and he returned
user system elapsed 169.836 0.428 170.469
I also did
system.time({ ncols = grep("^N(_surv_[0-9]+)?", names(pop), value=TRUE) pop[, Map( `-`, utils:::tail.default(.SD, -1), utils:::head.default(.SD, -1) ), .SDcols=ncols] })
who returned
user system elapsed 0.044 0.044 0.089
Finally making
system.time(melt(pop, id="group_id")[, tail(value, -1) - head(value, -1), by=group_id])
returns
user system elapsed 223.360 1.736 225.315
Frank Map solution is the fastest. If you take a copy from mine, then it becomes much closer to Frank's time, but he still wins in this case.