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

У меня есть довольно большой (около 100 тысяч наблюдений) набор данных, подобный этому:

data <- data.frame(
                 ID = seq(1, 5, 1),
                 Values = c("1,2,3", "4", " ", "4,1,6,5,1,1,6", "0,0"), 
                 stringsAsFactors=F)
data
  ID        Values
1  1         1,2,3
2  2             4
3  3              
4  4 4,1,6,5,1,1,6
5  5           0,0

Я хочу разделить столбец «Значения» на "," с NA для пропущенных ячеек:

ID v1 v2 v3 v4 v5 v6 v7
1  1  2  3  NA NA NA NA
2  4  NA NA NA NA NA NA
3  NA NA NA NA NA NA NA
4  4  1  6  5  1  1  6
5  0  0  NA NA NA NA NA
...

Лучшей попыткой было strsplit + rbind:

df <- data.frame(do.call(
                        "rbind",
                        strsplit(as.character(data$Values), split = "," , fixed = FALSE)
                        ))

Но функция rbind просто перерабатывает все «короткие» строки вместо того, чтобы установить «NA». Нашли аналогичная проблема

Большое спасибо, Лео


person Leo    schedule 11.08.2014    source источник
comment
+1 за воспроизводимый вопрос с желаемыми результатами и демонстрацией того, что вы пробовали.   -  person A5C1D2H2I1M1N2O1R2T1    schedule 12.08.2014


Ответы (2)


Я бы посоветовал взглянуть на мою cSplit функцию или решить проблему вручную.

Подход cSplit будет просто таким:

cSplit(data, "Values", ",")
#    ID Values_1 Values_2 Values_3 Values_4 Values_5 Values_6 Values_7
# 1:  1        1        2        3       NA       NA       NA       NA
# 2:  2        4       NA       NA       NA       NA       NA       NA
# 3:  3                NA       NA       NA       NA       NA       NA
# 4:  4        4        1        6        5        1        1        6
# 5:  5        0        0       NA       NA       NA       NA       NA

Подход к проблеме вручную будет выглядеть так:

## Split up the values
Split <- strsplit(data$Values, ",", fixed = TRUE)
## How long is each list element?
Ncol <- vapply(Split, length, 1L)
## Create an empty character matrix to store the results
M <- matrix(NA_character_, nrow = nrow(data),
            ncol = max(Ncol), 
            dimnames = list(NULL, paste0("V", sequence(max(Ncol)))))
## Use matrix indexing to figure out where to put the results
M[cbind(rep(1:nrow(data), Ncol), 
        sequence(Ncol))] <- unlist(Split, use.names = FALSE)
## Bind the values back together, here as a "data.table" (faster)
data.table(ID = data$ID, M)

^^ В значительной степени это то, что происходит в cSplit, но у функции есть несколько других опций и некоторая базовая проверка ошибок и т. д., что может сделать ее немного медленнее, чем чисто ручной подход (или функция, написанная для решения вашей конкретной проблемы). ).

Оба этих подхода будут быстрее, чем подход "data.table" + "reshape2". Кроме того, поскольку каждая строка обрабатывается индивидуально, у вас не должно возникнуть проблем, даже если у вас есть дублирующиеся значения идентификатора — ваш вывод должен иметь то же количество строк, что и ваш ввод.


Ориентиры

Я провел тесты на большем количестве строк и данных, которые дали бы «более широкие» результаты (поскольку это подразумевается в ваших комментариях к ответу Дэвида).

Вот пример данных:

set.seed(1)
a <- sample(0:100, 100000, TRUE)
Values <- vapply(a, function(x) 
  paste(sample(0:100, x, TRUE), collapse = ","), character(1L))
Values[sample(length(Values), length(Values) * .15)] <- ""
ID <- c(1:80000, 1:20000)
data <- data.frame(ID, Values, stringsAsFactors = FALSE)
DT <- as.data.table(data)

Вот функции для тестирования:

fun1a <- function(inDT) {
  data2 <- DT[, list(Values = unlist(
    strsplit(Values, ","))), by = ID]
  data2[, Var := paste0("v", seq_len(.N)), by = ID] 
  dcast.data.table(data2, ID ~ Var, 
                   fill = NA_character_, 
                   value.var = "Values")
}

fun1b <- function(inDT) {
  data2 <- DT[, list(Values = unlist(
    strsplit(Values, ",", fixed = TRUE), 
    use.names = FALSE)), by = ID]
  data2[, Var := paste0("v", seq_len(.N)), by = ID] 
  dcast.data.table(data2, ID ~ Var, 
                   fill = NA_character_, 
                   value.var = "Values")
}

fun2 <- function(inDT) {
  cSplit(DT, "Values", ",")
}

fun3 <- function(inDF) {
  Split <- strsplit(inDF$Values, ",", fixed = TRUE)
  Ncol <- vapply(Split, length, 1L)
  M <- matrix(NA_character_, nrow = nrow(inDF),
              ncol = max(Ncol), 
              dimnames = list(NULL, paste0("V", sequence(max(Ncol)))))
  M[cbind(rep(1:nrow(inDF), Ncol), 
          sequence(Ncol))] <- unlist(Split, use.names = FALSE)
  data.table(ID = inDF$ID, M)
}

Вот результаты:

library(microbenchmark)
microbenchmark(fun2(DT), fun3(data), times = 20)
# Unit: seconds
#        expr      min       lq   median       uq      max neval
#    fun2(DT) 4.810942 5.173103 5.498279 5.622279 6.003339    20
#  fun3(data) 3.847228 3.929311 4.058728 4.160082 4.664568    20

## Didn't want to microbenchmark here...
system.time(fun1a(DT))
#    user  system elapsed 
#   16.92    0.50   17.59
system.time(fun1b(DT))  # fixed = TRUE & use.names = FALSE
#    user  system elapsed 
#   11.54    0.42   12.01

ПРИМЕЧАНИЕ. Результаты fun1a и fun1b не будут такими же, как результаты fun2 и fun3, из-за повторяющихся идентификаторов.

person A5C1D2H2I1M1N2O1R2T1    schedule 11.08.2014
comment
Я могу согласиться, что cSplit будет быстрее, но я не думаю, что он достигнет желаемого результата только с этой строкой. Что касается vapply + sequence, я не уверен, что они будут такими же быстрыми, как оба в основном for цикла. Вы проводили тест на большом наборе данных? - person David Arenburg; 11.08.2014
comment
@DavidArenburg, чего бы не хватило? Только имена столбцов? Или я неправильно понимаю вопрос? Я вернусь с некоторыми тестами. - person A5C1D2H2I1M1N2O1R2T1; 11.08.2014
comment
@DavidArenburg, пока выполняются мои тесты, я ожидаю не vapply скорости, а матричной индексации.... - person A5C1D2H2I1M1N2O1R2T1; 11.08.2014
comment
Хорошо, я беру свои слова обратно cSplit. Кажется, он делает именно то, что ему нужно. Я думал использовать его, но был ленив, чтобы найти суть Id. Вам действительно нужно поработать с @Arun над добавлением его в пакет data.table - person David Arenburg; 11.08.2014
comment
cSplit работает фантастически! Мне даже не нужно добавлять дополнительный столбец с уникальными идентификаторами. Также есть лучшие имена столбцов, чем в DT+Reshape2. Большое спасибо за вашу помощь DavidArenburg, @AnandaMahto. Я очень ценю. Р классный! - person Leo; 12.08.2014
comment
Спасибо, что поделились этим, я слишком долго пытался понять это. - person Eddy Zavala; 15.09.2018

Вот подход data.table в сочетании с подходом reshape2 (должен быть очень эффективным)

library(data.table) # Loading `data.table` package
data2 <- setDT(data)[, list(Values = unlist(strsplit(Values, ","))), by = ID] # splitting the values by `,` for each `ID`
data2[, Var := paste0("v", seq_len(.N)), by = ID] # Adding the `Var` variable

library(reshape2) # Loading `reshape2` package
dcast.data.table(data2, ID ~ Var, fill = NA_character_, value.var = "Values") # decasting

#    ID v1 v2 v3 v4 v5 v6 v7
# 1:  1  1  2  3 NA NA NA NA
# 2:  2  4 NA NA NA NA NA NA
# 3:  3    NA NA NA NA NA NA
# 4:  4  4  1  6  5  1  1  6
# 5:  5  0  0 NA NA NA NA NA
person David Arenburg    schedule 11.08.2014
comment
Извините за этот беспорядок. Конечно, он должен иметь 7 столбцов. - person Leo; 11.08.2014
comment
Спасибо, @David Arenburg, это работает хорошо, но что, если идентификатор не уникален? - person Leo; 11.08.2014
comment
ID уникален. Я не понимаю вашего вопроса. Я точно достиг желаемого результата - person David Arenburg; 11.08.2014
comment
В моих реальных данных есть несколько строк с одинаковым идентификатором. Я думаю, что я немного поторопился, прежде чем отправить вопрос - person Leo; 11.08.2014
comment
Так? Это все еще должно работать. Я использую by = ID в коде, так что он будет работать - person David Arenburg; 11.08.2014
comment
Основные данные получили размер 90180x2, а данные 6867x4490. - person Leo; 11.08.2014
comment
Может быть, столбец ID в ваших данных не уникален? Попробуйте tail(sort(table(data$ID)), 10) например - person David Arenburg; 11.08.2014
comment
Да, это не уникально, и не должно быть. Я не думал об этом, когда задавал вопрос, потому что не осознавал, что это важно. - person Leo; 11.08.2014
comment
@ Лео, ты хочешь, чтобы на выходе было то же количество строк, что и на входе, верно? Это не должно быть слишком сложно исправить (просто добавьте еще один столбец идентификатора), но есть несколько более быстрых вариантов, которые вы можете рассмотреть. - person A5C1D2H2I1M1N2O1R2T1; 11.08.2014