How to keep dropping the first value, until the sum of the vector is less than 20?

Viewed 1269

I am looking for a function which takes a vector and keeps dropping the first value until the sum of the vector is less than 20. Return the remaining values.

I've tried both a for-loop and while-loop and can't find a solution.

vec <- c(3,5,3,4,3,9,1,8,2,5)

short <- function(vec){

 for (i in 1:length(vec)){
    while (!is.na((sum(vec)) < 20)){
      vec <- vec[i+1:length(vec)]
      #vec.remove(i)
  }
}

The expected output should be: 1,8,2,5 which is less than 20.

4 Answers

Looking at the expected output it looks like you want to drop values until sum of remaining values is less than 20.

We can create a function

drop_20 <- function(vec) {
  tail(vec, sum(cumsum(rev(vec)) < 20))
}

drop_20(vec)
#[1] 1 8 2 5

Trying it on another input

drop_20(1:10)
#[1]  9 10

Breaking down the function, first the vec

vec = c(3,5,3,4,3,9,1,8,2,5)

We then reverse it

rev(vec)
#[1] 5 2 8 1 9 3 4 3 5 3

take cumulative sum over it (cumsum)

cumsum(vec)
#[1]  3  8 11 15 18 27 28 36 38 43

Find out number of enteries that are less than 20

cumsum(rev(vec)) < 20
 #[1]  TRUE  TRUE  TRUE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE

sum(cumsum(rev(vec)) < 20)
#[1] 4

and finally subset these last enteries using tail.


A slight modification in the code and it should be able to handle NAs as well

drop_20 <- function(vec) {
   tail(vec, sum(cumsum(replace(rev(vec), is.na(rev(vec)), 0)) < 20))
}

vec = c(3, 2, NA, 4, 5, 1, 2, 3, 4, 9, NA, 1, 2)
drop_20(vec)
#[1]  3  4  9 NA  1  2

The logic being we replace NA with zeroes and then take the cumsum

You need to remove the first value each time, so your while loop should be,

while (sum(x, na.rm = TRUE) >= 20) {
    x <- x[-1]
}

#[1] 1 8 2 5

base solution without loops
not my most readable code ever, but it's pretty fast (see benchmarking below)

rev( rev(vec)[cumsum( replace( rev(vec), is.na( rev(vec) ), 0 ) ) < 20] )
#[1] 1 8 2 5

note: 'borrowed' the NA-handling from @Ronak's answer

sample data
vec = c(3, 2, NA, 4, 5, 1, 2, 3, 4, 9, NA, 1, 2)

benchmarks

microbenchmark::microbenchmark(
  Sotos = { 
    while (sum(vec, na.rm = TRUE) >= 20) {
      vec <- vec[-1] 
    } 
  },
  Ronak = tail(vec, sum(cumsum(replace(rev(vec), is.na(rev(vec)), 0)) < 20)),
  Wimpel = rev( rev(vec)[cumsum( replace( rev(vec), is.na( rev(vec) ), 0 ) ) < 20]),
  WimpelMarkus = vec[rev(cumsum(rev(replace(vec, is.na(vec), 0))) < 20)]
  )


# Unit: microseconds
#         expr      min       lq       mean    median        uq      max neval
#        Sotos 2096.795 2127.373 2288.15768 2152.6795 2425.4740 3071.684   100
#        Ronak   30.127   33.440   42.54770   37.2055   49.4080  101.827   100
#       Wimpel   13.557   15.063   17.65734   16.1175   18.5285   38.261   100
# WimpelMarkus    7.532    8.737   12.60520   10.0925   15.9680   45.491   100

I would go with Reduce

vec[Reduce(f = "+", x = vec, accumulate = T, right = T) < 20]
##[1] 1 8 2 5

Alternatively, define Reduce with function sum with the conditional argument na.rm = T in order to hanlde NAs if desired:

vec2 <- c(3, 2, NA, 4, 5, 1, 2, 3, 4, 9, NA, 1, 2)
vec2[Reduce(f = function(a,b) sum(a, b, na.rm = T), x = vec2, accumulate = TRUE, right = T) < 20]
##[1]  3  4  9 NA  1  2

I find the Reduce option to start from right (end of the integer vector), and hence not having to reverse it first, convenient.

Related