89_Simple
89_Simple

Reputation: 3805

Avoiding the use of for loop for cumsum

First generating some sample data:

 doy <- rep(1:365,times=2)
 year <- rep(2000:2001,each=365)
 set.seed(1)
 value <-runif(min=0,max=10,365*2)
 doy.range <- c(40,50,60,80)
 thres <- 200

 df <- data.frame(cbind(doy,year,value))

What I want to do is the following:

For the df$year == 2000, starting from doy.range == 40, start adding the df$value and calculate the df$doy when the cumualtive sum of df$value is >= thres

Here's my long for loop to achieve this:

# create a matrix to store results

 mat <- matrix(, nrow = length(doy.range)*length(unique(year)),ncol=3)
 mat[,1] <- rep(unique(year),each=4)
 mat[,2] <- rep(doy.range,times=2)

for(i in unique(df$year)){

     dat <- df[df$year== i,]

       for(j in doy.range){

         dat1 <- dat[dat$doy >= j,]
         dat1$cum.sum <-cumsum(dat1$value) 
         day.thres <- dat1[dat1$cum.sum >= thres,"doy"][1] # gives me the doy of the year where cumsum of df$value becomes >= thres
        mat[mat[,2] == j & mat[,1] == i,3] <- day.thres
  }
}

This loop gives me the in the third column of my matrix, the doy when cumsum$value exceeded thres

However, I really want to avoid the loops. Is there any way I can do it using less code?

Upvotes: 2

Views: 761

Answers (2)

Martin Schmelzer
Martin Schmelzer

Reputation: 23889

If I understand correctly you can use dplyr. Assume a threshold of 200:

library(dplyr)
df %>% group_by(year) %>% 
  filter(doy >= 40) %>% 
  mutate(CumSum = cumsum(value)) %>% 
  filter(CumSum >= 200) %>% 
  top_n(n = -1, wt = CumSum)

which yields

# A tibble: 2 x 4
# Groups:   year [2]
    doy  year    value   CumSum
  <dbl> <dbl>    <dbl>    <dbl>
1    78  2000 3.899895 201.4864
2    75  2001 9.205178 204.3171

The verbs used are self-explanatory I guess. If not, let me know.

For different doy create a function and use lapply:

f <- function(doy.range) {
  df %>% group_by(year) %>% 
    filter(doy >= doy.range) %>% 
    mutate(CumSum = cumsum(value)) %>% 
    filter(CumSum >= 200) %>% 
    top_n(n = -1, wt = CumSum)
}

lapply(doy.range, f)

[[1]]
# A tibble: 2 x 4
# Groups:   year [2]
    doy  year    value   CumSum
  <dbl> <dbl>    <dbl>    <dbl>
1    78  2000 3.899895 201.4864
2    75  2001 9.205178 204.3171

[[2]]
# A tibble: 2 x 4
# Groups:   year [2]
    doy  year    value   CumSum
  <dbl> <dbl>    <dbl>    <dbl>
1    89  2000 2.454885 200.2998
2    91  2001 6.578281 200.6544

[[3]]
# A tibble: 2 x 4
# Groups:   year [2]
    doy  year    value   CumSum
  <dbl> <dbl>    <dbl>    <dbl>
1    98  2000 4.100841 200.5048
2   102  2001 7.158333 200.3770

[[4]]
# A tibble: 2 x 4
# Groups:   year [2]
    doy  year    value   CumSum
  <dbl> <dbl>    <dbl>    <dbl>
1   120  2000 6.401010 204.9951
2   120  2001 5.884192 200.8252

Upvotes: 3

AntoniosK
AntoniosK

Reputation: 16121

The idea is to create a function that based on a given (starting) doy and threshold gets you the relevant info. Then apply this function to different combinations of starting doys and thresholds and get a dataset back with all relevant info:

# create example data
doy <- rep(1:365,times=2)
year <- rep(2000:2001,each=365)
set.seed(1)
value <-runif(min=0,max=10,365*2)

df <- data.frame(doy,year,value)


library(dplyr)
library(purrr)

# function (inputs: dr for doy range and t for threshold)
f = function(dr, t) {

  df %>% 
    filter(doy >= dr) %>%                    # keep rows with values aboven a given doy
    group_by(year) %>%                       # for each year
    mutate(CumSumValue = cumsum(value)) %>%  # get the cumulative sum of value
    filter(CumSumValue >= t) %>%             # keep rows equal or above a given threshold
    slice(1) %>%                             # keep the first row
    ungroup() %>%                            # forget the grouping
    select(-value) %>%                       # remove unnecessary variable
    mutate(doy_input=dr, thres_input=t) %>%  # add the input info as columns
    select(doy_input, thres_input, year, doy, CumSumValue)  # re arrange columns 

}

# input doy and threshold
doy.range <- c(40,50,60,80)
thres <- 200

# map those vectors to the function
map2_df(doy.range, thres, f)

# # A tibble: 8 x 5
#   doy_input thres_input  year   doy CumSumValue
#       <dbl>       <dbl> <int> <int>       <dbl>
# 1        40         200  2000    78    201.4864
# 2        40         200  2001    75    204.3171
# 3        50         200  2000    89    200.2998
# 4        50         200  2001    91    200.6544
# 5        60         200  2000    98    200.5048
# 6        60         200  2001   102    200.3770
# 7        80         200  2000   120    204.9951
# 8        80         200  2001   120    200.8252

Upvotes: 3

Related Questions