ส่งชื่อคอลัมน์ data.frame ไปยังฟังก์ชัน


119

ฉันกำลังพยายามเขียนฟังก์ชันเพื่อยอมรับ data.frame ( x) และ a columnจากมัน ฟังก์ชันทำการคำนวณบางอย่างบน x และส่งคืน data.frame อื่นในภายหลัง ฉันติดอยู่กับวิธีการปฏิบัติที่ดีที่สุดในการส่งชื่อคอลัมน์ไปยังฟังก์ชัน

สองตัวอย่างน้อยที่สุดfun1และfun2ด้านล่างให้ผลลัพธ์ที่ต้องการสามารถดำเนินการได้x$columnโดยใช้max()เป็นตัวอย่าง อย่างไรก็ตามทั้งสองต้องพึ่งพาสิ่งที่ดูเหมือน (อย่างน้อยก็สำหรับฉัน) ไม่สง่างาม

  1. โทรหาsubstitute()และอาจเป็นไปได้eval()
  2. จำเป็นต้องส่งชื่อคอลัมน์เป็นเวกเตอร์อักขระ

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

ฉันต้องการเรียกใช้ฟังก์ชันดังfun(df, B)ตัวอย่างเช่น ตัวเลือกอื่น ๆ ที่ฉันได้พิจารณาแล้ว แต่ยังไม่ได้ลอง:

  • ส่งผ่านcolumnเป็นจำนวนเต็มของหมายเลขคอลัมน์ substitute()ผมคิดว่านี่จะหลีกเลี่ยง ตามหลักการแล้วฟังก์ชันสามารถยอมรับได้เช่นกัน
  • with(x, get(column))แต่แม้ว่ามันจะใช้งานได้ฉันคิดว่าสิ่งนี้ยังคงต้องการ substitute
  • ทำให้การใช้งานformula()และmatch.call()ทั้งที่ผมมีประสบการณ์มากกับ

คำถามย่อย : เป็นdo.call()ที่ต้องการมากกว่าeval()หรือไม่?

คำตอบ:


108

คุณสามารถใช้ชื่อคอลัมน์ได้โดยตรง:

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

ไม่จำเป็นต้องใช้การทดแทน eval ฯลฯ

คุณสามารถส่งผ่านฟังก์ชันที่ต้องการเป็นพารามิเตอร์:

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

หรืออีกวิธีหนึ่งคือการใช้[[งานเพื่อเลือกทีละคอลัมน์:

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

14
มีวิธีใดบ้างที่จะส่งชื่อคอลัมน์ไม่เป็นสตริง
ก.ม.

2
คุณต้องส่งชื่อคอลัมน์ที่ยกมาเป็นอักขระหรือดัชนีจำนวนเต็มสำหรับคอลัมน์ เพียงแค่ผ่านBจะถือว่า B เป็นวัตถุนั้นเอง
เชน

ฉันเห็น. ฉันไม่แน่ใจว่าฉันลงเอยด้วยการแทนที่แบบซับซ้อน eval ฯลฯ ได้อย่างไร
kmm

3
ขอบคุณ! ฉันพบว่า[[ทางออกคือทางออกเดียวที่เหมาะกับฉัน
EcologyTom

1
สวัสดี @Luis ลองดูคำตอบนี้
EcologyTom

78

คำตอบนี้จะครอบคลุมองค์ประกอบหลายอย่างเช่นเดียวกับคำตอบที่มีอยู่ แต่ปัญหานี้ (การส่งชื่อคอลัมน์ไปยังฟังก์ชัน) มักเกิดขึ้นมากพอที่ฉันต้องการให้มีคำตอบที่ครอบคลุมสิ่งต่าง ๆ อย่างครอบคลุมมากขึ้นเล็กน้อย

สมมติว่าเรามี data frame ง่ายๆ:

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

และเราต้องการเขียนฟังก์ชันที่สร้างคอลัมน์ใหม่zซึ่งเป็นผลรวมของคอลัมน์xและ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)

นี่คือปัญหาที่ไม่ได้ประเมินการแสดงออกdf$col1 col1มันก็จะมองหาคอลัมน์ในที่เรียกว่าอักษรdf col1ลักษณะการทำงานนี้ได้อธิบายไว้ใน?Extractส่วน "วัตถุที่เกิดซ้ำ (เหมือนรายการ)"

วิธีแก้ปัญหาที่ง่ายที่สุดและแนะนำบ่อยที่สุดคือเปลี่ยนจาก$เป็น[[และส่งผ่านอาร์กิวเมนต์ของฟังก์ชันเป็นสตริง:

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

นี่มักถือเป็น "แนวทางปฏิบัติที่ดีที่สุด" เนื่องจากเป็นวิธีการที่ยากที่สุดในการแก้ไขปัญหา การส่งผ่านชื่อคอลัมน์เป็นสตริงนั้นไม่คลุมเครือเท่าที่คุณจะได้รับ

สองตัวเลือกต่อไปนี้เป็นขั้นสูง แพ็กเกจยอดนิยมจำนวนมากใช้เทคนิคประเภทนี้ แต่การใช้ให้ดีต้องใช้ความระมัดระวังและทักษะมากกว่าเนื่องจากสามารถแนะนำความซับซ้อนที่ละเอียดอ่อนและจุดล้มเหลวที่ไม่คาดคิดได้ ส่วนนี้ของหนังสือ Advanced R ของ Hadley เป็นข้อมูลอ้างอิงที่ดีเยี่ยมสำหรับปัญหาเหล่านี้

หากคุณจริงๆต้องการที่จะบันทึกผู้ใช้จากการพิมพ์คำพูดเหล่านั้นทั้งหมดเป็นทางเลือกหนึ่งอาจจะมีการแปลงเปลือยชื่อคอลัมน์ unquoted สตริงใช้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

ตรงไปตรงมาอาจเป็นเรื่องโง่เล็กน้อยเนื่องจากเรากำลังทำสิ่งเดียวกันกับในnew_column1เพียงแค่มีงานพิเศษมากมายในการแปลงชื่อเปล่าเป็นสตริง

ในที่สุดถ้าเราอยากได้แบบแฟนซีจริงๆเราอาจตัดสินใจได้ว่าแทนที่จะส่งชื่อสองคอลัมน์มาเพิ่มเราต้องการที่จะยืดหยุ่นมากขึ้นและอนุญาตให้มีการผสมสองตัวแปรอื่น ๆ ในกรณีนี้เราน่าจะใช้eval()กับนิพจน์ที่เกี่ยวข้องกับสองคอลัมน์:

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

เพื่อความสนุกสนานฉันยังคงใช้deparse(substitute())ชื่อคอลัมน์ใหม่ ที่นี่สิ่งต่อไปนี้ทั้งหมดจะใช้ได้:

> 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

ดังนั้นคำตอบสั้น ๆ คือโดยทั่วไป: ส่งชื่อคอลัมน์ data.frame เป็นสตริงและใช้[[เพื่อเลือกคอลัมน์เดียว เพียงเริ่มขุดคุ้ยeval, substituteฯลฯ ถ้าคุณรู้ว่าสิ่งที่คุณกำลังทำ


1
ไม่แน่ใจว่าเหตุใดจึงไม่ใช่คำตอบที่ดีที่สุดที่เลือกไว้
เอียน

ฉันอย่างใดอย่างหนึ่ง! คำอธิบายที่ยอดเยี่ยม!
Alfredo G Marquez

22

โดยส่วนตัวฉันคิดว่าการส่งคอลัมน์เป็นสตริงนั้นค่อนข้างน่าเกลียด ฉันชอบทำสิ่งที่ชอบ:

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

ซึ่งจะให้ผล:

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

สังเกตว่าข้อกำหนดของ data.frame เป็นทางเลือกอย่างไร คุณยังสามารถทำงานกับฟังก์ชันของคอลัมน์ของคุณ:

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

9
คุณต้องออกจากนิสัยการใช้คำพูดที่น่าเกลียด ไม่ใช้ก็น่าเกลียด! ทำไม? เนื่องจากคุณได้สร้างฟังก์ชันที่สามารถใช้โต้ตอบได้เท่านั้นจึงเป็นเรื่องยากมากที่จะตั้งโปรแกรมด้วย
hadley

27
ฉันดีใจที่ได้แสดงวิธีที่ดีกว่านี้ แต่ฉันไม่เห็นความแตกต่างระหว่างสิ่งนี้กับ qplot (x = mpg, data = mtcars) ggplot2 ไม่เคยส่งคอลัมน์เป็นสตริงและฉันคิดว่ามันดีกว่าสำหรับมัน ทำไมคุณถึงบอกว่าสิ่งนี้สามารถใช้โต้ตอบได้เท่านั้น? ภายใต้สถานการณ์ใดที่จะนำไปสู่ผลลัพธ์ที่ไม่พึงปรารถนา? โปรแกรมยากกว่าอย่างไร ในเนื้อหาของโพสต์ฉันแสดงให้เห็นว่ามันยืดหยุ่นได้อย่างไร
Ian Fellows

4
5 ปีต่อมา -) .. ทำไมเราถึงต้องการ: parent.frame ()?
mql4beginner

15
7 ปีต่อมา: การไม่ใช้เครื่องหมายคำพูดยังน่าเกลียดอยู่หรือเปล่า?
Spacedman

12

อีกวิธีหนึ่งคือการใช้tidy evaluationวิธีการ ค่อนข้างตรงไปตรงมาที่จะส่งผ่านคอลัมน์ของ data frame ไม่ว่าจะเป็นสตริงหรือชื่อคอลัมน์เปล่า ดูเพิ่มเติมเกี่ยวกับที่นี่tidyeval

library(rlang)
library(tidyverse)

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

ใช้ชื่อคอลัมน์เป็นสตริง

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

ใช้ชื่อคอลัมน์เปล่า

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

สร้างเมื่อ 2019-03-01 โดยreprex package (v0.2.1.9000)


ที่เกี่ยวข้อง: stackoverflow.com/questions/54940237/…
ตุง

1

ตามความคิดเพิ่มเติมหากจำเป็นต้องส่งชื่อคอลัมน์ที่ไม่มีเครื่องหมายคำพูดไปยังฟังก์ชันที่กำหนดเองบางทีmatch.call()อาจเป็นประโยชน์เช่นกันในกรณีนี้เป็นทางเลือกอื่นสำหรับ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

หากมีการพิมพ์ผิดในชื่อคอลัมน์การหยุดด้วยข้อผิดพลาดจะปลอดภัยกว่า:

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

สร้างเมื่อ 2019-01-11 โดยแพ็คเกจ reprex (v0.2.1)

ฉันไม่คิดว่าฉันจะใช้วิธีนี้เนื่องจากมีการพิมพ์พิเศษและซับซ้อนมากกว่าการส่งชื่อคอลัมน์ที่ยกมาตามที่ระบุไว้ในคำตอบข้างต้น แต่ก็เป็นแนวทางที่ดี

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.