Diviser les chaînes séparées par des virgules dans une colonne en lignes séparées


109

J'ai un bloc de données, comme ceci:

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

Comme vous pouvez le voir, certaines entrées de la directorcolonne sont des noms multiples séparés par des virgules. Je voudrais diviser ces entrées en lignes séparées tout en conservant les valeurs de l'autre colonne. Par exemple, la première ligne du bloc de données ci-dessus doit être divisée en deux lignes, avec un seul nom chacune dans la directorcolonne et «A» dans la ABcolonne.


2
Juste pour demander l'évidence: ces données devraient-elles être publiées sur les interwebs?
Ricardo Saporta

1
Ils "n'étaient pas tous des films B". Cela semble assez inoffensif.
Matthew Lundberg

24
Toutes ces personnes sont nominées aux Oscars, ce que je ne pense pas être un secret =)
RoyalTS

Réponses:


79

Cette vieille question est fréquemment utilisée comme cible de dupe (taguée avec r-faq). À ce jour, il a été répondu trois fois en proposant 6 approches différentes, mais il manque un point de référence pour déterminer laquelle des approches est la plus rapide 1 .

Les solutions de référence comprennent

Dans l'ensemble, 8 méthodes différentes ont été comparées sur 6 tailles différentes de trames de données à l'aide du microbenchmarkpackage (voir le code ci-dessous).

Les exemples de données fournis par l'OP ne comportent que 20 lignes. Pour créer des blocs de données plus volumineux, ces 20 lignes sont simplement répétées 1, 10, 100, 1000, 10000 et 100000 fois, ce qui donne des tailles de problème allant jusqu'à 2 millions de lignes.

Résultats de référence

entrez la description de l'image ici

Les résultats de référence montrent que pour des bases de données suffisamment grandes, toutes les data.tableméthodes sont plus rapides que toute autre méthode. Pour les trames de données de plus de 5000 lignes environ, la data.tableméthode 2 de Jaap et la variante DT3sont les plus rapides et les magnitudes plus rapides que les méthodes les plus lentes.

Remarquablement, les horaires des deux tidyverseméthodes et la splistackshapesolution sont si similaires qu'il est difficile de distinguer les courbes dans le graphique. Il s'agit de la plus lente des méthodes de référence pour toutes les tailles de trame de données.

Pour les trames de données plus petites, la solution de base R et la data.tableméthode 4 de Matt semblent avoir moins de frais généraux que les autres méthodes.

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)

Définir la fonction pour les exécutions de référence de taille de problème 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
  )
}

Exécutez un benchmark pour différentes tailles de problème

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

Préparer les données pour le traçage

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

Créer un graphique

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

Informations sur la session et versions du package (extrait)

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 Ma curiosité a été piquée par ce commentaire exubérant Brillant! Des ordres de grandeur plus rapides! à une tidyverseréponse à une question qui a été fermée comme un double de cette question.


Agréable! On dirait qu'il y a place à amélioration dans cSplit et Separ_rows (qui sont spécifiquement conçus pour cela). Btw, cSplit prend également un fixed = arg et est un package basé sur data.table, autant lui donner DT au lieu de DF. De plus, je ne pense pas que la conversion de facteur en caractère fasse partie du benchmark (car cela devrait être char pour commencer). J'ai vérifié et aucun de ces changements ne fait quoi que ce soit aux résultats qualitativement.
Frank

1
@Frank Merci pour vos suggestions pour améliorer les benchmarks et pour vérifier l'effet sur les résultats. Ce sera tout lorsque vous effectuez une mise à jour après la sortie des prochaines versions data.table, dplyretc.
Uwe

Je pense que les approches ne sont pas comparables, du moins pas dans toutes les occasions, car les approches datables ne produisent que des tableaux avec les colonnes "sélectionnées", tandis que dplyr produit un résultat avec toutes les colonnes (y compris celles qui ne sont pas impliquées dans l'analyse et sans avoir pour écrire leurs noms dans la fonction).
Ferroao

5
@Ferroao C'est faux, les approches data.tables modifient la "table" en place, toutes les colonnes sont conservées, bien sûr si vous ne modifiez pas en place vous obtenez une copie filtrée de ce que vous avez demandé. En bref, l'approche data.table n'est pas de produire un ensemble de données résultant mais de mettre à jour l'ensemble de données, c'est la vraie différence entre data.table et dplyr.
Tensibai

1
Vraiment belle comparaison! Peut-être que vous pouvez ajouter matt_mod et jaap_dplyr , en faisant strsplit fixed=TRUE. Comme les autres l'ont et cela aura un impact sur les horaires. Depuis R 4.0.0 , la valeur par défaut, lors de la création d'un data.frame, est stringsAsFactors = FALSE, donc as.characterpourrait être supprimé.
GKi

94

Plusieurs alternatives:

1) deux façons avec :

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) un / combinaison:

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

3) avec uniquement: Avec tidyr 0.5.0(et versions ultérieures), vous pouvez également simplement utiliser separate_rows:

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

Vous pouvez utiliser le convert = TRUEparamètre pour convertir automatiquement des nombres en colonnes numériques.

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

Existe-t-il un moyen de le faire pour plusieurs colonnes à la fois? Par exemple, 3 colonnes contenant chacune des chaînes séparées par ";" chaque colonne ayant le même nombre de chaînes. c'est-à-dire data.table(id= "X21", a = "chr1;chr1;chr1", b="123;133;134",c="234;254;268")devenir 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 vient de réaliser que cela fonctionne déjà pour plusieurs colonnes à la fois - c'est incroyable!
Reilstein

@Reilstein pourriez-vous partager comment vous avez adapté cela pour plusieurs colonnes? J'ai le même cas d'utilisation, mais je ne sais pas comment s'y prendre.
Moon_Watcher le

1
La méthode 1 de @Moon_Watcher dans la réponse ci-dessus fonctionne déjà pour plusieurs colonnes, ce que j'ai trouvé incroyable. setDT(dt)[,lapply(.SD, function(x) unlist(tstrsplit(x, ";",fixed=TRUE))), by = ID]est ce qui a fonctionné pour moi.
Reilstein

51

En nommant votre data.frame d'origine v, nous avons ceci:

> 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

Notez l'utilisation de reppour créer la nouvelle colonne AB. Ici, sapplyrenvoie le nombre de noms dans chacune des lignes d'origine.


1
Je me demande si `AB = rep (v $ AB, unlist (sapply (s, FUN = length)))` pourrait être plus facile à saisir que le plus obscur vapply? Y a-t-il quelque chose qui rend vapplyplus approprié ici?
IRTFM

7
De nos jours, sapply(s, length)pourrait être remplacé par lengths(s).
Rich Scriven

31

En retard à la fête, mais une autre alternative généralisée est d'utiliser à cSplitpartir de mon package "splitstackshape" qui a un directionargument. Définissez ceci sur "long"pour obtenir le résultat que vous spécifiez:

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 autre Benchmark résultant de l'utilisation strsplitde base pourrait actuellement être recommandé pour diviser une chaîne séparée par des virgules dans une colonne en lignes séparées , car c'était le plus rapide sur une large gamme de tailles:

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

Notez que l'utilisation fixed=TRUEa un impact significatif sur les horaires.

Courbes montrant le temps de calcul sur le nombre de lignes

Méthodes comparées:

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

Bibliothèques:

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

Les données:

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

Résultats de calcul et de chronométrage:

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

Remarque, des méthodes comme

(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

retourner un strsplitpour unique directeur et pourrait être comparable à

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

mais à ma connaissance, cela n'a pas été demandé.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.