Passare un nome di colonna data.frame a una funzione


119

Sto cercando di scrivere una funzione per accettare un data.frame ( x) e un columnda esso. La funzione esegue alcuni calcoli su x e successivamente restituisce un altro data.frame. Sono bloccato sul metodo delle migliori pratiche per passare il nome della colonna alla funzione.

I due esempi minimi fun1e di fun2seguito producono il risultato desiderato, potendo eseguire operazioni su x$column, utilizzando max()come esempio. Tuttavia, entrambi si affidano all'apparentemente (almeno per me) inelegante

  1. chiamare a substitute()e possibilmenteeval()
  2. la necessità di passare il nome della colonna come vettore di caratteri.

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

Vorrei poter chiamare la funzione come fun(df, B), ad esempio. Altre opzioni che ho considerato ma non ho provato:

  • Passa columncome numero intero del numero di colonna. Penso che questo eviterebbe substitute(). Idealmente, la funzione potrebbe accettare entrambi.
  • with(x, get(column)), ma, anche se funziona, penso che sarebbe comunque necessario substitute
  • Usa formula()e match.call(), nessuno dei quali ho molta esperienza.

Domanda secondaria : è do.call()preferibile rispetto a eval()?

Risposte:


108

Puoi semplicemente usare direttamente il nome della colonna:

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

Non è necessario utilizzare sostituto, valutazione, ecc.

Puoi anche passare la funzione desiderata come parametro:

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

In alternativa, utilizzando [[funziona anche per selezionare una singola colonna alla volta:

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

14
C'è un modo per passare il nome della colonna non come una stringa?
kmm

2
È necessario passare il nome della colonna citato come carattere o l'indice intero per la colonna. Il solo passaggio Bpresuppone che B sia un oggetto stesso.
Shane

Vedo. Non sono sicuro di come sono finito con il sostituto contorto, eval, ecc.
kmm

3
Grazie! Ho scoperto che la [[soluzione era l'unica che funzionava per me.
EcologyTom

1
Ciao @Luis, dai un'occhiata a questa risposta
EcologyTom

78

Questa risposta coprirà molti degli stessi elementi delle risposte esistenti, ma questo problema (passare i nomi delle colonne alle funzioni) si presenta abbastanza spesso che volevo che ci fosse una risposta che coprisse le cose in modo un po 'più completo.

Supponiamo di avere un data frame molto semplice:

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

e vorremmo scrivere una funzione che crea una nuova colonna zche è la somma delle colonne xe y.

Un ostacolo molto comune qui è che un tentativo naturale (ma errato) spesso assomiglia a questo:

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

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

Il problema qui è che df$col1non valuta l'espressione col1. Cerca semplicemente una colonna in dfletteralmente chiamata col1. Questo comportamento è descritto nella ?Extractsezione "Oggetti ricorsivi (simili a liste)".

La soluzione più semplice e più spesso consigliata è semplicemente passare da $a [[e passare gli argomenti della funzione come stringhe:

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

Questa è spesso considerata "best practice" poiché è il metodo più difficile da sbagliare. Passare i nomi delle colonne come stringhe è quanto di più inequivocabile è possibile ottenere.

Le due opzioni seguenti sono più avanzate. Molti pacchetti popolari fanno uso di questo tipo di tecniche, ma usarle bene richiede maggiore attenzione e abilità, poiché possono introdurre sottili complessità e punti di fallimento imprevisti. Questa sezione del libro Advanced R di Hadley è un eccellente riferimento per alcuni di questi problemi.

Se vuoi davvero salvare l'utente dalla digitazione di tutte quelle virgolette, un'opzione potrebbe essere quella di convertire i nomi di colonna spogli e non quotati in stringhe 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

Questo è, francamente, un po 'sciocco probabilmente, dal momento che stiamo davvero facendo la stessa cosa di in new_column1, solo con un po' di lavoro extra per convertire i nomi nudi in stringhe.

Infine, se vogliamo essere davvero fantasiosi, potremmo decidere che invece di passare i nomi di due colonne da aggiungere, vorremmo essere più flessibili e consentire altre combinazioni di due variabili. In tal caso, probabilmente ricorreremo all'uso di eval()un'espressione che coinvolge le due colonne:

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

Tanto per divertimento, sto ancora usando deparse(substitute())per il nome della nuova colonna. Qui funzioneranno tutti i seguenti elementi:

> 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

Quindi la risposta breve è fondamentalmente: passare i nomi delle colonne data.frame come stringhe e utilizzare [[per selezionare singole colonne. Solo iniziare approfondendo eval, substituteecc se davvero sa cosa si sta facendo.


1
Non sono sicuro del motivo per cui questa non è la migliore risposta selezionata.
Ian

Anche io! Ottima spiegazione!
Alfredo G Marquez

22

Personalmente penso che passare la colonna come stringa sia piuttosto brutto. Mi piace fare qualcosa come:

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

che produrrà:

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

Notare come la specifica di un data.frame sia facoltativa. puoi persino lavorare con le funzioni delle tue colonne:

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

9
Hai bisogno di uscire dall'abitudine di pensare che usare le virgolette sia brutto. Non usarli è brutto! Perché? Perché hai creato una funzione che può essere utilizzata solo in modo interattivo, è molto difficile programmarla.
Hadley

27
Sono felice che mi venga mostrato un modo migliore, ma non riesco a vedere la differenza tra questo e qplot (x = mpg, data = mtcars). ggplot2 non passa mai una colonna come stringa, e penso che sia meglio per questo. Perché dici che questo può essere utilizzato solo interattivamente? In quale situazione porterebbe a risultati indesiderati? In che modo è più difficile programmare? Nel corpo del post mostro come sia più flessibile.
Ian Fellows

4
5 anni dopo -) .. Perché abbiamo bisogno di: parent.frame ()?
mql4beginner

15
7 anni dopo: non è ancora brutto usare le virgolette?
Spacedman

12

Un altro modo è usare l' tidy evaluationapproccio. È abbastanza semplice passare le colonne di un frame di dati come stringhe o come nomi di colonne nude. Vedi di più tidyeval qui .

library(rlang)
library(tidyverse)

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

Usa i nomi delle colonne come stringhe

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

Usa nomi di colonna semplici

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

Creato il 01-03-2019 dal pacchetto reprex (v0.2.1.9000)



1

Come pensiero in più, se è necessario passare il nome della colonna non quotato alla funzione personalizzata, forse match.call()potrebbe essere utile anche in questo caso, in 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

Se c'è un errore di battitura nel nome della colonna, sarebbe più sicuro fermarsi con un errore:

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

Creato l'11 gennaio 2019 dal pacchetto reprex (v0.2.1)

Non penso che userei questo approccio poiché c'è una digitazione e una complessità extra rispetto al semplice passaggio del nome della colonna tra virgolette come indicato nelle risposte precedenti, ma beh, è ​​un approccio.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.