Pregunta

Estoy intentando escribir una función para aceptar una hoja.de.datos (x) y un column de ella. Las funciones realiza algunos cálculos sobre X y vuelve más tarde otro data.frame. Estoy atascado en el método de las mejores prácticas para pasar el nombre de la columna a la función.

Los dos ejemplos mínimos fun1 y fun2 debajo de producir el resultado deseado, siendo capaz de realizar operaciones en x$column, utilizando max() como un ejemplo. Sin embargo, ambos se basan en la apariencia (al menos para mí) poco elegante

  1. llamada a substitute() y posiblemente eval()
  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 ser capaz de llamar a la función como fun(df, B), por ejemplo. Otras opciones que he considerado, pero no he probado:

  • Pass column como un entero del número de columna. Creo que esto evitaría substitute(). Idealmente, la función podría aceptar tampoco.
  • with(x, get(column)), pero, incluso si funciona, creo que esto seguiría requiriendo substitute
  • Hacer uso de formula() y match.call(), ninguno de los cuales tengo mucha experiencia con.

de preguntas adicionales :? Es preferible do.call() sobre eval()

¿Fue útil?

Solución

Usted sólo 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 hay necesidad de usar sustituto, eval, etc.

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

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

Alternativamente, usando [[ también funciona para la selección de 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")

Otros consejos

Esta respuesta va a cubrir muchos de los mismos elementos que las respuestas existentes, pero este tema (pasando los nombres de las columnas para funciones) surge a menudo suficiente que yo quería que hubiera una respuesta que cubría las cosas un poco más amplia.

Supongamos que tenemos una trama de datos muy simple:

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

y nos gustaría escribir una función que crea un nuevo z columna que es la suma de las columnas y x y.

Un obstáculo muy común aquí es que un natural (pero incorrecta) tratan a menudo se parece a esto:

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 no df$col1 evaluar la col1 expresión. Simplemente busca una columna en df literalmente llamada col1. Este comportamiento se describe en ?Extract en la sección "Recursive (lista-like) Objetos".

El más simple, y la solución se recomienda en general simplemente se cambian de $ a [[ y pasan 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 "mejores prácticas" ya que es el método que sea más difícil de meter la pata. Al pasar los nombres de las columnas en forma de cadenas es tan inequívoca como usted puede conseguir.

Las siguientes dos opciones están más avanzados. Muchos paquetes populares hacen uso de este tipo de técnicas, pero su uso y requiere más cuidado y habilidad, ya que pueden introducir complejidades sutiles y los puntos no previstos de fracaso. Esta sección del libro de avanzada R de Hadley es una excelente referencia para algunos de estos problemas.

Si realmente desea guardar el usuario escriba todas esas cotizaciones, una opción podría ser la de convertir nombres de las columnas desnudas, sin cotización a 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, un poco tonto, probablemente, ya que realmente estamos haciendo lo mismo que en new_column1, sólo con un montón de trabajo extra para convertir nombres desnudos en cadenas.

Por último, si queremos obtener realmente de fantasía, que podría decidir que en lugar de pasar en los nombres de dos columnas para añadir, nos gustaría ser más flexibles y permiten otras combinaciones de dos variables. En ese caso estaríamos propensos recurrir al uso de eval() en una expresión que incluya 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
}

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

> 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

Así que la respuesta corta es básicamente: Paso de data.frame nombres de columna como cadenas y el uso [[ para seleccionar columnas individuales. Sólo empezar a ahondar en eval, substitute, etc. si realmente sabe lo que está haciendo.

En lo personal creo que la aprobación de la columna como una cadena es bastante feo. Me gustaría hacer algo como:

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

que Rendimiento:

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

Nótese como la especificación de un hoja.de.datos es opcional. Incluso puede trabajar con las funciones de sus columnas:

> get.max(1/mpg,mtcars)
[1] 0.09615385

Otra forma es utilizar tidy evaluation enfoque . Es bastante sencillo para pasar columnas de una trama de datos, ya sea en forma de cadenas o nombres de las columnas desnudas. Ver más sobre tidyeval aquí .

library(rlang)
library(tidyverse)

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

Use 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

Use nombres de columna desnuda

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 03/01/2019 por el reprex paquete (v0.2.1.9000)

Como un pensamiento adicional, si es que se necesita para pasar el nombre de columna no cotizados a la función personalizada, tal vez match.call() podría ser útil también en este caso, como una 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, entonces sería más seguro parada 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 11/01/2019 por el reprex paquete (v0.2.1)

No creo que me gustaría utilizar este método ya que no está escribiendo extra y la complejidad que simplemente pasando el nombre de la columna citado en punta en las respuestas anteriores, pero bueno, es una aproximación.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top