dplyr บน data.table ฉันใช้ data.table จริงหรือ


92

ถ้าฉันใช้ไวยากรณ์dplyrกับdatatableฉันจะได้รับประโยชน์ด้านความเร็วทั้งหมดของ datatable ในขณะที่ยังใช้ไวยากรณ์ของ dplyr หรือไม่ กล่าวอีกนัยหนึ่งฉันใช้ datatable ผิดหรือไม่หากฉันค้นหาด้วยไวยากรณ์ dplyr หรือฉันจำเป็นต้องใช้ไวยากรณ์ที่สามารถจัดเก็บข้อมูลได้อย่างแท้จริงเพื่อควบคุมพลังทั้งหมดของมัน

ขอบคุณล่วงหน้าสำหรับคำแนะนำใด ๆ ตัวอย่างรหัส:

library(data.table)
library(dplyr)

diamondsDT <- data.table(ggplot2::diamonds)
setkey(diamondsDT, cut) 

diamondsDT %>%
    filter(cut != "Fair") %>%
    group_by(cut) %>%
    summarize(AvgPrice = mean(price),
                 MedianPrice = as.numeric(median(price)),
                 Count = n()) %>%
    arrange(desc(Count))

ผล:

#         cut AvgPrice MedianPrice Count
# 1     Ideal 3457.542      1810.0 21551
# 2   Premium 4584.258      3185.0 13791
# 3 Very Good 3981.760      2648.0 12082
# 4      Good 3928.864      3050.5  4906

นี่คือความเท่าเทียมกันของข้อมูลที่ฉันคิดขึ้นมา ไม่แน่ใจว่าเป็นไปตามแนวทางปฏิบัติที่ดีของ DT หรือไม่ แต่ฉันสงสัยว่าโค้ดมีประสิทธิภาพมากกว่าไวยากรณ์ dplyr เบื้องหลังหรือไม่:

diamondsDT [cut != "Fair"
        ] [, .(AvgPrice = mean(price),
                 MedianPrice = as.numeric(median(price)),
                 Count = .N), by=cut
        ] [ order(-Count) ]

7
ทำไมคุณไม่ใช้ไวยากรณ์ตารางข้อมูล? มันหรูหราและมีประสิทธิภาพเช่นกัน คำถามไม่สามารถตอบได้จริงเนื่องจากเป็นคำถามที่กว้างมาก ใช่มีdplyrวิธีการสำหรับตารางข้อมูล แต่ตารางข้อมูลก็มีวิธีการเปรียบเทียบของตัวเองเช่นกัน
Rich Scriven

7
ฉันสามารถใช้ไวยากรณ์หรือหลักสูตรที่สามารถบันทึกข้อมูลได้ แต่อย่างใดฉันพบว่าไวยากรณ์ของ dplyr สวยงามกว่า โดยไม่คำนึงถึงความต้องการของฉันสำหรับไวยากรณ์ สิ่งที่ฉันอยากรู้ก็คือฉันจำเป็นต้องใช้ไวยากรณ์ที่สามารถจัดเรียงข้อมูลได้อย่างแท้จริงเพื่อให้ได้รับประโยชน์ 100% จากพลังที่สามารถจัดเก็บข้อมูลได้
Polymerase

3
สำหรับการเปรียบเทียบล่าสุดที่dplyrใช้กับdata.frames และdata.tables ที่เกี่ยวข้องโปรดดูที่นี่ (และการอ้างอิงในนั้น)
Henrik

2
@Polymerase - ฉันคิดว่าคำตอบของคำถามนั้นคือ "ใช่" แน่นอน
Rich Scriven

1
@Henrik: ฉันรู้ในภายหลังว่าฉันตีความหน้านั้นผิดเพราะพวกเขาแสดงรหัสสำหรับโครงสร้างดาต้าเฟรมเท่านั้น แต่ไม่ใช่รหัสที่ใช้สำหรับการสร้าง data.table เมื่อฉันรู้ฉันก็ลบความคิดเห็นของฉัน (หวังว่าคุณจะไม่เห็น)
IRTFM

คำตอบ:


77

ไม่มีคำตอบที่ตรงไปตรงมา / เรียบง่ายเนื่องจากปรัชญาของแพ็คเกจทั้งสองนี้แตกต่างกันในบางแง่มุม ดังนั้นการประนีประนอมบางอย่างจึงหลีกเลี่ยงไม่ได้ นี่คือข้อกังวลบางประการที่คุณอาจต้องจัดการ / พิจารณา

การดำเนินการที่เกี่ยวข้องกับi(== filter()และslice()ใน dplyr)

สมมติDTว่าพูด 10 คอลัมน์ พิจารณานิพจน์ data.table เหล่านี้:

DT[a > 1, .N]                    ## --- (1)
DT[a > 1, mean(b), by=.(c, d)]   ## --- (2)

(1) ให้จำนวนแถวในที่คอลัมน์DT a > 1(2) ส่งคืนที่mean(b)จัดกลุ่มโดยc,dสำหรับนิพจน์เดียวกันในi(1)

dplyrนิพจน์ที่ใช้โดยทั่วไปจะเป็น:

DT %>% filter(a > 1) %>% summarise(n())                        ## --- (3) 
DT %>% filter(a > 1) %>% group_by(c, d) %>% summarise(mean(b)) ## --- (4)

เห็นได้ชัดว่ารหัส data.table นั้นสั้นกว่า นอกจากนี้ยังมีหน่วยความจำที่มีประสิทธิภาพมากขึ้น1 . ทำไม? เนื่องจากในทั้ง (3) และ (4) filter()จะส่งคืนแถวสำหรับคอลัมน์ทั้ง 10 คอลัมน์ก่อนเมื่ออยู่ใน (3) เราต้องการจำนวนแถวและใน (4) เราต้องการเพียงแค่คอลัมน์b, c, dสำหรับการดำเนินการต่อเนื่อง เพื่อเอาชนะสิ่งนี้เราต้องselect()คอลัมน์ apriori:

DT %>% select(a) %>% filter(a > 1) %>% summarise(n()) ## --- (5)
DT %>% select(a,b,c,d) %>% filter(a > 1) %>% group_by(c,d) %>% summarise(mean(b)) ## --- (6)

สิ่งสำคัญคือต้องเน้นความแตกต่างทางปรัชญาที่สำคัญระหว่างสองแพ็คเกจ:

  • ในdata.tableเราต้องการให้การดำเนินการที่เกี่ยวข้องเหล่านี้เข้าด้วยกันและทำให้สามารถดูj-expression(จากการเรียกใช้ฟังก์ชันเดียวกัน) และตระหนักว่าไม่จำเป็นต้องมีคอลัมน์ใด ๆ ใน (1) นิพจน์ในiได้รับการคำนวณและ.Nเป็นเพียงผลรวมของเวกเตอร์ตรรกะที่ให้จำนวนแถว ส่วนย่อยทั้งหมดไม่เคยรับรู้ ใน (2) มีเพียงคอลัมน์b,c,dที่ปรากฏในชุดย่อยส่วนคอลัมน์อื่น ๆ จะถูกละเว้น

  • แต่ในdplyrปรัชญาคือการมีฟังก์ชั่นได้อย่างแม่นยำทำสิ่งหนึ่งที่ดี ไม่มีทาง (อย่างน้อยในปัจจุบัน) ที่จะบอกได้ว่าการดำเนินการหลังจากนั้นfilter()ต้องการคอลัมน์ทั้งหมดที่เรากรองหรือไม่ คุณจะต้องคิดล่วงหน้าหากคุณต้องการทำงานดังกล่าวอย่างมีประสิทธิภาพ โดยส่วนตัวแล้วฉันพบว่ามันขัดแย้งในกรณีนี้

โปรดทราบว่าใน (5) และ (6) เรายังคงเป็นคอลัมน์ย่อยaที่เราไม่ต้องการ แต่ฉันไม่แน่ใจว่าจะหลีกเลี่ยงสิ่งนั้นได้อย่างไร หากfilter()ฟังก์ชันมีอาร์กิวเมนต์เพื่อเลือกคอลัมน์ที่จะส่งคืนเราสามารถหลีกเลี่ยงปัญหานี้ได้ แต่ฟังก์ชันจะไม่ทำงานเพียงงานเดียว (ซึ่งเป็นตัวเลือกการออกแบบ dplyr ด้วย)

มอบหมายย่อยโดยการอ้างอิง

dplyr จะไม่อัปเดตโดยอ้างอิง นี่เป็นอีกหนึ่งความแตกต่างที่ยิ่งใหญ่ (เชิงปรัชญา) ระหว่างสองแพ็คเกจ

ตัวอย่างเช่นใน data.table คุณสามารถทำได้:

DT[a %in% some_vals, a := NA]

ซึ่งอัปเดตคอลัมน์a โดยอ้างอิง เฉพาะแถวที่ตรงตามเงื่อนไข ในขณะนี้ dplyr deep คัดลอกข้อมูลทั้งหมดตารางภายในเพื่อเพิ่มคอลัมน์ใหม่ @BrodieG พูดถึงเรื่องนี้แล้วในคำตอบของเขา

แต่สำเนาลึกจะถูกแทนที่ด้วยสำเนาตื้นเมื่อFR # 617จะดำเนินการ นอกจากนี้ยังเกี่ยวข้อง: dplyr: FR โปรดทราบว่าคอลัมน์ที่คุณแก้ไขจะถูกคัดลอกเสมอ (ดังนั้นจึงช้าลง / หน่วยความจำมีประสิทธิภาพน้อยลง) จะไม่มีวิธีอัปเดตคอลัมน์โดยการอ้างอิง

ฟังก์ชันอื่น ๆ

  • ใน data.table คุณสามารถรวมได้ในขณะที่เข้าร่วมและนี่เป็นวิธีที่ตรงกว่าที่จะเข้าใจและมีประสิทธิภาพของหน่วยความจำเนื่องจากผลการรวมระดับกลางไม่เคยปรากฏ ตรวจสอบโพสต์นี้เพื่อดูตัวอย่าง คุณไม่สามารถ (ในขณะนี้?) โดยใช้ไวยากรณ์ data.table / data.frame ของ dplyr

  • คุณลักษณะการรวมการกลิ้งของ data.table ไม่รองรับในไวยากรณ์ของ dplyr เช่นกัน

  • เมื่อเร็ว ๆ นี้เราได้ใช้การรวมแบบทับซ้อนใน data.table ที่จะรวมช่วงช่วงเวลา ( นี่คือตัวอย่าง ) ซึ่งเป็นฟังก์ชันfoverlaps()ที่แยกจากกันในขณะนี้ดังนั้นจึงสามารถใช้กับตัวดำเนินการไปป์ได้ (magrittr / pipeR? - ไม่เคยลองด้วยตัวเอง)

    แต่ท้ายที่สุดแล้วเป้าหมายของเราคือการรวมเข้าด้วยกัน[.data.tableเพื่อให้เราสามารถเก็บเกี่ยวคุณลักษณะอื่น ๆ เช่นการจัดกลุ่มการรวมกลุ่มในขณะที่เข้าร่วมเป็นต้นซึ่งจะมีข้อ จำกัด เดียวกันกับที่ระบุไว้ข้างต้น

  • ตั้งแต่ 1.9.4 เป็นต้นมา data.table ใช้การจัดทำดัชนีอัตโนมัติโดยใช้คีย์รองสำหรับการค้นหาแบบไบนารีชุดย่อยที่อิงตามไวยากรณ์ R ปกติ ตัวอย่าง: DT[x == 1]และDT[x %in% some_vals]จะสร้างดัชนีโดยอัตโนมัติในการรันครั้งแรกซึ่งจะใช้กับชุดย่อยที่ต่อเนื่องกันจากคอลัมน์เดียวกันไปจนถึงชุดย่อยที่รวดเร็วโดยใช้การค้นหาแบบไบนารี คุณลักษณะนี้จะยังคงพัฒนาต่อไป ตรวจสอบส่วนสำคัญนี้เพื่อดูภาพรวมสั้น ๆ ของคุณลักษณะนี้

    จากวิธีfilter()การใช้งานสำหรับ data.tables จะไม่ใช้ประโยชน์จากคุณสมบัตินี้

  • คุณลักษณะ dplyr คือมันยังมีส่วนต่อประสานกับฐานข้อมูลโดยใช้ไวยากรณ์เดียวกันซึ่ง data.table ไม่ได้ในขณะนี้

ดังนั้นคุณจะต้องชั่งน้ำหนักในประเด็นเหล่านี้ (และอาจเป็นประเด็นอื่น ๆ ) และตัดสินใจโดยพิจารณาว่าคุณยอมรับข้อแลกเปลี่ยนเหล่านี้หรือไม่

HTH


(1) โปรดทราบว่าการมีประสิทธิภาพของหน่วยความจำจะส่งผลโดยตรงต่อความเร็ว (โดยเฉพาะเมื่อข้อมูลมีขนาดใหญ่ขึ้น) เนื่องจากคอขวดในกรณีส่วนใหญ่คือการย้ายข้อมูลจากหน่วยความจำหลักไปยังแคช (และใช้ประโยชน์จากข้อมูลในแคชให้มากที่สุด - ลดการพลาดแคช - เพื่อลดการเข้าถึงหน่วยความจำหลัก) ยังไม่ลงรายละเอียดที่นี่


4
ยอดเยี่ยมแน่นอน ขอบคุณสำหรับสิ่งนั้น
David Arenburg

6
นั่นเป็นคำตอบที่ดี แต่จะเป็นไปได้ (ถ้าไม่น่าจะเป็นไปได้) สำหรับ dplyr ที่จะใช้การfilter()บวกที่มีประสิทธิภาพsummarise()โดยใช้วิธีการเดียวกับที่ dplyr ใช้สำหรับ SQL นั่นคือการสร้างนิพจน์จากนั้นดำเนินการเพียงครั้งเดียวตามความต้องการ ไม่น่าจะถูกนำมาใช้ในอนาคตอันใกล้นี้เนื่องจาก dplyr นั้นเร็วเพียงพอสำหรับฉันและการใช้เครื่องมือวางแผนการสืบค้น / เครื่องมือเพิ่มประสิทธิภาพนั้นค่อนข้างยาก
hadley

การมีหน่วยความจำที่มีประสิทธิภาพยังช่วยในส่วนที่สำคัญอีกอย่างหนึ่งคือต้องทำงานให้เสร็จก่อนที่หน่วยความจำจะหมด เมื่อทำงานกับชุดข้อมูลขนาดใหญ่ฉันประสบปัญหากับ dplyr เช่นเดียวกับแพนด้าในขณะที่ data.table จะทำงานให้เสร็จอย่างสง่างาม
Zaki

25

แค่ลองดู.

library(rbenchmark)
library(dplyr)
library(data.table)

benchmark(
dplyr = diamondsDT %>%
    filter(cut != "Fair") %>%
    group_by(cut) %>%
    summarize(AvgPrice = mean(price),
                 MedianPrice = as.numeric(median(price)),
                 Count = n()) %>%
    arrange(desc(Count)),
data.table = diamondsDT[cut != "Fair", 
                        list(AvgPrice = mean(price),
                             MedianPrice = as.numeric(median(price)),
                             Count = .N), by = cut][order(-Count)])[1:4]

ในปัญหานี้ดูเหมือนว่า data.table จะเร็วกว่า dplyr 2.4 เท่าโดยใช้ data.table:

        test replications elapsed relative
2 data.table          100    2.39    1.000
1      dplyr          100    5.77    2.414

แก้ไขตามความคิดเห็นของ Polymerase


2
เมื่อใช้microbenchmarkแพคเกจฉันพบว่าการเรียกใช้dplyrรหัสของ OP ในเวอร์ชันดั้งเดิม (data frame) diamondsใช้เวลาเฉลี่ย 0.012 วินาทีในขณะที่ใช้เวลาเฉลี่ย 0.024 วินาทีหลังจากแปลงdiamondsเป็นตารางข้อมูล การรันdata.tableโค้ดของG.Grothendieck ใช้เวลา 0.013 วินาที อย่างน้อยในระบบของฉันก็ดูเหมือนdplyrและdata.tableมีประสิทธิภาพเดียวกัน แต่ทำไมถึงdplyrช้าลงเมื่อเฟรมข้อมูลถูกแปลงเป็นตารางข้อมูลเป็นครั้งแรก?
eipi10

เรียน G.Grothendieck นี่วิเศษมาก ขอบคุณที่แสดงยูทิลิตี้มาตรฐานนี้ให้ฉันดู BTW คุณลืม [ลำดับ (-Count)] ในเวอร์ชันที่สามารถใช้ข้อมูลได้เพื่อทำให้ความเท่าเทียมกันของการจัดเรียงของ dplyr (desc (Count)) หลังจากเพิ่มสิ่งนี้แล้ว datatable ก็ยังเร็วขึ้นประมาณ x1.8 (แทนที่จะเป็น 2.9)
Polymerase

@ eipi10 คุณสามารถเรียกใช้บัลลังก์ของคุณใหม่อีกครั้งด้วยเวอร์ชันที่สามารถบันทึกข้อมูลได้ที่นี่ (เพิ่มการจัดเรียงตามจำนวนการนับในขั้นตอนสุดท้าย): DiamondDT [cut! = "Fair", list (AvgPrice = mean (price), MedianPrice = as.numeric (median (price)), Count = .N), by = cut] [order (-Count)]
Polymerase

ยังคงอยู่ 0.013 วินาที การดำเนินการสั่งซื้อแทบจะไม่ต้องใช้เวลาใด ๆ เลยเพราะมันเป็นเพียงการเรียงลำดับตารางสุดท้ายซึ่งมีเพียงสี่แถวเท่านั้น
eipi10

1
มีค่าโสหุ้ยคงที่สำหรับการแปลงจากไวยากรณ์ dplyr เป็นไวยากรณ์ของตารางข้อมูลดังนั้นจึงควรลองใช้ขนาดปัญหาที่แตกต่างกัน นอกจากนี้ฉันอาจไม่ได้ติดตั้งโค้ดตารางข้อมูลที่มีประสิทธิภาพสูงสุดใน dplyr ยินดีต้อนรับแพทช์เสมอ
hadley

23

เพื่อตอบคำถามของคุณ:

  • ใช่คุณกำลังใช้ data.table
  • แต่ไม่ได้มีประสิทธิภาพเท่าที่คุณทำกับdata.tableไวยากรณ์ที่บริสุทธิ์

ในหลาย ๆ กรณีสิ่งนี้จะเป็นการประนีประนอมที่ยอมรับได้สำหรับผู้ที่ต้องการdplyrไวยากรณ์แม้ว่าอาจจะช้ากว่าdplyrกรอบข้อมูลธรรมดาก็ตาม

ปัจจัยสำคัญอย่างหนึ่งก็คือdplyrจะคัดลอกdata.tableโดยค่าเริ่มต้นเมื่อจัดกลุ่ม พิจารณา (โดยใช้ microbenchmark):

Unit: microseconds
                                                               expr       min         lq    median
                                diamondsDT[, mean(price), by = cut]  3395.753  4039.5700  4543.594
                                          diamondsDT[cut != "Fair"] 12315.943 15460.1055 16383.738
 diamondsDT %>% group_by(cut) %>% summarize(AvgPrice = mean(price))  9210.670 11486.7530 12994.073
                               diamondsDT %>% filter(cut != "Fair") 13003.878 15897.5310 17032.609

การกรองมีความเร็วเทียบเท่ากัน แต่การจัดกลุ่มไม่ใช่ ฉันเชื่อว่าผู้กระทำผิดคือบรรทัดนี้ในdplyr:::grouped_dt:

if (copy) {
    data <- data.table::copy(data)
}

โดยที่copyค่าเริ่มต้นเป็นTRUE(และไม่สามารถเปลี่ยนเป็น FALSE ที่ฉันเห็นได้อย่างง่ายดาย) สิ่งนี้อาจไม่ได้คำนึงถึงความแตกต่าง 100% แต่ค่าใช้จ่ายทั่วไปเพียงอย่างเดียวสำหรับบางสิ่งที่ขนาดdiamondsส่วนใหญ่จะไม่ใช่ความแตกต่างทั้งหมด

ปัญหาคือเพื่อให้มีไวยากรณ์ที่สอดคล้องกันdplyrการจัดกลุ่มในสองขั้นตอน ขั้นแรกตั้งค่าคีย์บนสำเนาของตารางข้อมูลต้นฉบับที่ตรงกับกลุ่มและหลังจากนั้นจะจัดกลุ่ม data.tableเพียงแค่จัดสรรหน่วยความจำสำหรับกลุ่มผลลัพธ์ที่ใหญ่ที่สุดซึ่งในกรณีนี้เป็นเพียงแถวเดียวดังนั้นจึงสร้างความแตกต่างอย่างมากในจำนวนหน่วยความจำที่ต้องจัดสรร

FYI ถ้าใครสนใจฉันพบสิ่งนี้โดยใช้treeprof( install_github("brodieg/treeprof")) ซึ่งเป็นโปรแกรมดูต้นไม้ทดลอง (และยังเป็นอัลฟ่ามาก) สำหรับRprofเอาต์พุต:

ป้อนคำอธิบายภาพที่นี่

หมายเหตุข้างต้นใช้งานได้กับ Macs AFAIK เท่านั้น นอกจากนี้น่าเสียดายที่Rprofบันทึกการโทรประเภทpackagename::funnameไม่ระบุตัวตนดังนั้นจึงอาจเป็นสายใดก็ได้และทั้งหมดdatatable::ภายในgrouped_dtที่รับผิดชอบ แต่จากการทดสอบอย่างรวดเร็วดูเหมือนว่าdatatable::copyเป็นการโทรที่ใหญ่

ที่กล่าวว่าคุณสามารถดูได้อย่างรวดเร็วว่ามีค่าใช้จ่ายในการ[.data.tableโทรไม่มากนักแต่ยังมีสาขาแยกต่างหากสำหรับการจัดกลุ่ม


แก้ไข : เพื่อยืนยันการคัดลอก:

> tracemem(diamondsDT)
[1] "<0x000000002747e348>"    
> diamondsDT %>% group_by(cut) %>% summarize(AvgPrice = mean(price))
tracemem[0x000000002747e348 -> 0x000000002a624bc0]: <Anonymous> grouped_dt group_by_.data.table group_by_ group_by <Anonymous> freduce _fseq eval eval withVisible %>% 
Source: local data table [5 x 2]

        cut AvgPrice
1      Fair 4358.758
2      Good 3928.864
3 Very Good 3981.760
4   Premium 4584.258
5     Ideal 3457.542
> diamondsDT[, mean(price), by = cut]
         cut       V1
1:     Ideal 3457.542
2:   Premium 4584.258
3:      Good 3928.864
4: Very Good 3981.760
5:      Fair 4358.758
> untracemem(diamondsDT)

ยอดเยี่ยมมากขอบคุณ นั่นหมายความว่า dplyr :: group_by () จะเพิ่มความต้องการหน่วยความจำเป็นสองเท่า (เทียบกับไวยากรณ์ที่สามารถระบุข้อมูลได้) เนื่องจากขั้นตอนการคัดลอกข้อมูลภายในหรือไม่ หมายความว่าถ้าขนาดวัตถุที่สามารถบันทึกได้ของฉันคือ 1GB และฉันใช้ไวยากรณ์ dplyr chained ที่คล้ายกับในโพสต์ต้นฉบับ ฉันต้องการหน่วยความจำว่างอย่างน้อย 2GB เพื่อให้ได้ผลลัพธ์?
Polymerase

2
ฉันรู้สึกว่าฉันได้รับการแก้ไขแล้วในเวอร์ชัน dev?
hadley

@hadley ฉันกำลังทำงานจากเวอร์ชัน CRAN เมื่อมองไปที่ dev ดูเหมือนว่าคุณได้แก้ไขปัญหาแล้วบางส่วน แต่สำเนาจริงยังคงอยู่ (ยังไม่ได้ทดสอบเพียงแค่ดูที่บรรทัด c (20, 30:32) ใน R / grouped-dt.r ตอนนี้อาจเร็วกว่า แต่ ฉันพนันได้เลยว่าขั้นตอนที่ช้าคือสำเนา
BrodieG

3
ฉันยังรอฟังก์ชั่นการคัดลอกตื้น ๆ ใน data.table; ถึงตอนนั้นฉันคิดว่าปลอดภัยดีกว่าเร็ว
hadley

2

คุณสามารถใช้dtplyrในขณะนี้ซึ่งเป็นส่วนหนึ่งของtidyverse ช่วยให้คุณใช้คำสั่งสไตล์ dplyr ได้ตามปกติ แต่ใช้การประเมินแบบขี้เกียจและแปลคำสั่งของคุณเป็น data.table โค้ดภายใต้ประทุน ค่าใช้จ่ายในการแปลมีเพียงเล็กน้อย แต่คุณได้รับประโยชน์ทั้งหมดของ data.table หากไม่เป็นเช่นนั้น รายละเอียดเพิ่มเติมที่ repo คอมไพล์อย่างเป็นทางการที่นี่และ tidyverse หน้า

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.