Divida as strings separadas por vírgulas em uma coluna em linhas separadas

109

Eu tenho um quadro de dados, assim:

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

Como você pode ver, algumas entradas na directorcoluna são vários nomes separados por vírgulas. Eu gostaria de dividir essas entradas em linhas separadas, mantendo os valores da outra coluna. Por exemplo, a primeira linha no quadro de dados acima deve ser dividida em duas linhas, com um único nome cada na directorcoluna e 'A' na ABcoluna.

RoyalTS
fonte
2
Só para perguntar o óbvio: esses dados você deveria postar na internet?
Ricardo Saporta
1
Eles "não eram todos filmes B". Parece bastante inócuo.
Matthew Lundberg
24
Todas essas pessoas são indicadas ao Oscar, o que dificilmente acho que seja um segredo =)
RoyalTS

Respostas:

79

Esta velha questão freqüentemente está sendo usada como alvo enganoso (marcada com r-faq). Até hoje, ele foi respondido três vezes, oferecendo 6 abordagens diferentes, mas carece de uma referência como orientação de qual das abordagens é a mais rápida 1 .

As soluções comparadas incluem

No geral, 8 métodos diferentes foram avaliados em 6 tamanhos diferentes de quadros de dados usando o microbenchmarkpacote (veja o código abaixo).

Os dados de amostra fornecidos pelo OP consistem apenas em 20 linhas. Para criar quadros de dados maiores, essas 20 linhas são simplesmente repetidas 1, 10, 100, 1000, 10.000 e 100.000 vezes, o que resulta em tamanhos de problema de até 2 milhões de linhas.

Resultados de referência

insira a descrição da imagem aqui

Os resultados do benchmark mostram que para quadros de dados suficientemente grandes, todos os data.tablemétodos são mais rápidos do que qualquer outro método. Para quadros de dados com mais de cerca de 5000 linhas, o data.tablemétodo 2 de Jaap e a variante DT3são os mais rápidos, as magnitudes mais rápidas do que os métodos mais lentos.

Notavelmente, os tempos dos dois tidyversemétodos e da splistackshapesolução são tão semelhantes que é difícil distinguir as curvas no gráfico. Eles são os mais lentos dos métodos comparados em todos os tamanhos de quadros de dados.

Para quadros de dados menores, a solução R de base de Matt e o data.tablemétodo 4 parecem ter menos sobrecarga do que os outros métodos.

Código

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)

Defina a função para execuções de benchmark de tamanho de 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
  )
}

Execute benchmark para diferentes tamanhos de problemas

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

Prepare os dados para plotagem

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

Criar gráfico

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

Informações da sessão e versões do pacote (trecho)

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 Minha curiosidade foi despertada por este comentário exuberante Brilhante! Ordens de magnitude mais rápidas! a uma tidyverseresposta de uma pergunta que foi encerrada como uma duplicata desta pergunta.

Uwe
fonte
Agradável! Parece que há espaço para melhorias em cSplit e separar_rows (que são projetados especificamente para fazer isso). A propósito, cSplit também recebe um fixed = arg e é um pacote baseado em data.table, então pode muito bem dar a ele DT em vez de DF. Também fwiw, eu não acho que a conversão de fator para char pertence ao benchmark (já que deveria ser char para começar). Eu verifiquei e nenhuma dessas mudanças afeta os resultados qualitativamente.
Frank de
1
@Frank Obrigado por suas sugestões para melhorar os benchmarks e por verificar o efeito nos resultados. Vai pegar isso ao fazer uma atualização após o lançamento das próximas versões do data.table, dplyr, etc.
Uwe
Acho que as abordagens não são comparáveis, pelo menos não em todas as ocasiões, porque as abordagens da tabela de dados apenas produzem tabelas com as colunas "selecionadas", enquanto dplyr produz um resultado com todas as colunas (incluindo aquelas não envolvidas na análise e sem ter escrever seus nomes na função).
Ferroao
5
@Ferroao Isso está errado, as abordagens data.tables modificam a "tabela" no lugar, todas as colunas são mantidas, é claro que se você não modificar no lugar, receberá uma cópia filtrada apenas do que você pediu. Resumindo, a abordagem data.table é não produzir um conjunto de dados resultante, mas atualizá-lo, essa é a diferença real entre data.table e dplyr.
Tensibai
1
Comparação realmente agradável! Talvez você possa adicionar matt_mod e jaap_dplyr , ao fazer strsplit fixed=TRUE. Como o outro tem e isso terá impacto nos tempos. Desde R 4.0.0 , o padrão, ao criar um data.frame, é stringsAsFactors = FALSE, portanto, as.characterpode ser removido.
GKi
94

Várias alternativas:

1) duas maneiras com :

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) um / combinação:

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

3) com apenas: Com tidyr 0.5.0(e posterior), você também pode usar apenas separate_rows:

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

Você pode usar o convert = TRUEparâmetro para converter números automaticamente em colunas numéricas.

4) com 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))
Jaap
fonte
Existe alguma maneira de fazer isso para várias colunas de uma vez? Por exemplo, 3 colunas, cada uma com strings separadas por ";" com cada coluna tendo o mesmo número de strings. ou seja, data.table(id= "X21", a = "chr1;chr1;chr1", b="123;133;134",c="234;254;268")tornando-se data.table(id = c("X21","X21",X21"), a=c("chr1","chr1","chr1"), b=c("123","133","134"), c=c("234","254","268"))?
Reilstein
1
uau, acabei de perceber que já funciona para várias colunas ao mesmo tempo - isso é incrível!
Reilstein
@Reilstein você poderia compartilhar como adaptou isso para várias colunas? Tenho o mesmo caso de uso, mas não tenho certeza de como fazê-lo.
Moon_Watcher
1
O Método 1 do @Moon_Watcher na resposta acima já funciona para várias colunas, o que é o que achei incrível. setDT(dt)[,lapply(.SD, function(x) unlist(tstrsplit(x, ";",fixed=TRUE))), by = ID]é o que funcionou para mim.
Reilstein de
51

Nomeando seu data.frame original v, temos isto:

> 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

Observe o uso de reppara construir a nova coluna AB. Aqui, sapplyretorna o número de nomes em cada uma das linhas originais.

Matthew Lundberg
fonte
1
Estou me perguntando se `AB = rep (v $ AB, unlist (sapply (s, FUN = length)))` pode ser mais fácil de entender do que o mais obscuro vapply? Existe algo que o torna vapplymais apropriado aqui?
IRTFM
7
Hoje em dia sapply(s, length)pode ser substituído por lengths(s).
Rich Scriven
31

Atrasado para a festa, mas outra alternativa generalizada é usar cSplitdo meu pacote "splitstackshape" que tem um directionargumento. Defina como "long"para obter o resultado que você especificar:

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

Outro Benchmark resultante do uso strsplitde base pode atualmente ser recomendado para dividir strings separadas por vírgulas em uma coluna em linhas separadas , já que era o mais rápido em uma ampla gama de tamanhos:

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

Observe que o uso fixed=TRUEtem um impacto significativo nos tempos.

Curvas mostrando o tempo de cálculo ao longo do número de linhas

Métodos Comparados:

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

Bibliotecas:

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

Dados:

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

Resultados de computação e tempo:

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

Observe, métodos como

(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

retornar um strsplitpara unique diretor e pode ser comparável com

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

mas, no meu entendimento, isso não foi perguntado.

GKi
fonte