Truyền tên cột data.frame cho hàm


119

Tôi đang cố gắng viết một hàm để chấp nhận data.frame ( x) và columntừ nó. Hàm thực hiện một số tính toán trên x và sau đó trả về một data.frame khác. Tôi bị mắc kẹt trong phương thức thực hành tốt nhất để truyền tên cột cho hàm.

Hai ví dụ tối thiểu fun1fun2bên dưới tạo ra kết quả mong muốn, có thể thực hiện các thao tác trên x$column, sử dụng max()làm ví dụ. Tuy nhiên, cả hai đều dựa vào sự không phù hợp (ít nhất là với tôi)

  1. gọi đến substitute()và có thểeval()
  2. sự cần thiết phải vượt qua tên cột như một vector ký tự.

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

Tôi muốn có thể gọi các chức năng như fun(df, B), ví dụ. Các tùy chọn khác tôi đã xem xét nhưng chưa thử:

  • Truyền columndưới dạng một số nguyên của số cột. Tôi nghĩ rằng điều này sẽ tránh substitute(). Lý tưởng nhất là chức năng có thể chấp nhận.
  • with(x, get(column)), nhưng, ngay cả khi nó hoạt động, tôi nghĩ rằng điều này vẫn sẽ yêu cầu substitute
  • Sử dụng formula()match.call(), không ai trong số đó tôi có nhiều kinh nghiệm.

Subquestion : Được do.call()ưa thích hơn eval()?

Câu trả lời:


108

Bạn chỉ có thể sử dụng tên cột trực tiếp:

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

Không cần sử dụng thay thế, eval, v.v.

Bạn thậm chí có thể truyền hàm mong muốn dưới dạng tham số:

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

Ngoài ra, sử dụng [[cũng hoạt động để chọn một cột duy nhất tại một thời điểm:

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

13
Có cách nào để vượt qua tên cột không phải là một chuỗi?
kmm

2
Bạn cần phải vượt qua tên cột được trích dẫn dưới dạng ký tự hoặc chỉ số nguyên cho cột. Chỉ cần vượt qua Bsẽ cho rằng B là một đối tượng chính nó.
Shane

Tôi hiểu rồi. Tôi không chắc chắn làm thế nào tôi kết thúc với sự thay thế phức tạp, eval, v.v.
kmm

3
Cảm ơn! Tôi tìm thấy [[giải pháp là người duy nhất làm việc cho tôi.
Sinh thái học

1
Xin chào @Luis, hãy xem câu trả lời này
EcologyTom

78

Câu trả lời này sẽ bao gồm nhiều yếu tố giống như câu trả lời hiện có, nhưng vấn đề này (chuyển tên cột cho các hàm) xuất hiện thường xuyên đến mức tôi muốn có một câu trả lời bao quát mọi thứ một cách toàn diện hơn.

Giả sử chúng ta có một khung dữ liệu rất đơn giản:

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

và chúng tôi muốn viết một hàm tạo một cột mới zlà tổng của các cột xy.

Một vấp ngã rất phổ biến ở đây là một nỗ lực tự nhiên (nhưng không chính xác) thường trông như thế này:

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

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

Vấn đề ở đây là df$col1không đánh giá biểu thức col1. Nó chỉ đơn giản là tìm kiếm một cột theo dfnghĩa đen được gọi col1. Hành vi này được mô tả trong ?Extractphần "Đối tượng đệ quy (giống như danh sách)".

Giải pháp đơn giản nhất và thường được đề xuất nhất chỉ đơn giản là chuyển từ $sang [[và truyền các đối số hàm dưới dạng chuỗi:

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

Đây thường được coi là "thực hành tốt nhất" vì đây là phương pháp khó thực hiện nhất. Truyền tên cột dưới dạng chuỗi là không rõ ràng như bạn có thể nhận được.

Hai tùy chọn sau đây tiên tiến hơn. Nhiều gói phổ biến sử dụng các loại kỹ thuật này, nhưng sử dụng chúng tốt đòi hỏi nhiều kỹ năng và kỹ năng hơn, vì chúng có thể giới thiệu sự phức tạp tinh tế và các điểm thất bại không lường trước được. Đây phần của cuốn sách nâng cao R Hadley là một tài liệu tham khảo tuyệt vời cho một số trong những vấn đề này.

Nếu bạn thực sự muốn lưu người dùng nhập tất cả các trích dẫn đó, một tùy chọn có thể là chuyển đổi tên cột trống, không trích dẫn thành chuỗi bằng cách sử dụng 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

Thật ra, điều này có vẻ hơi ngớ ngẩn, vì chúng ta thực sự đang làm điều tương tự như trong new_column1, chỉ với một loạt các công việc phụ để chuyển đổi tên trần thành chuỗi.

Cuối cùng, nếu chúng ta muốn thực sự ưa thích, chúng ta có thể quyết định thay vì nhập tên của hai cột để thêm, chúng ta muốn linh hoạt hơn và cho phép kết hợp hai biến khác. Trong trường hợp đó, chúng tôi có thể sử dụng eval()một biểu thức liên quan đến hai cột:

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

Để cho vui, tôi vẫn đang sử dụng deparse(substitute())tên của cột mới. Ở đây, tất cả những điều sau đây sẽ hoạt động:

> 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

Vì vậy, câu trả lời ngắn về cơ bản là: truyền tên cột data.frame dưới dạng chuỗi và sử dụng [[để chọn các cột đơn. Chỉ bắt đầu đi sâu vào eval, substitutevv nếu bạn thực sự biết những gì bạn đang làm.


1
Không chắc chắn tại sao đây không phải là câu trả lời tốt nhất được chọn.
Ian

Tôi cũng vậy! Giải thích tuyệt vời!
Alfredo G Marquez

22

Cá nhân tôi nghĩ rằng việc vượt qua cột dưới dạng một chuỗi là khá xấu xí. Tôi thích làm một cái gì đó như:

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

sẽ mang lại:

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

Lưu ý cách đặc tả của data.frame là tùy chọn. bạn thậm chí có thể làm việc với các chức năng của các cột:

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

9
Bạn cần thoát khỏi thói quen suy nghĩ sử dụng dấu ngoặc kép là xấu xí. Không sử dụng chúng là xấu xí! Tại sao? Bởi vì bạn đã tạo một chức năng chỉ có thể được sử dụng tương tác - rất khó để lập trình với nó.
hadley

27
Tôi rất vui khi được chỉ ra một cách tốt hơn, nhưng tôi không thấy sự khác biệt giữa cái này và qplot (x = mpg, data = mtcars). ggplot2 không bao giờ vượt qua một cột dưới dạng một chuỗi và tôi nghĩ rằng nó tốt hơn cho nó. Tại sao bạn nói rằng điều này chỉ có thể được sử dụng tương tác? Trong tình huống nào nó sẽ dẫn đến kết quả không mong muốn? Làm thế nào là khó khăn hơn để lập trình với? Trong phần thân bài tôi hiển thị cách nó linh hoạt hơn.
Ian Fellows

4
5 năm sau -) .. Tại sao chúng ta cần: Parent.frame ()?
mql4beginner

15
7 năm sau: không sử dụng dấu ngoặc kép vẫn xấu?
Spainedman

11

Một cách khác là sử dụng tidy evaluationphương pháp tiếp cận. Việc chuyển các cột của khung dữ liệu dưới dạng chuỗi hoặc tên cột trần là khá đơn giản. Xem thêm về tidyeval đây .

library(rlang)
library(tidyverse)

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

Sử dụng tên cột làm chuỗi

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

Sử dụng tên cột trần

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

Được tạo vào ngày 2019-03-01 bởi gói reprex (v0.2.1.9000)



1

Như một suy nghĩ thêm, nếu cần thiết để chuyển tên cột không được trích dẫn cho chức năng tùy chỉnh, có lẽ match.call()cũng có thể hữu ích trong trường hợp này, như là một cách thay thế cho 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

Nếu có một lỗi đánh máy trong tên cột, thì sẽ an toàn hơn khi dừng với một lỗi:

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

Được tạo vào ngày 2019-01-11 bởi gói reprex (v0.2.1)

Tôi không nghĩ rằng tôi sẽ sử dụng cách tiếp cận này vì có thêm cách gõ và độ phức tạp hơn là chỉ chuyển tên cột được trích dẫn như được chỉ ra trong các câu trả lời ở trên, nhưng tốt, là một cách tiếp cận.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.