在部分列上执行dplyr mutate

23

我有一个数据框架,像这样(实际数据集有更多的行和列)

set.seed(15)
dd <- data.frame(id=letters[1:4], matrix(runif(5*4), nrow=4))

#   id        X1        X2        X3        X4        X5
# 1  a 0.6021140 0.3670719 0.6872308 0.5090904 0.4474437
# 2  b 0.1950439 0.9888592 0.8314290 0.7066286 0.9646670
# 3  c 0.9664587 0.8151934 0.1046694 0.8623137 0.1411871
# 4  d 0.6509055 0.2539684 0.6461509 0.8417851 0.7767125

我希望能够编写一个dplyr语句,以选择列的子集并对其进行变异(我试图做类似于在data.table中使用.SDcols的事情)。

以一个简化的例子为例,这里是我想要编写的函数,用于添加偶数“X”列的总和和平均值,同时保留所有其他列。使用基本R的期望输出为:

(cols<-paste0("X", c(2,4)))
# [1] "X2" "X4"
cbind(dd,evensum=rowSums(dd[,cols]),evenmean=rowMeans(dd[,cols]))

#   id        X1        X2        X3        X4        X5   evensum  evenmean
# 1  a 0.6021140 0.3670719 0.6872308 0.5090904 0.4474437 0.8761623 0.4380811
# 2  b 0.1950439 0.9888592 0.8314290 0.7066286 0.9646670 1.6954878 0.8477439
# 3  c 0.9664587 0.8151934 0.1046694 0.8623137 0.1411871 1.6775071 0.8387535
# 4  d 0.6509055 0.2539684 0.6461509 0.8417851 0.7767125 1.0957535 0.5478768

但我想使用类似于dplyr的管道来做同样的事情。在一般情况下,我希望能够使用任何select()的辅助函数,比如starts_withends_withmatches等以及任何函数。以下是我的尝试:

library(dplyr)
partial_mutate1 <- function(x, colspec, ...) {
    select_(x, .dots=list(lazyeval::lazy(colspec))) %>% 
    transmute_(.dots=lazyeval::lazy_dots(...)) %>% 
    cbind(x,.)
}

dd %>% partial_mutate1(num_range("X", c(2,4)), 
    evensum=rowSums(.), evenmean=rowMeans(.))

然而,这会抛出一个错误,提示

Error in rowSums(.) : 'x' must be numeric

看起来是因为 . 似乎是指整个数据框而不是所选子集。(与 rowSums(dd) 相同的错误)。不过请注意,这样可以产生所需的输出。

partial_mutate2 <- function(x, colspec) {
    select_(x, .dots=list(lazyeval::lazy(colspec))) %>% 
    transmute(evensum=rowSums(.), evenmean=rowMeans(.)) %>% 
    cbind(x,.)
}
dd %>% partial_mutate2(seq(2,ncol(dd),2))

我猜这是一些环境问题?您有什么建议可以将参数传递给partial_mutate1,以便.可以正确地从“select()”数据集中取值吗?


一种不太优雅的方法是:dd %>% select(X2, X4) %>% mutate(evensum = rowSums(.), evenmean = rowMeans(.)) %>% select(-X2, -X4) %>% cbind(., dd) - Steven Beaupré
我怀疑问题在于尝试使用SE会干扰%>%。换句话说,由于rowMeans(.)被嵌套在.dots中,%>%无法知道它也应该替换数据。这只是一个猜测。 - BrodieG
我认为你是对的 @BrodieG。经过更深入的挖掘,这更多是一个 magrittr 问题,而不是 dplyr 问题。例如:mutate(dd[,-1], sums=rowSums(.)) 不起作用("object '.' not found")。所以 . 符号对于 dplyr 并不是特殊的。试图使用它来跨列应用函数似乎是错误的想法。我想我应该先将数据重塑为“整洁”的格式。 - MrFlick
虽然SO在右侧显示链接,但我认为在这里更容易看到:此问题帮助了如何使用starts_with和其他选择辅助函数 - @Brandon Bertelsen建议的语法现在似乎可以工作,即mutate(new_col = rowSums(select(., starts_with(string)))) - tjebo
6个回答

13

我有所遗漏吗?还是这个代码可以如预期般工作:

cols <- paste0("X", c(2,4))
dd %>% mutate(evensum = rowSums(.[cols]), evenmean = rowMeans(.[cols]))
#  id        X1        X2        X3        X4        X5   evensum  evenmean
#1  a 0.6021140 0.3670719 0.6872308 0.5090904 0.4474437 0.8761623 0.4380811
#2  b 0.1950439 0.9888592 0.8314290 0.7066286 0.9646670 1.6954878 0.8477439
#3  c 0.9664587 0.8151934 0.1046694 0.8623137 0.1411871 1.6775071 0.8387535
#4  d 0.6509055 0.2539684 0.6461509 0.8417851 0.7767125 1.0957535 0.5478768

或者您是否专门在寻找一个自定义函数来执行此操作?


不完全是您要寻找的内容,但如果您想要在管道内执行此操作,您可以在mutate内明确使用select,像这样:

dd %>% mutate(xy = select(., num_range("X", c(2,4))) %>% rowSums)
#  id        X1        X2        X3        X4        X5        xy
#1  a 0.6021140 0.3670719 0.6872308 0.5090904 0.4474437 0.8761623
#2  b 0.1950439 0.9888592 0.8314290 0.7066286 0.9646670 1.6954878
#3  c 0.9664587 0.8151934 0.1046694 0.8623137 0.1411871 1.6775071
#4  d 0.6509055 0.2539684 0.6461509 0.8417851 0.7767125 1.0957535

然而,如果您想应用多个函数,则会变得更加复杂。您可以使用一些辅助函数(例如:..没有经过彻底测试..)来达到目的:

f <- function(x, ...) {
  n <- nrow(x)
  x <- lapply(list(...), function(y) if (length(y) == 1L) rep(y, n) else y)
  matrix(unlist(x), nrow = n, byrow = FALSE)
}

然后像这样应用:

dd %>% mutate(xy = select(., num_range("X", c(2,4))) %>% f(., rowSums(.), max(.)))
#  id        X1        X2        X3        X4        X5      xy.1      xy.2
#1  a 0.6021140 0.3670719 0.6872308 0.5090904 0.4474437 0.8761623 0.9888592
#2  b 0.1950439 0.9888592 0.8314290 0.7066286 0.9646670 1.6954878 0.9888592
#3  c 0.9664587 0.8151934 0.1046694 0.8623137 0.1411871 1.6775071 0.9888592
#4  d 0.6509055 0.2539684 0.6461509 0.8417851 0.7767125 1.0957535 0.9888592

缺失的部分是我想要能够使用?select提供的列选择函数,例如starts_with、ends_with等。在select()之外,它们将无法正常工作。 - MrFlick
1
当然,我可以使用 cols <- dplyr:::num_range(names(dd), "X", c(2,4))。问题是它不会成为链的一部分,并且如果在计算列索引后更改了链中的任何列顺序,它也会出错。如果可能的话,“按需”执行会更好。 - MrFlick
@MrFlick,我明白你的意思了。刚开始理解错了。 - talat

2
使用dplyr的不考虑列数的方法:

使用dplyr,您可以轻松实现不考虑列数的数据处理。

dd %>% 
  select(-id) %>% 
  mutate(evensum = rowSums(.[,1:length(.[1,])%%2==0]), 
         evenmean = rowMeans(.[,1:length(.[1,])%%2==0])) %>% 
  cbind(id=dd[,1],.)

  id        X1        X2        X3        X4        X5   evensum  evenmean
1  a 0.6021140 0.3670719 0.6872308 0.5090904 0.4474437 0.8761623 0.4380812
2  b 0.1950439 0.9888592 0.8314290 0.7066286 0.9646670 1.6954878 0.8477439
3  c 0.9664587 0.8151934 0.1046694 0.8623137 0.1411871 1.6775071 0.8387535
4  d 0.6509055 0.2539684 0.6461509 0.8417851 0.7767125 1.0957535 0.5478767

1

tidyr::nest()理解与dplyr::select()相同的选择器语法,因此一种方法是将感兴趣的列合并为单个数据框列,对该数据框列执行必要的操作,然后展开以获取平面数据框:

library( tidyverse )
dd %>% nest( X2, X4, .key="Slice" ) %>%
    mutate( evensum = map(Slice, rowSums),
           evenmean = map(Slice, rowMeans),
           evensd = map(Slice, pmap_dbl, lift_vd(sd)) ) %>%
    unnest
#   id       X1    X3    X5 evensum evenmean evensd    X2    X4
# 1 a     0.602 0.687 0.447   0.876    0.438 0.100  0.367 0.509
# 2 b     0.195 0.831 0.965   1.70     0.848 0.200  0.989 0.707
# 3 c     0.966 0.105 0.141   1.68     0.839 0.0333 0.815 0.862
# 4 d     0.651 0.646 0.777   1.10     0.548 0.416  0.254 0.842

由于数据框基本上是列表,因此使用purrr::pmap()函数族对任意一组列应用任意函数(例如上面的sd)自然非常合适。

副笔:由于sd适用于向量,我们使用purrr::lift_vd将其接口转换为适用于pmap

sd( c(0.367, 0.509) )        # 0.100
lift_vd(sd)( 0.367, .509 )   # 0.100

0
另一个选项是使用rowwise()加上c_across()。这种操作类型不能与rowSumsrowMeans一起使用,但可以与常规的sum()mean()函数一起使用。 c_across()函数将多个列作为简单向量返回。它还接受任何tidyselect辅助函数。因此,例如,您可以执行以下操作:
dd %>% 
  rowwise() %>% 
  mutate(
    evensum = sum( c_across(all_of(cols)) ), 
    evenmean = mean( c_across(all_of(cols)) )
)

你可以不使用 rowwise(它非常慢)来使其工作。dd %>% mutate( evensum = rowSums(across(all_of(cols))), evenmean = rowMeans(across(all_of(cols))) ) - Julien

0
使用all_of(或any_of)与across一起。
dd %>% 
  mutate(
    evensum = rowSums(across(all_of(cols))), 
    evenmean = rowMeans(across(all_of(cols)))
)

-2
在较新版本的dplyr中,您可以使用新的mutate_at()函数。
mutate_at(dd, vars(starts_with("X")), somefunction)

这并没有解决问题的本质。mutate_at 仍然只能一次操作一个列。使用 mutate_atrowSums 的情况是不可行的。 - MrFlick

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接