簡體   English   中英

從列表到data.table與hash的R快速單項查找

[英]R fast single item lookup from list vs data.table vs hash

我經常遇到的一個問題是需要從data.table中查找任意行。 我昨天遇到了一個問題,我試圖加快循環使用profvis我發現,從查找data.table是循環中最昂貴的部分。 然后我決定嘗試找到在R中進行單項查找的最快方法。

數據通常采用data.table的形式,其中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 RAM

碼:

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點

我收到了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太平洋標准時間

有人提到我在我測試的最內層循環中的訪問模式是低效的。 我同意。 我想要做的是盡可能地模仿我正在處理的情況。 這實際發生的循環不允許矢量化,這就是我不使用它的原因。 我意識到這不是嚴格意義上的'R'做事方式。 我的代碼中的data.table提供了參考信息,在我進入循環之前我不一定知道我需要哪一行,這就是我試圖弄清楚如何盡快訪問單個項目的原因,最好是數據仍然存儲在data.table 這也是一個好奇心問題,可以做到嗎?

更新2:2015-12-11 4:12太平洋標准時間

我收到了來自@jangrorecki的反饋,即使用Sys.time()是衡量函數性能的無效方法。 我已修改代碼,根據建議使用system.nanotime() 原始代碼已更新並且計時結果。

問題仍然存在:這是對data.table執行行查找的最快方法嗎?如果可以的話,是否可以創建指向行的指針的哈希以便快速查找? 在這一點上,我最好奇R可以推動多遠。 作為來自C ++的人,這是一個有趣的挑戰。

結論

我接受了Neal Fultz提供的答案,因為它討論了我真正想知道的事情。 也就是說,這不是data.table使用方式,因此沒有人應該將其解釋為data.table很慢,實際上速度非常快。 這是一個非常特殊的用例,我很好奇。 我的數據以data.table ,我想知道是否可以快速訪問行,同時將其保留為data.table 我還想將data.table訪問速度與散列表進行比較,散列表通常用於快速,非矢量化項目查找。

對於非向量化訪問模式,您可能希望嘗試內置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]]
)

在這里,你可以看到它的,甚至比zippier hash

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`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“錯誤”,但你也沒有按照預期的方式使用它(矢量化訪問)。 但是,您可以手動構建映射以進行索引,並獲得大部分性能。

您采用的方法效率非常低,因為您要查詢數據集中單個值的多倍。

一次查詢所有這些,然后循環整個批處理,而不是一個一個地查詢1e4會更有效。

有關矢量化方法,請參閱dt2 我仍然難以想象這個用例。

另一件事是450K行的數據很少能夠制作出合理的基准,你可能會得到4M或更高的完全不同的結果。 哈希方法而言,您可能還會更快地達到內存限制。

另外, Sys.time()可能不是測量時序的最佳方法,在?system.time讀取gc參數。

這是我使用microbenchmarkCore包中的system.nanotime()函數制作的基准。

通過將test_lookup_list折疊到data.table並執行merge到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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM