Pisahkan string yang dipisahkan koma dalam kolom menjadi baris terpisah

109

Saya memiliki bingkai data, seperti ini:

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'))

Seperti yang Anda lihat, beberapa entri di directorkolom adalah beberapa nama yang dipisahkan dengan koma. Saya ingin membagi entri ini menjadi baris terpisah sambil mempertahankan nilai kolom lainnya. Sebagai contoh, baris pertama dalam bingkai data di atas harus dipisahkan menjadi dua baris, masing-masing dengan satu nama di directorkolom dan 'A' di ABkolom.

RoyalTS
sumber
2
Sekadar bertanya yang sudah jelas: Apakah data ini harus Anda posting di interwebs?
Ricardo Saporta
1
Mereka "tidak semuanya film B." Sepertinya cukup tidak berbahaya.
Matthew Lundberg
24
Semua orang ini adalah nominasi Academy Award, yang menurut saya tidak rahasia =)
RoyalTS

Jawaban:

79

Pertanyaan lama ini sering digunakan sebagai target penipuan (diberi tag r-faq). Sampai hari ini, telah dijawab sebanyak tiga kali dengan menawarkan 6 pendekatan berbeda tetapi tidak memiliki tolok ukur sebagai pedoman mana pendekatan yang paling cepat 1 .

Solusi yang diukur termasuk

Secara keseluruhan 8 metode berbeda diukur pada 6 ukuran frame data yang berbeda menggunakan microbenchmarkpaket (lihat kode di bawah).

Sampel data yang diberikan oleh OP hanya terdiri dari 20 baris. Untuk membuat bingkai data yang lebih besar, 20 baris ini diulangi sebanyak 1, 10, 100, 1000, 10000, dan 100000 kali yang memberikan ukuran masalah hingga 2 juta baris.

Hasil benchmark

masukkan deskripsi gambar di sini

Hasil benchmark menunjukkan bahwa untuk frame data yang cukup besar, semua data.tablemetode lebih cepat daripada metode lainnya. Untuk bingkai data dengan lebih dari sekitar 5000 baris, data.tablemetode Jaap 2 dan variannya DT3adalah yang tercepat, besarnya lebih cepat daripada metode yang paling lambat.

Hebatnya, pengaturan waktu dari dua tidyversemetode dan splistackshapesolusinya sangat mirip sehingga sulit untuk membedakan kurva pada grafik. Mereka adalah yang paling lambat dari metode benchmark di semua ukuran bingkai data.

Untuk bingkai data yang lebih kecil, solusi R dasar Matt dan data.tablemetode 4 tampaknya memiliki overhead yang lebih sedikit daripada metode lainnya.

Kode

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)

Tentukan fungsi untuk benchmark run of problem size 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
  )
}

Jalankan tolok ukur untuk ukuran masalah yang berbeda

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

Siapkan data untuk plot

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)]

Buat bagan

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()

Info sesi & versi paket (kutipan)

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 Rasa ingin tahu saya terusik oleh komentar bersemangat ini Brilian! Urutan besarnya lebih cepat! untuk tidyversejawaban dari pertanyaan yang ditutup sebagai duplikat dari pertanyaan ini.

Uwe
sumber
Bagus! Sepertinya ada ruang untuk perbaikan dalam cSplit dan terpisah_rows (yang dirancang khusus untuk melakukan ini). Btw, cSplit juga menggunakan fixed = arg dan merupakan paket berbasis data.table, jadi sebaiknya berikan DT daripada DF. Juga fwiw, saya tidak berpikir konversi dari faktor ke karakter termasuk dalam tolok ukur (karena itu harus char untuk memulai). Saya telah memeriksa dan tidak ada perubahan ini yang berpengaruh pada hasil secara kualitatif.
Frank
1
@Frank Terima kasih atas saran Anda untuk meningkatkan tolok ukur dan untuk memeriksa efek pada hasil. Akan mengambil ini ketika melakukan update setelah rilis versi berikutnya data.table, dplyr, dll
Uwe
Menurut saya pendekatannya tidak dapat dibandingkan, setidaknya tidak di semua kesempatan, karena pendekatan yang dapat ditautkan hanya menghasilkan tabel dengan kolom "yang dipilih", sedangkan dplyr menghasilkan hasil dengan semua kolom (termasuk yang tidak terlibat dalam analisis dan tanpa memiliki untuk menuliskan nama mereka dalam fungsi).
Ferroao
5
@Ferroao Itu salah, pendekatan data.tables memodifikasi "tabel" di tempatnya, semua kolom disimpan, tentu saja jika Anda tidak memodifikasi di tempat Anda mendapatkan salinan yang difilter hanya dari apa yang Anda minta. Singkatnya pendekatan data.table adalah untuk tidak menghasilkan dataset yang dihasilkan tetapi untuk memperbarui dataset, itulah perbedaan nyata antara data.table dan dplyr.
Tensibai
1
Perbandingan yang sangat bagus! Mungkin Anda bisa menambahkan matt_mod dan jaap_dplyr , saat melakukannya strsplit fixed=TRUE. Seperti yang dimiliki orang lain dan ini akan berdampak pada pengaturan waktu. Sejak R 4.0.0 , default, saat membuat data.frame, adalah stringsAsFactors = FALSE, jadi as.characterbisa dihapus.
GKi
94

Beberapa alternatif:

1) dua cara dengan :

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) a / kombinasi:

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

3) dengan hanya: Dengan tidyr 0.5.0(dan lebih baru), Anda juga dapat menggunakan separate_rows:

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

Anda dapat menggunakan convert = TRUEparameter untuk secara otomatis mengubah angka menjadi kolom numerik.

4) dengan basis 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))
Jaap
sumber
Apakah ada cara untuk melakukan ini untuk beberapa kolom sekaligus? Misalnya 3 kolom yang masing-masing memiliki string yang dipisahkan oleh ";" dengan setiap kolom memiliki jumlah string yang sama. yaitu data.table(id= "X21", a = "chr1;chr1;chr1", b="123;133;134",c="234;254;268")menjadi data.table(id = c("X21","X21",X21"), a=c("chr1","chr1","chr1"), b=c("123","133","134"), c=c("234","254","268"))?
Reilstein
1
wow baru saja menyadari itu sudah berfungsi untuk beberapa kolom sekaligus - ini luar biasa!
Reilstein
@Reilstein Bisakah Anda berbagi bagaimana Anda mengadaptasi ini untuk beberapa kolom? Saya memiliki kasus penggunaan yang sama, tetapi tidak yakin bagaimana cara melakukannya.
Moon_Watcher
1
@Moon_Watcher Metode 1 dalam jawaban di atas sudah berfungsi untuk banyak kolom, yang menurut saya luar biasa. setDT(dt)[,lapply(.SD, function(x) unlist(tstrsplit(x, ";",fixed=TRUE))), by = ID]adalah yang berhasil untuk saya.
Reilstein
51

Menamai data.frame asli Anda v, kami memiliki ini:

> 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

Perhatikan penggunaan repuntuk membangun kolom AB baru. Di sini, sapplymengembalikan jumlah nama di setiap baris asli.

Matthew Lundberg
sumber
1
Saya bertanya-tanya apakah `AB = rep (v $ AB, unlist (sapply (s, FUN = length)))` mungkin lebih mudah dipahami daripada yang lebih tidak jelas vapply? Adakah yang vapplylebih pantas di sini?
IRTFM
7
Saat ini sapply(s, length)bisa diganti dengan lengths(s).
Rich Scriven
31

Terlambat ke pesta, tetapi alternatif umum lainnya adalah menggunakan cSplitdari paket "splitstackshape" saya yang memiliki directionargumen. Setel ini untuk "long"mendapatkan hasil yang Anda tentukan:

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
A5C1D2H2I1M1N2O1R2T1
sumber
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
zhang jing
sumber
0

Tolok ukur lain yang dihasilkan menggunakan strsplitdari basis saat ini dapat direkomendasikan untuk Memisahkan string yang dipisahkan koma dalam kolom menjadi baris terpisah , karena ini adalah yang tercepat dalam berbagai ukuran:

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

Perhatikan bahwa penggunaan fixed=TRUEberdampak signifikan pada pengaturan waktu.

Kurva yang menunjukkan waktu komputasi selama jumlah baris

Metode yang Dibandingkan:

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)]}
   )

Perpustakaan:

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

Data:

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'))

Hasil Perhitungan dan Pengaturan Waktu:

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

Perhatikan, metode seperti

(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

mengembalikan strsplituntuk unique sutradara dan mungkin sebanding dengan

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

tapi setahu saya, ini tidak ditanyakan.

GKi
sumber