在 `dplyr` 中为新列/变量使用动态名称

290
我想使用 dplyr::mutate() 在数据框中创建多个新列。列名和内容应该是动态生成的。
来自 iris 数据集的示例数据:
library(dplyr)
iris <- as_tibble(iris)

我已经创建了一个函数来从Petal.Width变量中改变我的新列:

multipetal <- function(df, n) {
    varname <- paste("petal", n , sep=".")
    df <- mutate(df, varname = Petal.Width * n)  ## problem arises here
    df
}

现在我创建一个循环来构建我的列:

for(i in 2:5) {
    iris <- multipetal(df=iris, n=i)
}

然而,由于 mutate 认为 varname 是一个字面变量名,循环仅会创建一个新变量(称为 varname),而不是四个变量(称为 petal.2 - petal.5)。

我该如何让 mutate() 使用我的动态名称作为变量名?


18
这个小故事甚至没有提到mutate_,而且从其他函数中也很难看出如何使用它。 - nacnudus
2
多年来,我一直在努力理解quosure等文件的内容。虽然上面的vignette链接不再有效,但是那个评论引导我找到了这篇总结,说的是关于tidyevaluation的:https://shipt.tech/https-shipt-tech-advanced-programming-and-non-standard-evaluation-with-dplyr-e043f89deb3d。我终于明白了!谢谢。 - Josh
2
与基本的 R 相比,在使用 dplyr 进行循环时,其动态命名似乎过于复杂。 - Markm0705
1
@MarioReutter 最优 _为什么_?我不同意例如用一个1000万行的键值列替换100万行的10列数据无歧义地是一种改进。循环遍历列名一直是并且应该保持完全可接受,而“整洁”则可以忽略不计。 - shadowtalker
2
@Markm0705 将Dplyr/Rlang视为在基本R功能as.symbolsubstitute等之上实现简洁的宏/元编程DSL。这些基本功能可能会很笨重和冗长。我喜欢他们所做的事情,但我真的不喜欢所有新术语、设计中的不断变化以及文档中过于复杂的描述,好像这是一件晦涩难懂的事情,人们不应该需要这样做。 - shadowtalker
显示剩余3条评论
10个回答

368

由于您正在使用字符值动态构建变量名,因此更合理的做法是使用标准的数据框索引进行赋值,允许使用列名称的字符值。例如:

multipetal <- function(df, n) {
    varname <- paste("petal", n , sep=".")
    df[[varname]] <- with(df, Petal.Width * n)
    df
}

使用mutate函数,通过命名参数非常容易创建新列。但这假定您在键入命令时就已经知道了名称。如果您要动态指定列名,则还需要构建命名参数。


dplyr版本>=1.0

使用最新的dplyr版本,可以在使用:=时使用来自glue包的语法来命名参数。因此,在名称中使用{}可以通过评估其中的表达式来获取值。

multipetal <- function(df, n) {
  mutate(df, "petal.{n}" := Petal.Width * n)
}
如果您将列名传递给函数,您可以在字符串中以及列名中使用{{}}
meanofcol <- function(df, col) {
  mutate(df, "Mean of {{col}}" := mean({{col}}))
}
meanofcol(iris, Petal.Width)


dplyr版本 >= 0.7

dplyr自版本0.7开始允许您使用:=动态分配参数名称。您可以将函数编写为:

# --- dplyr version 0.7+---
multipetal <- function(df, n) {
    varname <- paste("petal", n , sep=".")
    mutate(df, !!varname := Petal.Width * n)
}

欲了解更多信息,请参见可用于文档的vignette("programming", "dplyr")


dplyr (>=0.3 & <0.7)

dplyr的稍早版本(>=0.3 <0.7)鼓励使用“标准评估”函数的替代方法。有关更多信息,请参见非标准评估vignette (vignette("nse"))。

因此,在这里,答案是使用mutate_()而不是mutate()并执行:

# --- dplyr version 0.3-0.5---
multipetal <- function(df, n) {
    varname <- paste("petal", n , sep=".")
    varval <- lazyeval::interp(~Petal.Width * n, n=n)
    mutate_(df, .dots= setNames(list(varval), varname))
}

dplyr < 0.3

请注意,早期版本的dplyr也可以实现此操作。这需要仔细使用quotesetName

# --- dplyr versions < 0.3 ---
multipetal <- function(df, n) {
    varname <- paste("petal", n , sep=".")
    pp <- c(quote(df), setNames(list(quote(Petal.Width * n)), varname))
    do.call("mutate", pp)
}

30
谢谢,这很有帮助。顺便说一下,我总是创建非常戏剧化的变量。 - Timm S.
29
呵呵,这可能是我最喜欢的打字错误之一。我想我会让它保持不变。 - MrFlick
4
如果我理解你的观点@hadley,我已经更新了上面的do.call,使用do.call("mutate")并在列表中引用了df。这是你建议的吗?当dplyrlazyeval版本成为发布版本时,那么mutate_(df, .dots= setNames(list(~Petal.Width * n), varname))会是一个更好的解决方案? - MrFlick
5
如果我不仅需要将变量列标题放在赋值语句的左侧,还需要将其放在右侧怎么办?例如,mutate(df, !!newVar := (!!var1 + !!var2) / 2)不能实现 :( - Mario Reutter
2
@Mario Reutter:你有没有得到你评论的答案?我在这里提出了同样的问题,并希望能解决它! - MsGISRocker
显示剩余20条评论

80
在新版本的dplyr0.6.0将于2017年四月发布)中,我们还可以使用赋值符号 :=,通过非引用变量名(!!)将变量作为列名传递,而不进行计算。
 library(dplyr)
 multipetalN <- function(df, n){
      varname <- paste0("petal.", n)
      df %>%
         mutate(!!varname := Petal.Width * n)
 }

 data(iris)
 iris1 <- tbl_df(iris)
 iris2 <- tbl_df(iris)
 for(i in 2:5) {
     iris2 <- multipetalN(df=iris2, n=i)
 }   

基于 @MrFlick 的 multipetal 应用在 'iris1' 上的输出检查

identical(iris1, iris2)
#[1] TRUE

1
当使用非引用变量(即!!varname)进行赋值时,为什么需要使用 := - cmo
1
这是一个很好的解决方案,但它只适用于传递字符向量,而不是直接传递数字(之所以在这里起作用是因为您先进行了粘贴步骤)。 - tjebo

49
经过多次尝试,我发现模式!!rlang::sym("my variable"))(在早期的R版本中:UQ(rlang::sym("my variable"))))对于处理字符串和dplyr动词非常有用。它似乎在许多出人意料的情况下都能起作用。
以下是一个使用mutate的示例。我们想要创建一个函数,将两列相加,其中你需要将两个列名作为字符串传递给函数。我们可以使用这个模式,结合赋值运算符:=来实现这一点。
## Take column `name1`, add it to column `name2`, and call the result `new_name`
mutate_values <- function(new_name, name1, name2){
  mtcars %>% 
    mutate(!!rlang::sym(new_name) :=  !!rlang::sym(name1) + !!rlang::sym(name2))
}
mutate_values('test', 'mpg', 'cyl')

这个模式也适用于其他 dplyr 函数。这是 filter

## filter a column by a value 
filter_values <- function(name, value){
  mtcars %>% 
    filter(!!rlang::sym(name) != value)
}
filter_values('gear', 4)

或者 arrange

## transform a variable and then sort by it 
arrange_values <- function(name, transform){
  mtcars %>% 
    arrange((!!rlang::sym(name)) %>% (!!rlang::sym(transform)))
}
arrange_values('mpg', 'sin')

对于select,您不需要使用模式。相反,您可以使用!!

## select a column 
select_name <- function(name){
  mtcars %>% 
    select(!!name)
}
select_name('mpg')

谢谢您的回答!这是一个非常简单的例子,展示了我如何使用它:varname = sym("Petal.Width"); ggplot(iris, aes(x=!!varname)) + geom_histogram() - bdemarest
这对我在公式中起作用,而 !!varname 却不起作用。 - daknowles
UQ 来自哪个库? - Dima Lituiev
UQ 来自于 rlang,但已被弃用。 - Tyler R.
1
现在应该使用!!rlang::sym(name)而不是UQ(rlang::sym(name)) - Tyler R.
显示剩余5条评论

26

使用 rlang 0.4.0 版本,我们有了花括号操作符 ({{}}),这使得这个过程变得非常简单。当动态列名出现在赋值的左侧时,请使用 :=

library(dplyr)
library(rlang)

iris1 <- tbl_df(iris)

multipetal <- function(df, n) {
   varname <- paste("petal", n , sep=".")
   mutate(df, {{varname}} := Petal.Width * n)
}

multipetal(iris1, 4)

# A tibble: 150 x 6
#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species petal.4
#          <dbl>       <dbl>        <dbl>       <dbl> <fct>     <dbl>
# 1          5.1         3.5          1.4         0.2 setosa      0.8
# 2          4.9         3            1.4         0.2 setosa      0.8
# 3          4.7         3.2          1.3         0.2 setosa      0.8
# 4          4.6         3.1          1.5         0.2 setosa      0.8
# 5          5           3.6          1.4         0.2 setosa      0.8
# 6          5.4         3.9          1.7         0.4 setosa      1.6
# 7          4.6         3.4          1.4         0.3 setosa      1.2
# 8          5           3.4          1.5         0.2 setosa      0.8
# 9          4.4         2.9          1.4         0.2 setosa      0.8
#10          4.9         3.1          1.5         0.1 setosa      0.4
# … with 140 more rows

我们还可以传递引用/未引用的变量名来作为列名进行赋值。

multipetal <- function(df, name, n) {
   mutate(df, {{name}} := Petal.Width * n)
}

multipetal(iris1, temp, 3)

# A tibble: 150 x 6
#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species  temp
#          <dbl>       <dbl>        <dbl>       <dbl> <fct>   <dbl>
# 1          5.1         3.5          1.4         0.2 setosa  0.6  
# 2          4.9         3            1.4         0.2 setosa  0.6  
# 3          4.7         3.2          1.3         0.2 setosa  0.6  
# 4          4.6         3.1          1.5         0.2 setosa  0.6  
# 5          5           3.6          1.4         0.2 setosa  0.6  
# 6          5.4         3.9          1.7         0.4 setosa  1.2  
# 7          4.6         3.4          1.4         0.3 setosa  0.900
# 8          5           3.4          1.5         0.2 setosa  0.6  
# 9          4.4         2.9          1.4         0.2 setosa  0.6  
#10          4.9         3.1          1.5         0.1 setosa  0.3  
# … with 140 more rows

它与之一样有效

multipetal(iris1, "temp", 3)

mutate(df, 'petal.{n}' := Petal.Width * n) - moodymudskipper
请注意,当在循环中将字符向量传递给函数时,此方法会失败 - 然后将创建一个带有函数参数名称的列。 - tjebo

14

这里是另一个版本,可以说更简单一些。

multipetal <- function(df, n) {
    varname <- paste("petal", n, sep=".")
    df<-mutate_(df, .dots=setNames(paste0("Petal.Width*",n), varname))
    df
}

for(i in 2:5) {
    iris <- multipetal(df=iris, n=i)
}

> head(iris)
Sepal.Length Sepal.Width Petal.Length Petal.Width Species petal.2 petal.3 petal.4 petal.5
1          5.1         3.5          1.4         0.2  setosa     0.4     0.6     0.8       1
2          4.9         3.0          1.4         0.2  setosa     0.4     0.6     0.8       1
3          4.7         3.2          1.3         0.2  setosa     0.4     0.6     0.8       1
4          4.6         3.1          1.5         0.2  setosa     0.4     0.6     0.8       1
5          5.0         3.6          1.4         0.2  setosa     0.4     0.6     0.8       1
6          5.4         3.9          1.7         0.4  setosa     0.8     1.2     1.6       2

5
你可能会喜欢 friendlyeval 包,它为新手/休闲使用者提供了简化的 tidy eval API 和文档。
你正在创建一些字符串,希望它们被 mutate 视为列名。因此,使用 friendlyeval,你可以编写如下代码:
multipetal <- function(df, n) {
  varname <- paste("petal", n , sep=".")
  df <- mutate(df, !!treat_string_as_col(varname) := Petal.Width * n)
  df
}

for(i in 2:5) {
  iris <- multipetal(df=iris, n=i)
}

底层调用rlang函数检查varname是否合法作为列名。

friendlyeval代码可以随时使用RStudio插件转换为等效的纯净的Tidy Eval代码。


我认为这个软件包不再可用。 - Michael Bellhouse
它比上面的任何东西都更好,而且它运行得非常顺畅!当case_when()无法正常工作时,它可以完美地解决问题。 - David Muñoz Tord

4
我还想补充一些内容,因为当我在寻找答案时,这篇文章几乎已经满足了我的需求,但我需要更多的帮助,我通过@MrFlik的答案和R lazyeval vignettes得到了更多帮助。我想制作一个函数,该函数可以接收一个数据框和一个要转换为日期类型对象的列名向量(作为字符串)。 我无法弄清楚如何使as.Date()接受一个字符串参数并将其转换为列,所以我按照下面的方法完成了此操作。以下是我使用SE mutate (mutate_()) 和 .dots参数实现此目的的方法。欢迎提出改进意见。
library(dplyr)

dat <- data.frame(a="leave alone",
                  dt="2015-08-03 00:00:00",
                  dt2="2015-01-20 00:00:00")

# This function takes a dataframe and list of column names
# that have strings that need to be
# converted to dates in the data frame
convertSelectDates <- function(df, dtnames=character(0)) {
    for (col in dtnames) {
        varval <- sprintf("as.Date(%s)", col)
        df <- df %>% mutate_(.dots= setNames(list(varval), col))
    }
    return(df)
}

dat <- convertSelectDates(dat, c("dt", "dt2"))
dat %>% str

4

虽然我喜欢使用dplyr来进行交互式操作,但我发现在使用dplyr时它异常棘手,因为你必须通过使用lazyeval::interp()、setNames等解决方案来实现。

这里有一个使用base R的简化版本,对我来说更直观,它将循环放在函数内部,并扩展了@MrFlicks的解决方案。

multipetal <- function(df, n) {
   for (i in 1:n){
      varname <- paste("petal", i , sep=".")
      df[[varname]] <- with(df, Petal.Width * i)
   }
   df
}
multipetal(iris, 3) 

2
+1,尽管我在非交互式环境中仍经常使用dplyr,但在函数内部使用它与变量输入的语法非常笨拙。 - Paul Hiemstra

1

另一种选择:在引号内使用{}来轻松创建动态名称。这类似于其他解决方案,但并不完全相同,我觉得这更容易。

library(dplyr)
library(tibble)

iris <- as_tibble(iris)

multipetal <- function(df, n) {
  df <- mutate(df, "petal.{n}" := Petal.Width * n)  ## problem arises here
  df
}

for(i in 2:5) {
  iris <- multipetal(df=iris, n=i)
}
iris

我认为这是来自于dplyr 1.0.0,但不确定(如果有影响,我也有rlang 4.7.0)。


0
如果您需要多次执行相同的操作,通常意味着您的数据格式不够优化。您需要一个更长的格式,其中n是数据框中的一列,可以通过交叉连接来实现:
library(tidyverse)
iris %>% mutate(identifier = 1:n()) %>% #necessary to disambiguate row 102 from row 143 (complete duplicates)
   full_join(tibble(n = 1:5), by=character()) %>% #cross join for long format
   mutate(petal = Petal.Width * n) %>% #calculation in long format
   pivot_wider(names_from=n, values_from=petal, names_prefix="petal.width.") #back to wider format (if desired)

结果:

# A tibble: 150 x 11
   Sepal.Length Sepal.Width Petal.Length Petal.Width Species identifier petal.width.1 petal.width.2 petal.width.3
          <dbl>       <dbl>        <dbl>       <dbl> <fct>        <int>         <dbl>         <dbl>         <dbl>
 1          5.1         3.5          1.4         0.2 setosa           1           0.2           0.4           0.6
 2          4.9         3            1.4         0.2 setosa           2           0.2           0.4           0.6
 3          4.7         3.2          1.3         0.2 setosa           3           0.2           0.4           0.6
 4          4.6         3.1          1.5         0.2 setosa           4           0.2           0.4           0.6
 5          5           3.6          1.4         0.2 setosa           5           0.2           0.4           0.6
 6          5.4         3.9          1.7         0.4 setosa           6           0.4           0.8           1.2
 7          4.6         3.4          1.4         0.3 setosa           7           0.3           0.6           0.9
 8          5           3.4          1.5         0.2 setosa           8           0.2           0.4           0.6
 9          4.4         2.9          1.4         0.2 setosa           9           0.2           0.4           0.6
10          4.9         3.1          1.5         0.1 setosa          10           0.1           0.2           0.3
# ... with 140 more rows, and 2 more variables: petal.width.4 <dbl>, petal.width.5 <dbl>

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