Suddividi le stringhe separate da virgole in una colonna in righe separate


109

Ho un data frame, in questo modo:

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

Come puoi vedere, alcune voci nella directorcolonna sono più nomi separati da virgole. Vorrei suddividere queste voci in righe separate mantenendo i valori dell'altra colonna. Ad esempio, la prima riga nel data frame sopra dovrebbe essere suddivisa in due righe, con un singolo nome ciascuna nella directorcolonna e "A" nella ABcolonna.


2
Solo per chiedere l'ovvio: sono questi dati che dovresti pubblicare sugli interweb?
Ricardo Saporta

1
"Non erano tutti i film di serie B". Sembra abbastanza innocuo.
Matthew Lundberg

24
Tutte queste persone sono candidate all'Oscar, che difficilmente penso sia un segreto =)
RoyalTS

Risposte:


79

Questa vecchia domanda viene spesso utilizzata come bersaglio duplicato (contrassegnata con r-faq). Ad oggi, è stato risposto tre volte offrendo 6 diversi approcci, ma manca un punto di riferimento per indicare quale degli approcci è il più veloce 1 .

Le soluzioni valutate includono

Complessivamente sono stati confrontati 8 metodi diversi su 6 diverse dimensioni di frame di dati utilizzando il microbenchmarkpacchetto (vedere il codice sotto).

I dati campione forniti dal PO consistono solo di 20 righe. Per creare frame di dati più grandi, queste 20 righe vengono semplicemente ripetute 1, 10, 100, 1000, 10000 e 100000 volte che danno dimensioni del problema fino a 2 milioni di righe.

Risultati benchmark

inserisci qui la descrizione dell'immagine

I risultati del benchmark mostrano che per frame di dati sufficientemente grandi tutti i data.tablemetodi sono più veloci di qualsiasi altro metodo. Per i frame di dati con più di 5000 righe circa, il data.tablemetodo 2 di Jaap e la variante DT3sono i più veloci, le magnitudini più veloci dei metodi più lenti.

Sorprendentemente, i tempi dei due tidyversemetodi e la splistackshapesoluzione sono così simili che è difficile distinguere le curve nel grafico. Sono i più lenti dei metodi di benchmark su tutte le dimensioni dei frame di dati.

Per frame di dati più piccoli, la soluzione R di base e il data.tablemetodo 4 di Matt sembrano avere meno overhead rispetto agli altri metodi.

Codice

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)

Definire la funzione per le esecuzioni di benchmark delle dimensioni del problema 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
  )
}

Esegui benchmark per problemi di dimensioni diverse

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

Preparare i dati per la stampa

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

Crea grafico

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

Informazioni sulla sessione e versioni del pacchetto (estratto)

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 La mia curiosità è stata stuzzicata da questo commento esuberante Brillante! Ordini di grandezza più veloci! a una tidyverserisposta a una domanda che è stata chiusa come duplicato di questa domanda.


Bello! Sembra che ci siano margini di miglioramento in cSplit e separate_rows (che sono specificamente progettati per fare questo). A proposito, cSplit accetta anche un fixed = arg ed è un pacchetto basato su data.table, quindi potresti anche dargli DT invece di DF. Inoltre, non penso che la conversione da factor a char appartenga al benchmark (poiché dovrebbe essere char per cominciare). Ho controllato e nessuna di queste modifiche ha alcun effetto qualitativo sui risultati.
Frank

1
@Frank Grazie per i tuoi suggerimenti per migliorare i benchmark e per aver verificato l'effetto sui risultati. Prenderà questo in su quando fare un aggiornamento dopo il rilascio delle prossime versioni di data.table, dplyrecc
Uwe

Penso che gli approcci non siano confrontabili, almeno non in tutte le occasioni, perché gli approcci databili producono solo tabelle con le colonne "selezionate", mentre dplyr produce un risultato con tutte le colonne (comprese quelle non coinvolte nell'analisi e senza avere per scrivere i loro nomi nella funzione).
Ferroao

5
@Ferroao È sbagliato, gli approcci data.tables modificano la "tabella" in posizione, tutte le colonne vengono mantenute, ovviamente se non modifichi sul posto ottieni una copia filtrata solo di ciò che hai richiesto. In breve, l'approccio di data.table consiste nel non produrre un set di dati risultante ma nell'aggiornamento del set di dati, questa è la vera differenza tra data.table e dplyr.
Tensibai

1
Davvero un bel confronto! Forse puoi aggiungere matt_mod e jaap_dplyr , quando lo fai strsplit fixed=TRUE. Come gli altri hanno e questo avrà un impatto sui tempi. Dalla R 4.0.0 , il valore predefinito, quando si crea un data.frame, è stringsAsFactors = FALSE, quindi as.characterpotrebbe essere rimosso.
GKi

94

Diverse alternative:

1) due modi con :

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 / combinazione:

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

3) con solo: con tidyr 0.5.0(e versioni successive), puoi anche usare solo separate_rows:

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

È possibile utilizzare il convert = TRUEparametro per convertire automaticamente i numeri in colonne numeriche.

4) con base 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))

C'è un modo per farlo per più colonne contemporaneamente? Ad esempio 3 colonne ciascuna con stringhe separate da ";" con ogni colonna con lo stesso numero di stringhe. cioè data.table(id= "X21", a = "chr1;chr1;chr1", b="123;133;134",c="234;254;268")diventare 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 ho appena capito che funziona già per più colonne contemporaneamente: è fantastico!
Reilstein

@Reilstein potresti condividere come hai adattato questo per più colonne? Ho lo stesso caso d'uso, ma non sono sicuro di come procedere.
Moon_Watcher

1
@Moon_Watcher Il metodo 1 nella risposta sopra funziona già per più colonne, che è quello che ho pensato fosse sorprendente. setDT(dt)[,lapply(.SD, function(x) unlist(tstrsplit(x, ";",fixed=TRUE))), by = ID]è quello che ha funzionato per me.
Reilstein

51

Denominando il tuo data.frame originale v, abbiamo questo:

> 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

Notare l'uso di repper costruire la nuova colonna AB. Qui, sapplyrestituisce il numero di nomi in ciascuna delle righe originali.


1
Mi chiedo se `AB = rep (v $ AB, unlist (sapply (s, FUN = length)))` potrebbe essere più facile da capire rispetto al più oscuro vapply? C'è qualcosa che rende vapplypiù appropriato qui?
IRTFM

7
Al giorno d'oggi sapply(s, length)potrebbe essere sostituito con lengths(s).
Rich Scriven

31

In ritardo alla festa, ma un'altra alternativa generalizzata è quella di utilizzare cSplitdal mio pacchetto "splitstackshape" che ha un directionargomento. Impostalo su "long"per ottenere il risultato specificato:

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

Un altro benchmark risultante utilizzando strsplitdalla base potrebbe attualmente essere raccomandato per dividere stringhe separate da virgole in una colonna in righe separate , poiché è stato il più veloce su un'ampia gamma di dimensioni:

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

Si noti che l'utilizzo fixed=TRUEha un impatto significativo sui tempi.

Curve che mostrano il tempo di calcolo sul numero di righe

Metodi confrontati:

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

biblioteche:

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

Dati:

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

Risultati di calcolo e tempistica:

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

Nota, metodi come

(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

restituire un strsplitper unique regista e potrebbe essere paragonabile a

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

ma a mia comprensione, questo non è stato chiesto.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.