ฉันจะพยายามให้คำแนะนำที่ดีที่สุดของฉัน แต่มันก็ไม่ใช่เรื่องง่ายเพราะต้องมีความคุ้นเคยกับ {data.table}, {dplyr}, {dtplyr} และ {base R} ทั้งหมด ฉันใช้ {data.table} และแพ็คเกจ {tidy-world} จำนวนมาก (ยกเว้น {dplyr}) รักทั้งคู่ แต่ฉันชอบไวยากรณ์ของ data.table ถึง dplyr's ฉันหวังว่าแพ็คเกจที่เป็นระเบียบเรียบร้อยทั่วโลกจะใช้ {dtplyr} หรือ {data.table} เป็นแบ็กเอนด์เมื่อใดก็ตามที่จำเป็น
เช่นเดียวกับการแปลอื่น ๆ (คิดว่า dplyr-to-sparkly / SQL) มีบางสิ่งที่สามารถหรือไม่สามารถแปลได้อย่างน้อยตอนนี้ ฉันหมายความว่าบางทีวันหนึ่ง {dtplyr} สามารถแปลได้ 100% ใครจะรู้ รายการด้านล่างไม่ครบถ้วนสมบูรณ์หรือไม่ถูกต้อง 100% เนื่องจากฉันจะพยายามอย่างดีที่สุดเพื่อตอบตามความรู้ของฉันเกี่ยวกับหัวข้อ / แพ็คเกจ / ปัญหา / อื่น ๆ ที่เกี่ยวข้อง
ที่สำคัญสำหรับคำตอบที่ไม่ถูกต้องทั้งหมดฉันหวังว่ามันจะให้คำแนะนำบางอย่างแก่คุณเกี่ยวกับแง่มุมของ {data.table} ที่คุณควรใส่ใจและเปรียบเทียบกับ {dtplyr} และค้นหาคำตอบด้วยตัวเอง อย่านำคำตอบเหล่านี้ไปใช้
และฉันหวังว่าโพสต์นี้สามารถใช้เป็นหนึ่งในแหล่งข้อมูลสำหรับ {dplyr}, {data.table} หรือ {dtplyr} ผู้ใช้ / ผู้สร้างสำหรับการสนทนาและการทำงานร่วมกันและทำให้ #RStats ดียิ่งขึ้น
{data.table} ไม่ได้ใช้สำหรับการดำเนินการที่รวดเร็วและมีประสิทธิภาพของหน่วยความจำเท่านั้น มีหลายคนรวมถึงตัวผมเองชอบไวยากรณ์ที่หรูหราของ {data.table} นอกจากนี้ยังรวมถึงการทำงานที่รวดเร็วอื่น ๆ เช่นฟังก์ชั่นอนุกรมเวลาเช่นการหมุนของตระกูล (เช่นfrollapply
) ที่เขียนใน C มันสามารถใช้กับฟังก์ชั่นใด ๆ รวมถึง tidyverse ฉันใช้ {data.table} + {purrr} มาก!
ความซับซ้อนของการดำเนินงาน
สามารถแปลได้อย่างง่ายดาย
library(data.table)
library(dplyr)
library(flights)
data <- data.table(diamonds)
# dplyr
diamonds %>%
filter(cut != "Fair") %>%
group_by(cut) %>%
summarize(
avg_price = mean(price),
median_price = as.numeric(median(price)),
count = n()
) %>%
arrange(desc(count))
# data.table
data [
][cut != 'Fair', by = cut, .(
avg_price = mean(price),
median_price = as.numeric(median(price)),
count = .N
)
][order( - count)]
{data.table} มีความรวดเร็วและหน่วยความจำที่มีประสิทธิภาพเพราะ (เกือบ?) ทุกอย่างถูกสร้างขึ้นจากพื้นดินขึ้นจาก C ด้วยแนวคิดหลักของการอัปเดตโดยอ้างอิงคีย์ (คิดว่า SQL) และการเพิ่มประสิทธิภาพอย่างไม่หยุดยั้งทุกที่ในแพ็คเกจ (เช่นfifelse
, การfread/fread
เรียงลำดับ radix ที่นำมาใช้โดยฐาน R), ในขณะที่ทำให้แน่ใจว่าไวยากรณ์มีความกระชับและสอดคล้องกันนั่นคือเหตุผลที่ฉันคิดว่ามันสง่างาม
จากIntroduction to data.tableการดำเนินการจัดการข้อมูลหลักเช่นเซ็ตย่อยกลุ่มอัปเดตเข้าร่วม ฯลฯจะถูกเก็บไว้ด้วยกัน
กระชับและไวยากรณ์ที่สอดคล้องกัน ...
ดำเนินการวิเคราะห์อย่างคล่องแคล่วโดยไม่ต้องรับภาระความรู้ความเข้าใจในการทำแผนที่แต่ละการดำเนินการ ...
ปรับการดำเนินการโดยอัตโนมัติภายในและมีประสิทธิภาพมากโดยการรู้ข้อมูลที่จำเป็นสำหรับการดำเนินการแต่ละครั้งอย่างแม่นยำนำไปสู่รหัสที่รวดเร็วและมีประสิทธิภาพ
จุดสุดท้ายเป็นตัวอย่าง
# Calculate the average arrival and departure delay for all flights with “JFK” as the origin airport in the month of June.
flights[origin == 'JFK' & month == 6L,
.(m_arr = mean(arr_delay), m_dep = mean(dep_delay))]
เราเซตย่อยแรกใน i เพื่อค้นหาดัชนีแถวที่ตรงกันที่สนามบินต้นทางเท่ากับ "JFK" และเดือนเท่ากับ 6L เรายังไม่ได้ย่อย data.table ทั้งหมดที่สอดคล้องกับแถวเหล่านั้น
ตอนนี้เราดูที่ j และพบว่ามันใช้เพียงสองคอลัมน์ และสิ่งที่เราต้องทำคือการคำนวณค่าเฉลี่ย () ดังนั้นเราจึงเซ็ตเฉพาะคอลัมน์ที่สอดคล้องกับแถวที่ตรงกันและคำนวณค่าเฉลี่ย ()
เพราะสามองค์ประกอบหลักของแบบสอบถาม (ฉัน j และ) อยู่ด้วยกันภายใน [ ... ] , data.table สามารถมองเห็นทั้งสามและเพิ่มประสิทธิภาพการค้นหาทั้งหมดก่อนที่จะประเมินผลไม่ได้แยกจากกัน เราสามารถหลีกเลี่ยงชุดย่อยทั้งหมด (เช่นการแบ่งคอลัมน์นอกเหนือจาก arr_delay และ dep_delay) สำหรับทั้งความเร็วและประสิทธิภาพของหน่วยความจำ
ระบุว่าในการเก็บเกี่ยวผลประโยชน์ของ {data.table} การแปลของ {dtplr} จะต้องถูกต้องตามลำดับนั้น การดำเนินงานที่ซับซ้อนมากขึ้นการแปลที่ยากขึ้น สำหรับการดำเนินการอย่างง่ายเช่นด้านบนจะสามารถแปลได้อย่างง่ายดาย สำหรับผู้ที่มีความซับซ้อนหรือผู้ที่ไม่ได้รับการสนับสนุนโดย {dtplyr} คุณต้องค้นหาด้วยตัวคุณเองตามที่กล่าวไว้ข้างต้นเราจะต้องเปรียบเทียบไวยากรณ์ที่แปลแล้วและการเปรียบเทียบและเป็นแพคเกจที่เกี่ยวข้องที่คุ้นเคย
สำหรับการดำเนินการที่ซับซ้อนหรือการดำเนินการที่ไม่ได้รับการสนับสนุนฉันอาจให้ตัวอย่างด้านล่าง อีกครั้งฉันแค่พยายามทำให้ดีที่สุด อ่อนโยนกับฉัน
ปรับปรุงโดยการอ้างอิง
ฉันจะไม่เข้าไปในบทนำ / รายละเอียด แต่นี่คือลิงค์
ทรัพยากรหลัก: ความหมายอ้างอิง
รายละเอียดเพิ่มเติม: ทำความเข้าใจอย่างถูกต้องเมื่อ data.table เป็นการอ้างอิงถึง (เทียบกับสำเนา) data.table อื่น
โดยอ้างอิงจากการปรับปรุงฉันคิดว่าคุณสมบัติที่สำคัญที่สุดของ {data.table} และนั่นคือสิ่งที่ทำให้มันเร็วและมีประสิทธิภาพของหน่วยความจำ dplyr::mutate
ไม่สนับสนุนตามค่าเริ่มต้น เนื่องจากฉันไม่คุ้นเคยกับ {dtplyr} ฉันจึงไม่แน่ใจว่าจำนวนเท่าใดและ {dtplyr} สนับสนุนการดำเนินการใดได้บ้างและไม่สามารถรองรับได้ ดังกล่าวข้างต้นก็ยังขึ้นอยู่กับความซับซ้อนของการดำเนินงานซึ่งจะมีผลต่อการแปล
มีสองวิธีในการใช้การอัปเดตโดยอ้างอิงใน {data.table}
ผู้ประกอบการที่ได้รับมอบหมายของ {data.table} :=
set
-Family: set
, setnames
, setcolorder
, setkey
, setDT
, fsetdiff
และอื่น ๆ อีกมากมาย
:=
set
เป็นที่นิยมใช้มากขึ้นเมื่อเทียบกับ สำหรับชุดข้อมูลที่ซับซ้อนและมีขนาดใหญ่การอัพเดทโดยอ้างอิงเป็นกุญแจสำคัญในการรับความเร็วสูงสุดและประสิทธิภาพของหน่วยความจำ วิธีคิดที่ง่าย (ไม่ถูกต้อง 100% เนื่องจากรายละเอียดซับซ้อนกว่านี้มากเนื่องจากเกี่ยวข้องกับการคัดลอกที่ยาก / ตื้นและปัจจัยอื่น ๆ อีกมากมาย) สมมติว่าคุณกำลังจัดการกับชุดข้อมูลขนาดใหญ่ 10GB โดยมี 10 คอลัมน์และ 1GB แต่ละรายการ . หากต้องการจัดการหนึ่งคอลัมน์คุณต้องจัดการกับ 1GB เท่านั้น
จุดสำคัญคือด้วยการอ้างอิงโดยการอัพเดตคุณจะต้องจัดการกับข้อมูลที่จำเป็นเท่านั้น นั่นเป็นเหตุผลที่เมื่อใช้ {data.table} โดยเฉพาะการจัดการกับชุดข้อมูลขนาดใหญ่เราใช้การอัปเดตอ้างอิงตลอดเวลาเมื่อใดก็ตามที่เป็นไปได้ ตัวอย่างเช่นการจัดการชุดข้อมูลการสร้างแบบจำลองขนาดใหญ่
# Manipulating list columns
df <- purrr::map_dfr(1:1e5, ~ iris)
dt <- data.table(df)
# data.table
dt [,
by = Species, .(data = .( .SD )) ][, # `.(` shorthand for `list`
model := map(data, ~ lm(Sepal.Length ~ Sepal.Width, data = . )) ][,
summary := map(model, summary) ][,
plot := map(data, ~ ggplot( . , aes(Sepal.Length, Sepal.Width)) +
geom_point())]
# dplyr
df %>%
group_by(Species) %>%
nest() %>%
mutate(
model = map(data, ~ lm(Sepal.Length ~ Sepal.Width, data = . )),
summary = map(model, summary),
plot = map(data, ~ ggplot( . , aes(Sepal.Length, Sepal.Width)) +
geom_point())
)
การดำเนินการซ้อนlist(.SD)
อาจไม่รองรับโดย {dtlyr} ตามที่ผู้ใช้ tidyverse ใช้tidyr::nest
? ดังนั้นฉันไม่แน่ใจว่าการดำเนินการที่ตามมาสามารถแปลเป็นวิธีของ {data.table} ได้เร็วขึ้นและมีหน่วยความจำน้อยลง
หมายเหตุ: ผลลัพธ์ของ data.table เป็น "มิลลิวินาที", dplyr ใน "นาที"
df <- purrr::map_dfr(1:1e5, ~ iris)
dt <- copy(data.table(df))
bench::mark(
check = FALSE,
dt[, by = Species, .(data = list(.SD))],
df %>% group_by(Species) %>% nest()
)
# # A tibble: 2 x 13
# expression min median `itr/sec` mem_alloc `gc/sec` n_itr n_gc
# <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl> <int> <dbl>
# 1 dt[, by = Species, .(data = list(.SD))] 361.94ms 402.04ms 2.49 705.8MB 1.24 2 1
# 2 df %>% group_by(Species) %>% nest() 6.85m 6.85m 0.00243 1.4GB 2.28 1 937
# # ... with 5 more variables: total_time <bch:tm>, result <list>, memory <list>, time <list>,
# # gc <list>
มีหลายกรณีการใช้งานของการอัพเดทโดยอ้างอิงและแม้กระทั่งผู้ใช้ {data.table} จะไม่ใช้เวอร์ชันขั้นสูงตลอดเวลาเนื่องจากต้องการรหัสเพิ่มเติม ไม่ว่าจะเป็น {dtplyr} สนับสนุนนอกกรอบเหล่านี้คุณต้องค้นหาด้วยตนเอง
การอัพเดทโดยอ้างอิงหลายครั้งสำหรับฟังก์ชั่นเดียวกัน
ทรัพยากรหลัก: การกำหนดคอลัมน์หลายคอลัมน์อย่างสง่างามใน data.table ด้วย lapply ()
นี้เกี่ยวข้องกับการอย่างใดอย่างหนึ่งที่ใช้กันทั่วไปมากขึ้นหรือ:=
set
dt <- data.table( matrix(runif(10000), nrow = 100) )
# A few variants
for (col in paste0('V', 20:100))
set(dt, j = col, value = sqrt(get(col)))
for (col in paste0('V', 20:100))
dt[, (col) := sqrt(get(col))]
# I prefer `purrr::map` to `for`
library(purrr)
map(paste0('V', 20:100), ~ dt[, (.) := sqrt(get(.))])
ตามผู้สร้างของ {data.table} Matt Dowle
(โปรดสังเกตว่าอาจเป็นการทั่วไปมากกว่าที่จะวนซ้ำตั้งแถวจำนวนมากมากกว่าคอลัมน์จำนวนมาก)
เข้าร่วม + setkey + อัปเดตโดยอ้างอิง
ฉันต้องการการเข้าร่วมที่รวดเร็วด้วยข้อมูลที่ค่อนข้างใหญ่และรูปแบบการเข้าร่วมที่คล้ายกันเมื่อเร็ว ๆ นี้ดังนั้นฉันจึงใช้พลังของการอัปเดตโดยอ้างอิงแทนการเข้าร่วมปกติ setjoin
ขณะที่พวกเขาต้องใช้รหัสมากขึ้นผมห่อไว้ในแพคเกจส่วนตัวที่มีการประเมินผลที่ไม่ได้มาตรฐานสำหรับนำมาใช้และการอ่านที่ผมเรียกมันว่า
ฉันทำเกณฑ์มาตรฐานที่นี่: data.table join + update-by-reference + setkey
สรุป
# For brevity, only the codes for join-operation are shown here. Please refer to the link for details
# Normal_join
x <- y[x, on = 'a']
# update_by_reference
x_2[y_2, on = 'a', c := c]
# setkey_n_update
setkey(x_3, a) [ setkey(y_3, a), on = 'a', c := c ]
หมายเหตุ: dplyr::left_join
ได้รับการทดสอบแล้วและช้าที่สุดด้วย ~ 9,000 ms ใช้หน่วยความจำมากกว่าทั้ง {data.table} update_by_reference
และsetkey_n_update
ใช้หน่วยความจำน้อยกว่า normal_join {data.table} ของ ใช้หน่วยความจำประมาณ ~ 2.0GB ฉันไม่ได้รวมไว้เพราะฉันต้องการมุ่งเน้นที่ {data.table} เพียงอย่างเดียว
การค้นพบที่สำคัญ
setkey + update
และupdate
จะเร็วกว่า ~ 11 และ ~ 6.5 เท่าnormal join
ตามลำดับ
- เมื่อเข้าร่วมครั้งแรกประสิทธิภาพการทำงานของ
setkey + update
จะคล้ายกับupdate
ค่าใช้จ่ายsetkey
ส่วนใหญ่ชดเชยประสิทธิภาพการทำงานของตัวเอง
- ในการเข้าร่วมครั้งที่สองและครั้งต่อไปตามที่
setkey
ไม่จำเป็นsetkey + update
จะเร็วกว่าupdate
~ 1.8 เท่า (หรือเร็วกว่าnormal join
โดย ~ 11 ครั้ง)
ตัวอย่าง
สำหรับนักแสดงและการรวมหน่วยความจำอย่างมีประสิทธิภาพให้ใช้วิธีใดวิธีหนึ่งupdate
หรือsetkey + update
ในกรณีที่รหัสหลังเร็วกว่าโดยมีรหัสเพิ่มเติม
เรามาดูโค้ดหลอกๆ เพื่อความกระชับ logics เหมือนกัน
สำหรับหนึ่งหรือสองสามคอลัมน์
a <- data.table(x = ..., y = ..., z = ..., ...)
b <- data.table(x = ..., y = ..., z = ..., ...)
# `update`
a[b, on = .(x), y := y]
a[b, on = .(x), `:=` (y = y, z = z, ...)]
# `setkey + update`
setkey(a, x) [ setkey(b, x), on = .(x), y := y ]
setkey(a, x) [ setkey(b, x), on = .(x), `:=` (y = y, z = z, ...) ]
สำหรับหลายคอลัมน์
cols <- c('x', 'y', ...)
# `update`
a[b, on = .(x), (cols) := mget( paste0('i.', cols) )]
# `setkey + update`
setkey(a, x) [ setkey(b, x), on = .(x), (cols) := mget( paste0('i.', cols) ) ]
Wrapper ได้อย่างรวดเร็วและมีประสิทธิภาพหน่วยความจำร่วม ... หลายคน ... ที่มีลักษณะคล้ายกันเข้าร่วมรูปแบบห่อพวกเขาเช่นsetjoin
เหนือ - กับupdate
- มีหรือไม่มีsetkey
setjoin(a, b, on = ...) # join all columns
setjoin(a, b, on = ..., select = c('columns_to_be_included', ...))
setjoin(a, b, on = ..., drop = c('columns_to_be_excluded', ...))
# With that, you can even use it with `magrittr` pipe
a %>%
setjoin(...) %>%
setjoin(...)
ด้วยsetkey
อาร์กิวเมนต์on
สามารถละเว้นได้ นอกจากนี้ยังสามารถรวมไว้เพื่อให้สามารถอ่านได้โดยเฉพาะอย่างยิ่งสำหรับการร่วมมือกับผู้อื่น
การดำเนินงานแถวขนาดใหญ่
- ดังกล่าวข้างต้นใช้
set
- เติมข้อมูลในตารางของคุณล่วงหน้าใช้เทคนิคการอัพเดทอ้างอิง
- เซ็ตย่อยโดยใช้คีย์ (เช่น
setkey
)
ทรัพยากรที่เกี่ยวข้อง: เพิ่มแถวโดยการอ้างอิงที่ท้ายวัตถุ data.table
สรุปการอัพเดทโดยอ้างอิง
เหล่านี้เป็นเพียงบางกรณีการใช้การปรับปรุงโดยการอ้างอิง ยังมีอีกมากมาย
อย่างที่คุณเห็นสำหรับการใช้งานขั้นสูงในการจัดการกับข้อมูลขนาดใหญ่มีหลายกรณีการใช้งานและเทคนิคการใช้การปรับปรุงโดยอ้างอิงสำหรับชุดข้อมูลขนาดใหญ่ ไม่ใช่เรื่องง่ายที่จะใช้ใน {data.table} และไม่ว่า {dtplyr} สนับสนุนคุณสามารถค้นหาด้วยตนเอง
ฉันมุ่งเน้นที่การอ้างอิงโดยอัปเดตในโพสต์นี้เนื่องจากฉันคิดว่ามันเป็นคุณสมบัติที่ทรงพลังที่สุดของ {data.table} สำหรับการดำเนินการที่รวดเร็วและมีประสิทธิภาพของหน่วยความจำ ที่กล่าวว่ามีหลายแง่มุมอื่น ๆ อีกมากมายที่ทำให้มีประสิทธิภาพเช่นกันและฉันคิดว่าไม่สนับสนุนโดย {dtplyr}
ประเด็นสำคัญอื่น ๆ
อะไรคือสิ่งที่ / ไม่สนับสนุนก็ยังขึ้นอยู่กับความซับซ้อนของการดำเนินงานและไม่ว่าจะเกี่ยวข้องกับของ data.table คุณลักษณะพื้นเมืองเช่นการปรับปรุงโดยการอ้างอิงsetkey
หรือ และไม่ว่าโค้ดที่แปลแล้วจะมีประสิทธิภาพมากกว่าหรือไม่ (หนึ่งที่ผู้ใช้ data.table เขียนจะเขียน) ก็เป็นอีกปัจจัยหนึ่ง (นั่นคือการแปลโค้ด แต่เป็นรุ่นที่มีประสิทธิภาพหรือไม่) หลายสิ่งเชื่อมต่อกัน
หลายแง่มุมเหล่านี้เกี่ยวข้องกับประเด็นที่กล่าวถึงข้างต้น
คุณสามารถค้นหาว่า {dtplyr} สนับสนุนการดำเนินการเหล่านี้โดยเฉพาะเมื่อรวมเข้าด้วยกันหรือไม่
อีกเทคนิคที่มีประโยชน์เมื่อจัดการกับชุดข้อมูลขนาดเล็กหรือขนาดใหญ่ในระหว่างเซสชันแบบโต้ตอบ {data.table} มีชีวิตจริงตามคำสัญญาในการลดการเขียนโปรแกรมและคำนวณเวลาอย่างมาก
การตั้งค่าคีย์สำหรับตัวแปรที่ใช้ซ้ำสำหรับทั้งความเร็วและ 'ชื่อแถวที่อัดมากเกินไป' (ชุดย่อยโดยไม่ระบุชื่อตัวแปร)
dt <- data.table(iris)
setkey(dt, Species)
dt['setosa', do_something(...), ...]
dt['virginica', do_another(...), ...]
dt['setosa', more(...), ...]
# `by` argument can also be omitted, particularly useful during interactive session
# this ultimately becomes what I call 'naked' syntax, just type what you want to do, without any placeholders.
# It's simply elegant
dt['setosa', do_something(...), Species, ...]
หากการดำเนินการของคุณเกี่ยวข้องกับวิธรรมดา ๆ อย่างในตัวอย่างแรก {dtplyr} สามารถทำให้งานสำเร็จ สำหรับผู้ที่มีความซับซ้อน / ไม่ได้รับการสนับสนุนคุณสามารถใช้คำแนะนำนี้เพื่อเปรียบเทียบคำแปลที่แปลแล้วของ {dtplyr} กับผู้ใช้ data.table ที่มีประสบการณ์ในการเขียนโค้ดด้วยวิธีที่รวดเร็วและมีประสิทธิภาพของหน่วยความจำกับไวยากรณ์ที่หรูหราของ data.table การแปลไม่ได้หมายความว่าเป็นวิธีที่มีประสิทธิภาพมากที่สุดเนื่องจากอาจมีเทคนิคที่แตกต่างกันในการจัดการกับกรณีข้อมูลขนาดใหญ่ที่แตกต่างกัน สำหรับชุดข้อมูลที่ใหญ่ขึ้นคุณสามารถรวม {data.table} กับ{disk.frame} , {fst}และ{drake}และแพ็คเกจสุดเจ๋งอื่น ๆ เพื่อให้ได้ประโยชน์สูงสุด นอกจากนี้ยังมี{big.data.table}แต่ขณะนี้ไม่ได้ใช้งาน
ฉันหวังว่ามันจะช่วยทุกคน ขอให้มีความสุขมาก ๆ ในวันนี้☺☺
dplyr
สิ่งที่คุณทำไม่ได้data.table
ใช่ไหม ถ้าไม่ได้เปลี่ยนไปเป็นไปได้ดีกว่าdata.table
dtplyr