Scorri le colonne per sommare i 2 numeri precedenti di ogni riga

Aug 15 2020

In R, ho un dataframe, con colonne 'A', 'B', 'C', 'D'. Le colonne hanno 100 righe.

Ho bisogno di scorrere le colonne per eseguire un calcolo per tutte le righe nel dataframe che somma le 2 righe precedenti di quella colonna, quindi impostare in nuove colonne ('AA', 'AB', ecc.) qual è quella somma:

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

a

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

Qualcuno può spiegare come creare una funzione/loop che mi permetta di impostare le colonne su cui voglio iterare (colonne selezionate, non tutte le colonne) e le colonne che voglio impostare?

Risposte

2 DarrenTsai Aug 15 2020 at 17:50

Una baseriga:

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

Se i tuoi dati sono di grandi dimensioni, questo potrebbe essere il più veloce perché manipola l'intero file data.frame.


Una dplyrsoluzione che usa mutate()con 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 vuoi sommare le 3 righe precedenti, il secondo argomento di across(), ie .fns, dovrebbe essere

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

che è equivalente all'uso di rollsum()in zoo:

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

Prova delle prestazioni

Un test di benchmark con microbenchmarkpacchetto su un nuovo data.framecon 10000 righe e 100 colonne e valuta ogni espressione per 10 volte.

# 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

Puoi usare dplyr's across(e impostare nomi facoltativi) con somma mobile (come implementato ad esempio in zoo):

library(dplyr)
library(zoo)

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

Produzione:

  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

Con A:Dabbiamo specificato l'intervallo di nomi di colonna a cui vogliamo applicare la funzione. Il presupposto sopra .namesnell'argomento è che si desidera incollare insieme Acome prefisso e il nome della colonna ( {col}).

1 pseudospin Aug 15 2020 at 19:34

Ecco una soluzione data.table. Come richiesto, ti consente di selezionare a quali colonne desideri applicarlo anziché solo a tutte le colonne.

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 Questa è 2 o 3 volte più veloce dell'altra risposta più veloce.

1 ivan866 Aug 15 2020 at 17:59

Questo è un approccio ingenuo con forloop nidificati. Fai attenzione, è dannatamente lento se esegui l'iterazione su centinaia di migliaia di righe.

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
  }
}  

Questo è un cumsummetodo:

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),]))  

Prova delle prestazioni:

#    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  

Credo che la maggior parte delle volte il metodo cumsum sprechi nelle allocazioni df. Se correttamente adattato al backend data.table, potrebbe essere il più veloce.

1 G.Grothendieck Aug 16 2020 at 03:14

Specifica le colonne che vogliamo. Mostriamo diversi modi per farlo. Quindi usa rollsumrper ottenere le colonne desiderate, imposta i nomi delle colonne e cbind DFcon esso.

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

Nota

L'input in forma riproducibile:

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