Teilen Sie durch Kommas getrennte Zeichenfolgen in einer Spalte in separate Zeilen auf

109

Ich habe einen Datenrahmen wie folgt:

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

Wie Sie sehen können, sind einige Einträge in der directorSpalte mehrere durch Kommas getrennte Namen. Ich möchte diese Einträge in separate Zeilen aufteilen und dabei die Werte der anderen Spalte beibehalten. Als Beispiel sollte die erste Zeile im obigen Datenrahmen in zwei Zeilen aufgeteilt werden, mit jeweils einem Namen in der directorSpalte und 'A' in der ABSpalte.

RoyalTS
quelle
2
Nur um das Offensichtliche zu fragen: Sollten Sie diese Daten in den Interwebs veröffentlichen?
Ricardo Saporta
1
Sie "waren nicht alle B-Filme". Scheint harmlos genug.
Matthew Lundberg
23
Alle diese Leute sind Oscar-Nominierte, was ich kaum für ein Geheimnis halte =)
RoyalTS

Antworten:

79

Diese alte Frage wird häufig als betrogenes Ziel verwendet (markiert mit r-faq). Bis heute wurde es dreimal mit 6 verschiedenen Ansätzen beantwortet, es fehlt jedoch ein Benchmark als Leitfaden, welcher der Ansätze der schnellste ist 1 .

Die Benchmark-Lösungen umfassen

Insgesamt wurden 8 verschiedene Methoden mit dem microbenchmarkPaket an 6 verschiedenen Datenrahmengrößen verglichen (siehe Code unten).

Die vom OP angegebenen Beispieldaten bestehen nur aus 20 Zeilen. Um größere Datenrahmen zu erstellen, werden diese 20 Zeilen einfach 1, 10, 100, 1000, 10000 und 100000 Mal wiederholt, was zu Problemgrößen von bis zu 2 Millionen Zeilen führt.

Benchmark-Ergebnisse

Geben Sie hier die Bildbeschreibung ein

Die Benchmark-Ergebnisse zeigen, dass bei ausreichend großen Datenrahmen alle data.tableMethoden schneller sind als jede andere Methode. Für Datenrahmen mit mehr als etwa 5000 Zeilen sind Jaaps data.tableMethode 2 und die Variante DT3die schnellsten, Größenordnungen schneller als die langsamsten Methoden.

Bemerkenswerterweise sind die Zeitpunkte der beiden tidyverseMethoden und der splistackshapeLösung so ähnlich, dass es schwierig ist, die Kurven im Diagramm zu unterscheiden. Sie sind die langsamste der Benchmark-Methoden für alle Datenrahmengrößen.

Bei kleineren Datenrahmen data.tablescheinen Matts Basis-R-Lösung und Methode 4 weniger Overhead zu haben als die anderen Methoden.

Code

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)

Definieren Sie die Funktion für Benchmark-Läufe mit Problemgröße 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
  )
}

Führen Sie einen Benchmark für verschiedene Problemgrößen aus

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

Bereiten Sie die Daten für das Plotten vor

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

Diagramm erstellen

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

Sitzungsinfo & Paketversionen (Auszug)

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 Meine Neugier wurde durch diesen üppigen Kommentar geweckt. Brilliant! Größenordnungen schneller! auf eine tidyverseAntwort auf eine Frage, die als Duplikat dieser Frage geschlossen wurde.

Uwe
quelle
Nett! Es scheint Raum für Verbesserungen in cSplit und separate_rows zu geben (die speziell dafür entwickelt wurden). Übrigens nimmt cSplit auch ein festes = arg und ist ein auf data.table basierendes Paket. Geben Sie es also genauso gut DT anstelle von DF. Auch fwiw, ich denke nicht, dass die Umrechnung von Faktor zu Zeichen in den Benchmark gehört (da es zunächst Zeichen sein sollte). Ich habe es überprüft und keine dieser Änderungen hat qualitativ etwas mit den Ergebnissen zu tun.
Frank
1
@Frank Vielen Dank für Ihre Vorschläge zur Verbesserung der Benchmarks und zur Überprüfung der Auswirkungen auf die Ergebnisse. Werden diese abholen , wenn ein Update nach der Freigabe der nächsten Versionen zu tun data.table, dplyrusw.
Uwe
Ich denke, die Ansätze sind nicht vergleichbar, zumindest nicht in allen Fällen, da die datierbaren Ansätze nur Tabellen mit den "ausgewählten" Spalten erzeugen, während dplyr mit allen Spalten (einschließlich derjenigen, die nicht an der Analyse beteiligt sind und keine haben) ein Ergebnis erzeugt ihre Namen in die Funktion schreiben).
Ferroao
5
@Ferroao Das ist falsch, die data.tables-Ansätze ändern die "Tabelle" an Ort und Stelle, alle Spalten bleiben erhalten. Wenn Sie nicht an Ort und Stelle ändern, erhalten Sie natürlich eine gefilterte Kopie nur dessen, was Sie angefordert haben. Kurz gesagt, der Ansatz von data.table besteht darin, kein resultierendes Dataset zu erstellen, sondern das Dataset zu aktualisieren. Dies ist der wahre Unterschied zwischen data.table und dplyr.
Tensibai
1
Wirklich schöner Vergleich! Vielleicht können Sie in hinzufügen matt_mod und jaap_dplyr , dabei strsplit fixed=TRUE. Wie die anderen haben und dies wird Auswirkungen auf das Timing haben. Da R 4.0.0 , die Standardeinstellung , wenn ein zu schaffen data.frame, ist stringsAsFactors = FALSE, so as.characterkönnte entfernt werden.
GKi
94

Mehrere Alternativen:

1) zwei Wege mit ::

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 /. Kombination:

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

3) mit nur: Mit tidyr 0.5.0(und später) können Sie auch nur verwenden separate_rows:

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

Mit dem convert = TRUEParameter können Sie Zahlen automatisch in numerische Spalten konvertieren.

4) mit 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
quelle
Gibt es eine Möglichkeit, dies für mehrere Spalten gleichzeitig zu tun? Zum Beispiel 3 Spalten, deren Zeichenfolgen jeweils durch ";" wobei jede Spalte die gleiche Anzahl von Zeichenfolgen hat. dh data.table(id= "X21", a = "chr1;chr1;chr1", b="123;133;134",c="234;254;268")werden 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, ich habe gerade festgestellt, dass es bereits für mehrere Spalten gleichzeitig funktioniert - das ist erstaunlich!
Reilstein
@Reilstein können Sie uns mitteilen, wie Sie dies für mehrere Spalten angepasst haben? Ich habe den gleichen Anwendungsfall, bin mir aber nicht sicher, wie ich vorgehen soll.
Moon_Watcher
1
@Moon_Watcher Methode 1 in der obigen Antwort funktioniert bereits für mehrere Spalten, was ich für erstaunlich hielt. setDT(dt)[,lapply(.SD, function(x) unlist(tstrsplit(x, ";",fixed=TRUE))), by = ID]hat bei mir funktioniert.
Reilstein
51

Wenn Sie Ihren ursprünglichen data.frame benennen v, haben wir Folgendes :

> 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

Beachten Sie die Verwendung von rep, um die neue AB-Spalte zu erstellen. Hier wird sapplydie Anzahl der Namen in jeder der ursprünglichen Zeilen zurückgegeben.

Matthew Lundberg
quelle
1
Ich frage mich, ob "AB = rep (v $ AB, unlist (sapply (s, FUN = length))") möglicherweise leichter zu erfassen ist als das dunkelere vapply? Gibt es etwas, das vapplyhier angemessener ist?
IRTFM
7
Heutzutage sapply(s, length)könnte durch ersetzt werden lengths(s).
Rich Scriven
31

Spät zur Party, aber eine andere verallgemeinerte Alternative ist die Verwendung cSplitaus meinem "splitstackshape" -Paket, das ein directionArgument hat. Stellen Sie dies auf ein "long", um das von Ihnen angegebene Ergebnis zu erhalten:

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
quelle
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
quelle
0

Ein weiterer Benchmark, der sich aus der Verwendung strsplitvon base ergibt, könnte derzeit empfohlen werden, durch Kommas getrennte Zeichenfolgen in einer Spalte in separate Zeilen aufzuteilen , da dies über einen weiten Größenbereich der schnellste war:

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

Beachten Sie, dass die Verwendung fixed=TRUEerhebliche Auswirkungen auf das Timing hat.

Kurven, die die Rechenzeit über die Anzahl der Zeilen zeigen

Vergleichte Methoden:

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

Bibliotheken:

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

Daten:

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

Berechnungs- und Timing-Ergebnisse:

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

Beachten Sie, Methoden wie

(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

Geben Sie ein strsplitfür unique Regisseur zurück und könnte mit vergleichbar sein

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

aber nach meinem Verständnis wurde dies nicht gefragt.

GKi
quelle