Dividir cadenas separadas por comas en una columna en filas separadas


109

Tengo un marco de datos, así:

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 puede ver, algunas entradas en la directorcolumna son varios nombres separados por comas. Me gustaría dividir estas entradas en filas separadas mientras mantengo los valores de la otra columna. Como ejemplo, la primera fila en el marco de datos anterior debe dividirse en dos filas, con un solo nombre en la directorcolumna y una 'A' en la ABcolumna.


2
Solo para preguntar lo obvio: ¿Es esta información la que debería publicar en Internet?
Ricardo Saporta

1
"No eran todas películas B". Parece bastante inocuo.
Matthew Lundberg

24
Todas estas personas son nominadas al Premio de la Academia, lo cual no creo que sea un secreto =)
RoyalTS

Respuestas:


79

Esta vieja pregunta se utiliza con frecuencia como objetivo engañoso (etiquetado con r-faq). Hasta el día de hoy, se ha respondido tres veces ofreciendo 6 enfoques diferentes, pero carece de un punto de referencia como guía sobre cuál de los enfoques es el más rápido 1 .

Las soluciones comparadas incluyen

En general, se compararon 8 métodos diferentes en 6 tamaños diferentes de marcos de datos utilizando el microbenchmarkpaquete (consulte el código a continuación).

Los datos de muestra proporcionados por el OP constan solo de 20 filas. Para crear marcos de datos más grandes, estas 20 filas simplemente se repiten 1, 10, 100, 1000, 10000 y 100000 veces, lo que genera problemas de hasta 2 millones de filas.

Resultados comparativos

ingrese la descripción de la imagen aquí

Los resultados de la evaluación comparativa muestran que para marcos de datos suficientemente grandes, todos los data.tablemétodos son más rápidos que cualquier otro método. Para marcos de datos con más de aproximadamente 5000 filas, el data.tablemétodo 2 de Jaap y la variante DT3son los más rápidos, magnitudes más rápidas que los métodos más lentos.

Sorprendentemente, los tiempos de los dos tidyversemétodos y la splistackshapesolución son tan similares que es difícil distinguir las curvas en el gráfico. Son los métodos comparativos más lentos de todos los tamaños de marcos de datos.

Para marcos de datos más pequeños, la solución R base de Matt y data.table método 4 parecen tener menos gastos generales que los otros 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)

Definir función para ejecuciones de referencia de tamaño 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
  )
}

Ejecute un banco de pruebas para diferentes tamaños de problemas

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

Preparar datos para graficar

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

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

Información de la sesión y versiones del paquete (extracto)

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 Este comentario exuberante despertó mi curiosidad . ¡Brillante! Órdenes de magnitud más rápido! a una respuesta de una pregunta que se cerró como un duplicado de esta pregunta. tidyverse


¡Agradable! Parece que hay margen de mejora en cSplit y independent_rows (que están diseñados específicamente para hacer esto). Por cierto, cSplit también toma un fixed = arg y es un paquete basado en data.table, por lo que también podría darle DT en lugar de DF. También fwiw, no creo que la conversión de factor a char pertenezca al punto de referencia (ya que debería ser char para empezar). Verifiqué y ninguno de estos cambios afecta los resultados cualitativamente.
Frank

1
@Frank Gracias por sus sugerencias para mejorar los puntos de referencia y por comprobar el efecto en los resultados. Será recoger esto al hacer una actualización después de la liberación de las próximas versiones de data.table, dplyr, etc.
Uwe

Creo que los enfoques no son comparables, al menos no en todas las ocasiones, porque los enfoques de tabla de datos solo producen tablas con las columnas "seleccionadas", mientras que dplyr produce un resultado con todas las columnas (incluidas las que no participan en el análisis y sin tener para escribir sus nombres en la función).
Ferroao

5
@Ferroao Eso es incorrecto, los enfoques de data.tables modifican la "tabla" en su lugar, todas las columnas se mantienen, por supuesto, si no modifica en su lugar, obtiene una copia filtrada de solo lo que ha solicitado. En resumen, el enfoque de data.table no es producir un conjunto de datos resultante, sino actualizar el conjunto de datos, esa es la diferencia real entre data.table y dplyr.
Tensibai

1
¡Una comparación realmente agradable! Tal vez puedas agregar matt_mod y jaap_dplyr , al hacerlo strsplit fixed=TRUE. Como el otro lo tiene y esto tendrá un impacto en los tiempos. Desde R 4.0.0 , el valor predeterminado, al crear un data.frame, es stringsAsFactors = FALSE, por lo que as.characterpodría eliminarse.
GKi

94

Varias alternativas:

1) dos formas 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) una / combinación:

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

3) con solo: Con tidyr 0.5.0(y posterior), también puede usar separate_rows:

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

Puede utilizar el convert = TRUEparámetro para convertir automáticamente números en columnas numéricas.

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

¿Hay alguna forma de hacer esto para varias columnas a la vez? Por ejemplo, 3 columnas que tienen cadenas separadas por ";" teniendo cada columna el mismo número de cadenas. es decir, data.table(id= "X21", a = "chr1;chr1;chr1", b="123;133;134",c="234;254;268")convirtiéndose 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 me acabo de dar cuenta de que ya funciona para varias columnas a la vez, ¡esto es increíble!
Reilstein

@Reilstein, ¿podría compartir cómo adaptó esto para múltiples columnas? Tengo el mismo caso de uso, pero no estoy seguro de cómo hacerlo.
Moon_Watcher

1
@Moon_Watcher El método 1 en la respuesta anterior ya funciona para múltiples columnas, que es lo que pensé que era increíble. setDT(dt)[,lapply(.SD, function(x) unlist(tstrsplit(x, ";",fixed=TRUE))), by = ID]es lo que funcionó para mí.
Reilstein

51

Nombrando su data.frame original v, tenemos esto:

> 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

Tenga en cuenta el uso de reppara construir la nueva columna AB. Aquí, sapplydevuelve el número de nombres en cada una de las filas originales.


1
Me pregunto si `AB = rep (v $ AB, unlist (sapply (s, FUN = length)))` podría ser más fácil de entender que el más oscuro vapply. ¿Hay algo que lo haga vapplymás apropiado aquí?
IRTFM

7
Hoy en día sapply(s, length)podría reemplazarse por lengths(s).
Rich Scriven

31

Tarde para la fiesta, pero otra alternativa generalizada es usar cSplitmi paquete "splitstackshape" que tiene un directionargumento. Establezca esto en "long"para obtener el resultado que especifique:

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

Actualmente, se podría recomendar otro Benchmark resultante del uso strsplitde base para dividir cadenas separadas por comas en una columna en filas separadas , ya que era la más rápida en una amplia gama de tamaños:

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

Tenga en cuenta que el uso fixed=TRUEtiene un impacto significativo en los tiempos.

Curvas que muestran el tiempo de cálculo sobre el número de filas

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

Datos:

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 cálculo y cronometraje:

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, 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

devolver un strsplitpara unique director y podría ser comparable con

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

pero a mi entender, esto no se preguntó.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.