Passe um nome de coluna data.frame para uma função

119

Estou tentando escrever uma função para aceitar um data.frame ( x) e um columndele. A função executa alguns cálculos em xe posteriormente retorna outro data.frame. Estou preso no método de práticas recomendadas para passar o nome da coluna para a função.

Os dois exemplos mínimos fun1e fun2abaixo produzem o resultado desejado, podendo realizar operações no x$column, utilizando max()como exemplo. No entanto, ambos contam com o aparentemente (pelo menos para mim) deselegante

  1. ligue para substitute()e possivelmenteeval()
  2. a necessidade de passar o nome da coluna como um vetor de caracteres.

fun1 <- function(x, column){
  do.call("max", list(substitute(x[a], list(a = column))))
}

fun2 <- function(x, column){
  max(eval((substitute(x[a], list(a = column)))))
}

df <- data.frame(B = rnorm(10))
fun1(df, "B")
fun2(df, "B")

Gostaria de poder chamar a função como fun(df, B), por exemplo. Outras opções que considerei, mas não tentei:

  • Passe columncomo um inteiro do número da coluna. Acho que isso evitaria substitute(). Idealmente, a função poderia aceitar qualquer um.
  • with(x, get(column)), mas, mesmo que funcione, acho que ainda exigiria substitute
  • Faça uso de formula()e match.call(), nenhum dos quais tenho muita experiência.

Subquestão : É do.call()preferível eval()?

kmm
fonte

Respostas:

108

Você pode apenas usar o nome da coluna diretamente:

df <- data.frame(A=1:10, B=2:11, C=3:12)
fun1 <- function(x, column){
  max(x[,column])
}
fun1(df, "B")
fun1(df, c("B","A"))

Não há necessidade de usar substituto, eval, etc.

Você pode até passar a função desejada como parâmetro:

fun1 <- function(x, column, fn) {
  fn(x[,column])
}
fun1(df, "B", max)

Como alternativa, usar [[também funciona para selecionar uma única coluna por vez:

df <- data.frame(A=1:10, B=2:11, C=3:12)
fun1 <- function(x, column){
  max(x[[column]])
}
fun1(df, "B")
Shane
fonte
14
Existe alguma maneira de passar o nome da coluna não como uma string?
kmm
2
Você precisa passar o nome da coluna entre aspas como um caractere ou o índice inteiro para a coluna. Basta passar Bpara supor que B é um objeto em si.
Shane
Entendo. Não tenho certeza de como acabei com o substituto complicado, eval, etc.
kmm
3
Obrigado! Achei que a [[solução era a única que funcionava para mim.
EcologyTom
1
Olá @Luis, verifique esta resposta
EcologyTom
78

Essa resposta abrangerá muitos dos mesmos elementos das respostas existentes, mas esse problema (passar nomes de colunas para funções) surge com frequência o suficiente para que houvesse uma resposta que abrangesse as coisas de forma um pouco mais abrangente.

Suponha que temos um quadro de dados muito simples:

dat <- data.frame(x = 1:4,
                  y = 5:8)

e gostaríamos de escrever uma função que crie uma nova coluna zque é a soma das colunas xe y.

Um obstáculo muito comum aqui é que uma tentativa natural (mas incorreta) geralmente se parece com isto:

foo <- function(df,col_name,col1,col2){
      df$col_name <- df$col1 + df$col2
      df
}

#Call foo() like this:    
foo(dat,z,x,y)

O problema aqui é que df$col1não avalia a expressão col1. Ele simplesmente procura uma coluna dfliteralmente chamada col1. Este comportamento é descrito em?Extract seção "Objetos recursivos (semelhantes a listas)".

A solução mais simples e mais frequentemente recomendada é simplesmente alternar de $para [[e passar os argumentos da função como strings:

new_column1 <- function(df,col_name,col1,col2){
    #Create new column col_name as sum of col1 and col2
    df[[col_name]] <- df[[col1]] + df[[col2]]
    df
}

> new_column1(dat,"z","x","y")
  x y  z
1 1 5  6
2 2 6  8
3 3 7 10
4 4 8 12

Isso geralmente é considerado a "melhor prática", pois é o método mais difícil de errar. Passar os nomes das colunas como strings é o mais inequívoco que você pode imaginar.

As duas opções a seguir são mais avançadas. Muitos pacotes populares fazem uso desses tipos de técnicas, mas usá-los bem requer mais cuidado e habilidade, pois podem apresentar complexidades sutis e pontos de falha imprevistos. Esta seção do livro Advanced R de Hadley é uma excelente referência para alguns desses problemas.

Se você realmente deseja evitar que o usuário digite todas as aspas, uma opção pode ser converter os nomes das colunas vazias e não citadas em strings usando deparse(substitute()):

new_column2 <- function(df,col_name,col1,col2){
    col_name <- deparse(substitute(col_name))
    col1 <- deparse(substitute(col1))
    col2 <- deparse(substitute(col2))

    df[[col_name]] <- df[[col1]] + df[[col2]]
    df
}

> new_column2(dat,z,x,y)
  x y  z
1 1 5  6
2 2 6  8
3 3 7 10
4 4 8 12

Isso é, francamente, um pouco bobo provavelmente, já que estamos realmente fazendo a mesma coisa que em new_column1, apenas com um monte de trabalho extra para converter nomes simples em strings.

Finalmente, se quisermos ser realmente sofisticados, podemos decidir que, em vez de passar os nomes de duas colunas a serem adicionadas, gostaríamos de ser mais flexíveis e permitir outras combinações de duas variáveis. Nesse caso, provavelmente recorreríamos ao uso eval()de uma expressão envolvendo as duas colunas:

new_column3 <- function(df,col_name,expr){
    col_name <- deparse(substitute(col_name))
    df[[col_name]] <- eval(substitute(expr),df,parent.frame())
    df
}

Só por diversão, ainda estou usando deparse(substitute())para o nome da nova coluna. Aqui, todos os itens a seguir funcionarão:

> new_column3(dat,z,x+y)
  x y  z
1 1 5  6
2 2 6  8
3 3 7 10
4 4 8 12
> new_column3(dat,z,x-y)
  x y  z
1 1 5 -4
2 2 6 -4
3 3 7 -4
4 4 8 -4
> new_column3(dat,z,x*y)
  x y  z
1 1 5  5
2 2 6 12
3 3 7 21
4 4 8 32

Portanto, a resposta curta é basicamente: passe os nomes das colunas data.frame como strings e use [[para selecionar colunas únicas. Apenas começar a se aprofundar eval, substituteetc. se você realmente sabe o que está fazendo.

Joran
fonte
1
Não sei por que essa não é a melhor resposta selecionada.
Ian
Eu também! Ótima explicação!
Alfredo G Marquez
22

Pessoalmente, acho que passar a coluna como uma string é muito feio. Eu gosto de fazer algo como:

get.max <- function(column,data=NULL){
    column<-eval(substitute(column),data, parent.frame())
    max(column)
}

que renderá:

> get.max(mpg,mtcars)
[1] 33.9
> get.max(c(1,2,3,4,5))
[1] 5

Observe como a especificação de um data.frame é opcional. você pode até trabalhar com funções de suas colunas:

> get.max(1/mpg,mtcars)
[1] 0.09615385
Ian Fellows
fonte
9
Você precisa abandonar o hábito de pensar que usar aspas é feio. Não usá-los é feio! Por quê? Porque você criou uma função que só pode ser usada interativamente - é muito difícil programar com ela.
hadley de
27
Estou feliz em ver uma maneira melhor, mas não consigo ver a diferença entre isso e qplot (x = mpg, dados = mtcars). O ggplot2 nunca passa uma coluna como uma string, e acho que é melhor assim. Por que você diz que isso só pode ser usado interativamente? Em que situação isso levaria a resultados indesejáveis? Como é mais difícil programar? No corpo do post eu mostro como é mais flexível.
Ian Fellows de
4
5 anos depois -) .. Por que precisamos: parent.frame ()?
mql4beginner
15
7 anos depois: não usar aspas ainda é feio?
Spacedman
11

Outra maneira é usar a tidy evaluationabordagem. É muito simples passar colunas de um quadro de dados como strings ou nomes de coluna vazios. Veja mais sobre tidyeval aqui .

library(rlang)
library(tidyverse)

set.seed(123)
df <- data.frame(B = rnorm(10), D = rnorm(10))

Use os nomes das colunas como strings

fun3 <- function(x, ...) {
  # capture strings and create variables
  dots <- ensyms(...)
  # unquote to evaluate inside dplyr verbs
  summarise_at(x, vars(!!!dots), list(~ max(., na.rm = TRUE)))
}

fun3(df, "B")
#>          B
#> 1 1.715065

fun3(df, "B", "D")
#>          B        D
#> 1 1.715065 1.786913

Use nomes de coluna simples

fun4 <- function(x, ...) {
  # capture expressions and create quosures
  dots <- enquos(...)
  # unquote to evaluate inside dplyr verbs
  summarise_at(x, vars(!!!dots), list(~ max(., na.rm = TRUE)))
}

fun4(df, B)
#>          B
#> 1 1.715065

fun4(df, B, D)
#>          B        D
#> 1 1.715065 1.786913
#>

Criado em 01/03/2019 pelo pacote reprex (v0.2.1.9000)

Tung
fonte
1

Como um pensamento extra, se for necessário passar o nome da coluna sem aspas para a função personalizada, talvez match.call()possa ser útil também neste caso, como uma alternativa para deparse(substitute()):

df <- data.frame(A = 1:10, B = 2:11)

fun <- function(x, column){
  arg <- match.call()
  max(x[[arg$column]])
}

fun(df, A)
#> [1] 10

fun(df, B)
#> [1] 11

Se houver um erro de digitação no nome da coluna, seria mais seguro parar com um erro:

fun <- function(x, column) max(x[[match.call()$column]])
fun(df, typo)
#> Warning in max(x[[match.call()$column]]): no non-missing arguments to max;
#> returning -Inf
#> [1] -Inf

# Stop with error in case of typo
fun <- function(x, column){
  arg <- match.call()
  if (is.null(x[[arg$column]])) stop("Wrong column name")
  max(x[[arg$column]])
}

fun(df, typo)
#> Error in fun(df, typo): Wrong column name
fun(df, A)
#> [1] 10

Criado em 11/01/2019 pelo pacote reprex (v0.2.1)

Não acho que usaria essa abordagem, pois há digitação e complexidade extras do que apenas passar o nome da coluna citada conforme apontado nas respostas acima, mas bem, é uma abordagem.

Valentin
fonte