How to do a running calculation across vectors in R?

Viewed 116

I created this data frame:

Count <- c(1:10)
Give <- c(0,0,5,0,0,5,0,5,0,5)
X <- c(rep(0,10))
Y <- c(rep(0,10))
Z <- c(rep(0,10))

X_Target <- 5
Y_Target <- 10
Z_Target <- 5

Basically I have 3 vectors (X,Y,Z) and a target for each one of them. I want to have a new calculation for X,Y and Z that based on the vector Give. Once the number on Give is bigger than 0 then it's need to be added to Vector X until it equel to X_Target. Then - the calcultion need to move to the next vector (Y) and do the same, and then to next vector...

The output should be like the following:

   Count Give X Y  Z
     1    0   0 0  0
     2    0   0 0  0
     3    5   5 0  0
     4    0   5 0  0
     5    0   5 0  0
     6    5   5 5  0
     7    0   5 5  0
     8    5   5 10 0
     9    0   5 10 0
    10    5   5 10 5

In this example I have only 3 vectors but please keep in mind that I'll have at least 60 vectors so I need it to be automatic as it can.

Hope I manage to explain myself :) Thnanks!

4 Answers

Turn it into a data frame :

tab1 = data.frame(
  Count = c(1:10),
  Give =c(0,0,5,0,0,5,0,5,0,5),
  X = c(rep(0,10)),
  Y = c(rep(0,10)),
  Z = c(rep(0,10))
)

# create a list of targets for looping
targets = c(X_Target, Y_Target, Z_Target)

Without using data.table you can just put the whole thing in a loop. It will work, but be much slower.

# loop through each column
for(col in seq(1,length(targets))){
    print(col)
  
  # loop through each row
  for(row in seq(1, dim(tab1[2+col])[1])){
    
    # condition
    while(tab1[row,(2+col)] < targets[col] & tab1[row,2]>0){
      tab1[row,(2+col)] = tab1[row,(2+col)] +tab1[row,2] 
    }
  }
}

My approach was to make use of the cumulative sums of the Give and then track if that exceeds the targeted values for the columns. Then do some cleaning up.

targets <- c(X_Target, Y_Target, Z_Target)
targets_0 <- c(0, targets)
csum_give <- cumsum(Give)

# from cumsum give take off sum of previous targets 
result  <- sapply(1:length(targets), 
            function(x) csum_give - sum(targets_0[1:x]))

# Set max value to target max of column
sapply(1:length(targets), 
    function(x) result[result[, x] > targets[x], x] <<- targets[x])

# set min value to zero
result[which(result < 0)] <- 0


result
#       [,1] [,2] [,3]
#  [1,]    0    0    0
#  [2,]    0    0    0
#  [3,]    5    0    0
#  [4,]    5    0    0
#  [5,]    5    0    0
#  [6,]    5    5    0
#  [7,]    5    5    0
#  [8,]    5   10    0
#  [9,]    5   10    0
# [10,]    5   10    5

It's ugly, but it gives the desired result.


tab1 = data.frame(
  Count = c(1:10),
  Give = c(0,0,5,0,0,5,0,5,0,5),
  X = c(rep(0,10)),
  Y = c(rep(0,10)),
  Z = c(rep(0,10))
)

targets <- c(5,10,5)
tab2 <- tab1
start <- 2

for(col in 3:ncol(tab2)) {
  
  target <- targets[col-2]
  
  
  
  for(row in start:nrow(tab2)) {
    
    if(tab2[row, 2] > 0 & tab2[row, col] < target) {
      tab2[row, col] <- pmin(tab2[row - 1, col] + tab2[row, col - 1], target)
    } else {
      tab2[row, col] <- tab2[row - 1, col]
    }
    
  }
  
  start <- which(tab2[, 2] > 0 & tab2[, col] == target)[2]
}

> tab2
   Count Give X  Y Z
1      1    0 0  0 0
2      2    0 0  0 0
3      3    5 5  0 0
4      4    0 5  0 0
5      5    0 5  0 0
6      6    5 5  5 0
7      7    0 5  5 0
8      8    5 5 10 0
9      9    0 5 10 0
10    10    5 5 10 5

Here is something else to try, using tidyverse.

Put your data into long form, and include targets with a join.

In a loop through Count, find the first row for a given Count that is below target. For current and following rows that have matching names (X, Y, or Z), add Give amount.

In the end, put result back into wide form.

library(tidyverse)

df <- data.frame(Count, Give, X, Y, Z) %>%
  pivot_longer(cols = X:Z) %>%
  left_join(data.frame(X_Target, Y_Target, Z_Target) %>%
              pivot_longer(cols = everything(), 
                           names_to = c("name", ".value"), 
                           names_pattern = "(\\w+)_(\\w+)")) 

for (i in seq_along(Count)) {
  below_target <- min(which(df$Count == i & df$value < df$Target))
  name_rows <- which(df$name == df[below_target, "name", drop = T])
  rows_to_change <- name_rows[name_rows >= below_target]
  df[rows_to_change, "value"] <- df[rows_to_change, "value"] + df[below_target, "Give", drop = T]
}

df %>%
  pivot_wider(id_cols = Count)

Output

   Count     X     Y     Z
   <int> <dbl> <dbl> <dbl>
 1     1     0     0     0
 2     2     0     0     0
 3     3     5     0     0
 4     4     5     0     0
 5     5     5     0     0
 6     6     5     5     0
 7     7     5     5     0
 8     8     5    10     0
 9     9     5    10     0
10    10     5    10     5
Related