Passer un nom de colonne data.frame à une fonction

119

J'essaye d'écrire une fonction pour accepter un data.frame ( x) et un columnde celui-ci. La fonction effectue quelques calculs sur x et retourne plus tard un autre data.frame. Je suis bloqué sur la méthode des meilleures pratiques pour passer le nom de la colonne à la fonction.

Les deux exemples minimaux fun1et fun2ci - dessous produisent le résultat souhaité, étant capable d'effectuer des opérations sur x$column, en utilisant max()comme exemple. Cependant, les deux s'appuient sur l'apparence (du moins pour moi) inélégante

  1. appeler substitute()et éventuellementeval()
  2. la nécessité de passer le nom de la colonne comme vecteur de caractères.

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

Je voudrais pouvoir appeler la fonction comme fun(df, B), par exemple. Autres options que j'ai envisagées mais que je n'ai pas essayées:

  • Passez columncomme un entier du numéro de colonne. Je pense que cela éviterait substitute(). Idéalement, la fonction pourrait accepter l'un ou l'autre.
  • with(x, get(column)), mais, même si cela fonctionne, je pense que cela nécessiterait toujours substitute
  • Utilisez formula()et match.call(), avec lesquels je n'ai pas beaucoup d'expérience.

Sous-question : est-il do.call()préférable à eval()?

kmm
la source

Réponses:

108

Vous pouvez simplement utiliser le nom de la colonne directement:

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

Il n'est pas nécessaire d'utiliser substitute, eval, etc.

Vous pouvez même passer la fonction souhaitée en paramètre:

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

Sinon, l'utilisation [[fonctionne également pour sélectionner une seule colonne à la fois:

df <- data.frame(A=1:10, B=2:11, C=3:12)
fun1 <- function(x, column){
  max(x[[column]])
}
fun1(df, "B")
Shane
la source
14
Existe-t-il un moyen de ne pas transmettre le nom de la colonne sous forme de chaîne?
kmm
2
Vous devez transmettre le nom de la colonne entre guillemets sous forme de caractère ou l'index entier de la colonne. Le simple fait de passer Bsupposera que B est un objet lui-même.
Shane
Je vois. Je ne sais pas comment je me suis retrouvé avec le remplaçant alambiqué, l'évaluation, etc.
kmm
3
Merci! J'ai trouvé que la [[solution était la seule qui fonctionnait pour moi.
EcologyTom
1
Salut @Luis, consultez cette réponse
EcologyTom
78

Cette réponse couvrira bon nombre des mêmes éléments que les réponses existantes, mais ce problème (passer les noms de colonnes aux fonctions) revient assez souvent pour que je souhaite qu'il y ait une réponse qui couvre les choses de manière un peu plus complète.

Supposons que nous ayons une trame de données très simple:

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

et nous aimerions écrire une fonction qui crée une nouvelle colonne zqui est la somme des colonnes xet y.

Une pierre d'achoppement très courante ici est qu'une tentative naturelle (mais incorrecte) ressemble souvent à ceci:

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

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

Le problème ici est que cela df$col1n'évalue pas l'expression col1. Il recherche simplement une colonne dflittéralement appelée col1. Ce comportement est décrit dans ?Extractla section "Objets récursifs (de type liste)".

La solution la plus simple et la plus souvent recommandée consiste simplement à passer de $à [[et à transmettre les arguments de la fonction sous forme de chaînes:

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

Ceci est souvent considéré comme une «meilleure pratique» car c'est la méthode la plus difficile à bousiller. Passer les noms de colonnes sous forme de chaînes est à peu près aussi clair que possible.

Les deux options suivantes sont plus avancées. De nombreux forfaits les plus populaires utilisent ce genre de techniques, mais leur utilisation bien nécessite plus de soins et de compétences, car ils peuvent introduire des complexités subtiles et des points imprévus d'échec. Cette section du livre Advanced R de Hadley est une excellente référence pour certains de ces problèmes.

Si vous voulez vraiment éviter à l'utilisateur de taper tous ces guillemets, une option peut être de convertir les noms de colonnes nus et sans guillemets en chaînes en utilisant 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

C'est, franchement, probablement un peu idiot, car nous faisons vraiment la même chose que dans new_column1, juste avec un tas de travail supplémentaire pour convertir les noms nus en chaînes.

Enfin, si nous voulons vraiment avoir de la fantaisie, nous pourrions décider qu'au lieu de passer les noms de deux colonnes à ajouter, nous aimerions être plus flexibles et permettre d'autres combinaisons de deux variables. Dans ce cas, nous aurions probablement recours à l'utilisation eval()d'une expression impliquant les deux colonnes:

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

Juste pour le plaisir, j'utilise toujours deparse(substitute())le nom de la nouvelle colonne. Ici, tous les éléments suivants fonctionneront:

> 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

La réponse courte est donc essentiellement: passez les noms de colonnes data.frame sous forme de chaînes et utilisez [[pour sélectionner des colonnes uniques. Commencer à ne se plonger dans eval, substituteetc. si vous savez vraiment ce que vous faites.

joran
la source
1
Je ne sais pas pourquoi ce n'est pas la meilleure réponse choisie.
Ian
Moi non plus! Excellente explication!
Alfredo G Marquez le
22

Personnellement, je pense que passer la colonne sous forme de chaîne est assez moche. J'aime faire quelque chose comme:

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

ce qui donnera:

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

Notez que la spécification d'un data.frame est facultative. vous pouvez même travailler avec les fonctions de vos colonnes:

> get.max(1/mpg,mtcars)
[1] 0.09615385
Boursiers Ian
la source
9
Vous devez sortir de l'habitude de penser qu'utiliser des guillemets est moche. Ne pas les utiliser est moche! Pourquoi? Parce que vous avez créé une fonction qui ne peut être utilisée que de manière interactive, il est très difficile de programmer avec elle.
hadley
27
Je suis heureux d'avoir une meilleure façon de voir, mais je ne vois pas la différence entre ceci et qplot (x = mpg, data = mtcars). ggplot2 ne passe jamais une colonne sous forme de chaîne, et je pense que c'est mieux pour cela. Pourquoi dites-vous que cela ne peut être utilisé que de manière interactive? Dans quelle situation cela conduirait-il à des résultats indésirables? En quoi est-il plus difficile de programmer avec? Dans le corps du message, je montre comment il est plus flexible.
Ian Fellows
4
5 ans plus tard -) .. Pourquoi avons-nous besoin de: parent.frame ()?
mql4beginner
15
7 ans plus tard: les citations ne sont-elles pas encore moche?
Spacedman
12

Une autre façon est d'utiliser l' tidy evaluationapproche. Il est assez simple de passer des colonnes d'un bloc de données sous forme de chaînes ou de noms de colonnes nues. En savoir plus tidyeval ici .

library(rlang)
library(tidyverse)

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

Utiliser les noms de colonnes comme chaînes

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

Utiliser des noms de colonnes nus

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

Créé le 01/03/2019 par le package reprex (v0.2.1.9000)

Tung
la source
1

En guise de réflexion supplémentaire, s'il est nécessaire de passer le nom de la colonne sans guillemets à la fonction personnalisée, cela match.call()pourrait peut -être être également utile dans ce cas, comme alternative à 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

S'il y a une faute de frappe dans le nom de la colonne, il serait plus sûr de s'arrêter avec une erreur:

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

Créé le 11/01/2019 par le package reprex reprex (v0.2.1)

Je ne pense pas que j'utiliserais cette approche car il y a plus de typage et de complexité que de simplement passer le nom de colonne cité comme indiqué dans les réponses ci-dessus, mais bien, c'est une approche.

Valentin
la source