Iterar pelas colunas para somar os 2 números anteriores de cada linha

Aug 15 2020

No R, tenho um dataframe, com as colunas 'A', 'B', 'C', 'D'. As colunas têm 100 linhas.

Eu preciso iterar pelas colunas para realizar um cálculo para todas as linhas no dataframe que soma as 2 linhas anteriores dessa coluna e, em seguida, definir em novas colunas ('AA', 'AB', etc) qual é essa soma:

A  B  C  D 
1  2  3  4
2  3  4  5
3  4  5  6
4  5  6  7 
5  6  7  8
6  7  8  9

para

A   B   C   D   AA   AB   AC   AD
1   2   3   4   NA   NA   NA   NA
2   3   4   5   3    5    7    9
3   4   5   6   5    7    9    11
4   5   6   7   7    9    11   13
5   6   7   8   9    11   13   15
6   7   8   9   11   13   15   17

Alguém pode explicar como criar uma função/loop que me permita definir as colunas que desejo iterar (colunas selecionadas, nem todas as colunas) e as colunas que desejo definir?

Respostas

2 DarrenTsai Aug 15 2020 at 17:50

Uma baselinha:

cbind(df, setNames(df + df[c(NA, 1:(nrow(df)-1)), ], paste0("A", names(df))))

Se seus dados forem grandes, este pode ser o mais rápido porque manipula todo o arquivo data.frame.


Uma dplyrsolução usando mutate()com across().

library(dplyr)

df %>%
  mutate(across(A:D,
                ~ .x + lag(.x),
                .names = "A{col}"))

#   A B C D AA AB AC AD
# 1 1 2 3 4 NA NA NA NA
# 2 2 3 4 5  3  5  7  9
# 3 3 4 5 6  5  7  9 11
# 4 4 5 6 7  7  9 11 13
# 5 5 6 7 8  9 11 13 15
# 6 6 7 8 9 11 13 15 17

Se você quiser somar as 3 linhas anteriores, o segundo argumento de across(), ou seja .fns, deve ser

~ .x + lag(.x) + lag(.x, 2)

que é equivalente ao uso de rollsum()em zoo:

~ zoo::rollsum(.x, k = 3, fill = NA, align = 'right')

referência

Um teste de benchmark com microbenchmarkpacote em um novo data.framecom 10.000 linhas e 100 colunas e avalia cada expressão por 10 vezes.

# Unit: milliseconds
#                     expr        min         lq       mean     median         uq        max neval
#              darren_base   18.58418   20.88498   35.51341   33.64953   39.31909   80.24725    10
#         darren_dplyr_lag   39.49278   40.27038   47.26449   42.89170   43.20267   76.72435    10
# arg0naut91_dplyr_rollsum  436.22503  482.03199  524.54800  516.81706  534.94317  677.64242    10
#    Grothendieck_rollsumr 3423.92097 3611.01573 3650.16656 3622.50895 3689.26404 4060.98054    10
1 arg0naut91 Aug 15 2020 at 17:49

Você pode usar dplyr's across(e definir nomes opcionais) com soma contínua (conforme implementado, por exemplo, em zoo):

library(dplyr)
library(zoo)

df %>%
  mutate(
    across(
      A:D,
      ~ rollsum(., k = 2, fill = NA, align = 'right'), 
      .names = 'A{col}'
    )
  )

Resultado:

  A B C D AA AB AC AD
1 1 2 3 4 NA NA NA NA
2 2 3 4 5  3  5  7  9
3 3 4 5 6  5  7  9 11
4 4 5 6 7  7  9 11 13
5 5 6 7 8  9 11 13 15
6 6 7 8 9 11 13 15 17

Com A:Despecificamos o intervalo de nomes de colunas aos quais queremos aplicar a função. A suposição acima no .namesargumento é que você deseja colar junto Acomo prefixo e o nome da coluna ( {col}).

1 pseudospin Aug 15 2020 at 19:34

Aqui está uma solução data.table. Conforme você pede, ele permite que você selecione em quais colunas deseja aplicá-lo, em vez de apenas para todas as colunas.

library(data.table)
x <- data.table(A=1:6, B=2:7, C=3:8, D=4:9)
selected_cols <- c('A','B','D')
new_cols <- paste0("A",selected_cols)
x[, (new_cols) := lapply(.SD, function(col) col+shift(col, 1)), .SDcols = selected_cols]
x[]

NB Isso é 2 ou 3 vezes mais rápido que a outra resposta mais rápida.

1 ivan866 Aug 15 2020 at 17:59

Essa é uma abordagem ingênua com forloops aninhados. Cuidado, é muito lento se você for iterar centenas de milhares de linhas.

i <- 1
n <- 5
df <- data.frame(A=i:(i+n), B=(i+1):(i+n+1), C=(i+2):(i+n+2), D=(i+3):(i+n+3))
for (col in colnames(df)) {
  for (ind in 1:nrow(df)) {
    if (ind-1==0) {next}
    s <- sum(df[c(ind-1, ind), col])
    df[ind, paste0('S', col)] <- s
  }
}  

Isso é um cumsummétodo:

na.df <- data.frame(matrix(NA, 2, ncol(df)))
colnames(na.df) <- colnames(df)
cs1 <- cumsum(df)
cs2 <- rbind(cs1[-1:-2,], na.df)
sum.diff <- cs2-cs1
cbind(df, rbind(na.df[1,], cs1[2,], sum.diff[1:(nrow(sum.diff)-2),]))  

Referência:

#    Unit: milliseconds  
#                      expr     min       lq     mean   median       uq     max neval  
#          darrentsai.rbind 11.5623 12.28025 23.38038 16.78240 20.83420 91.9135   100  
#     darrentsai.rbind.rev1  8.8267  9.10945 15.63652  9.54215 14.25090 62.6949   100  
#             pseudopsin.dt  7.2696  7.52080 20.26473 12.61465 17.61465 69.0110   100  
#            ivan866.cumsum 25.3706 30.98860 43.11623 33.78775 37.36950 91.6032   100  

Acredito que, na maioria das vezes, o método cumsum desperdiça em alocações df. Se adaptado corretamente ao back-end data.table, pode ser o mais rápido.

1 G.Grothendieck Aug 16 2020 at 03:14

Especifique as colunas que queremos. Mostramos várias maneiras diferentes de fazer isso. Em seguida, use rollsumrpara obter as colunas desejadas, defina os nomes das colunas e cbind DFcom elas.

library(zoo)

# jx <- names(DF)  # if all columns wanted
# jx <- sapply(DF, is.numeric)  # if all numeric columns
# jx <- c("A", "B", "C", "D")  # specify columns by name
jx <- 1:4   # specify columns by position

r <- rollsumr(DF[jx], 2, fill = NA)
colnames(r) <- paste0("A", colnames(r))
cbind(DF, r)

dando:

  A B C D AA AB AC AD
1 1 2 3 4 NA NA NA NA
2 2 3 4 5  3  5  7  9
3 3 4 5 6  5  7  9 11
4 4 5 6 7  7  9 11 13
5 5 6 7 8  9 11 13 15
6 6 7 8 9 11 13 15 17

Observação

A entrada em forma reproduzível:

DF <- structure(list(A = 1:6, B = 2:7, C = 3:8, D = 4:9), 
  class = "data.frame", row.names = c(NA, -6L))