Разделить разделенные запятыми строки в столбце на отдельные строки

109

У меня есть фрейм данных, например:

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

Как видите, некоторые записи в directorстолбце представляют собой несколько имен, разделенных запятыми. Я хотел бы разбить эти записи на отдельные строки, сохранив значения другого столбца. Например, первая строка в фрейме данных выше должна быть разделена на две строки, каждая из которых имеет одно имя в directorстолбце и букву «A» в ABстолбце.

RoyalTS
источник
2
Просто чтобы спросить очевидное: нужно ли размещать эти данные в Интернете?
Рикардо Сапорта
1
Это «не все фильмы категории Б». Кажется достаточно безобидным.
Мэтью Лундберг,
24
Все эти люди номинированы на премию Оскар, что я не думаю, что это секрет =)
RoyalTS

Ответы:

79

Этот старый вопрос часто используется в качестве цели для обмана (помечен r-faq). На сегодняшний день на него трижды ответили, предлагая 6 различных подходов, но не хватает ориентира в качестве руководства, какой из подходов является самым быстрым 1 .

Тестируемые решения включают

Всего с помощью этого microbenchmarkпакета было протестировано 8 различных методов на 6 различных размерах фреймов данных (см. Код ниже).

Данные образца, предоставленные OP, состоят всего из 20 строк. Чтобы создать большие фреймы данных, эти 20 строк просто повторяются 1, 10, 100, 1000, 10000 и 100000 раз, что дает размер проблемы до 2 миллионов строк.

Результаты тестов

введите описание изображения здесь

Результаты тестов показывают, что для достаточно больших кадров данных все data.tableметоды работают быстрее, чем любой другой метод. Для фреймов данных, содержащих более 5000 строк, data.tableметод 2 и вариант Яапа DT3являются самыми быстрыми, на порядок быстрее, чем самые медленные методы.

Примечательно, что синхронизация двух tidyverseметодов и splistackshapeрешения настолько схожа, что трудно различить кривые на графике. Это самые медленные из протестированных методов для всех размеров фреймов данных.

Для небольших фреймов данных базовое решение Мэтта R и data.tableметод 4, похоже, имеют меньше накладных расходов, чем другие методы.

Код

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)

Определите функцию для эталонных прогонов размера проблемы 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
  )
}

Запустите тест для разных размеров проблем

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

Подготовить данные для построения графика

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

Создать диаграмму

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

Информация о сеансе и версии пакета (отрывок)

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 Мое любопытство было задето этим обильным комментарием Brilliant! На порядки быстрее! на tidyverseответ на вопрос, который был закрыт как дубликат этого вопроса.

Уве
источник
Ницца! Похоже, есть возможности для улучшения cSplit и отдельных строк (которые специально созданы для этого). Кстати, cSplit также принимает fixed = arg и является пакетом на основе data.table, так что может также дать ему DT вместо DF. Также fwiw, я не думаю, что преобразование из factor в char относится к тесту (поскольку для начала это должно быть char). Я проверил, и ни одно из этих изменений не влияет на результат качественно.
Фрэнк
1
@Frank Спасибо за ваши предложения по улучшению тестов и за проверку их влияния на результаты. Будет ли забрать это при выполнении обновления после выпуска следующей версии data.table, dplyrи т.д.
Уве
Я думаю, что подходы несопоставимы, по крайней мере, не во всех случаях, потому что подходы с использованием данных создают таблицы только с «выбранными» столбцами, в то время как dplyr дает результат со всеми столбцами (включая те, которые не участвуют в анализе и не имеют написать их имена в функции).
Ferroao
5
@Ferroao Это неправильно, подходы data.tables изменяют «таблицу» на месте, все столбцы сохраняются, конечно, если вы не изменяете на месте, вы получаете отфильтрованную копию только того, что вы просили. Вкратце подход data.table заключается не в создании результирующего набора данных, а в обновлении набора данных, в этом реальная разница между data.table и dplyr.
Tensibai
1
Действительно красивое сравнение! Возможно, вы можете добавить matt_mod и jaap_dplyr , когда делаете strsplit fixed=TRUE. Поскольку это есть у другого, это повлияет на тайминги. Так как R 4.0.0 , по умолчанию, при создании data.frame, является stringsAsFactors = FALSE, таким образом as.characterможет быть удален.
GKi
94

Несколько альтернатив:

1) два способа с :

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) а / комбинация:

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

3) с только: с tidyr 0.5.0(и более поздними версиями) вы также можете просто использовать separate_rows:

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

Вы можете использовать convert = TRUEпараметр для автоматического преобразования чисел в числовые столбцы.

4) с основанием 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))
Яап
источник
Есть ли способ сделать это сразу для нескольких столбцов? Например, 3 столбца, в каждом из которых есть строки, разделенные знаком ";" с одинаковым количеством строк в каждом столбце. т.е. data.table(id= "X21", a = "chr1;chr1;chr1", b="123;133;134",c="234;254;268")становление data.table(id = c("X21","X21",X21"), a=c("chr1","chr1","chr1"), b=c("123","133","134"), c=c("234","254","268"))?
Reilstein
1
вау, только что понял, что это уже работает для нескольких столбцов одновременно - это потрясающе!
Reilstein
@Reilstein, не могли бы вы рассказать, как вы адаптировали это для нескольких столбцов? У меня такой же вариант использования, но я не знаю, как это сделать.
Moon_Watcher
1
@Moon_Watcher Метод 1 из приведенного выше ответа уже работает для нескольких столбцов, что я считаю потрясающим. setDT(dt)[,lapply(.SD, function(x) unlist(tstrsplit(x, ";",fixed=TRUE))), by = ID]это то, что сработало для меня.
Reilstein
51

Называя ваш исходный data.frame v, мы получаем следующее:

> 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

Обратите внимание на использование repдля создания нового столбца AB. Здесь sapplyвозвращается количество имен в каждой из исходных строк.

Мэтью Лундберг
источник
1
Мне интересно, может ли быть легче понять `AB = rep (v $ AB, unlist (sapply (s, FUN = length))), чем более неясное vapply? Есть ли здесь что-нибудь vapplyболее подходящее?
IRTFM 03
7
В настоящее время sapply(s, length)можно заменить на lengths(s).
Rich
31

Поздно, но другая обобщенная альтернатива - использовать cSplitиз моего пакета "splitstackshape", у которого есть directionаргумент. Установите это значение, "long"чтобы получить указанный вами результат:

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
источник
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

Другой контрольный показатель в результате , используя strsplitиз базы в настоящее время может быть рекомендованы разделить запятую строк в столбце в отдельные строки , так как он был самым быстрым в широком диапазоне размеров:

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

Обратите внимание, что использование fixed=TRUEсущественно влияет на тайминги.

Кривые, показывающие время вычисления по количеству строк

Сравниваемые методы:

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

Библиотеки:

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

Данные:

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

Результаты вычислений и хронометража:

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

Обратите внимание, такие методы, как

(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

вернуть strsplitдля unique директора и может быть сопоставимо с

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

но, насколько я понимаю, об этом не спрашивали.

GKi
источник