Meneruskan nama kolom data.frame ke suatu fungsi

119

Saya mencoba menulis fungsi untuk menerima data.frame ( x) dan columndari itu. Fungsi ini melakukan beberapa kalkulasi pada x dan kemudian mengembalikan data.frame lain. Saya terjebak pada metode praktik terbaik untuk meneruskan nama kolom ke fungsi.

Dua contoh minimal fun1dan di fun2bawah ini menghasilkan hasil yang diinginkan, dapat melakukan operasi pada x$column, menggunakan max()sebagai contoh. Namun, keduanya mengandalkan yang tampaknya (setidaknya bagi saya) janggal

  1. panggilan ke substitute()dan mungkineval()
  2. kebutuhan untuk melewatkan nama kolom sebagai vektor karakter.

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

Saya ingin dapat memanggil fungsi tersebut sebagai fun(df, B), misalnya. Opsi lain yang telah saya pertimbangkan tetapi belum saya coba:

  • Lulus columnsebagai bilangan bulat dari nomor kolom. Saya pikir ini akan menghindar substitute(). Idealnya, fungsinya bisa menerima keduanya.
  • with(x, get(column)), tetapi, bahkan jika berhasil, saya pikir ini masih akan dibutuhkan substitute
  • Memanfaatkan formula()dan match.call(), tidak ada yang saya punya banyak pengalaman dengannya.

Subquestion : Apakah do.call()lebih disukai eval()?

kmm
sumber

Jawaban:

108

Anda bisa langsung menggunakan nama kolom:

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

Tidak perlu menggunakan substitusi, eval, dll.

Anda bahkan dapat meneruskan fungsi yang diinginkan sebagai parameter:

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

Alternatifnya, menggunakan [[juga berfungsi untuk memilih satu kolom dalam satu waktu:

df <- data.frame(A=1:10, B=2:11, C=3:12)
fun1 <- function(x, column){
  max(x[[column]])
}
fun1(df, "B")
Shane
sumber
14
Apakah ada cara untuk melewatkan nama kolom bukan sebagai string?
kmm
2
Anda harus meneruskan nama kolom yang dikutip sebagai karakter atau indeks integer untuk kolom tersebut. Melewati saja Bakan menganggap bahwa B adalah objek itu sendiri.
Shane
Saya melihat. Saya tidak yakin bagaimana saya berakhir dengan pengganti yang berbelit-belit, eval, dll.
kmm
3
Terima kasih! Saya menemukan [[solusinya adalah satu-satunya yang berhasil untuk saya.
EcologyTom
1
Hai @Luis, lihat jawaban ini
EcologyTom
78

Jawaban ini akan mencakup banyak elemen yang sama dengan jawaban yang sudah ada, tetapi masalah ini (meneruskan nama kolom ke fungsi) cukup sering muncul sehingga saya ingin ada jawaban yang mencakup hal-hal sedikit lebih komprehensif.

Misalkan kita memiliki kerangka data yang sangat sederhana:

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

dan kami ingin menulis fungsi yang membuat kolom baru zyang merupakan jumlah kolom xdan y.

Batu sandungan yang sangat umum di sini adalah bahwa upaya alami (tetapi tidak benar) sering kali terlihat seperti ini:

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

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

Masalahnya di sini adalah itu df$col1tidak mengevaluasi ekspresi col1. Ini hanya mencari kolom yang dfsecara harfiah disebut col1. Perilaku ini dijelaskan di ?Extractbawah bagian "Objek rekursif (seperti daftar)".

Solusi paling sederhana, dan paling sering direkomendasikan adalah dengan beralih dari $ke [[dan meneruskan argumen fungsi sebagai string:

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

Ini sering dianggap "praktik terbaik" karena ini adalah metode yang paling sulit untuk gagal. Meneruskan nama kolom sebagai string sama jelasnya dengan yang Anda bisa.

Dua opsi berikut ini lebih maju. Banyak paket populer yang menggunakan jenis teknik ini, tetapi menggunakannya dengan baik membutuhkan lebih banyak perhatian dan keterampilan, karena paket tersebut dapat menimbulkan kerumitan halus dan titik kegagalan yang tidak terduga. Ini bagian dari buku Lanjutan R Hadley adalah referensi yang sangat baik untuk beberapa masalah ini.

Jika Anda benar - benar ingin menyelamatkan pengguna agar tidak mengetik semua tanda kutip tersebut, salah satu opsi mungkin adalah mengonversi nama kolom yang kosong dan tidak bertanda kutip menjadi string menggunakan 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

Ini, sejujurnya, mungkin agak konyol, karena kami benar-benar melakukan hal yang sama seperti di new_column1, hanya dengan banyak pekerjaan tambahan untuk mengubah nama kosong menjadi string.

Terakhir, jika kita ingin benar - benar mewah, kita mungkin memutuskan bahwa daripada memasukkan nama dua kolom untuk ditambahkan, kita ingin lebih fleksibel dan memungkinkan kombinasi lain dari dua variabel. Dalam hal ini kami kemungkinan akan menggunakan eval()ekspresi yang melibatkan dua kolom:

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

Cuma iseng, saya masih pakai deparse(substitute())untuk nama kolom baru. Di sini, semua hal berikut akan berfungsi:

> 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

Jadi jawaban singkatnya pada dasarnya adalah: berikan nama kolom data.frame sebagai string dan gunakan [[untuk memilih kolom tunggal. Hanya mulai menggali eval, substitute, dll jika Anda benar-benar tahu apa yang Anda lakukan.

joran
sumber
1
Tidak yakin mengapa ini bukan jawaban terbaik yang dipilih.
Ian
Begitu juga dengan saya! Penjelasan yang bagus!
Alfredo G Marquez
22

Secara pribadi saya berpikir bahwa melewatkan kolom sebagai string cukup jelek. Saya suka melakukan sesuatu seperti:

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

yang akan menghasilkan:

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

Perhatikan bagaimana spesifikasi data.frame bersifat opsional. Anda bahkan dapat bekerja dengan fungsi kolom Anda:

> get.max(1/mpg,mtcars)
[1] 0.09615385
Ian Fellows
sumber
9
Anda perlu keluar dari kebiasaan berpikir menggunakan tanda kutip itu jelek. Tidak menggunakannya itu jelek! Mengapa? Karena Anda telah membuat fungsi yang hanya dapat digunakan secara interaktif - sangat sulit untuk memprogramnya.
hadley
27
Saya senang ditunjukkan cara yang lebih baik, tetapi saya gagal melihat perbedaan antara ini dan qplot (x = mpg, data = mtcars). ggplot2 tidak pernah melewatkan kolom sebagai string, dan saya pikir lebih baik untuk itu. Mengapa Anda mengatakan bahwa ini hanya dapat digunakan secara interaktif? Dalam situasi apa hal itu akan mengarah pada hasil yang tidak diinginkan? Bagaimana lebih sulitnya membuat program? Di badan postingan saya menampilkan cara yang lebih fleksibel.
Ian Fellows
4
5 tahun kemudian -) .. Mengapa kita membutuhkan: parent.frame ()?
mql4beginner
15
7 tahun kemudian: apakah menggunakan kutipan masih jelek?
Spacedman
12

Cara lain adalah dengan menggunakan tidy evaluationpendekatan. Sangat mudah untuk melewatkan kolom dari bingkai data baik sebagai string atau nama kolom kosong. Lihat lebih lanjut di tidyeval sini .

library(rlang)
library(tidyverse)

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

Gunakan nama kolom sebagai string

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

Gunakan nama kolom kosong

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

Dibuat pada 01-03-2019 oleh paket reprex (v0.2.1.9000)

Tung
sumber
1

Sebagai pemikiran tambahan, jika diperlukan untuk meneruskan nama kolom tanpa tanda kutip ke fungsi kustom, mungkin match.call()dapat berguna juga dalam kasus ini, sebagai alternatif untuk 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

Jika ada kesalahan ketik pada nama kolom, maka akan lebih aman untuk menghentikan kesalahan:

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

Dibuat pada 2019-01-11 oleh paket reprex (v0.2.1)

Saya tidak berpikir saya akan menggunakan pendekatan ini karena ada pengetikan dan kerumitan ekstra daripada hanya meneruskan nama kolom yang dikutip seperti yang ditunjukkan pada jawaban di atas, tetapi yah, ini adalah pendekatan.

Valentin
sumber