【问题标题】:Sum across multiple columns with dplyr使用 dplyr 跨多列求和
【发布时间】:2015-05-06 12:56:39
【问题描述】:

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

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?为什么不只是来自基本 R 的一个简单的df$sumrow &lt;- rowSums(df, na.rm = TRUE)?或者df$sumrow &lt;- Reduce(`+`, df),如果你想复制你对dplyr所做的事情。
  • 你也可以同时使用dplyr,如df %&gt;% mutate(sumrow = Reduce(`+`, .))df %&gt;% mutate(sumrow = rowSums(.))
  • 更新到最新的dplyr 版本就可以了。
  • David Arenburg 的建议在更新软件包 dplyr @DavidArenburg 后起作用
  • @boern David Arenburgs 的评论是最好的答案和最直接的解决方案。您的答案会起作用,但它涉及将 NA 值替换为零的额外步骤,这在某些情况下可能不适合。

标签: r dplyr


【解决方案1】:

dplyr >= 1.0.0 使用cross

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

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

总结每一列

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

dplyr

总结每一行

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

使用superseeded summarise_all 对每一列求和:

df %>%
   replace(is.na(.), 0) %>%
   summarise_all(funs(sum))

【讨论】:

  • summarise_each 沿每一列求和,而所需的是沿每一行求和
  • 我试图达到同样的效果,但我的 DF 有一列是一个字符,因此我无法对所有列求和。我想我应该修改(.[1:5]) 部分,但不幸的是我不熟悉语法,也不知道如何寻求帮助。尝试使用mutate(sum = rowSums(is.numeric(.))),但没有成功。
  • 我明白了。您可能想试一试df %&gt;% replace(is.na(.), 0) %&gt;% select_if(is.numeric) %&gt;% summarise_each(funs(sum))
  • 使用 summarise_all 而不是 summarise_each,因为它已被弃用。
  • 如果您不知道需要处理多少列,语法mutate(sum = rowSums(.[,-1])) 可能会派上用场。
【解决方案2】:

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

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的语法了。

【讨论】:

  • 我比其他方法更喜欢这种方法,因为它不需要将 NA 强制为 0
  • 而且比 grep 更好,因为更容易处理 x4:x11 之类的事情
【解决方案3】:

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

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

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

【讨论】:

  • 很好的解决方案!我正在寻找在最近的版本中执行此操作的特定 dplyr 函数,但找不到
  • 这个解决方案很棒。如果有您不想包含的列,您只需设计 grep() 语句来选择与特定模式匹配的列。
  • @TrentonHoffman 这里是位取消选择列的特定模式。只需要- 符号:rowSums(.[-grep("x[3-5]", names(.))], na.rm = TRUE)
【解决方案4】:

我经常遇到这个问题,最简单的方法是在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))

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

或者,如果使用非 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() 函数选择列并计算总和,您可以使用NA 值做任何您想做的事情。这种方法的缺点是,虽然它非常灵活,但它并不真正适合dplyr 数据清理步骤流。

【讨论】:

  • 使用 apply 似乎很愚蠢,因为这是 rowSums 的设计目的。
  • 在这种情况下,rowSumsrowMeans 一样好用,但我总是觉得有点奇怪,想知道“如果我需要计算的东西不是总和或平均值怎么办? "但是,在 99% 的情况下,我必须做这样的事情,它要么是总和,要么是平均值,所以使用通用 apply 函数时可能没有额外的灵活性。
【解决方案5】:

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

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

请参阅this 了解时间

【讨论】:

  • 我喜欢这个,但是当你需要的时候你会怎么做na.rm = TRUE
  • @see24 我不确定我明白你的意思。这对长度相同的向量 a + b + c 求和。由于每个向量在不同位置可能有也可能没有 NA,因此您不能忽略它们。这将使向量不对齐。如果要删除 NA 值,则必须之后使用例如 drop_na
  • 我最终选择了rowSums(select(., matches("myregex")) , na.rm = TRUE)),因为这是我在忽略 NA 方面所需要的。所以如果数字是sum(NA, 5),结果是5。但是你说reduce比rowSums好,所以我想知道在这种情况下是否有办法使用它?
  • 我明白了。如果您想要总和并忽略 NA 值,那么rowSums 版本可能是最好的。主要缺点是只有rowSumsrowMeans 可用(它比reduce 稍慢,但不会慢很多)。如果您需要执行另一个操作(而不是总和),那么reduce 版本可能是唯一的选择。在这种情况下,请避免使用apply
【解决方案6】:

dplyr >= 1.0.0

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

由于rowwise() 只是一种特殊的分组形式,它改变了动词的工作方式,因此您可能希望在进行逐行操作后将其通过管道传递给ungroup()

按名称选择范围

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

按类型选择

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

按列名选择:

您可以使用任意数量的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()

按列索引选择

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() 适用于任何摘要函数。但是,在您的特定情况下,存在逐行变体 (rowSums),因此您可以执行以下操作(请注意改用 across),这样会更快:

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

有关更多信息,请参阅rowwise 上的页面。


基准测试

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

rowwise 与逐行变体函数

对于这个例子,逐行变体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 

没有逐行变体函数的大型数据框

如果您的函数没有逐行变体并且您的数据框很大,请考虑使用长格式,它比rowwise 更有效。虽然可能有更快的非 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 与cross

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 的逐行输出是一个向量(因此是 c_),而 across 的逐行输出是一个 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) 存在的逐行变量,那么在这种情况下应使用 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 的原因。

【讨论】:

    猜你喜欢
    • 2018-04-13
    • 1970-01-01
    • 2016-08-02
    • 1970-01-01
    • 1970-01-01
    • 2023-04-07
    • 2016-01-29
    • 1970-01-01
    相关资源
    最近更新 更多