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