Fügen Sie mehrere Spalten zusammen


98

Ich habe eine Reihe von Spalten in einem Datenrahmen, die ich wie folgt zusammenfügen möchte (getrennt durch "-"):

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  

Was ich werden möchte:

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

Normalerweise könnte ich das machen mit:

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

und dann die alten Spalten entfernen, aber leider kenne ich die Namen der Spalten nicht speziell, nur einen Sammelnamen für alle Spalten, zB würde ich das wissen cols <- c('b','c','d')

Kennt jemand einen Weg, dies zu tun?

Antworten:


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

7
hier muss man sich nicht bewerben; Paste ist vektorisiert, und das ist effizienter
Taufe

1
@baptiste ..möglich ohne do.call?
Anthony Damico

1
Sicher, Sie könnten zum Beispiel verwenden evil(parse(...)), aber ich glaube, hier do.callist der richtige Anruf.
Taufe

Do.call hier ist die bessere Technik; behält die Vektorisierung bei.
Clayton Stanley

1
hmm .. wie würdest du das collapse = "-"durchgehen? zu paste?
Anthony Damico

47

Als Variante der Antwort von baptiste , mit datadefiniert wie Sie haben und die Spalten, die Sie zusammenstellen möchten, definiert incols

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

Sie können die neue Spalte hinzufügen dataund die alten mit löschen

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

was gibt

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

Fehlt in "c (data [cols], ..." ein Komma? Wie
folgt

2
@roschu Entweder wird funktionieren. Das Indizieren eines data.framemit einem einzelnen Zeichenvektor ist eine Spaltenindizierung, obwohl das erste Argument normalerweise der Zeilenindex ist.
Brian Diggs

schnell und klug. Vielen Dank
Ali Khosro

32

Mit tidyrpackage kann dies einfach in einem Funktionsaufruf erledigt werden.

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

Bearbeiten: Erste Spalte ausschließen, alles andere wird eingefügt.

# 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

3
Ich denke, OP hat erwähnt, dass sie den Spaltennamen nicht im Voraus kennen. Andernfalls könnten sie es so machen, within(data, x <- paste(b,c,d,sep='-'))wie sie es dargestellt haben.
David Arenburg

Ich stimme @DavidArenburg zu, dies geht nicht auf die Situation des OP ein. Ich denke unite_(data, "b_c_d", cols), dass oder abhängig von ihren tatsächlichen data.frame unite(data, b_c_d, -a)auch ein Kandidat sein könnte.
Sam Firke

12

Ich würde einen neuen data.frame erstellen:

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

Beachten Sie, dass Sie anstelle von d[ , cols]möglicherweise verwenden möchten, d[ , names(d) != 'a']wenn alle außer der aSpalte zusammen eingefügt werden sollen.
Täufer

1
Eine der kanonischen Lösungen für SO, ich denke, Sie könnten dies verkürzen cbind(a = d['a'], x = do.call(paste, c(d[cols], sep = '-'))), um beispielsweise die Kommas zu vermeiden, listund data.framewährend Sie die data.frameMethode voncbind
David Arenburg

9

Nur um eine zusätzliche Lösung hinzuzufügen, Reducedie wahrscheinlich langsamer als, do.callaber wahrscheinlich besser ist, als applyweil sie die matrixKonvertierung vermeidet . Stattdessen forkönnten wir stattdessen auch eine Schleife verwenden setdiff, um unerwünschte Spalten zu entfernen

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

Alternativ könnten wir datadas data.tablePaket mithilfe des Pakets aktualisieren (unter der Annahme neuer Daten).

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

Eine andere Option ist die Verwendung .SDcolsanstelle von mgetwie in

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

5

Ich habe die Antworten von Anthony Damico, Brian Diggs und data_steve an einer kleinen Stichprobe verglichen tbl_dfund die folgenden Ergebnisse erhalten.

> 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

Als ich jedoch alleine tbl_dfmit ~ 1 Million Zeilen und 10 Spalten auswertete, waren die Ergebnisse ganz anders.

> 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

5

Meiner Meinung nach sprintfverdient die Funktion auch einen Platz unter diesen Antworten. Sie können sprintfwie folgt verwenden:

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

was gibt:

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

Und um den erforderlichen Datenrahmen zu erstellen:

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

Geben:

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

Obwohl sprintfdies keinen klaren Vorteil gegenüber der do.call/ paste-Kombination von @BrianDiggs hat, ist es besonders nützlich, wenn Sie auch bestimmte Teile der gewünschten Zeichenfolge auffüllen oder die Anzahl der Ziffern angeben möchten. Siehe ?sprintffür die verschiedenen Optionen.

Eine andere Variante wäre die Verwendung pmapvon::

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

Hinweis: Diese pmapLösung funktioniert nur, wenn die Spalten keine Faktoren sind.


Ein Benchmark für einen größeren Datensatz:

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

Ergebnisse in:

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 

Verwendete Daten:

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

3

Hier ist ein ziemlich unkonventioneller (aber schneller) Ansatz: Verwenden Sie fwritevon, data.tableum die Spalten zusammenzufügen und freadwieder einzulesen. Der Einfachheit halber habe ich die Schritte als eine Funktion mit dem Namen geschrieben fpaste:

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

Hier ist ein Beispiel:

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

Wie funktioniert es?

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

Was ist, wenn Sie auf Ramdisk schreiben und lesen? Der Vergleich wäre etwas fairer.
Jangorecki

@jangorecki, ich bin mir nicht sicher, ob ich es richtig mache (ich habe R mit gestartet TMPDIR=/dev/shm R), aber ich bemerke keinen großen Unterschied im Vergleich zu diesen Ergebnissen. Ich habe auch überhaupt nicht mit der Anzahl der verwendeten Threads herumgespielt freadoder um fwritezu sehen, wie sich dies auf die Ergebnisse auswirkt.
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="-"))))    

0

Ich weiß, dass dies eine alte Frage ist, dachte aber, dass ich die einfache Lösung trotzdem mit der vom Fragesteller vorgeschlagenen Funktion paste () präsentieren sollte:

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
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.