Use nomes de variáveis ​​dinâmicas no `dplyr`

168

Eu quero usar dplyr::mutate()para criar várias novas colunas em um quadro de dados. Os nomes das colunas e seu conteúdo devem ser gerados dinamicamente.

Dados de exemplo da íris:

library(dplyr)
iris <- tbl_df(iris)

Eu criei uma função para alterar minhas novas colunas da Petal.Widthvariável:

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

Agora eu crio um loop para construir minhas colunas:

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

No entanto, como mutate acha que varname é um nome literal de variável, o loop cria apenas uma nova variável (chamada varname) em vez de quatro (chamada petal.2 - petal.5).

Como posso mutate()usar meu nome dinâmico como nome de variável?

Timm S.
fonte
1
Não estou insistindo em mudar, estou perguntando se é possível. Talvez seja apenas um pequeno truque que eu não sei. Se houver outra maneira, vamos ouvi-la.
Timm S.
Eu creio que há um espaço para relógio no pacote lazyeval
Baptiste
1
Neste ponto, dplyrtem uma vinheta todo na avaliação não-padrão
Gregor Thomas
16
A vinheta nem menciona mutate_, e realmente não é óbvio pelas outras funções como usá-la.
Nacnudus

Respostas:

191

Como você está construindo dinamicamente um nome de variável como um valor de caractere, faz mais sentido fazer a atribuição usando a indexação data.frame padrão que permite valores de caracteres para nomes de colunas. Por exemplo:

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

A mutatefunção facilita muito o nome de novas colunas por meio de parâmetros nomeados. Mas isso pressupõe que você saiba o nome ao digitar o comando. Se você deseja especificar dinamicamente o nome da coluna, também precisa criar o argumento nomeado.


versão dplyr> = 0.7

A versão mais recente do dplyr(0.7) faz isso usando :=para atribuir dinamicamente nomes de parâmetros. Você pode escrever sua função como:

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

Para mais informações, consulte o formulário disponível na documentação vignette("programming", "dplyr").


dplyr (> = 0,3 e <0,7)

A versão ligeiramente anterior de dplyr(> = 0.3 <0.7) incentivou o uso de alternativas de "avaliação padrão" para muitas das funções. Consulte a vinheta de avaliação não padrão para obter mais informações ( vignette("nse")).

Então aqui, a resposta é usar mutate_()e não mutate()fazer:

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

Observe que isso também é possível nas versões mais antigas dplyrque existiam quando a pergunta foi feita originalmente. Requer o uso cuidadoso quotee setName:

# --- 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)
}
MrFlick
fonte
24
Obrigado, isso é útil. Aliás, eu sempre crio variáveis ​​realmente dramáticas.
Timm S.
27
Ele Ele. esse é provavelmente um dos meus erros de digitação favoritos que cometi há algum tempo. Eu acho que vou deixar isso.
precisa saber é o seguinte
1
do.call()provavelmente não faz o que você pensa: rpubs.com/hadley/do-call2 . Veja também a vinheta nse na versão dev do dplyr.
Hadley
4
Portanto, se entendi seu ponto @hadley, atualizei o item do.callacima para usar do.call("mutate")e citar dfna lista. Era isso que você estava sugerindo? E quando a lazyevalversão do dplyré a versão lançada, então mutate_(df, .dots= setNames(list(~Petal.Width * n), varname))seria uma solução melhor?
precisa saber é o seguinte
1
E se eu precisar do cabeçalho da coluna variável, não apenas no lado esquerdo da tarefa, mas também no lado direito? por exemplo mutate(df, !!newVar := (!!var1 + !!var2) / 2), não funciona :(
Mario Reutter
55

Na nova versão de dplyr( 0.6.0aguardando em abril de 2017), também podemos fazer uma atribuição ( :=) e passar variáveis ​​como nomes de colunas, sem citar ( !!) para não avaliá-la

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

Verificando a saída com base no @ MrFlick multipetalaplicado em 'iris1'

identical(iris1, iris2)
#[1] TRUE
akrun
fonte
26

Após muitas tentativas e erros, achei o padrão UQ(rlang::sym("some string here")))realmente útil para trabalhar com strings e verbos dplyr. Parece funcionar em muitas situações surpreendentes.

Aqui está um exemplo com mutate. Queremos criar uma função que adicione duas colunas, onde você passa a função pelos nomes de colunas como strings. Podemos usar esse padrão, juntamente com o operador de atribuição :=, para fazer isso.

## Take column `name1`, add it to column `name2`, and call the result `new_name`
mutate_values <- function(new_name, name1, name2){
  mtcars %>% 
    mutate(UQ(rlang::sym(new_name)) :=  UQ(rlang::sym(name1)) +  UQ(rlang::sym(name2)))
}
mutate_values('test', 'mpg', 'cyl')

O padrão também funciona com outras dplyrfunções. Aqui está filter:

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

Ou arrange:

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

Para select, você não precisa usar o padrão. Em vez disso, você pode usar !!:

## select a column 
select_name <- function(name){
  mtcars %>% 
    select(!!name)
}
select_name('mpg')
Tom Roth
fonte
Suas dicas funcionam muito bem, mas tenho um pequeno problema. Altero uma coluna inicial myColpara uma URL (por exemplo) e copio a coluna antiga myColInitialValueno final do quadro de dados dfcom um novo nome. Mas which(colnames(df)=='myCol')envie de volta o número da coluna myColInitialValue. Ainda não escrevi um problema porque não encontrei um reprex. Meu objetivo é o escapeparâmetro de DT::datatable(). Eu uso escape=FALSEesperando isso. Com constantes, ele também não funciona, mas o pacote DT também parece ter a coluna # ruim. :)
phili_b
Parece que variáveis ​​dinâmicas não são a causa. (btw reprex added)
phili_b
Obrigado por esta resposta! Aqui está um exemplo super-simples de como eu usei-o:varname = sym("Petal.Width"); ggplot(iris, aes(x=!!varname)) + geom_histogram()
bdemarest
Isso funcionou para mim dentro de uma fórmula em que !! varname não estava funcionando.
daknowles
12

Aqui está outra versão, e é sem dúvida um pouco mais simples.

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
user2946432
fonte
8

Com rlang 0.4.0, temos operadores curly-curly ( {{}}), o que facilita muito isso.

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

Também podemos passar nomes de variáveis ​​entre aspas / não citadas para serem atribuídos como nomes de colunas.

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

Funciona da mesma maneira com

multipetal(iris1, "temp", 3)
Ronak Shah
fonte
4

Também estou adicionando uma resposta que aumenta um pouco isso, porque cheguei a esta entrada ao procurar uma resposta, e isso tinha quase o que eu precisava, mas precisava de um pouco mais, o que recebi através da resposta do @MrFlik e do R vinhetas preguiçosas.

Eu queria criar uma função que pudesse levar um quadro de dados e um vetor de nomes de colunas (como seqüências de caracteres) que desejo que sejam convertidos de uma sequência para um objeto Date. Não consegui descobrir como as.Date()usar um argumento que é uma string e convertê-lo em uma coluna, então fiz como mostrado abaixo.

Abaixo está como eu fiz isso via SE mutate ( mutate_()) e o .dotsargumento. Críticas que melhoram isso são bem-vindas.

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
mpettis
fonte
3

Embora eu goste de usar o dplyr para uso interativo, acho extraordinariamente complicado fazer isso usando o dplyr, porque você precisa passar por etapas para usar as soluções alternativas lazyeval :: interp (), setNames, etc.

Aqui está uma versão mais simples usando a base R, na qual parece mais intuitivo, pelo menos para mim, colocar o loop dentro da função e que estende a solução do @ 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) 
hackR
fonte
2
+1, embora eu ainda use dplyrmuito em configurações não interativas, usá-lo com entrada variável dentro de uma função usa sintaxe muito desajeitada.
Paul Hiemstra
3

Você pode aproveitar o pacote friendlyevalque apresenta uma API de avaliação organizada e simplificada para dplyrusuários mais novos / casuais .

Você está criando cadeias que deseja mutatetratar como nomes de colunas. Então, usando friendlyevalvocê pode escrever:

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

Que sob o capô chama rlangfunções que a verificação varnameé legal como nome da coluna.

friendlyeval O código pode ser convertido em código de avaliação simples e arrumado a qualquer momento com um complemento do RStudio.

MilesMcBain
fonte
0

Outra alternativa: use {}aspas para criar facilmente nomes dinâmicos. Isso é semelhante a outras soluções, mas não exatamente o mesmo, e acho mais fácil.

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

Eu acho que isso vem, dplyr 1.0.0mas não tenho certeza (eu também tenho, rlang 4.7.0se importa).

bretauv
fonte