Podziel ciągi rozdzielone przecinkami w kolumnie na oddzielne wiersze

109

Mam ramkę danych, taką jak ta:

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

Jak widać, niektóre wpisy w directorkolumnie to wiele nazw oddzielonych przecinkami. Chciałbym podzielić te wpisy na osobne wiersze, zachowując wartości z drugiej kolumny. Na przykład, pierwszy wiersz powyższej ramki danych powinien zostać podzielony na dwa wiersze, z których każdy ma jedną nazwę w directorkolumnie i „A” w ABkolumnie.

RoyalTS
źródło
2
Wystarczy zapytać o to, co oczywiste: czy to dane, które powinieneś publikować w sieciach internetowych?
Ricardo Saporta
1
„Nie wszystkie były filmami klasy B”. Wydaje się wystarczająco nieszkodliwe.
Matthew Lundberg
24
Wszyscy ci ludzie są nominowani do Oscara, co chyba nie jest tajemnicą =)
RoyalTS,

Odpowiedzi:

79

To stare pytanie jest często używane jako cel podwójny (oznaczony r-faq). Na dzień dzisiejszy trzykrotnie udzielono odpowiedzi, oferując 6 różnych podejść, ale brakuje w nim punktu odniesienia jako wskazówki, które z podejść jest najszybsze 1 .

Wzorcowe rozwiązania obejmują

Ogólnie 8 różnych metod zostało przetestowanych na 6 różnych rozmiarach ramek danych przy użyciu microbenchmark pakietu (patrz kod poniżej).

Przykładowe dane podane przez PO składają się tylko z 20 wierszy. Aby utworzyć większe ramki danych, te 20 wierszy jest po prostu powtarzanych 1, 10, 100, 1000, 10000 i 100000 razy, co daje rozmiar problemu do 2 milionów wierszy.

Wyniki testów porównawczych

wprowadź opis obrazu tutaj

Wyniki testów porównawczych pokazują, że w przypadku wystarczająco dużych ramek danych wszystkie data.tablemetody są szybsze niż jakakolwiek inna metoda. W przypadku ramek danych zawierających więcej niż około 5000 wierszy, data.tablemetoda 2 Jaapa i wariantDT3 są najszybsze, a wielkości są szybsze niż najwolniejsze metody.

Co ciekawe, czasy obu tidyversemetod isplistackshape rozwiązania są tak podobne, że trudno jest rozróżnić krzywe na wykresie. Są to najwolniejsze z testowanych metod dla wszystkich rozmiarów ramek danych.

W przypadku mniejszych ramek danych podstawowe rozwiązanie Matta R i data.table metoda 4 wydają się mieć mniejszy narzut niż inne metody.

Kod

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)

Zdefiniuj funkcję dla serii testów porównawczych o rozmiarze problemu 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
  )
}

Uruchom test porównawczy dla różnych rozmiarów problemów

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

Przygotuj dane do wykreślenia

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

Utwórz wykres

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

Informacje o sesji i wersje pakietów (fragment)

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 Ten żywiołowy komentarz wzbudził moją ciekawość. Świetnie ! Rzędy wielkości szybciej! na odpowiedź na pytanie, które zostało zamknięte jako duplikat tego pytania. tidyverse

Uwe
źródło
Miły! Wygląda na to, że można ulepszyć cSplit i oddzielne_wiersze (które są specjalnie zaprojektowane do tego celu). Btw, cSplit również przyjmuje ustaloną wartość = arg i jest pakietem opartym na data.table, więc równie dobrze może dać mu DT zamiast DF. Również fwiw, nie sądzę, aby konwersja współczynnika na znak należała do testu porównawczego (ponieważ powinien to być znak typu char). Sprawdziłem i żadna z tych zmian jakościowo nie wpływa na wyniki.
Frank
1
@Frank Dziękujemy za sugestie dotyczące ulepszenia testów porównawczych i sprawdzenia ich wpływu na wyniki. Wzrośnie to do góry, gdy robi aktualizacji po wydaniu kolejnych wersji data.table, dplyritp
Uwe
Myślę, że podejścia nie są porównywalne, przynajmniej nie we wszystkich przypadkach, ponieważ metody datatable tworzą tylko tabele z „wybranymi” kolumnami, podczas gdy dplyr daje wynik ze wszystkimi kolumnami (łącznie z tymi, które nie są zaangażowane w analizę i bez konieczności zapisanie ich nazw w funkcji).
Ferroao
5
@Ferroao To źle, podejście data.tables modyfikuje „tabelę” na miejscu, wszystkie kolumny są zachowane, oczywiście jeśli nie zmodyfikujesz w miejscu, otrzymasz przefiltrowaną kopię tylko tego, o co prosiłeś. W skrócie podejście data.table nie polega na utworzeniu wynikowego zestawu danych, ale na zaktualizowaniu zestawu danych, na tym polega prawdziwa różnica między data.table a dplyr.
Tensibai
1
Naprawdę fajne porównanie! Może podczas robienia możesz dodać matt_mod i jaap_dplyrstrsplit fixed=TRUE . Tak jak inni to mają, a to będzie miało wpływ na czasy. Od wersji 4.0.0 wartość domyślna podczas tworzenia pliku data.frameto stringsAsFactors = FALSE, więc as.charactermogła zostać usunięta.
GKi
94

Kilka alternatyw:

1) na dwa sposoby z :

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 / połączenie:

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

3) z tylko: Z tidyr 0.5.0(i później), można też po prostu użyć separate_rows:

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

Możesz użyć tego convert = TRUEparametru, aby automatycznie przekształcić liczby w kolumny liczbowe.

4) z podstawą 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
źródło
Czy można to zrobić dla wielu kolumn jednocześnie? Na przykład 3 kolumny, z których każda ma ciągi znaków oddzielone znakiem „;” gdzie każda kolumna ma taką samą liczbę ciągów. czyli data.table(id= "X21", a = "chr1;chr1;chr1", b="123;133;134",c="234;254;268")stawanie się 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 właśnie zdałem sobie sprawę, że to już działa dla wielu kolumn jednocześnie - to jest niesamowite!
Reilstein
@Reilstein czy mógłbyś podzielić się tym, jak dostosowałeś to dla wielu kolumn? Mam ten sam przypadek użycia, ale nie jestem pewien, jak się do tego zabrać.
Moon_Watcher,
1
@Moon_Watcher Metoda 1 w powyższej odpowiedzi działa już dla wielu kolumn, co moim zdaniem było niesamowite. setDT(dt)[,lapply(.SD, function(x) unlist(tstrsplit(x, ";",fixed=TRUE))), by = ID]to działa dla mnie.
Reilstein
51

Nazywając swój oryginalny data.frame v, mamy to:

> 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

Zwróć uwagę na użycie repdo zbudowania nowej kolumny AB. Tutaj sapplyzwraca liczbę nazw w każdym z oryginalnych wierszy.

Matthew Lundberg
źródło
1
Zastanawiam się, czy `` AB = rep (v $ AB, unlist (sapply (s, FUN = length))) `może być łatwiejsze do zrozumienia niż bardziej niejasne vapply? Czy jest coś, co jest vapplybardziej odpowiednie?
IRTFM,
7
W dzisiejszych czasach sapply(s, length)można by go zastąpić lengths(s).
Rich Scriven
31

Spóźniłem się na imprezę, ale inną uogólnioną alternatywą jest użycie cSplitmojego pakietu "splitstackshape", który ma directionargument. Ustaw to, aby "long"uzyskać określony wynik:

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
źródło
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
źródło
0

Można obecnie zalecić inny test porównawczy wynikający strsplitz bazy, aby podzielić ciągi oddzielone przecinkami w kolumnie na oddzielne wiersze , ponieważ był najszybszy w szerokim zakresie rozmiarów:

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

Pamiętaj, że używanie fixed=TRUEma znaczący wpływ na czasy.

Krzywe przedstawiające czas obliczeń w odniesieniu do liczby wierszy

Porównywane metody:

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

Biblioteki:

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

Dane:

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

Wyniki obliczeń i czasu:

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

Uwaga, metody takie jak

(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

zwraca strsplitdla unique dyrektora i może być porównywalny z

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

ale według mojego rozumienia nie było to wymagane.

GKi
źródło