Is there a way to calculate the sum (or mean, etc.) for a range of values that are defined for that row in another column?
Here's some sample data:
structure(list(start = c("cmi_apr", "cmi_may", "cmi_may"), end = c("cmi_oct",
"cmi_oct", "cmi_dec"), cmi_jan = c(2.35, 2.24, 37.66), cmi_feb = c(1.33,
5.65, 43.23), cmi_mar = c(0.08, 4.43, 22.2), cmi_apr = c(0.17,
6.48, 18.56), cmi_may = c(-5.61, 0.54, 21.52), cmi_jun = c(-6.37,
-0.92, 13.86), cmi_jul = c(-6.53, 5.18, 2.81), cmi_aug = c(-2.37,
4.4, 21.32), cmi_sep = c(1.28, 0.92, 19.48), cmi_oct = c(0.33,
11.21, 26.43), cmi_nov = c(1.41, 9.18, 43.87), cmi_dec = c(2.21,
10.96, 30.54)), row.names = c(NA, -3L), class = c("tbl_df", "tbl",
"data.frame"))
I want to generate range sums based on the start
and end
variable like this:
I have a solution, however my real dataset has over 60,000 rows and it takes way too long to complete the calculation. I figure this should be a lot faster since addition is vectorized. Here's my current solution:
compute_growing_season <- function(df, start_colname, end_colname, FUN) {
# Generate column index vectors
start_idx = sapply(start_colname, function(x) { which(x == names(df))} )
end_idx = sapply(end_colname, function(x) { which(x == names(df))} )
# Generate computed vector
results <- numeric(nrow(df))
for (i in 1:nrow(df)) {
results[i] <- FUN(df[i, start_idx[i]:end_idx[i]], na.rm = F)
}
return(results)
}
output <- sample %>%
mutate(
cmi_growingseason_sum = compute_growing_season(., start, end, sum)
)
There might be better names than sample
for a tibble
. In dplyr
syntax (data and code suggest you are already using those packages), you might want to start developing something from here. Staying in long format may bring advantages.
library(tidyr)
library(dplyr)
sample |>
rename_with(~sub('^cmi_', '', .), starts_with('cmi')) |>
mutate(row = row_number(), start = sub('^cmi_', '', start),
end = sub('^cmi_', '', end), .before = start) |>
pivot_longer(cols = -c(row, start, end), names_to = 'month', values_to = 'value') |>
mutate(across(c(start, end, month), ~match(., tolower(month.abb)), .names = '{.col}_i')) |>
mutate(gs_sum = sum(value[between(month_i, start_i, end_i)]), .by = row) |>
pivot_wider(id_cols = -ends_with('_i'), names_from = 'month', values_from = 'value')
# A tibble: 3 × 15
start end gs_sum jan feb mar apr may jun jul aug sep oct nov dec
<chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 apr oct -19.1 2.35 1.33 0.08 0.17 -5.61 -6.37 -6.53 -2.37 1.28 0.33 1.41 2.21
2 may oct 21.3 2.24 5.65 4.43 6.48 0.54 -0.92 5.18 4.4 0.92 11.2 9.18 11.0
3 may dec 180. 37.7 43.2 22.2 18.6 21.5 13.9 2.81 21.3 19.5 26.4 43.9 30.5
If you like the idea of going from wide to long back to wide. Surely, there is room for optimisation, but not needed at 60k rows. Included some re-arrangements: removing cmi
-prefix; if you need it, keep it instead.
Base R. (1) Reshaping (might be useful later), (2) Months to integer representation and aggregation.
names(sample) = sub('^cmi_', '', names(sample))
sample[c('start', 'end')] = lapply(sample[c('start', 'end')],
\(i) match(sub('^cmi_', '', i), tolower(month.abb)))
reshape(as.data.frame(sample), varying=tolower(month.abb),
v.names='value', timevar='month', direction='l') |>
subset(month >= start & month <= end) |>
aggregate(value~id, data=_, sum) # id = row
## or aggregate(cbind(gs_sum=value)~cbind(row=id), data=_, sum) to re-name
id value
1 1 -19.10
2 2 21.33
3 3 179.83