使用 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栏。 此外,列名称在循环的不同迭代中也会发生变化,我想在循环中实现这一点 操作,所以我想尝试避免给任何列名称。

我怎样才能最有效地做到这一点? 如果您能提供帮助,我将不胜感激。

236340 次浏览

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

对每一行进行总结

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

使用 被超越了 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))

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

或者,如果使用非整洁的函数的想法没有吸引力,那么您可以收集列,总结它们,并最终将结果连接回原始数据框架。

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流的数据清理步骤。

purrr中使用 reduce()rowSums稍微快一点,而且肯定比 apply快,因为您避免了对所有行进行迭代,而只是利用了向量化操作:

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

有关计时,请参见 这个

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()

选择 列名:

您可以使用任意数量的 整齐的选择帮手,如 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与行变异函数的比较

在这个例子中,行变量 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更有效。虽然可能有更快的非整齐宇宙选项,这里有一个整齐宇宙选项(使用 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 _ 交叉对交叉

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一起工作的原因。

如果您希望使用向量对列或行进行求和,但是在本例中修改了 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

按行顺序求和(矢量 + 数据帧) :

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

按列顺序的和(矢量 + 数据框架) :

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是一样的

  • MARGIN = 1是列方面的
  • MARGIN = 2是行方向的。

是的,您可以使用扫描:

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

另一种方法是在列方面使用 Reduce:

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)有助于进行比较:

enter image description here

结论(主观的!)

  1. 尽管 nestrowwise/c_across具有很强的可读性,但对于较大的数据集(> 100.000行或重复操作) ,不推荐使用它们
  2. 显式和之所以获胜,是因为它在内部最好地利用了 sum 函数的向量化,rowSums也利用了这一点,但计算开销很小
  3. purrr::reduce是相对 新潮流(但众所周知的巨蟒) ,作为 Reduce在基地 R 非常有效,因此赢得了一个地方在 Top3。由于显式表单编写起来很麻烦,而且除了 rowSums/rowMeanscolSums/colMeans之外没有多少向量化方法,所以我建议所有其他函数(例如 sd)都应用 purrr::reduce