Übergeben Sie einen data.frame-Spaltennamen an eine Funktion

119

Ich versuche eine Funktion zu schreiben, um ein data.frame ( x) und ein columndavon zu akzeptieren . Die Funktion führt einige Berechnungen für x durch und gibt später einen anderen data.frame zurück. Ich bleibe bei der Best-Practice-Methode, um den Spaltennamen an die Funktion zu übergeben.

Die beide minimal Beispiele fun1und fun2unter dem gewünschten Ergebnis zu führen in der Lage , Operationen an x$column, indem max()als Beispiel. Beide verlassen sich jedoch auf das scheinbar (zumindest für mich) Unelegante

  1. Anruf bei substitute()und möglicherweiseeval()
  2. die Notwendigkeit, den Spaltennamen als Zeichenvektor zu übergeben.

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

Ich möchte die Funktion beispielsweise als aufrufen können fun(df, B). Andere Optionen, die ich in Betracht gezogen, aber nicht ausprobiert habe:

  • Übergeben Sie columnals Ganzzahl der Spaltennummer. Ich denke das würde vermeiden substitute(). Idealerweise könnte die Funktion beides akzeptieren.
  • with(x, get(column)), aber selbst wenn es funktioniert, denke ich, würde dies immer noch erfordern substitute
  • Nutzen Sie formula()und match.call(), mit denen ich keine Erfahrung habe.

Unterfrage : Wird do.call()bevorzugt eval()?

kmm
quelle

Antworten:

108

Sie können den Spaltennamen einfach direkt verwenden:

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

Es ist nicht erforderlich, Ersatz, Bewertung usw. zu verwenden.

Sie können sogar die gewünschte Funktion als Parameter übergeben:

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

Alternativ [[funktioniert die Verwendung auch zum Auswählen einer einzelnen Spalte gleichzeitig:

df <- data.frame(A=1:10, B=2:11, C=3:12)
fun1 <- function(x, column){
  max(x[[column]])
}
fun1(df, "B")
Shane
quelle
13
Gibt es eine Möglichkeit, den Spaltennamen nicht als Zeichenfolge zu übergeben?
kmm
2
Sie müssen entweder den als Zeichen angegebenen Spaltennamen oder den Ganzzahlindex für die Spalte übergeben. Beim Bestehen Bwird davon ausgegangen, dass B selbst ein Objekt ist.
Shane
Aha. Ich bin nicht sicher, wie ich zu dem
verschlungenen
3
Vielen Dank! Ich fand, dass die [[Lösung die einzige war, die für mich funktionierte.
EcologyTom
1
Hallo @Luis, überprüfen Sie diese Antwort
EcologyTom
78

Diese Antwort wird viele der gleichen Elemente wie vorhandene Antworten abdecken, aber dieses Problem (Übergabe von Spaltennamen an Funktionen) tritt häufig genug auf, dass ich wollte, dass es eine Antwort gibt, die die Dinge etwas umfassender behandelt.

Angenommen, wir haben einen sehr einfachen Datenrahmen:

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

und wir möchten eine Funktion schreiben, die eine neue Spalte erstellt z, die die Summe der Spalten xund ist y.

Ein sehr häufiger Stolperstein ist, dass ein natürlicher (aber falscher) Versuch oft so aussieht:

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

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

Das Problem hierbei ist, dass df$col1der Ausdruck nicht ausgewertet wird col1. Es wird einfach nach einer Spalte dfgesucht, die buchstäblich aufgerufen wird col1. Dieses Verhalten wird im ?ExtractAbschnitt "Rekursive (listähnliche) Objekte" beschrieben.

Die einfachste und am häufigsten empfohlene Lösung besteht darin, einfach von $zu zu wechseln [[und die Funktionsargumente als Zeichenfolgen zu übergeben:

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

Dies wird oft als "Best Practice" bezeichnet, da es die Methode ist, die am schwierigsten zu vermasseln ist. Das Übergeben der Spaltennamen als Zeichenfolgen ist so eindeutig wie möglich.

Die folgenden zwei Optionen sind weiter fortgeschritten. Viele beliebte Pakete verwenden diese Art von Techniken, aber ihre gute Verwendung erfordert mehr Sorgfalt und Geschick, da sie subtile Komplexitäten und unerwartete Fehlerquellen mit sich bringen können. Dieser Abschnitt von Hadleys Advanced R-Buch ist eine hervorragende Referenz für einige dieser Probleme.

Wenn Sie den Benutzer wirklich vor der Eingabe all dieser Anführungszeichen bewahren möchten, besteht eine Option möglicherweise darin, nackte, nicht in Anführungszeichen gesetzte Spaltennamen in Zeichenfolgen zu konvertieren, indem Sie Folgendes verwenden 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

Das ist, ehrlich gesagt, wahrscheinlich ein bisschen albern, da wir wirklich das Gleiche tun wie in new_column1, nur mit ein paar zusätzlichen Arbeiten, um nackte Namen in Zeichenfolgen umzuwandeln.

Wenn wir wirklich ausgefallen sein möchten , können wir uns entscheiden, dass wir flexibler sein und andere Kombinationen von zwei Variablen zulassen möchten , anstatt die Namen von zwei hinzuzufügenden Spalten zu übergeben. In diesem Fall würden wir wahrscheinlich eval()auf einen Ausdruck zurückgreifen, der die beiden Spalten umfasst:

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

Nur zum Spaß verwende ich immer noch deparse(substitute())den Namen der neuen Spalte. Hier funktioniert alles:

> 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

Die kurze Antwort lautet also im Grunde: Übergeben Sie die Spaltennamen von data.frame als Zeichenfolgen und [[wählen Sie einzelne Spalten aus. Fangen Sie erst an eval, sich substitutemit usw. zu beschäftigen, wenn Sie wirklich wissen, was Sie tun.

Joran
quelle
1
Ich bin mir nicht sicher, warum dies nicht die beste Antwort ist.
Ian
Ich auch! Tolle Erklärung!
Alfredo G Marquez
22

Persönlich finde ich es ziemlich hässlich, die Spalte als String zu übergeben. Ich mache gerne so etwas wie:

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

was ergeben wird:

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

Beachten Sie, dass die Angabe eines data.frame optional ist. Sie können sogar mit Funktionen Ihrer Spalten arbeiten:

> get.max(1/mpg,mtcars)
[1] 0.09615385
Ian Fellows
quelle
9
Sie müssen aus der Gewohnheit herauskommen, mit Anführungszeichen zu denken, ist hässlich. Sie nicht zu benutzen ist hässlich! Warum? Da Sie eine Funktion erstellt haben, die nur interaktiv verwendet werden kann, ist es sehr schwierig, damit zu programmieren.
Hadley
27
Ich bin froh, dass mir ein besserer Weg gezeigt wird, aber ich sehe keinen Unterschied zwischen diesem und qplot (x = mpg, data = mtcars). ggplot2 übergibt niemals eine Spalte als Zeichenfolge, und ich denke, es ist besser dafür. Warum kann dies nur interaktiv genutzt werden? In welcher Situation würde dies zu unerwünschten Ergebnissen führen? Wie ist es schwieriger zu programmieren? Im Hauptteil des Beitrags zeige ich, wie flexibler es ist.
Ian Fellows
4
5 Jahre später -) .. Warum brauchen wir: parent.frame ()?
mql4beginner
15
7 Jahre später: Ist die Verwendung von Zitaten nicht immer noch hässlich?
Spacedman
11

Ein anderer Weg ist die Verwendung des tidy evaluationAnsatzes. Es ist ziemlich einfach, Spalten eines Datenrahmens entweder als Zeichenfolgen oder als bloße Spaltennamen zu übergeben. Weitere Informationen finden Sie tidyeval hier .

library(rlang)
library(tidyverse)

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

Verwenden Sie Spaltennamen als Zeichenfolgen

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

Verwenden Sie bloße Spaltennamen

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

Erstellt am 01.03.2019 durch das reprex-Paket (v0.2.1.9000)

Tung
quelle
Siehe auch
Tung
1

Wenn ein zusätzlicher Gedanke erforderlich ist, um den nicht zitierten Spaltennamen an die benutzerdefinierte Funktion zu übergeben, kann dies möglicherweise match.call()auch in diesem Fall als Alternative zu Folgendem nützlich sein 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

Wenn der Spaltenname einen Tippfehler enthält, ist es sicherer, mit einem Fehler aufzuhören:

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

Erstellt am 2019-01-11 vom reprex-Paket (v0.2.1)

Ich glaube nicht, dass ich diesen Ansatz verwenden würde, da es mehr Typisierung und Komplexität gibt, als nur den zitierten Spaltennamen zu übergeben, wie in den obigen Antworten angegeben, aber gut, ist ein Ansatz.

Valentin
quelle