Tempelkan beberapa kolom menjadi satu

100

Saya memiliki banyak kolom dalam kerangka data yang ingin saya tempelkan bersama (dipisahkan oleh "-") sebagai berikut:

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  

Yang saya ingin menjadi:

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

Saya biasanya dapat melakukan ini dengan:

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

dan kemudian menghapus kolom lama, tapi sayangnya saya tidak tahu nama kolom secara spesifik, hanya nama kolektif untuk semua kolom, misalnya saya akan tahu bahwa cols <- c('b','c','d')

Adakah yang tahu cara melakukan ini?

pengguna1165199
sumber

Jawaban:

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

# columns to paste together
cols <- c( 'b' , 'c' , 'd' )

# create a new column `x` with the three columns collapsed together
data$x <- apply( data[ , cols ] , 1 , paste , collapse = "-" )

# remove the unnecessary columns
data <- data[ , !( names( data ) %in% cols ) ]
Anthony Damico
sumber
8
tidak perlu melamar di sini; pasta adalah vektorisasi, dan itu lebih efisien
baptiste
1
@baptiste ..mungkin tanpa do.call?
Anthony Damico
1
Tentu, Anda bisa misalnya menggunakan evil(parse(...)), tapi saya yakin do.callitu panggilan yang tepat di sini.
baptiste
Do.call di sini adalah teknik yang lebih baik; mempertahankan vektorisasi tersebut.
Clayton Stanley
1
hmm .. bagaimana kamu akan melewatinya collapse = "-"? kepada paste?
Anthony Damico
48

Sebagai varian dari jawaban baptiste , dengan datadidefinisikan sebagai yang Anda miliki dan kolom yang ingin Anda susun ditentukancols

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

Anda dapat menambahkan kolom baru ke datadan menghapus yang lama dengan

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

pemberian yang mana

> data
  a     x
1 1 a-d-g
2 2 b-e-h
3 3 c-f-i
Brian Diggs
sumber
Apakah ada koma yang hilang di "c (data [cols], ..."? Seperti: "c (data [, cols], ..."
roschu
2
@roschu Entah akan bekerja. Mengindeks data.framevektor dengan satu karakter akan menjadi pengindeksan kolom, meskipun argumen pertama biasanya adalah indeks baris.
Brian Diggs
cepat dan cerdas. Terima kasih
Ali Khosro
33

Dengan menggunakan tidyrpaket, ini dapat dengan mudah ditangani dalam 1 panggilan fungsi.

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

Edit: Kecualikan kolom pertama, yang lainnya akan ditempel.

# 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
data_steve
sumber
3
Saya pikir OP menyebutkan bahwa mereka tidak tahu nama kolom sebelumnya., Jika tidak, mereka bisa melakukannya dengan within(data, x <- paste(b,c,d,sep='-'))seperti yang mereka ilustrasikan.
David Arenburg
Saya setuju dengan @DavidArenburg, ini tidak mengatasi situasi OP. Saya pikir unite_(data, "b_c_d", cols)akan, atau tergantung pada data.frame aktual mereka, unite(data, b_c_d, -a)mungkin juga menjadi kandidat.
Sam Firke
14

Saya akan membuat data.frame baru:

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 = '-'))))
baptiste
sumber
perhatikan bahwa alih-alih d[ , cols]Anda mungkin ingin menggunakan d[ , names(d) != 'a']jika semua kecuali akolom akan ditempelkan bersama.
baptiste
2
Salah satu solusi kanonik pada SO, saya pikir Anda dapat mempersingkatnya menjadi cbind(a = d['a'], x = do.call(paste, c(d[cols], sep = '-'))), misalnya menghindari koma, listdan data.framesaat menggunakan data.framemetodecbind
David Arenburg
9

Hanya untuk menambahkan solusi tambahan Reduceyang mungkin lebih lambat do.calltetapi mungkin lebih baik daripada applykarena itu akan menghindari matrixkonversi. Juga, sebagai gantinya forkita bisa menggunakan loop setdiffuntuk menghapus kolom yang tidak diinginkan

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

Atau kami dapat memperbarui datadi tempat menggunakan data.tablepaket (dengan asumsi data baru)

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

Pilihan lainnya adalah menggunakan .SDcolssebagai pengganti mgetdalam

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

Saya membandingkan jawaban Anthony Damico, Brian Diggs dan data_steve pada sampel kecil tbl_dfdan mendapatkan hasil sebagai berikut.

> 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

Namun, ketika saya mengevaluasi sendiri tbl_dfdengan ~ 1 juta baris dan 10 kolom, hasilnya sangat berbeda.

> 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
ChristopherTull
sumber
5

Menurut pendapat saya, sprintf-fungsi juga layak mendapat tempat di antara jawaban-jawaban ini. Anda dapat menggunakan sprintfsebagai berikut:

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

pemberian yang mana:

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

Dan untuk membuat kerangka data yang dibutuhkan:

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

memberi:

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

Meskipun sprintftidak memiliki keunggulan yang jelas atas kombinasi do.call/ paste@BrianDiggs, ini sangat berguna ketika Anda juga ingin memasukkan bagian tertentu dari string yang diinginkan atau ketika Anda ingin menentukan jumlah digit. Lihat?sprintf beberapa opsi.

Varian lain akan digunakan pmap dari:

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

Catatan: pmapsolusi ini hanya berfungsi jika kolom bukan merupakan faktor.


Tolok ukur pada kumpulan data yang lebih besar:

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

menghasilkan:

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 

Data yang digunakan:

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

Berikut adalah pendekatan yang tidak biasa (tapi cepat): gunakan fwritedari data.tableuntuk "menempel" kolom bersama-sama, dan freaduntuk membacanya kembali. Demi kenyamanan, saya telah menulis langkah-langkah sebagai fungsi yang disebut fpaste:

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

Berikut contohnya:

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

Bagaimana cara kerjanya?

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
A5C1D2H2I1M1N2O1R2T1
sumber
Bagaimana jika Anda menulis dan membaca ramdisk? Perbandingan akan sedikit lebih adil.
jangorecki
@jangorecki, tidak yakin apakah saya melakukannya dengan benar (saya memulai R dengan TMPDIR=/dev/shm R) tetapi saya tidak melihat perbedaan besar dalam perbandingan dengan hasil ini. Saya juga belum bermain-main sama sekali dengan jumlah utas yang digunakan untuk freadatau fwriteuntuk melihat bagaimana pengaruhnya terhadap hasil.
A5C1D2H2I1M1N2O1R2T1
1
library(plyr)

ldply(apply(data, 1, function(x) data.frame(
                      x = paste(x[2:4],sep="",collapse="-"))))

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

#  and with just the vector of names you have:

ldply(apply(data, 1, function(x) data.frame(
                      x = paste(x[c('b','c','d')],sep="",collapse="-"))))

# or equally:
mynames <-c('b','c','d')
ldply(apply(data, 1, function(x) data.frame(
                      x = paste(x[mynames],sep="",collapse="-"))))    
pengguna1317221_G
sumber
0

Saya tahu ini adalah pertanyaan lama, tetapi saya berpikir bahwa saya tetap harus menyajikan solusi sederhana menggunakan fungsi paste () seperti yang disarankan oleh penanya:

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
Rikki Franklin Frederiksen
sumber