使用 dplyr 跨多列求和

Sum across multiple columns with dplyr

我的问题涉及对数据框的多个列的值求和,并使用 dplyr 创建与该求和相对应的新列。列中的数据条目是二进制 (0,1)。我正在考虑 dplyrsummarise_eachmutate_each 函数的逐行模拟。以下是数据框的最小示例:

library(dplyr)
df=data.frame(
  x1=c(1,0,0,NA,0,1,1,NA,0,1),
  x2=c(1,1,NA,1,1,0,NA,NA,0,1),
  x3=c(0,1,0,1,1,0,NA,NA,0,1),
  x4=c(1,0,NA,1,0,0,NA,0,0,1),
  x5=c(1,1,NA,1,1,1,NA,1,0,1))

> df
   x1 x2 x3 x4 x5
1   1  1  0  1  1
2   0  1  1  0  1
3   0 NA  0 NA NA
4  NA  1  1  1  1
5   0  1  1  0  1
6   1  0  0  0  1
7   1 NA NA NA NA
8  NA NA NA  0  1
9   0  0  0  0  0
10  1  1  1  1  1

我可以使用类似的东西:

df <- df %>% mutate(sumrow= x1 + x2 + x3 + x4 + x5)

但这需要写出每一列的名称。我有 50 列。 此外,列名在我要实现它的循环的不同迭代中发生变化 操作,所以我想尽量避免提供任何列名。

我怎样才能最有效地做到这一点? 任何帮助将不胜感激。

dplyr >= 1.0.0 使用跨

使用 rowSums 对每一行求和(rowwise 适用于任何聚合,但速度较慢)

df %>%
   replace(is.na(.), 0) %>%
   mutate(sum = rowSums(across(where(is.numeric))))

对每一列求和

df %>%
   summarise(across(everything(), ~ sum(., is.na(.), 0)))

dplyr < 1.0.0

对每一行求和

df %>%
   replace(is.na(.), 0) %>%
   mutate(sum = rowSums(.[1:5]))

使用 superseeded summarise_all:

对每一列求和
df %>%
   replace(is.na(.), 0) %>%
   summarise_all(funs(sum))

如果你只想对某些列求和,我会使用这样的东西:

library(dplyr)
df=data.frame(
  x1=c(1,0,0,NA,0,1,1,NA,0,1),
  x2=c(1,1,NA,1,1,0,NA,NA,0,1),
  x3=c(0,1,0,1,1,0,NA,NA,0,1),
  x4=c(1,0,NA,1,0,0,NA,0,0,1),
  x5=c(1,1,NA,1,1,1,NA,1,0,1))
df %>% select(x3:x5) %>% rowSums(na.rm=TRUE) -> df$x3x5.total
head(df)

这样你就可以使用dplyr::select的语法了。

我会使用正则表达式匹配来对具有特定模式名称的变量求和。例如:

df <- df %>% mutate(sum1 = rowSums(.[grep("x[3-5]", names(.))], na.rm = TRUE),
                    sum_all = rowSums(.[grep("x", names(.))], na.rm = TRUE))

通过这种方式,您可以创建多个变量作为数据框中某组变量的总和。

我经常遇到这个问题,最简单的方法是在 mutate 命令中使用 apply() 函数。

library(tidyverse)
df=data.frame(
  x1=c(1,0,0,NA,0,1,1,NA,0,1),
  x2=c(1,1,NA,1,1,0,NA,NA,0,1),
  x3=c(0,1,0,1,1,0,NA,NA,0,1),
  x4=c(1,0,NA,1,0,0,NA,0,0,1),
  x5=c(1,1,NA,1,1,1,NA,1,0,1))

df %>%
  mutate(sum = select(., x1:x5) %>% apply(1, sum, na.rm=TRUE))

在这里您可以使用任何您想要的东西 select 使用标准 dplyr 技巧(例如 starts_with()contains())的列。通过在单个 mutate 命令中完成所有工作,此操作可以在 dplyr 处理步骤流中的任何位置发生。最后,通过使用 apply() 函数,您可以灵活地使用您需要的任何摘要,包括您自己专门构建的摘要函数。

或者,如果使用 non-tidyverse 函数的想法没有吸引力,那么您可以收集列,对其进行汇总,最后将结果连接回原始数据框。

df <- df %>% mutate( id = 1:n() )   # Need some ID column for this to work

df <- df %>%
  group_by(id) %>%
  gather('Key', 'value', starts_with('x')) %>%
  summarise( Key.Sum = sum(value) ) %>%
  left_join( df, . )

这里我使用了 starts_with() 函数来 select 列并计算总和,你可以用 NA 值做任何你想做的事情。这种方法的缺点是虽然它非常灵活,但它并不真正适合 dplyr 数据清理步骤流。

使用 purrr 中的 reduce()rowSums 稍快,而且肯定比 apply 快,因为您避免遍历所有行而只是利用矢量化操作:

library(purrr)
library(dplyr)
iris %>% mutate(Petal = reduce(select(., starts_with("Petal")), `+`))

请参阅 this 了解时间安排

dplyr >= 1.0.0

在较新版本的 dplyr 中,您可以使用 rowwise()c_across 对没有特定 row-wise 变体的函数执行 row-wise 聚合, 但是 如果存在 row-wise 变体,它应该比使用 rowwise 更快(例如 rowSums, rowMeans).

由于 rowwise() 只是一种特殊形式的分组并改变了动词的工作方式,您可能希望在完成 row-wise 操作后将其通过管道传递给 ungroup()

至 select 一个 名称范围 :

df %>%
  rowwise() %>% 
  mutate(sumrange = sum(c_across(x1:x5), na.rm = T))
# %>% ungroup() # you'll likely want to ungroup after using rowwise()

至 select 按类型:

df %>%
  rowwise() %>% 
  mutate(sumnumeric = sum(c_across(where(is.numeric)), na.rm = T))
# %>% ungroup() # you'll likely want to ungroup after using rowwise()

到select按列名:

您可以使用任意数量的 tidy selection helpers,例如 starts_withends_withcontains

df %>%
    rowwise() %>% 
    mutate(sum_startswithx = sum(c_across(starts_with("x")), na.rm = T))
# %>% ungroup() # you'll likely want to ungroup after using rowwise()

至select 按列索引:

df %>% 
  rowwise() %>% 
  mutate(sumindex = sum(c_across(c(1:4, 5)), na.rm = T))
# %>% ungroup() # you'll likely want to ungroup after using rowwise()

rowise() 将适用于 任何汇总函数 。但是,在您的特定情况下,存在 row-wise 变体 (rowSums),因此您可以执行以下操作(请注意使用 across),这样会更快:

df %>%
  mutate(sumrow = rowSums(across(x1:x5), na.rm = T))

有关详细信息,请参阅 rowwise 上的页面。


基准测试

rowwise 使管道链非常可读并且适用于较小的数据帧。然而,它是低效的。

rowwise 对比 row-wise 变体函数

对于此示例,row-wise 变体 rowSums 快得多

library(microbenchmark)

set.seed(1)
large_df <- slice_sample(df, n = 1E5, replace = T) # 100,000 obs

microbenchmark(
  large_df %>%
    rowwise() %>% 
    mutate(sumrange = sum(c_across(x1:x5), na.rm = T)),
  large_df %>%
    mutate(sumrow = rowSums(across(x1:x5), na.rm = T)),
  times = 10L
)

Unit: milliseconds
         min           lq         mean       median           uq          max neval cld
 11108.459801 11464.276501 12144.871171 12295.362251 12690.913301 12918.106801    10   b
     6.533301     6.649901     7.633951     7.808201     8.296101     8.693101    10  a 

没有row-wise变量函数的大数据框

如果您的函数没有 row-wise 变体并且您有一个大数据框,请考虑 long-format,它比 rowwise 更有效。虽然可能有更快的 non-tidyverse 选项,但这里有一个 tidyverse 选项(使用 tidyr::pivot_longer):

library(tidyr)

tidyr_pivot <- function(){
  large_df %>% 
    mutate(rn = row_number()) %>% 
    pivot_longer(cols = starts_with("x")) %>% 
    group_by(rn) %>% 
    summarize(std = sd(value, na.rm = T), .groups = "drop") %>% 
    bind_cols(large_df, .) %>% 
    select(-rn)
}

dplyr_rowwise <- function(){
  large_df %>% 
    rowwise() %>% 
    mutate(std = sd(c_across(starts_with("x")), na.rm = T)) %>% 
    ungroup()
}

microbenchmark(dplyr_rowwise(),
               tidyr_pivot(),
               times = 10L)

Unit: seconds
            expr       min       lq      mean   median        uq       max neval cld
 dplyr_rowwise() 12.845572 13.48340 14.182836 14.30476 15.155155 15.409750    10   b
   tidyr_pivot()  1.404393  1.56015  1.652546  1.62367  1.757428  1.981293    10  a 

c_across 对比

sum 函数的特殊情况下,acrossc_across 为上面的大部分代码提供相同的输出:

sum_across <- df %>%
    rowwise() %>% 
    mutate(sumrange = sum(across(x1:x5), na.rm = T))

sum_c_across <- df %>%
    rowwise() %>% 
    mutate(sumrange = sum(c_across(x1:x5), na.rm = T)

all.equal(sum_across, sum_c_across)
[1] TRUE

c_across 的 row-wise 输出是向量(因此 c_),而 across 的 row-wise 输出是 1 行tibble 对象:

df %>% 
  rowwise() %>% 
  mutate(c_across = list(c_across(x1:x5)),
         across = list(across(x1:x5)),
         .keep = "unused") %>% 
  ungroup() 

# A tibble: 10 x 2
   c_across  across          
   <list>    <list>          
 1 <dbl [5]> <tibble [1 x 5]>
 2 <dbl [5]> <tibble [1 x 5]>
 3 <dbl [5]> <tibble [1 x 5]>
 4 <dbl [5]> <tibble [1 x 5]>
 5 <dbl [5]> <tibble [1 x 5]>
 6 <dbl [5]> <tibble [1 x 5]>
 7 <dbl [5]> <tibble [1 x 5]>
 8 <dbl [5]> <tibble [1 x 5]>
 9 <dbl [5]> <tibble [1 x 5]>
10 <dbl [5]> <tibble [1 x 5]>

您要应用的功能将需要您使用哪个动词。如上所示 sum 您几乎可以互换使用它们。但是,mean 和许多其他常见函数期望(数字)向量作为其第一个参数:

class(df[1,])
"data.frame"

sum(df[1,]) # works with data.frame
[1] 4

mean(df[1,]) # does not work with data.frame
[1] NA
Warning message:
In mean.default(df[1, ]) : argument is not numeric or logical: returning NA
class(unname(unlist(df[1,])))
"numeric"

sum(unname(unlist(df[1,]))) # works with numeric vector
[1] 4

mean(unname(unlist(df[1,]))) # works with numeric vector
[1] 0.8

忽略均值 (rowMean) 存在的 row-wise 变体,那么在这种情况下应使用 c_across

df %>% 
  rowwise() %>% 
  mutate(avg = mean(c_across(x1:x5), na.rm = T)) %>% 
  ungroup()

# A tibble: 10 x 6
      x1    x2    x3    x4    x5   avg
   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
 1     1     1     0     1     1   0.8
 2     0     1     1     0     1   0.6
 3     0    NA     0    NA    NA   0  
 4    NA     1     1     1     1   1  
 5     0     1     1     0     1   0.6
 6     1     0     0     0     1   0.4
 7     1    NA    NA    NA    NA   1  
 8    NA    NA    NA     0     1   0.5
 9     0     0     0     0     0   0  
10     1     1     1     1     1   1  

# Does not work
df %>% 
  rowwise() %>% 
  mutate(avg = mean(across(x1:x5), na.rm = T)) %>% 
  ungroup()

rowSumsrowMeans 等可以将数字数据框作为第一个参数,这就是它们使用 across.

的原因

如果您想使用向量对列或行求和,但在这种情况下修改 df 而不是向 df 添加新列。

可以使用扫一扫功能:

library(dplyr)
df=data.frame(
  x1=c(1,0,0,NA,0,1,1,NA,0,1),
  x2=c(1,1,NA,1,1,0,NA,NA,0,1),
  x3=c(0,1,0,1,1,0,NA,NA,0,1),
  x4=c(1,0,NA,1,0,0,NA,0,0,1),
  x5=c(1,1,NA,1,1,1,NA,1,0,1))
> df
   x1 x2 x3 x4 x5
1   1  1  0  1  1
2   0  1  1  0  1
3   0 NA  0 NA NA
4  NA  1  1  1  1
5   0  1  1  0  1
6   1  0  0  0  1
7   1 NA NA NA NA
8  NA NA NA  0  1
9   0  0  0  0  0
10  1  1  1  1  1

按row-wise顺序求和(向量+数据帧):

vector = 1:5
sweep(df, MARGIN=2, vector, `+`)
   x1 x2 x3 x4 x5
1   2  3  3  5  6
2   1  3  4  4  6
3   1 NA  3 NA NA
4  NA  3  4  5  6
5   1  3  4  4  6
6   2  2  3  4  6
7   2 NA NA NA NA
8  NA NA NA  4  6
9   1  2  3  4  5
10  2  3  4  5  6

按column-wise顺序求和(向量+数据帧):

vector <- 1:10  
sweep(df, MARGIN=1, vector, `+`)
   x1 x2 x3 x4 x5
1   2  2  1  2  2
2   2  3  3  2  3
3   3 NA  3 NA NA
4  NA  5  5  5  5
5   5  6  6  5  6
6   7  6  6  6  7
7   8 NA NA NA NA
8  NA NA NA  8  9
9   9  9  9  9  9
10 11 11 11 11 11

这个同理vector+df

  • 保证金 = 1 是 column-wise
  • 保证金 = 2 是 row-wise。

是的。您可以使用扫描:

sweep(df, MARGIN=2, vector, `-`)
sweep(df, MARGIN=2, vector, `*`)
sweep(df, MARGIN=2, vector, `/`)
sweep(df, MARGIN=2, vector, `^`)

另一种方法是使用 Reduce 和 column-wise:

vector = 1:5
.df <- list(df, vector)
Reduce('+', .df)

对(几乎)所有选项进行基准测试以跨列求和

由于很难在@skd、@LMc 和其他人给出的所有有趣答案中做出决定,我对所有相当长的备选方案进行了基准测试。

与其他示例的不同之处在于我使用了更大的数据集(10.000 行)和真实世界的数据集(菱形),因此这些发现可能更多地反映了真实世界数据的差异。

可重现的基准测试代码是:

set.seed(17)
dataset <- diamonds %>% sample_n(1e4)
cols <- c("depth", "table", "x", "y", "z")

sum.explicit <- function() {
  dataset %>%
    mutate(sum.cols = depth + table + x + y + z)
}

sum.rowSums <- function() {
  dataset %>%
    mutate(sum.cols = rowSums(across(cols)))
}

sum.reduce <- function() {
  dataset %>%
    mutate(sum.cols = purrr::reduce(select(., cols), `+`))
}

sum.nest <- function() {
  dataset %>%
  group_by(id = row_number()) %>%
  nest(data = cols) %>%
  mutate(sum.cols = map_dbl(data, sum))
}

# NOTE: across with rowwise doesn't work with all functions!
sum.across <- function() {
  dataset %>%
    rowwise() %>%
    mutate(sum.cols = sum(across(cols)))
}

sum.c_across <- function() {
  dataset %>%
  rowwise() %>%
  mutate(sum.cols = sum(c_across(cols)))
}

sum.apply <- function() {
  dataset %>%
    mutate(sum.cols = select(., cols) %>%
             apply(1, sum, na.rm = TRUE))
}

bench <- microbenchmark::microbenchmark(
  sum.nest(),
  sum.across(),
  sum.c_across(),
  sum.apply(),
  sum.explicit(),
  sum.reduce(),
  sum.rowSums(),
  times = 10
)

bench %>% print(order = 'mean', signif = 3)
Unit: microseconds
           expr     min      lq    mean  median      uq     max neval
 sum.explicit()     796     839    1160     950    1040    3160    10
  sum.rowSums()    1430    1450    1770    1650    1800    2980    10
   sum.reduce()    1650    1700    2090    2000    2140    3300    10
    sum.apply()    9290    9400    9720    9620    9840   11000    10
 sum.c_across()  341000  348000  353000  356000  359000  360000    10
     sum.nest()  793000  827000  854000  843000  871000  945000    10
   sum.across() 4810000 4830000 4880000 4900000 4920000 4940000    10

将其可视化(没有异常值 sum.across)有助于比较:

结论(主观!)

  1. 尽管可读性很好,nestrowwise/c_across 不推荐用于较大的数据集(> 100.000 行或重复操作)
  2. 显式求和获胜,因为它在内部最好地利用了求和函数的矢量化,rowSums 也利用了它,但计算开销很小
  3. purrr::reduce相对new in the tidyverse(但在python中广为人知),而作为Reduce在base R中非常高效,因此在Top3中占有一席之地.因为显式写起来很麻烦,而且除rowSums/rowMeanscolSums/colMeans外向量化的方法不多,其他的函数我都推荐(例如 sd) 应用 purrr::reduce.