Przekaż nazwę kolumny data.frame do funkcji

119

Próbuję napisać funkcję akceptującą data.frame ( x) iz columnniej. Funkcja wykonuje pewne obliczenia na x, a później zwraca kolejną ramkę data.frame. Utknąłem na najlepszej metodzie przekazywania nazwy kolumny do funkcji.

Dwa minimalne przykłady fun1ifun2 poniżej uzyskania pożądanego rezultatu, jest w stanie wykonywać operacje na x$columnużywając max()jako przykład. Jednak obaj opierają się na pozornie (przynajmniej dla mnie) nieeleganckiej

  1. zadzwonić substitute()i ewentualnieeval()
  2. konieczność przekazania nazwy kolumny jako wektora znakowego.

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

Chciałbym móc wywołać tę funkcję jako fun(df, B) . Inne opcje, które rozważałem, ale nie próbowałem:

  • Przechodzić column jako liczbę całkowitą numeru kolumny. Myślę, że to pozwoliłoby uniknąć substitute(). Idealnie, funkcja mogłaby zaakceptować oba.
  • with(x, get(column)), ale myślę, że nawet jeśli to zadziała, nadal będzie to wymagało substitute
  • Skorzystaj z formula()imatch.call() , z którymi nie mam dużego doświadczenia.

Pytanie dodatkowe : Czy jest do.call()preferowane eval()?

kmm
źródło

Odpowiedzi:

108

Możesz po prostu bezpośrednio użyć nazwy kolumny:

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

Nie ma potrzeby używania substytutów, ewaluacji itp.

Możesz nawet przekazać żądaną funkcję jako parametr:

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

Alternatywnie, użycie [[działa również do zaznaczania jednej kolumny naraz:

df <- data.frame(A=1:10, B=2:11, C=3:12)
fun1 <- function(x, column){
  max(x[[column]])
}
fun1(df, "B")
Shane
źródło
14
Czy istnieje sposób na przekazanie nazwy kolumny nie jako ciągu?
kmm
2
Musisz przekazać nazwę kolumny w cudzysłowie jako znak lub indeks w postaci liczby całkowitej. Samo zdanie Bzakłada, że ​​sam B jest obiektem.
Shane,
Widzę. Nie jestem pewien, jak skończyłem z zawiłym substytutem,
evalem
3
Dzięki! Okazało się [[, że jedyne rozwiązanie działało dla mnie.
Ekologia Tom
1
Cześć @Luis, sprawdź tę odpowiedź
EcologyTom
78

Ta odpowiedź obejmie wiele takich samych elementów, jak istniejące odpowiedzi, ale ten problem (przekazywanie nazw kolumn do funkcji) pojawia się na tyle często, że chciałem, aby była odpowiedź, która obejmowałaby sprawy nieco bardziej kompleksowo.

Załóżmy, że mamy bardzo prostą ramkę danych:

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

i chcielibyśmy napisać funkcję, która tworzy nową kolumnę będącą zsumą kolumn xi y.

Bardzo częstą przeszkodą jest tutaj to, że naturalna (ale błędna) próba często wygląda tak:

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

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

Problem polega na tym, df$col1że nie ocenia wyrażenia col1. Po prostu szuka kolumny w dfdosłownie nazwanym col1. To zachowanie opisano w ?Extractsekcji „Obiekty rekurencyjne (podobne do list)”.

Najprostszym i najczęściej zalecanym rozwiązaniem jest po prostu przełączenie się z opcji $na [[i przekazanie argumentów funkcji jako ciągów:

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

Jest to często uważane za „najlepszą praktykę”, ponieważ jest to metoda najtrudniejsza do zepsucia. Przekazywanie nazw kolumn jako ciągów jest tak jednoznaczne, jak to tylko możliwe.

Poniższe dwie opcje są bardziej zaawansowane. Wiele popularnych pakietów korzysta z tego rodzaju technik, ale ich dobre użycie wymaga więcej uwagi i umiejętności, ponieważ mogą one wprowadzić subtelne zawiłości i nieprzewidziane punkty awarii. Ta sekcja książki Hadley's Advanced R jest doskonałym źródłem informacji na temat niektórych z tych zagadnień.

Jeśli naprawdę chcesz uchronić użytkownika przed wpisywaniem wszystkich tych cudzysłowów, jedną z opcji może być przekonwertowanie pustych, niecytowanych nazw kolumn na ciągi przy użyciu 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

Jest to, szczerze mówiąc, trochę głupie, ponieważ tak naprawdę robimy to samo, co w programie new_column1, tylko z masą dodatkowej pracy nad konwersją nagich nazw na ciągi.

Wreszcie, jeśli chcemy uzyskać naprawdę wymyślny wygląd, możemy zdecydować, że zamiast podawać nazwy dwóch kolumn do dodania, chcielibyśmy być bardziej elastyczni i pozwolić na inne kombinacje dwóch zmiennych. W takim przypadku prawdopodobnie uciekniemy się do eval()wyrażenia obejmującego dwie kolumny:

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

Dla zabawy nadal używam deparse(substitute())nazwy nowej kolumny. Tutaj będą działać wszystkie poniższe elementy:

> 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

Krótka odpowiedź brzmi więc w zasadzie: przekazuj nazwy kolumn data.frame jako ciągi i użyj [[do zaznaczania pojedynczych kolumn. Uruchomić tylko zagłębiając się eval, substituteitp jeśli naprawdę wiesz co robisz.

joran
źródło
1
Nie wiem, dlaczego nie jest to wybrana najlepsza odpowiedź.
Ian
Ja też! Świetne wyjaśnienie!
Alfredo G Marquez
22

Osobiście uważam, że przekazywanie kolumny jako łańcucha jest dość brzydkie. Lubię robić coś takiego:

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

co da:

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

Zwróć uwagę, że specyfikacja data.frame jest opcjonalna. możesz nawet pracować z funkcjami swoich kolumn:

> get.max(1/mpg,mtcars)
[1] 0.09615385
Ian Fellows
źródło
9
Musisz wyjść z nawyku myślenia cytatami, które są brzydkie. Nie używanie ich jest brzydkie! Czemu? Ponieważ stworzyłeś funkcję, której można używać tylko interaktywnie - bardzo trudno jest z nią programować.
hadley
27
Cieszę się, że pokazano mi lepszy sposób, ale nie widzę różnicy między tym a qplot (x = mpg, data = mtcars). ggplot2 nigdy nie przekazuje kolumny jako łańcucha i myślę, że jest to dla niego lepsze. Dlaczego mówisz, że można tego używać tylko interaktywnie? W jakiej sytuacji doprowadziłoby to do niepożądanych skutków? Jak trudniej jest programować? W treści wpisu pokazuję, że jest bardziej elastyczny.
Ian Fellows
4
5 lat później -) .. Po co nam: parent.frame ()?
mql4beginner
15
7 lat później: czy cytaty nie są nadal brzydkie?
Spacedman
12

Innym sposobem jest użycie tidy evaluationpodejścia. Przekazywanie kolumn ramki danych jako łańcuchów lub nagich nazw kolumn jest całkiem proste. Zobacz więcej tidyeval tutaj .

library(rlang)
library(tidyverse)

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

Użyj nazw kolumn jako ciągów

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

Użyj nazw kolumn

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

Utworzono 01.03.2019 przez pakiet reprex (v0.2.1.9000)

Tung
źródło
1

Jako dodatkowa myśl, jeśli konieczne jest przekazanie nazwy kolumny bez cudzysłowu do funkcji niestandardowej, być może match.call()może być również przydatne w tym przypadku, jako alternatywa dla 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

Jeśli w nazwie kolumny jest literówka, bezpieczniej byłoby zatrzymać się z błędem:

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

Utworzono 11.01.2019 przez pakiet reprex (v0.2.1)

Nie sądzę, żebym użył tego podejścia, ponieważ istnieje dodatkowe wpisywanie i złożoność niż zwykłe przekazywanie cytowanej nazwy kolumny, jak wskazano w powyższych odpowiedziach, ale cóż, jest to podejście.

Valentin
źródło