从列表到data.table与hash

时间:2015-12-11 21:29:19

标签: r hash data.table

我经常遇到的一个问题是需要从data.table中查找任意行。昨天我遇到了一个问题,我试图加速循环并使用profvis我发现来自data.table的查找是循环中成本最高的部分。然后我决定尝试找到在R中进行单项查找的最快方法。

数据通常采用data.table的形式,其中包含字符类型的键列。其余列通常是数值。我试图创建一个具有与我经常处理的相似特征的随机表,这意味着> 100K行。我比较了本机列表data.table包和hash包。本地列表和data.table在单个项目查找性能方面具有可比性。 Hash似乎快了两个数量级。测试由随机抽样的10组10,000个密钥组成,以提供访问行为的变化。每种查找方法都使用相同的密钥集。

最终我的偏好是要让data.table的行查找更快,而不是必须创建我的数据的哈希表,或者确定它不能完成,只需在我不得不使用哈希包时快速查找。我不知道是否可能,但是你可以创建一个对data.table中行的引用的哈希表,以允许使用哈希包快速查找吗?我知道在C ++中可以使用这种类型的东西,但据我所知,由于缺少指针,R不允许这种事情。

总结: 1)我是否正确地使用data.table进行查找,因此这是单行查找所需的速度? 2)是否可以创建指向data.table行的指针散列以允许以这种方式快速查找?

测试系统:

Windows 10 Pro x64

R 3.2.2

data.table 1.9.6

哈希2.2.6

Intel Core i7-5600U,内存为16 GB

代码:

library(microbenchmarkCore) # install.packages("microbenchmarkCore", repos="http://olafmersmann.github.io/drat")
library(data.table)
library(hash)

# Set seed to 42 to ensure repeatability
set.seed(42)

# Setting up test ------

# Generate product ids
product_ids <- as.vector(
  outer(LETTERS[seq(1, 26, 1)],
    outer(outer(LETTERS[seq(1, 26, 1)], LETTERS[seq(1, 26, 1)], paste, sep=""),
          LETTERS[seq(1, 26, 1)], paste, sep = ""
    ), paste, sep = ""
  )
)

# Create test lookup data
test_lookup_list <- lapply(product_ids, function(id){
  return_list <- list(
    product_id = id,
    val_1 = rnorm(1),
    val_2 = rnorm(1),
    val_3 = rnorm(1),
    val_4 = rnorm(1),
    val_5 = rnorm(1),
    val_6 = rnorm(1),
    val_7 = rnorm(1),
    val_8 = rnorm(1)
  )
  return(return_list)
})

# Set names of items in list
names(test_lookup_list) <- sapply(test_lookup_list, function(elem) elem[['product_id']])

# Create lookup hash
lookup_hash <- hash(names(test_lookup_list), test_lookup_list)

# Create data.table from list and set key of data.table to product_id field
test_lookup_dt <- rbindlist(test_lookup_list)
setkey(test_lookup_dt, product_id)

test_lookup_env <- list2env(test_lookup_list)

# Generate sample of keys to be used for speed testing
lookup_tests <- lapply(1:10, function(x){
  lookups <- sample(test_lookup_dt$product_id, 10000)
  return(lookups)
})

# Native list timing
native_list_timings <- sapply(lookup_tests, function(lookups){
  timing <- system.nanotime(
    for(lookup in lookups){
      return_value <- test_lookup_list[[lookup]]
    }    
  )
  return(timing[['elapsed']])
})

# Data.table timing
datatable_timings <- sapply(lookup_tests, function(lookups){
  timing <- system.nanotime(
    for(lookup in lookups){
      return_value <- test_lookup_dt[lookup]
    }
  )
  return(timing[['elapsed']])
})


# Hashtable timing
hashtable_timings <- sapply(lookup_tests, function(lookups){
  timing <- system.nanotime(
    for(lookup in lookups){
      return_value <- lookup_hash[[lookup]]
    }
  )
  return(timing[['elapsed']])
})

# Environment timing
environment_timings <- sapply(lookup_tests, function(lookups){
  timing <- system.nanotime(
    for(lookup in lookups){
      return_value <- test_lookup_env[[lookup]]
    }
  )
  return(timing[['elapsed']])
})

# Summary of timing results
summary(native_list_timings)
summary(datatable_timings)
summary(hashtable_timings)
summary(environment_timings)

结果如下:

> # Summary of timing results
> summary(native_list_timings)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  35.12   36.20   37.28   37.05   37.71   39.24 
> summary(datatable_timings)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  49.13   51.51   52.64   52.76   54.39   55.13 
> summary(hashtable_timings)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
 0.1588  0.1857  0.2107  0.2213  0.2409  0.3258 
> summary(environment_timings)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
0.09322 0.09524 0.10680 0.11850 0.13760 0.17140 

在此特定情况下,hash查找似乎比本机列表或data.table快大约两个数量级。

更新时间:2015-12-11 3:00 PM PST

我收到了Neal Fultz的反馈建议使用本机Environment对象。这是我得到的代码和结果:

test_lookup_env <- list2env(test_lookup_list)
# Environment timing
environment_timings <- sapply(lookup_tests, function(lookups){
  timing <- system.nanotime(
    for(lookup in lookups){
      return_value <- test_lookup_env[[lookup]]
    }
  )
  return(timing[['elapsed']])
})
summary(environment_timings)
> summary(environment_timings)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
0.09322 0.09524 0.10680 0.11850 0.13760 0.17140 

事实上,在这种情况下,单个项目访问的环境似乎更快。谢谢你Neal Fultz指出这个方法。我很感激能够更全面地理解R中可用的对象类型。我的问题仍然存在:我是否正确使用data.table(我希望如此,但我愿意批评)并且有办法提供行访问权限data.table的行使用某种指针魔法,可以提供更快的单个行访问。

澄清:2015-12-11 3:52 PM PST

有人提到我在我测试的最内层循环中的访问模式是低效的。我同意。我想要做的是尽可能地模仿我正在处理的情况。这实际发生的循环不允许矢量化,这就是我不使用它的原因。我意识到这不是严格意义上的'R'做事方式。我的代码中的data.table提供了参考信息,在我进入循环之前,我不一定知道我需要哪一行,这就是为什么我想要弄清楚如何尽快访问单个项目,最好是数据仍存储在data.table中。这也是一个好奇心问题,可以吗?

更新2:2015-12-11 4:12 PM PST

我收到了来自@jangrorecki的反馈,使用Sys.time()是衡量一个函数性能的无效方法。我已根据建议修改了代码以使用system.nanotime()。原始代码已更新并且计时结果。

问题仍然存在:这是对data.table进行行查找的最快方法吗?如果是这样,是否可以创建指向行的指针散列以进行快速查找?在这一点上,我最好奇R可以推动多远。作为来自C ++的人,这是一个有趣的挑战。

结论

我接受了Neal Fultz提供的答案,因为它讨论了我真正想知道的事情。也就是说,这不是data.table打算使用的方式,所以没有人应该将其解释为data.table很慢,实际上速度非常快。这是一个非常特殊的用例,我很好奇。我的数据以data.table形式出现,我想知道是否可以快速访问行,同时将其保留为data.table。我还想将data.table访问速度与散列表进行比较,散列表通常用于快速,非矢量化项目查找。

2 个答案:

答案 0 :(得分:7)

对于非矢量化访问模式,您可能想尝试内置environment个对象:

require(microbenchmark)

test_lookup_env <- list2env(test_lookup_list)


x <- lookup_tests[[1]][1]
microbenchmark(
    lookup_hash[[x]],
    test_lookup_list[[x]],
    test_lookup_dt[x],
    test_lookup_env[[x]]
)

在这里你可以看到它甚至比hash

更加zippier
Unit: microseconds
                  expr      min        lq       mean    median        uq      max neval
      lookup_hash[[x]]   10.767   12.9070   22.67245   23.2915   26.1710   68.654   100
 test_lookup_list[[x]]  847.700  853.2545  887.55680  863.0060  893.8925 1369.395   100
     test_lookup_dt[x] 2652.023 2711.9405 2771.06400 2758.8310 2803.9945 3373.273   100
  test_lookup_env[[x]]    1.588    1.9450    4.61595    2.5255    6.6430   27.977   100

修改

单步执行data.table:::`[.data.table`对于你看到dt减速的原因是有益的。当你使用一个字符进行索引并且有一个键集时,它会进行相当多的簿记,然后下拉到bmerge,这是一个二进制搜索。二进制搜索是O(log n),随着n的增加会变慢。

另一方面,环境使用散列(默认情况下)并且相对于n具有恒定的访问时间。

要解决此问题,您可以通过它手动构建地图和索引:

x <- lookup_tests[[2]][2]

e <- list2env(setNames(as.list(1:nrow(test_lookup_dt)), test_lookup_dt$product_id))

#example access:
test_lookup_dt[e[[x]], ]

但是,在data.table方法中看到如此多的簿记代码,我也尝试了普通的旧数据框架:

test_lookup_df <- as.data.frame(test_lookup_dt)

rownames(test_lookup_df) <- test_lookup_df$product_id

如果我们真的很偏执,我们可以完全跳过[方法并直接对列进行讨论。

以下是一些时间(来自不同于上面的机器):

> microbenchmark(
+   test_lookup_dt[x,],
+   test_lookup_dt[x],
+   test_lookup_dt[e[[x]],],
+   test_lookup_df[x,],
+   test_lookup_df[e[[x]],],
+   lapply(test_lookup_df, `[`, e[[x]]),
+   lapply(test_lookup_dt, `[`, e[[x]]),
+   lookup_hash[[x]]
+ )
Unit: microseconds
                                expr       min         lq        mean     median         uq       max neval
                 test_lookup_dt[x, ]  1658.585  1688.9495  1992.57340  1758.4085  2466.7120  2895.592   100
                   test_lookup_dt[x]  1652.181  1695.1660  2019.12934  1764.8710  2487.9910  2934.832   100
            test_lookup_dt[e[[x]], ]  1040.869  1123.0320  1356.49050  1280.6670  1390.1075  2247.503   100
                 test_lookup_df[x, ] 17355.734 17538.6355 18325.74549 17676.3340 17987.6635 41450.080   100
            test_lookup_df[e[[x]], ]   128.749   151.0940   190.74834   174.1320   218.6080   366.122   100
 lapply(test_lookup_df, `[`, e[[x]])    18.913    25.0925    44.53464    35.2175    53.6835   146.944   100
 lapply(test_lookup_dt, `[`, e[[x]])    37.483    50.4990    94.87546    81.2200   124.1325   241.637   100
                    lookup_hash[[x]]     6.534    15.3085    39.88912    49.8245    55.5680   145.552   100

总的来说,要回答你的问题,你没有使用data.table&#34;错误&#34;但你也没有按照预期的方式使用它(矢量化访问)。但是,您可以手动构建映射以进行索引并获得大部分性能。

答案 1 :(得分:5)

您采用的方法效率非常低,因为您要从数据集中多次查询单个值。

一次查询所有这些,然后循环整个批处理,而不是逐个查询1e4会更有效。

请参阅 dt2 以了解矢量化方法。我仍然难以想象这个用例。

另一件事是450K行的数据很少能够制作出合理的基准,你可能会得到4M或更高的完全不同的结果。就 hash 方法而言,您可能还会更快地达到内存限制。

此外,Sys.time()可能不是衡量时间的最佳方式,请在gc中读取?system.time参数。

以下是我使用microbenchmarkCore包中的system.nanotime()函数制作的基准。

通过将test_lookup_list折叠到data.table并执行合并到test_lookup_dt,可以进一步加快data.table方法,但是为了与哈希解决方案进行比较,我还需要对其进行预处理。

library(microbenchmarkCore) # install.packages("microbenchmarkCore", repos="http://olafmersmann.github.io/drat")
library(data.table)
library(hash)

# Set seed to 42 to ensure repeatability
set.seed(42)

# Setting up test ------

# Generate product ids
product_ids = as.vector(
    outer(LETTERS[seq(1, 26, 1)],
          outer(outer(LETTERS[seq(1, 26, 1)], LETTERS[seq(1, 26, 1)], paste, sep=""),
                LETTERS[seq(1, 26, 1)], paste, sep = ""
          ), paste, sep = ""
    )
)

# Create test lookup data
test_lookup_list = lapply(product_ids, function(id) list(
    product_id = id,
    val_1 = rnorm(1),
    val_2 = rnorm(1),
    val_3 = rnorm(1),
    val_4 = rnorm(1),
    val_5 = rnorm(1),
    val_6 = rnorm(1),
    val_7 = rnorm(1),
    val_8 = rnorm(1)
))

# Set names of items in list
names(test_lookup_list) = sapply(test_lookup_list, `[[`, "product_id")

# Create lookup hash
lookup_hash = hash(names(test_lookup_list), test_lookup_list)

# Create data.table from list and set key of data.table to product_id field
test_lookup_dt <- rbindlist(test_lookup_list)
setkey(test_lookup_dt, product_id)

# Generate sample of keys to be used for speed testing
lookup_tests = lapply(1:10, function(x) sample(test_lookup_dt$product_id, 1e4))

native = lapply(lookup_tests, function(lookups) system.nanotime(for(lookup in lookups) test_lookup_list[[lookup]]))
dt1 = lapply(lookup_tests, function(lookups) system.nanotime(for(lookup in lookups) test_lookup_dt[lookup]))
hash = lapply(lookup_tests, function(lookups) system.nanotime(for(lookup in lookups) lookup_hash[[lookup]]))
dt2 = lapply(lookup_tests, function(lookups) system.nanotime(test_lookup_dt[lookups][, .SD, 1:length(product_id)]))

summary(sapply(native, `[[`, 3L))
#   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
#  27.65   28.15   28.47   28.97   28.78   33.45
summary(sapply(dt1, `[[`, 3L))
#   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
#  15.30   15.73   15.96   15.96   16.29   16.52
summary(sapply(hash, `[[`, 3L))
#   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
# 0.1209  0.1216  0.1221  0.1240  0.1225  0.1426 
summary(sapply(dt2, `[[`, 3L))
#   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
#0.02421 0.02438 0.02445 0.02476 0.02456 0.02779