แยกสตริงที่คั่นด้วยจุลภาคในคอลัมน์เป็นแถวแยกกัน


109

ฉันมีกรอบข้อมูลดังนี้:

data.frame(director = c("Aaron Blaise,Bob Walker", "Akira Kurosawa", 
                        "Alan J. Pakula", "Alan Parker", "Alejandro Amenabar", "Alejandro Gonzalez Inarritu", 
                        "Alejandro Gonzalez Inarritu,Benicio Del Toro", "Alejandro González Iñárritu", 
                        "Alex Proyas", "Alexander Hall", "Alfonso Cuaron", "Alfred Hitchcock", 
                        "Anatole Litvak", "Andrew Adamson,Marilyn Fox", "Andrew Dominik", 
                        "Andrew Stanton", "Andrew Stanton,Lee Unkrich", "Angelina Jolie,John Stevenson", 
                        "Anne Fontaine", "Anthony Harvey"), AB = c('A', 'B', 'A', 'A', 'B', 'B', 'B', 'A', 'B', 'A', 'B', 'A', 'A', 'B', 'B', 'B', 'B', 'B', 'B', 'A'))

ดังที่คุณเห็นบางรายการในdirectorคอลัมน์มีหลายชื่อโดยคั่นด้วยเครื่องหมายจุลภาค ฉันต้องการแยกรายการเหล่านี้ออกเป็นแถวแยกกันโดยยังคงรักษาค่าของคอลัมน์อื่นไว้ ตัวอย่างเช่นแถวแรกในกรอบข้อมูลด้านบนควรแบ่งออกเป็นสองแถวโดยมีชื่อเดียวในdirectorคอลัมน์และ "A" ในABคอลัมน์


2
เพื่อถามสิ่งที่ชัดเจน: ข้อมูลนี้คุณควรจะโพสต์บนเว็บอินเตอร์หรือไม่?
Ricardo Saporta

1
พวกเขา "ไม่ใช่หนัง B ทั้งหมด" ดูเหมือนไม่มีพิษภัยพอ
แมทธิวลุนด์เบิร์ก

24
ทุกคนเหล่านี้เป็นผู้เข้าชิงรางวัลออสการ์ซึ่งฉันแทบจะไม่คิดว่าเป็นความลับ =)
RoyalTS

คำตอบ:


79

คำถามเก่า ๆ นี้มักจะถูกใช้เป็นเป้าหมายหลอก (ติดแท็กr-faq) ณ วันนี้มีคำตอบ 3 ครั้งเสนอแนวทางที่แตกต่างกัน 6 วิธี แต่ขาดเกณฑ์มาตรฐานเป็นแนวทางว่าแนวทางใดเร็วที่สุด1 1

โซลูชันที่ได้รับการเปรียบเทียบ ได้แก่

วิธีการที่แตกต่างกันโดยรวม 8 วิธีถูกเปรียบเทียบกับเฟรมข้อมูล 6 ขนาดที่แตกต่างกันโดยใช้microbenchmarkแพ็คเกจ (ดูรหัสด้านล่าง)

ข้อมูลตัวอย่างที่กำหนดโดย OP ประกอบด้วย 20 แถวเท่านั้น ในการสร้างเฟรมข้อมูลที่ใหญ่ขึ้น 20 แถวเหล่านี้จะทำซ้ำเพียง 1, 10, 100, 1000, 10,000 และ 100000 ครั้งซึ่งให้ขนาดของปัญหาสูงสุด 2 ล้านแถว

ผลการเปรียบเทียบ

ใส่คำอธิบายภาพที่นี่

ผลการเปรียบเทียบแสดงให้เห็นว่าสำหรับเฟรมข้อมูลที่มีขนาดใหญ่เพียงพอทุกdata.tableวิธีจะเร็วกว่าวิธีอื่น ๆ สำหรับเฟรมข้อมูลที่มีมากกว่า 5,000 แถวdata.tableวิธีที่ 2 และตัวแปรของ Jaap DT3จะเร็วที่สุดและเร็วกว่าวิธีที่ช้าที่สุด

ที่น่าสังเกตคือการกำหนดเวลาของทั้งสองtidyverseวิธีและวิธีการsplistackshapeแก้ปัญหานั้นคล้ายคลึงกันมากจนยากที่จะแยกแยะเส้นโค้งในแผนภูมิ วิธีนี้เป็นวิธีที่ช้าที่สุดในบรรดาขนาดเฟรมข้อมูลทั้งหมด

สำหรับเฟรมข้อมูลขนาดเล็กโซลูชัน base R และdata.tableวิธีที่ 4 ของ Matt ดูเหมือนจะมีค่าใช้จ่ายน้อยกว่าวิธีอื่น ๆ

รหัส

director <- 
  c("Aaron Blaise,Bob Walker", "Akira Kurosawa", "Alan J. Pakula", 
    "Alan Parker", "Alejandro Amenabar", "Alejandro Gonzalez Inarritu", 
    "Alejandro Gonzalez Inarritu,Benicio Del Toro", "Alejandro González Iñárritu", 
    "Alex Proyas", "Alexander Hall", "Alfonso Cuaron", "Alfred Hitchcock", 
    "Anatole Litvak", "Andrew Adamson,Marilyn Fox", "Andrew Dominik", 
    "Andrew Stanton", "Andrew Stanton,Lee Unkrich", "Angelina Jolie,John Stevenson", 
    "Anne Fontaine", "Anthony Harvey")
AB <- c("A", "B", "A", "A", "B", "B", "B", "A", "B", "A", "B", "A", 
        "A", "B", "B", "B", "B", "B", "B", "A")

library(data.table)
library(magrittr)

กำหนดฟังก์ชันสำหรับการวัดขนาดของปัญหา n

run_mb <- function(n) {
  # compute number of benchmark runs depending on problem size `n`
  mb_times <- scales::squish(10000L / n , c(3L, 100L)) 
  cat(n, " ", mb_times, "\n")
  # create data
  DF <- data.frame(director = rep(director, n), AB = rep(AB, n))
  DT <- as.data.table(DF)
  # start benchmarks
  microbenchmark::microbenchmark(
    matt_mod = {
      s <- strsplit(as.character(DF$director), ',')
      data.frame(director=unlist(s), AB=rep(DF$AB, lengths(s)))},
    jaap_DT1 = {
      DT[, lapply(.SD, function(x) unlist(tstrsplit(x, ",", fixed=TRUE))), by = AB
         ][!is.na(director)]},
    jaap_DT2 = {
      DT[, strsplit(as.character(director), ",", fixed=TRUE), 
         by = .(AB, director)][,.(director = V1, AB)]},
    jaap_dplyr = {
      DF %>% 
        dplyr::mutate(director = strsplit(as.character(director), ",")) %>%
        tidyr::unnest(director)},
    jaap_tidyr = {
      tidyr::separate_rows(DF, director, sep = ",")},
    cSplit = {
      splitstackshape::cSplit(DF, "director", ",", direction = "long")},
    DT3 = {
      DT[, strsplit(as.character(director), ",", fixed=TRUE),
         by = .(AB, director)][, director := NULL][
           , setnames(.SD, "V1", "director")]},
    DT4 = {
      DT[, .(director = unlist(strsplit(as.character(director), ",", fixed = TRUE))), 
         by = .(AB)]},
    times = mb_times
  )
}

เรียกใช้เกณฑ์มาตรฐานสำหรับขนาดปัญหาต่างๆ

# define vector of problem sizes
n_rep <- 10L^(0:5)
# run benchmark for different problem sizes
mb <- lapply(n_rep, run_mb)

เตรียมข้อมูลสำหรับการลงจุด

mbl <- rbindlist(mb, idcol = "N")
mbl[, n_row := NROW(director) * n_rep[N]]
mba <- mbl[, .(median_time = median(time), N = .N), by = .(n_row, expr)]
mba[, expr := forcats::fct_reorder(expr, -median_time)]

สร้างแผนภูมิ

library(ggplot2)
ggplot(mba, aes(n_row, median_time*1e-6, group = expr, colour = expr)) + 
  geom_point() + geom_smooth(se = FALSE) + 
  scale_x_log10(breaks = NROW(director) * n_rep) + scale_y_log10() + 
  xlab("number of rows") + ylab("median of execution time [ms]") +
  ggtitle("microbenchmark results") + theme_bw()

ข้อมูลเซสชันและเวอร์ชันแพ็คเกจ (ข้อความที่ตัดตอนมา)

devtools::session_info()
#Session info
# version  R version 3.3.2 (2016-10-31)
# system   x86_64, mingw32
#Packages
# data.table      * 1.10.4  2017-02-01 CRAN (R 3.3.2)
# dplyr             0.5.0   2016-06-24 CRAN (R 3.3.1)
# forcats           0.2.0   2017-01-23 CRAN (R 3.3.2)
# ggplot2         * 2.2.1   2016-12-30 CRAN (R 3.3.2)
# magrittr        * 1.5     2014-11-22 CRAN (R 3.3.0)
# microbenchmark    1.4-2.1 2015-11-25 CRAN (R 3.3.3)
# scales            0.4.1   2016-11-09 CRAN (R 3.3.2)
# splitstackshape   1.4.2   2014-10-23 CRAN (R 3.3.3)
# tidyr             0.6.1   2017-01-10 CRAN (R 3.3.2)

1 ความอยากรู้อยากเห็นของฉันถูกกระตุ้นโดยความคิดเห็นอันอุดมสมบูรณ์นี้ Brilliant! คำสั่งขนาดเร็วขึ้น! สำหรับtidyverseคำตอบของคำถามที่ถูกปิดเนื่องจากซ้ำกับคำถามนี้


ดี! ดูเหมือนจะมีช่องว่างสำหรับการปรับปรุงใน cSplit และ Sepa_rows (ซึ่งออกแบบมาเพื่อทำสิ่งนี้โดยเฉพาะ) Btw, cSplit ยังใช้ค่าคงที่ = arg และเป็นแพ็คเกจที่ใช้ data.table ดังนั้นอาจให้ DT แทน DF นอกจากนี้ฉันไม่คิดว่าการแปลงจากปัจจัยเป็น char จะอยู่ในเกณฑ์มาตรฐาน (เนื่องจากควรเป็นอักขระเริ่มต้นด้วย) ฉันตรวจสอบแล้วและไม่มีการเปลี่ยนแปลงใด ๆ ที่ทำอะไรกับผลลัพธ์ในเชิงคุณภาพ
Frank

1
@Frank ขอบคุณสำหรับคำแนะนำในการปรับปรุงเกณฑ์มาตรฐานและสำหรับการตรวจสอบผลกระทบต่อผลลัพธ์ จะรับเรื่องนี้เมื่อทำการปรับปรุงหลังจากการเปิดตัวของรุ่นต่อไปของdata.table, dplyrฯลฯ
Uwe

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

5
@Ferroao ไม่ถูกต้อง data.tables เข้าใกล้การแก้ไข "ตาราง" ในสถานที่คอลัมน์ทั้งหมดจะถูกเก็บไว้แน่นอนถ้าคุณไม่แก้ไขในสถานที่คุณจะได้รับสำเนาที่กรองเฉพาะสิ่งที่คุณขอเท่านั้น โดยสรุป data.table วิธีการคือการไม่สร้างชุดข้อมูลที่เป็นผลลัพธ์ แต่เพื่ออัปเดตชุดข้อมูลนั่นคือความแตกต่างที่แท้จริงระหว่าง data.table และ dplyr
Tensibai

1
การเปรียบเทียบที่ดีจริงๆ! บางทีคุณอาจจะเพิ่มในmatt_modและjaap_dplyrstrsplit fixed=TRUEเมื่อทำ อย่างอื่นมีและสิ่งนี้จะมีผลต่อการกำหนดเวลา ตั้งแต่R 4.0.0เริ่มต้นเมื่อสร้างdata.frameเป็นstringsAsFactors = FALSEดังนั้นas.characterอาจถูกลบออก
GKi

94

ทางเลือกหลายทาง:

1) สองวิธีด้วย :

library(data.table)
# method 1 (preferred)
setDT(v)[, lapply(.SD, function(x) unlist(tstrsplit(x, ",", fixed=TRUE))), by = AB
         ][!is.na(director)]
# method 2
setDT(v)[, strsplit(as.character(director), ",", fixed=TRUE), by = .(AB, director)
         ][,.(director = V1, AB)]

2) ก / การรวมกัน:

library(dplyr)
library(tidyr)
v %>% 
  mutate(director = strsplit(as.character(director), ",")) %>%
  unnest(director)

3) ด้วย เท่านั้น:ด้วยtidyr 0.5.0(และใหม่กว่า) คุณสามารถใช้separate_rows:

separate_rows(v, director, sep = ",")

คุณสามารถใช้convert = TRUEพารามิเตอร์เพื่อแปลงตัวเลขเป็นคอลัมน์ตัวเลขโดยอัตโนมัติ

4) พร้อมฐาน R:

# if 'director' is a character-column:
stack(setNames(strsplit(df$director,','), df$AB))

# if 'director' is a factor-column:
stack(setNames(strsplit(as.character(df$director),','), df$AB))

มีวิธีใดบ้างในการดำเนินการนี้สำหรับหลายคอลัมน์พร้อมกัน ตัวอย่างเช่น 3 คอลัมน์ที่แต่ละคอลัมน์มีสตริงคั่นด้วย ";" โดยแต่ละคอลัมน์มีจำนวนสตริงเท่ากัน คือdata.table(id= "X21", a = "chr1;chr1;chr1", b="123;133;134",c="234;254;268")กลายเป็นdata.table(id = c("X21","X21",X21"), a=c("chr1","chr1","chr1"), b=c("123","133","134"), c=c("234","254","268"))?
Reilstein

1
ว้าวเพิ่งรู้ว่ามันใช้งานได้หลายคอลัมน์พร้อมกัน - มันยอดเยี่ยมมาก!
Reilstein

@Reilstein คุณสามารถแบ่งปันวิธีที่คุณปรับเปลี่ยนสิ่งนี้สำหรับหลายคอลัมน์ได้หรือไม่? ฉันมีกรณีการใช้งานเหมือนกัน แต่ไม่แน่ใจว่าจะดำเนินการอย่างไร
Moon_Watcher

1
@Moon_Watcher วิธีที่ 1 ในคำตอบด้านบนใช้ได้กับหลายคอลัมน์แล้วซึ่งเป็นสิ่งที่ฉันคิดว่าน่าทึ่ง setDT(dt)[,lapply(.SD, function(x) unlist(tstrsplit(x, ";",fixed=TRUE))), by = ID]คือสิ่งที่ใช้ได้ผลสำหรับฉัน
Reilstein

51

การตั้งชื่อ data.frame เดิมของคุณvเรามีสิ่งนี้:

> s <- strsplit(as.character(v$director), ',')
> data.frame(director=unlist(s), AB=rep(v$AB, sapply(s, FUN=length)))
                      director AB
1                 Aaron Blaise  A
2                   Bob Walker  A
3               Akira Kurosawa  B
4               Alan J. Pakula  A
5                  Alan Parker  A
6           Alejandro Amenabar  B
7  Alejandro Gonzalez Inarritu  B
8  Alejandro Gonzalez Inarritu  B
9             Benicio Del Toro  B
10 Alejandro González Iñárritu  A
11                 Alex Proyas  B
12              Alexander Hall  A
13              Alfonso Cuaron  B
14            Alfred Hitchcock  A
15              Anatole Litvak  A
16              Andrew Adamson  B
17                 Marilyn Fox  B
18              Andrew Dominik  B
19              Andrew Stanton  B
20              Andrew Stanton  B
21                 Lee Unkrich  B
22              Angelina Jolie  B
23              John Stevenson  B
24               Anne Fontaine  B
25              Anthony Harvey  A

สังเกตการใช้repเพื่อสร้างคอลัมน์ AB ใหม่ ที่นี่sapplyส่งคืนจำนวนชื่อในแต่ละแถวเดิม


1
ฉันสงสัยว่า `AB = rep (v $ AB, Unlist (sapply (s, FUN = length)))` อาจจะเข้าใจได้ง่ายกว่าสิ่งที่คลุมเครือมากขึ้นvapply? มีอะไรที่vapplyเหมาะสมกว่าที่นี่หรือไม่?
IRTFM

7
ปัจจุบันsapply(s, length)สามารถแทนที่ด้วยlengths(s).
Rich Scriven

31

สายไปงานปาร์ตี้ แต่ทางเลือกทั่วไปอีกทางหนึ่งคือใช้cSplitจากแพ็คเกจ "Splitstackshape" ของฉันที่มีการdirectionโต้แย้ง ตั้งค่านี้เพื่อ"long"ให้ได้ผลลัพธ์ที่คุณระบุ:

library(splitstackshape)
head(cSplit(mydf, "director", ",", direction = "long"))
#              director AB
# 1:       Aaron Blaise  A
# 2:         Bob Walker  A
# 3:     Akira Kurosawa  B
# 4:     Alan J. Pakula  A
# 5:        Alan Parker  A
# 6: Alejandro Amenabar  B

2
devtools::install_github("yikeshu0611/onetree")

library(onetree)

dd=spread_byonecolumn(data=mydata,bycolumn="director",joint=",")

head(dd)
            director AB
1       Aaron Blaise  A
2         Bob Walker  A
3     Akira Kurosawa  B
4     Alan J. Pakula  A
5        Alan Parker  A
6 Alejandro Amenabar  B

0

ปัจจุบันเกณฑ์มาตรฐานอื่นที่ใช้strsplitจากฐานสามารถแนะนำให้แยกสตริงที่คั่นด้วยเครื่องหมายจุลภาคในคอลัมน์ออกเป็นแถวแยกกันเนื่องจากเร็วที่สุดในช่วงขนาดต่างๆ:

s <- strsplit(v$director, ",", fixed=TRUE)
s <- data.frame(director=unlist(s), AB=rep(v$AB, lengths(s)))

โปรดทราบว่าการใช้fixed=TRUEมีผลกระทบอย่างมากต่อการกำหนดเวลา

เส้นโค้งแสดงเวลาในการคำนวณตามจำนวนแถว

วิธีการเปรียบเทียบ:

met <- alist(base = {s <- strsplit(v$director, ",") #Matthew Lundberg
   s <- data.frame(director=unlist(s), AB=rep(v$AB, sapply(s, FUN=length)))}
 , baseLength = {s <- strsplit(v$director, ",") #Rich Scriven
   s <- data.frame(director=unlist(s), AB=rep(v$AB, lengths(s)))}
 , baseLeFix = {s <- strsplit(v$director, ",", fixed=TRUE)
   s <- data.frame(director=unlist(s), AB=rep(v$AB, lengths(s)))}
 , cSplit = s <- cSplit(v, "director", ",", direction = "long") #A5C1D2H2I1M1N2O1R2T1
 , dt = s <- setDT(v)[, lapply(.SD, function(x) unlist(tstrsplit(x, "," #Jaap
   , fixed=TRUE))), by = AB][!is.na(director)]
#, dt2 = s <- setDT(v)[, strsplit(director, "," #Jaap #Only Unique
#  , fixed=TRUE), by = .(AB, director)][,.(director = V1, AB)]
 , dplyr = {s <- v %>%  #Jaap
    mutate(director = strsplit(director, ",", fixed=TRUE)) %>%
    unnest(director)}
 , tidyr = s <- separate_rows(v, director, sep = ",") #Jaap
 , stack = s <- stack(setNames(strsplit(v$director, ",", fixed=TRUE), v$AB)) #Jaap
#, dt3 = {s <- setDT(v)[, strsplit(director, ",", fixed=TRUE), #Uwe #Only Unique
#  by = .(AB, director)][, director := NULL][, setnames(.SD, "V1", "director")]}
 , dt4 = {s <- setDT(v)[, .(director = unlist(strsplit(director, "," #Uwe
   , fixed = TRUE))), by = .(AB)]}
 , dt5 = {s <- vT[, .(director = unlist(strsplit(director, "," #Uwe
   , fixed = TRUE))), by = .(AB)]}
   )

ห้องสมุด:

library(microbenchmark)
library(splitstackshape) #cSplit
library(data.table) #dt, dt2, dt3, dt4
#setDTthreads(1) #Looks like it has here minor effect
library(dplyr) #dplyr
library(tidyr) #dplyr, tidyr

ข้อมูล:

v0 <- data.frame(director = c("Aaron Blaise,Bob Walker", "Akira Kurosawa", 
                        "Alan J. Pakula", "Alan Parker", "Alejandro Amenabar", "Alejandro Gonzalez Inarritu", 
                        "Alejandro Gonzalez Inarritu,Benicio Del Toro", "Alejandro González Iñárritu", 
                        "Alex Proyas", "Alexander Hall", "Alfonso Cuaron", "Alfred Hitchcock", 
                        "Anatole Litvak", "Andrew Adamson,Marilyn Fox", "Andrew Dominik", 
                        "Andrew Stanton", "Andrew Stanton,Lee Unkrich", "Angelina Jolie,John Stevenson", 
                        "Anne Fontaine", "Anthony Harvey"), AB = c('A', 'B', 'A', 'A', 'B', 'B', 'B', 'A', 'B', 'A', 'B', 'A', 'A', 'B', 'B', 'B', 'B', 'B', 'B', 'A'))

ผลการคำนวณและเวลา:

n <- 10^(0:5)
x <- lapply(n, function(n) {v <- v0[rep(seq_len(nrow(v0)), n),]
  vT <- setDT(v)
  ti <- min(100, max(3, 1e4/n))
  microbenchmark(list = met, times = ti, control=list(order="block"))})

y <- do.call(cbind, lapply(x, function(y) aggregate(time ~ expr, y, median)))
y <- cbind(y[1], y[-1][c(TRUE, FALSE)])
y[-1] <- y[-1] / 1e6 #ms
names(y)[-1] <- paste("n:", n * nrow(v0))
y #Time in ms
#         expr     n: 20    n: 200    n: 2000   n: 20000   n: 2e+05   n: 2e+06
#1        base 0.2989945 0.6002820  4.8751170  46.270246  455.89578  4508.1646
#2  baseLength 0.2754675 0.5278900  3.8066300  37.131410  442.96475  3066.8275
#3   baseLeFix 0.2160340 0.2424550  0.6674545   4.745179   52.11997   555.8610
#4      cSplit 1.7350820 2.5329525 11.6978975  99.060448 1053.53698 11338.9942
#5          dt 0.7777790 0.8420540  1.6112620   8.724586  114.22840  1037.9405
#6       dplyr 6.2425970 7.9942780 35.1920280 334.924354 4589.99796 38187.5967
#7       tidyr 4.0323765 4.5933730 14.7568235 119.790239 1294.26959 11764.1592
#8       stack 0.2931135 0.4672095  2.2264155  22.426373  289.44488  2145.8174
#9         dt4 0.5822910 0.6414900  1.2214470   6.816942   70.20041   787.9639
#10        dt5 0.5015235 0.5621240  1.1329110   6.625901   82.80803   636.1899

หมายเหตุวิธีการเช่น

(v <- rbind(v0[1:2,], v0[1,]))
#                 director AB
#1 Aaron Blaise,Bob Walker  A
#2          Akira Kurosawa  B
#3 Aaron Blaise,Bob Walker  A

setDT(v)[, strsplit(director, "," #Jaap #Only Unique
  , fixed=TRUE), by = .(AB, director)][,.(director = V1, AB)]
#         director AB
#1:   Aaron Blaise  A
#2:     Bob Walker  A
#3: Akira Kurosawa  B

ส่งคืนstrsplitสำหรับunique ผู้กำกับและอาจเทียบได้กับ

tmp <- unique(v)
s <- strsplit(tmp$director, ",", fixed=TRUE)
s <- data.frame(director=unlist(s), AB=rep(tmp$AB, lengths(s)))

แต่ตามความเข้าใจของฉันไม่ได้ถามสิ่งนี้

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