Вставить несколько столбцов вместе

У меня есть несколько столбцов в фрейме данных, которые я хочу вставить вместе (разделенные знаком «-») следующим образом:

data <- data.frame('a' = 1:3, 
                   'b' = c('a','b','c'), 
                   'c' = c('d', 'e', 'f'), 
                   'd' = c('g', 'h', 'i'))
i.e.     
     a   b   c  d  
     1   a   d   g  
     2   b   e   h  
     3   c   f   i  

Кем я хочу стать:

a x  
1 a-d-g  
2 b-e-h  
3 c-f-i  

Обычно я мог бы сделать это с помощью:

within(data, x <- paste(b,c,d,sep='-'))

а затем удаляя старые столбцы, но, к сожалению, я не знаю конкретно имен столбцов, только общее имя для всех столбцов, например Я бы знал, что cols <- c('b','c','d')

Кто-нибудь знает, как это сделать?


person user1165199    schedule 28.01.2013    source источник


Ответы (9)


Как вариант в ответе баптиста, где data определен так, как у вас, и столбцы, которые вы хотите объединить, определены в cols

cols <- c("b", "c", "d")

Вы можете добавить новый столбец в data и удалить старые с помощью

data$x <- do.call(paste, c(data[cols], sep="-"))
for (co in cols) data[co] <- NULL

который дает

> data
  a     x
1 1 a-d-g
2 2 b-e-h
3 3 c-f-i
person Brian Diggs    schedule 28.01.2013
comment
Отсутствует ли запятая в c (data [cols], ...? Примерно так: c (data [, cols], ... - person roschu; 14.02.2015
comment
@roschu Либо подойдет. Индексирование data.frame с помощью односимвольного вектора будет индексированием столбца, несмотря на то, что первым аргументом обычно является индекс строки. - person Brian Diggs; 15.02.2015
comment
быстро и умно. Спасибо - person Ali Khosro; 04.03.2017

Используя пакет tidyr, с этим можно легко справиться за 1 вызов функции.

data <- data.frame('a' = 1:3, 
                   'b' = c('a','b','c'), 
                   'c' = c('d', 'e', 'f'), 
                   'd' = c('g', 'h', 'i'))

tidyr::unite_(data, paste(colnames(data)[-1], collapse="_"), colnames(data)[-1])

  a b_c_d
1 1 a_d_g
2 2 b_e_h
3 3 c_f_i

Изменить: исключить первый столбец, все остальное будет вставлено.

# tidyr_0.6.3

unite(data, newCol, -a) 
# or by column index unite(data, newCol, -1)

#   a newCol
# 1 1  a_d_g
# 2 2  b_e_h
# 3 3  c_f_i
person data_steve    schedule 07.10.2015
comment
Я думаю, что OP упомянул, что они не знают имя столбца заранее., Иначе они могли бы сделать это с помощью всего within(data, x <- paste(b,c,d,sep='-')), как они проиллюстрировали. - person David Arenburg; 08.10.2015
comment
Я согласен с @DavidArenburg, это не касается ситуации OP. Я думаю, unite_(data, "b_c_d", cols) будет, или, в зависимости от их фактического data.frame, unite(data, b_c_d, -a) также может быть кандидатом. - person Sam Firke; 08.10.2015

Я бы построил новый data.frame:

d <- data.frame('a' = 1:3, 'b' = c('a','b','c'), 'c' = c('d', 'e', 'f'), 'd' = c('g', 'h', 'i')) 

cols <- c( 'b' , 'c' , 'd' )

data.frame(a = d[, 'a'], x = do.call(paste, c(d[ , cols], list(sep = '-'))))
person baptiste    schedule 28.01.2013
comment
обратите внимание, что вместо d[ , cols] вы можете использовать d[ , names(d) != 'a'], если все столбцы, кроме a, должны быть вставлены вместе. - person baptiste; 28.01.2013
comment
Одно из канонических решений для SO, я думаю, вы могли бы сократить это до cbind(a = d['a'], x = do.call(paste, c(d[cols], sep = '-'))), например. избегайте запятых, list и data.frame при использовании data.frame метода cbind - person David Arenburg; 08.10.2015

Просто чтобы добавить дополнительное решение с Reduce, которое, вероятно, медленнее, чем do.call, но вероятно лучше, чем apply, потому что это позволит избежать matrix преобразования. Кроме того, вместо цикла for мы могли бы просто использовать setdiff, чтобы удалить ненужные столбцы

cols <- c('b','c','d')
data$x <- Reduce(function(...) paste(..., sep = "-"), data[cols])
data[setdiff(names(data), cols)]
#   a     x
# 1 1 a-d-g
# 2 2 b-e-h
# 3 3 c-f-i

В качестве альтернативы мы могли бы обновить data на месте, используя пакет data.table (при условии наличия свежих данных)

library(data.table)
setDT(data)[, x := Reduce(function(...) paste(..., sep = "-"), .SD[, mget(cols)])]
data[, (cols) := NULL]
data
#    a     x
# 1: 1 a-d-g
# 2: 2 b-e-h
# 3: 3 c-f-i

Другой вариант - использовать .SDcols вместо mget, как в

setDT(data)[, x := Reduce(function(...) paste(..., sep = "-"), .SD), .SDcols = cols]
person David Arenburg    schedule 08.10.2015

Я сравнил ответы Энтони Дамико, Брайана Диггса и data_steve на небольшой выборке tbl_df и получил следующие результаты.

> data <- data.frame('a' = 1:3, 
+                    'b' = c('a','b','c'), 
+                    'c' = c('d', 'e', 'f'), 
+                    'd' = c('g', 'h', 'i'))
> data <- tbl_df(data)
> cols <- c("b", "c", "d")
> microbenchmark(
+     do.call(paste, c(data[cols], sep="-")),
+     apply( data[ , cols ] , 1 , paste , collapse = "-" ),
+     tidyr::unite_(data, "x", cols, sep="-")$x,
+     times=1000
+ )
Unit: microseconds
                                         expr     min      lq      mean  median       uq       max neval
do.call(paste, c(data[cols], sep = "-"))       65.248  78.380  93.90888  86.177  99.3090   436.220  1000
apply(data[, cols], 1, paste, collapse = "-") 223.239 263.044 313.11977 289.514 338.5520   743.583  1000
tidyr::unite_(data, "x", cols, sep = "-")$x   376.716 448.120 556.65424 501.877 606.9315 11537.846  1000

Однако, когда я самостоятельно оценивал tbl_df с ~ 1 миллионом строк и 10 столбцами, результаты были совсем другими.

> microbenchmark(
+     do.call(paste, c(data[c("a", "b")], sep="-")),
+     apply( data[ , c("a", "b") ] , 1 , paste , collapse = "-" ),
+     tidyr::unite_(data, "c", c("a", "b"), sep="-")$c,
+     times=25
+ )
Unit: milliseconds
                                                       expr        min         lq      mean     median        uq       max neval
do.call(paste, c(data[c("a", "b")], sep="-"))                 930.7208   951.3048  1129.334   997.2744  1066.084  2169.147    25
apply( data[ , c("a", "b") ] , 1 , paste , collapse = "-" )  9368.2800 10948.0124 11678.393 11136.3756 11878.308 17587.617    25
tidyr::unite_(data, "c", c("a", "b"), sep="-")$c              968.5861  1008.4716  1095.886  1035.8348  1082.726  1759.349    25
person ChristopherTull    schedule 09.11.2016

На мой взгляд, sprintf-функция тоже заслуживает места среди этих ответов. Вы можете использовать sprintf следующим образом:

do.call(sprintf, c(d[cols], '%s-%s-%s'))

который дает:

 [1] "a-d-g" "b-e-h" "c-f-i"

И чтобы создать необходимый фрейм данных:

data.frame(a = d$a, x = do.call(sprintf, c(d[cols], '%s-%s-%s')))

давая:

  a     x
1 1 a-d-g
2 2 b-e-h
3 3 c-f-i

Хотя sprintf не имеет явного преимущества перед комбинацией _8 _ / _ 9_ @BrianDiggs, это особенно полезно, когда вы также хотите дополнить определенные части желаемой строки или когда вы хотите указать количество цифр. См. ?sprintf для получения информации о нескольких вариантах.

Другой вариант - использовать pmap из purrr:

pmap(d[2:4], paste, sep = '-')

Примечание: это pmap решение работает только тогда, когда столбцы не являются факторами.


Тест на большом наборе данных:

# create a larger dataset
d2 <- d[sample(1:3,1e6,TRUE),]
# benchmark
library(microbenchmark)
microbenchmark(
  docp = do.call(paste, c(d2[cols], sep="-")),
  appl = apply( d2[, cols ] , 1 , paste , collapse = "-" ),
  tidr = tidyr::unite_(d2, "x", cols, sep="-")$x,
  docs = do.call(sprintf, c(d2[cols], '%s-%s-%s')),
  times=10)

приводит к:

Unit: milliseconds
 expr       min        lq      mean    median        uq       max neval cld
 docp  214.1786  226.2835  297.1487  241.6150  409.2495  493.5036    10 a  
 appl 3832.3252 4048.9320 4131.6906 4072.4235 4255.1347 4486.9787    10   c
 tidr  206.9326  216.8619  275.4556  252.1381  318.4249  407.9816    10 a  
 docs  413.9073  443.1550  490.6520  453.1635  530.1318  659.8400    10  b 

Используемые данные:

d <- data.frame(a = 1:3, b = c('a','b','c'), c = c('d','e','f'), d = c('g','h','i')) 
person Jaap    schedule 05.01.2017

Вот довольно нетрадиционный (но быстрый) подход: используйте fwrite из data.table, чтобы вставить столбцы вместе, и fread, чтобы прочитать его обратно. Для удобства я написал шаги в виде функции с именем fpaste:

fpaste <- function(dt, sep = ",") {
  x <- tempfile()
  fwrite(dt, file = x, sep = sep, col.names = FALSE)
  fread(x, sep = "\n", header = FALSE)
}

Вот пример:

d <- data.frame(a = 1:3, b = c('a','b','c'), c = c('d','e','f'), d = c('g','h','i')) 
cols = c("b", "c", "d")

fpaste(d[cols], "-")
#       V1
# 1: a-d-g
# 2: b-e-h
# 3: c-f-i

Как это работает?

d2 <- d[sample(1:3,1e6,TRUE),]
  
library(microbenchmark)
microbenchmark(
  docp = do.call(paste, c(d2[cols], sep="-")),
  tidr = tidyr::unite_(d2, "x", cols, sep="-")$x,
  docs = do.call(sprintf, c(d2[cols], '%s-%s-%s')),
  appl = apply( d2[, cols ] , 1 , paste , collapse = "-" ),
  fpaste = fpaste(d2[cols], "-")$V1,
  dt2 = as.data.table(d2)[, x := Reduce(function(...) paste(..., sep = "-"), .SD), .SDcols = cols][],
  times=10)
# Unit: milliseconds
#    expr        min         lq      mean     median         uq       max neval
#    docp  215.34536  217.22102  220.3603  221.44104  223.27224  225.0906    10
#    tidr  215.19907  215.81210  220.7131  220.09636  225.32717  229.6822    10
#    docs  281.16679  285.49786  289.4514  286.68738  290.17249  312.5484    10
#    appl 2816.61899 3106.19944 3259.3924 3266.45186 3401.80291 3804.7263    10
#  fpaste   88.57108   89.67795  101.1524   90.59217   91.76415  197.1555    10
#     dt2  301.95508  310.79082  384.8247  316.29807  383.94993  874.4472    10
person A5C1D2H2I1M1N2O1R2T1    schedule 23.06.2020
comment
Что делать, если вы пишете и читаете на рамдиск? Сравнение было бы чуточку честнее. - person jangorecki; 24.06.2020
comment
@jangorecki, не уверен, правильно ли я делаю (я начал R с TMPDIR=/dev/shm R), но я не замечаю огромной разницы по сравнению с этими результатами. Я также вообще не играл с количеством потоков, используемых для fread или fwrite, чтобы увидеть, как это влияет на результаты. - person A5C1D2H2I1M1N2O1R2T1; 25.06.2020

Я знаю, что это старый вопрос, но подумал, что в любом случае должен представить простое решение, используя функцию paste (), как было предложено вопрошающим:

data_1<-data.frame(a=data$a,"x"=paste(data$b,data$c,data$d,sep="-")) 
data_1
  a     x
1 1 a-d-g
2 2 b-e-h
3 3 c-f-i
person Rikki Franklin Frederiksen    schedule 15.07.2020

person    schedule
comment
здесь не нужно подавать заявку; паста векторизована, и это более эффективно - person baptiste; 29.01.2013
comment
@baptiste .. можно без do.call? - person Anthony Damico; 29.01.2013
comment
конечно, вы могли бы, например, использовать evil(parse(...)), но я считаю, что do.call здесь правильный выбор. - person baptiste; 29.01.2013
comment
Do.call - лучший метод; поддерживает векторизацию. - person Clayton Stanley; 30.01.2013
comment
хм .. как бы вы прошли collapse = "-" через? кому paste? - person Anthony Damico; 13.05.2014
comment
Как ни странно, когда я объединяю 2 столбца (1 факторизованный и 1 числовой), где второй столбец идет от 1 до 12 (2 цифры!), Я получаю пробел между разделителем и второй частью, например. Blue_ 1, Blue_ 2 вместо Blue_1, Blue_2. Эта проблема не возникает с do.call. Если во втором столбце только однозначные цифры, пробелы не вводятся. - person Saren Tasciyan; 13.05.2020