首页 文章

r data.table函数式编程/元编程/语言计算

提问于
浏览
4

我正在探索使用data.table包装聚合函数(但实际上它可以是任何类型的函数)的不同方法(也提供了一个dplyr示例),并且想知道关于函数式编程/元编程的最佳实践

  • 性能(对于data.table可能适用的潜在优化,实现是否重要)

  • 可读性(有一个共同商定的标准,例如在大多数使用data.table的包中)

  • 易于推广(元编程的方式是"generalizable")

基本应用是灵活地聚合表,即参数化变量以聚合,聚合的维度,两者的相应结果变量名称和聚合函数 . 我已经在三个data.table和一个dplyr方式中实现了(几乎)相同的功能:

  • fn_dt_agg1(这里我无法弄清楚如何参数化聚合函数)

  • fn_dt_agg2(灵感来自@jangorecki的答案here,他称之为"computing on the language")

  • fn_dt_agg3(灵感来自@Arun的答案here,这似乎是元编程的另一种方法)

  • fn_df_agg1(我在dplyr中的谦虚态度)

libraries

library(data.table)
library(dplyr)

data

n_size <- 1*10^6
sample_metrics <- sample(seq(from = 1, to = 100, by = 1), n_size, rep = T)
sample_dimensions <- sample(letters[10:12], n_size, rep = T)
df <- 
  data.frame(
    a = sample_metrics,
    b = sample_metrics,
    c = sample_dimensions,
    d = sample_dimensions,
    x = sample_metrics,
    y = sample_dimensions,
    stringsAsFactors = F)

dt <- as.data.table(df)

implementations

  1. fn_dt_agg1
fn_dt_agg1 <- 
  function(dt, metric, metric_name, dimension, dimension_name) {

  temp <- dt[, setNames(lapply(.SD, function(x) {sum(x, na.rm = T)}), 
                        metric_name), 
             keyby = dimension, .SDcols = metric]
  temp[]
}

res_dt1 <- 
  fn_dt_agg1(
    dt = dt, metric = c("a", "b"), metric_name = c("a", "b"),
    dimension = c("c", "d"), dimension_name = c("c", "d"))
  1. fn_dt_agg2
fn_dt_agg2 <- 
  function(dt, metric, metric_name, dimension, dimension_name,
           agg_type) {

  j_call = as.call(c(
    as.name("."),
    sapply(setNames(metric, metric_name), 
           function(var) as.call(list(as.name(agg_type), 
                                      as.name(var), na.rm = T)), 
           simplify = F)
    ))

  dt[, eval(j_call), keyby = dimension][]
}

res_dt2 <- 
  fn_dt_agg2(
    dt = dt, metric = c("a", "b"), metric_name = c("a", "b"),
    dimension = c("c", "d"), dimension_name = c("c", "d"),
    agg_type = c("sum"))

all.equal(res_dt1, res_dt2)
#TRUE
  1. fn_dt_agg3
fn_dt_agg3 <- 
  function(dt, metric, metric_name, dimension, dimension_name, agg_type) {

  e <- eval(parse(text=paste0("function(x) {", 
                              agg_type, "(", "x, na.rm = T)}"))) 

  temp <- dt[, setNames(lapply(.SD, e), 
                        metric_name), 
             keyby = dimension, .SDcols = metric]
  temp[]
}

res_dt3 <- 
  fn_dt_agg3(
    dt = dt, metric = c("a", "b"), metric_name = c("a", "b"),
    dimension = c("c", "d"), dimension_name = c("c", "d"), 
    agg_type = "sum")

all.equal(res_dt1, res_dt3)
#TRUE
  1. fn_df_agg1
fn_df_agg1 <-
  function(df, metric, metric_name, dimension, dimension_name, agg_type) {

    all_vars <- c(dimension, metric)
    all_vars_new <- c(dimension_name, metric_name)
    dots_group <- lapply(dimension, as.name)

    e <- eval(parse(text=paste0("function(x) {", 
                                agg_type, "(", "x, na.rm = T)}")))

    df %>%
      select_(.dots = all_vars) %>%
      group_by_(.dots = dots_group) %>%
      summarise_each_(funs(e), metric) %>%
      rename_(.dots = setNames(all_vars, all_vars_new))
}

res_df1 <- 
  fn_df_agg1(
    df = df, metric = c("a", "b"), metric_name = c("a", "b"),
    dimension = c("c", "d"), dimension_name = c("c", "d"),
    agg_type = "sum")

all.equal(res_dt1, as.data.table(res_df1))
#"Datasets has different keys. 'target': c, d. 'current' has no key."

benchmarking

出于好奇和我未来的自我和其他感兴趣的各方,我运行了所有4个实现的基准,这可能已经揭示了性能问题(尽管我不是基准专家,所以请原谅我是否通常没有应用商定的最佳做法) . 我期望fn_dt_agg1是最快的,因为它有一个参数少(聚合函数),但似乎没有相当大的影响 . 我也对相对较慢的dplyr功能感到惊讶,但这可能是由于我的设计选择不好 .

library(microbenchmark)
bench_res <- 
  microbenchmark(
    fn_dt_agg1 = 
      fn_dt_agg1(
      dt = dt, metric = c("a", "b"), 
      metric_name = c("a", "b"), 
      dimension = c("c", "d"), 
      dimension_name = c("c", "d")), 
    fn_dt_agg2 = 
      fn_dt_agg2(
        dt = dt, metric = c("a", "b"), 
        metric_name = c("a", "b"), 
        dimension = c("c", "d"), 
        dimension_name = c("c", "d"),
        agg_type = c("sum")),
    fn_dt_agg3 =
      fn_dt_agg3(
        dt = dt, metric = c("a", "b"), 
        metric_name = c("a", "b"),
        dimension = c("c", "d"), 
        dimension_name = c("c", "d"),
        agg_type = c("sum")),
    fn_df_agg1 =
      fn_df_agg1(
        df = df, metric = c("a", "b"), metric_name = c("a", "b"),
        dimension = c("c", "d"), dimension_name = c("c", "d"),
        agg_type = "sum"),
    times = 100L)

bench_res

# Unit: milliseconds
#       expr      min       lq     mean   median       uq       max neval
# fn_dt_agg1 28.96324 30.49507 35.60988 32.62860 37.43578 140.32975   100
# fn_dt_agg2 27.51993 28.41329 31.80023 28.93523 33.17064  84.56375   100
# fn_dt_agg3 25.46765 26.04711 30.11860 26.64817 30.28980 153.09715   100
# fn_df_agg1 88.33516 90.23776 97.84826 94.28843 97.97154 172.87838   100

other resources

1 回答

  • 5

    我不推荐 eval(parse()) . 如果没有它,您可以实现与方法三相同:

    fn_dt_agg4 <- 
      function(dt, metric, metric_name, dimension, dimension_name, agg_type) {
    
        e <- function(x) getFunction(agg_type)(x, na.rm = T)
    
        temp <- dt[, setNames(lapply(.SD, e), 
                              metric_name), 
                   keyby = dimension, .SDcols = metric]
        temp[]
      }
    

    这也避免了一些安全风险 .

    PS:您可以通过设置 options("datatable.verbose" = TRUE) 来检查data.table在优化方面做了什么 .

相关问题