Pasar un nombre de columna data.frame a una función

119

Estoy tratando de escribir una función para aceptar un data.frame ( x) y un columnde él. La función realiza algunos cálculos en x y luego devuelve otro data.frame. Estoy atascado en el método de mejores prácticas para pasar el nombre de la columna a la función.

Los dos ejemplos mínimos fun1y fun2siguientes producen el resultado deseado, pudiendo realizar operaciones sobre x$column, utilizando max()como ejemplo. Sin embargo, ambos confían en lo aparentemente (al menos para mí) poco elegante

  1. llamar substitute()y posiblementeeval()
  2. la necesidad de pasar el nombre de la columna como un vector 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")

Me gustaría poder llamar a la función como fun(df, B), por ejemplo. Otras opciones que he considerado pero no he probado:

  • Pasa columncomo un número entero del número de columna. Creo que esto evitaríasubstitute() . Idealmente, la función podría aceptar cualquiera.
  • with(x, get(column)), pero, incluso si funciona, creo que esto aún requeriría substitute
  • Hacer uso de formula()ymatch.call() , ninguno de los cuales tengo mucha experiencia.

Subpregunta : ¿Se do.call()prefiere sobre eval()?

kmm
fuente

Respuestas:

108

Puede usar el nombre de la columna directamente:

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

No es necesario utilizar sustituto, eval, etc.

Incluso puede pasar la función deseada como parámetro:

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

Alternativamente, el uso [[también funciona para seleccionar una sola columna a la vez:

df <- data.frame(A=1:10, B=2:11, C=3:12)
fun1 <- function(x, column){
  max(x[[column]])
}
fun1(df, "B")
Shane
fuente
14
¿Hay alguna forma de pasar el nombre de la columna no como una cadena?
kmm
2
Debe pasar el nombre de la columna entre comillas como carácter o el índice entero de la columna. Simplemente pasar Bsupondrá que B es un objeto en sí mismo.
Shane
Veo. No estoy seguro de cómo terminé con el intrincado sustituto, eval, etc.
kmm
3
¡Gracias! Descubrí que la [[solución era la única que funcionaba para mí.
EcologyTom
1
Hola @Luis, mira esta respuesta
EcologyTom
78

Esta respuesta cubrirá muchos de los mismos elementos que las respuestas existentes, pero este problema (pasar los nombres de las columnas a las funciones) surge con tanta frecuencia que quería que hubiera una respuesta que cubriera las cosas de manera un poco más completa.

Supongamos que tenemos un marco de datos muy simple:

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

y nos gustaría escribir una función que cree una nueva columna zque es la suma de las columnas xy y.

Un obstáculo muy común aquí es que un intento natural (pero incorrecto) a menudo se ve así:

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

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

El problema aquí es que df$col1no evalúa la expresión col1. Simplemente busca una columna en dfliteralmente llamada col1. Este comportamiento se describe en ?Extractla sección "Objetos recursivos (en forma de lista)".

La solución más simple, y más a menudo simplemente se recomienda cambiar de $a [[y pasar los argumentos de la función como cadenas:

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

Esto a menudo se considera "mejor práctica", ya que es el método más difícil de estropear. Pasar los nombres de las columnas como cadenas es lo más inequívoco posible.

Las siguientes dos opciones son más avanzadas. Muchos paquetes populares hacen uso de este tipo de técnicas, pero usarlas bien requiere más cuidado y habilidad, ya que pueden introducir complejidades sutiles y puntos de falla imprevistos. Esta sección del libro Advanced R de Hadley es una excelente referencia para algunos de estos temas.

Si realmente desea evitar que el usuario escriba todas esas comillas, una opción podría ser convertir los nombres de columnas desnudos y sin comillas en cadenas 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

Esto es, francamente, probablemente un poco tonto, ya que en realidad estamos haciendo lo mismo que en new_column1, solo que con un montón de trabajo extra para convertir nombres desnudos en cadenas.

Finalmente, si queremos ser realmente sofisticados, podemos decidir que en lugar de pasar los nombres de dos columnas para agregar, nos gustaría ser más flexibles y permitir otras combinaciones de dos variables. En ese caso, probablemente recurriremos al uso eval()de una expresión que involucre las dos columnas:

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

Solo por diversión, todavía lo estoy usando deparse(substitute())para el nombre de la nueva columna. Aquí, todo lo siguiente funcionará:

> 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

Entonces, la respuesta corta es básicamente: pasar los nombres de las columnas data.frame como cadenas y usarlos [[para seleccionar columnas individuales. Sólo empezar a ahondar en eval, substitute, etc, si usted realmente sabe lo que está haciendo.

joran
fuente
1
No estoy seguro de por qué esta no es la mejor respuesta seleccionada.
Ian
¡Yo tampoco! ¡Gran explicación!
Alfredo G Marquez
22

Personalmente, creo que pasar la columna como una cadena es bastante feo. Me gusta hacer algo como:

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

que producirá:

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

Observe que la especificación de un data.frame es opcional. incluso puedes trabajar con funciones de tus columnas:

> get.max(1/mpg,mtcars)
[1] 0.09615385
Becarios Ian
fuente
9
Necesitas salir del hábito de pensar que usar comillas es feo. ¡No usarlos es feo! ¿Por qué? Debido a que ha creado una función que solo se puede usar de forma interactiva, es muy difícil programar con ella.
hadley
27
Estoy feliz de que me muestren una mejor manera, pero no veo la diferencia entre esto y qplot (x = mpg, data = mtcars). ggplot2 nunca pasa una columna como una cadena, y creo que es mejor para eso. ¿Por qué dice que esto solo se puede usar de forma interactiva? ¿En qué situación conduciría a resultados indeseables? ¿Cómo es más difícil programar? En el cuerpo de la publicación muestro cómo es más flexible.
Ian Fellows
4
5 años después -) .. ¿Por qué necesitamos: parent.frame ()?
mql4beginner
15
7 años después: ¿no es todavía feo usar comillas?
Spacedman
11

Otra forma es utilizar el tidy evaluationenfoque. Es bastante sencillo pasar columnas de un marco de datos como cadenas o nombres de columnas desnudos. Vea más sobre tidyeval aquí .

library(rlang)
library(tidyverse)

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

Usar nombres de columna como cadenas

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

Usar nombres de columnas desnudos

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

Creado el 2019-03-01 por el paquete reprex (v0.2.1.9000)

Tung
fuente
1

Como idea adicional, si es necesario pasar el nombre de la columna sin comillas a la función personalizada, quizás también match.call()podría ser útil en este caso, como alternativa a 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

Si hay un error tipográfico en el nombre de la columna, sería más seguro detenerse con un error:

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

Creado el 2019-01-11 por el paquete reprex (v0.2.1)

No creo que usaría este enfoque, ya que hay una escritura y una complejidad adicionales que simplemente pasar el nombre de la columna entre comillas como se indica en las respuestas anteriores, pero bueno, es un enfoque.

Valentin
fuente