Dividir la columna de cadena del marco de datos en varias columnas

245

Me gustaría tomar datos del formulario

before = data.frame(attr = c(1,30,4,6), type=c('foo_and_bar','foo_and_bar_2'))
  attr          type
1    1   foo_and_bar
2   30 foo_and_bar_2
3    4   foo_and_bar
4    6 foo_and_bar_2

y use split()en la columna " type" desde arriba para obtener algo como esto:

  attr type_1 type_2
1    1    foo    bar
2   30    foo  bar_2
3    4    foo    bar
4    6    foo  bar_2

Se me ocurrió algo increíblemente complejo que involucra alguna forma de applyque funcionó, pero desde entonces lo he perdido. Parecía demasiado complicado para ser la mejor manera. Puedo usarlo a strsplitcontinuación, pero luego no está claro cómo volver a colocarlo en 2 columnas en el marco de datos.

> strsplit(as.character(before$type),'_and_')
[[1]]
[1] "foo" "bar"

[[2]]
[1] "foo"   "bar_2"

[[3]]
[1] "foo" "bar"

[[4]]
[1] "foo"   "bar_2"

Gracias por cualquier puntero. Todavía no he asimilado las listas R.

jkebinger
fuente

Respuestas:

279

Utilizar stringr::str_split_fixed

library(stringr)
str_split_fixed(before$type, "_and_", 2)
Hadley
fuente
2
Esto funcionó bastante bien para mi problema hoy también ... pero agregaba una 'c' al comienzo de cada fila. ¿Alguna idea de por qué es eso? left_right <- str_split_fixed(as.character(split_df),'\">',2)
Estudiante
Me gustaría dividir con un patrón que tiene "...", cuando aplico esa función, no devuelve nada. Cual podría ser el problema. mi tipo es algo así como "prueba ... puntaje"
user3841581
2
@ user3841581 - antigua consulta tuya que conozco, pero esto está cubierto en la documentación - str_split_fixed("aaa...bbb", fixed("..."), 2)funciona bien fixed()para "Hacer coincidir una cadena fija" en el pattern=argumento. .significa 'cualquier personaje' en expresiones regulares.
thelatemail
Gracias Hadley, método muy conveniente, pero hay una cosa que se puede mejorar, si hay NA en la columna original, después de la separación se convertirá en una cadena vacía vacía en las columnas de resultados, lo cual no es deseado, quiero mantener el NA aún NA después de separación
cloudcomputes
Funciona bien, es decir, si falta el separador! es decir, si tengo un vector 'a <-c ("1N", "2N")' que me gustaría separar en las columnas '1,1, "N", "N"' Ejecuto 'str_split_fixed (s, " ", 2)". Simplemente no estoy seguro de cómo nombrar mis nuevas columnas en este enfoque, 'col1 <-c (1,1)' y 'col2 <-c ("N", "N")'
maycca
173

Otra opción es usar el nuevo paquete tidyr.

library(dplyr)
library(tidyr)

before <- data.frame(
  attr = c(1, 30 ,4 ,6 ), 
  type = c('foo_and_bar', 'foo_and_bar_2')
)

before %>%
  separate(type, c("foo", "bar"), "_and_")

##   attr foo   bar
## 1    1 foo   bar
## 2   30 foo bar_2
## 3    4 foo   bar
## 4    6 foo bar_2
Hadley
fuente
¿Hay alguna manera de limitar el número de divisiones con separador? Digamos que quiero dividir en '_' solo una vez (o hacerlo str_split_fixedy agregar columnas al marco de datos existente)?
JelenaČuklina
66

5 años después agregando la data.tablesolución obligatoria

library(data.table) ## v 1.9.6+ 
setDT(before)[, paste0("type", 1:2) := tstrsplit(type, "_and_")]
before
#    attr          type type1 type2
# 1:    1   foo_and_bar   foo   bar
# 2:   30 foo_and_bar_2   foo bar_2
# 3:    4   foo_and_bar   foo   bar
# 4:    6 foo_and_bar_2   foo bar_2

También podríamos asegurarnos de que las columnas resultantes tengan los tipos correctos y mejorar el rendimiento agregando type.converty fixedargumentos (ya "_and_"que no es realmente una expresión regular)

setDT(before)[, paste0("type", 1:2) := tstrsplit(type, "_and_", type.convert = TRUE, fixed = TRUE)]
David Arenburg
fuente
si el número de sus '_and_'patrones varía, puede averiguar el número máximo de coincidencias (es decir, columnas futuras) conmax(lengths(strsplit(before$type, '_and_')))
cargue el
Esta es mi respuesta favorita, ¡funciona muy bien! ¿Podría explicar cómo funciona? ¿Por qué transponer (strsplit (...)) y no pegar0 para concatenar cadenas - no dividirlas ...
Gecko
1
@ Gecko No estoy seguro de cuál es la pregunta. Si solo lo usa strsplit, crea un solo vector con 2 valores en cada ranura, por tstrsplitlo que lo transpone en 2 vectores con un solo valor en cada uno. paste0solo se usa para crear los nombres de columna, no se usa en los valores. En el LHS de la ecuación están los nombres de las columnas, en el RHS está la operación de división + transposición en la columna. :=significa " asignar en el lugar ", por lo tanto, no ve el <-operador de asignación allí.
David Arenburg
57

Otro enfoque: uso rbinden out:

before <- data.frame(attr = c(1,30,4,6), type=c('foo_and_bar','foo_and_bar_2'))  
out <- strsplit(as.character(before$type),'_and_') 
do.call(rbind, out)

     [,1]  [,2]   
[1,] "foo" "bar"  
[2,] "foo" "bar_2"
[3,] "foo" "bar"  
[4,] "foo" "bar_2"

Y para combinar:

data.frame(before$attr, do.call(rbind, out))
Aniko
fuente
44
Otra alternativa en las nuevas versiones de R esstrcapture("(.*)_and_(.*)", as.character(before$type), data.frame(type_1 = "", type_2 = ""))
alexis_laz
36

Observe que se puede usar sapply con "[" para extraer el primer o el segundo elemento de esas listas, por lo tanto:

before$type_1 <- sapply(strsplit(as.character(before$type),'_and_'), "[", 1)
before$type_2 <- sapply(strsplit(as.character(before$type),'_and_'), "[", 2)
before$type <- NULL

Y aquí hay un método gsub:

before$type_1 <- gsub("_and_.+$", "", before$type)
before$type_2 <- gsub("^.+_and_", "", before$type)
before$type <- NULL
IRTFM
fuente
31

Aquí hay una línea en la misma línea que la solución de Aniko, pero usando el paquete stringr de Hadley:

do.call(rbind, str_split(before$type, '_and_'))
Ramnath
fuente
1
Buena captura, la mejor solución para mí. Aunque un poco más lento que con el stringrpaquete.
Melka
20

Para agregar a las opciones, también puede usar mi splitstackshape::cSplitfunción de esta manera:

library(splitstackshape)
cSplit(before, "type", "_and_")
#    attr type_1 type_2
# 1:    1    foo    bar
# 2:   30    foo  bar_2
# 3:    4    foo    bar
# 4:    6    foo  bar_2
A5C1D2H2I1M1N2O1R2T1
fuente
3 años después, esta opción funciona mejor para un problema similar que tengo, sin embargo, el marco de datos con el que estoy trabajando tiene 54 columnas y necesito dividirlas todas en dos. ¿Hay alguna manera de hacer esto usando este método, salvo escribir el comando anterior 54 veces? Muchas gracias Nicki.
Nicki
@ Nicki, ¿Has intentado proporcionar un vector de los nombres de columna o las posiciones de columna? Eso debería hacerlo ...
A5C1D2H2I1M1N2O1R2T1
No era solo cambiar el nombre de las columnas: necesitaba literalmente dividir las columnas como se indicó anteriormente, duplicando efectivamente el número de columnas en mi df. Lo siguiente fue lo que usé al final: df2 <- cSplit (df1, splitCols = 1:54, "/")
Nicki
14

Una manera fácil es usar sapply()y la [función:

before <- data.frame(attr = c(1,30,4,6), type=c('foo_and_bar','foo_and_bar_2'))
out <- strsplit(as.character(before$type),'_and_')

Por ejemplo:

> data.frame(t(sapply(out, `[`)))
   X1    X2
1 foo   bar
2 foo bar_2
3 foo   bar
4 foo bar_2

sapply()El resultado es una matriz y necesita transposición y conversión a un marco de datos. Son entonces algunas manipulaciones simples las que producen el resultado que deseabas:

after <- with(before, data.frame(attr = attr))
after <- cbind(after, data.frame(t(sapply(out, `[`))))
names(after)[2:3] <- paste("type", 1:2, sep = "_")

En este punto, afteres lo que querías

> after
  attr type_1 type_2
1    1    foo    bar
2   30    foo  bar_2
3    4    foo    bar
4    6    foo  bar_2
Gavin Simpson
fuente
12

El tema está casi agotado, aunque me gustaría ofrecer una solución a una versión un poco más general en la que no se conoce el número de columnas de salida, a priori. Entonces por ejemplo tienes

before = data.frame(attr = c(1,30,4,6), type=c('foo_and_bar','foo_and_bar_2', 'foo_and_bar_2_and_bar_3', 'foo_and_bar'))
  attr                    type
1    1             foo_and_bar
2   30           foo_and_bar_2
3    4 foo_and_bar_2_and_bar_3
4    6             foo_and_bar

No podemos usar dplyr separate()porque no sabemos el número de columnas de resultados antes de la división, por lo que he creado una función que utiliza stringrpara dividir una columna, dado el patrón y un prefijo de nombre para las columnas generadas. Espero que los patrones de codificación utilizados sean correctos.

split_into_multiple <- function(column, pattern = ", ", into_prefix){
  cols <- str_split_fixed(column, pattern, n = Inf)
  # Sub out the ""'s returned by filling the matrix to the right, with NAs which are useful
  cols[which(cols == "")] <- NA
  cols <- as.tibble(cols)
  # name the 'cols' tibble as 'into_prefix_1', 'into_prefix_2', ..., 'into_prefix_m' 
  # where m = # columns of 'cols'
  m <- dim(cols)[2]

  names(cols) <- paste(into_prefix, 1:m, sep = "_")
  return(cols)
}

Luego podemos usar split_into_multipleen una tubería dplyr de la siguiente manera:

after <- before %>% 
  bind_cols(split_into_multiple(.$type, "_and_", "type")) %>% 
  # selecting those that start with 'type_' will remove the original 'type' column
  select(attr, starts_with("type_"))

>after
  attr type_1 type_2 type_3
1    1    foo    bar   <NA>
2   30    foo  bar_2   <NA>
3    4    foo  bar_2  bar_3
4    6    foo    bar   <NA>

Y luego podemos usar gatherpara ordenar ...

after %>% 
  gather(key, val, -attr, na.rm = T)

   attr    key   val
1     1 type_1   foo
2    30 type_1   foo
3     4 type_1   foo
4     6 type_1   foo
5     1 type_2   bar
6    30 type_2 bar_2
7     4 type_2 bar_2
8     6 type_2   bar
11    4 type_3 bar_3
Yannis P.
fuente
Saludos, creo que esto es extremadamente útil.
Tjebo
8

Aquí hay una base R one liner que se superpone a varias soluciones anteriores, pero devuelve un data.frame con los nombres correctos.

out <- setNames(data.frame(before$attr,
                  do.call(rbind, strsplit(as.character(before$type),
                                          split="_and_"))),
                  c("attr", paste0("type_", 1:2)))
out
  attr type_1 type_2
1    1    foo    bar
2   30    foo  bar_2
3    4    foo    bar
4    6    foo  bar_2

Se utiliza strsplitpara dividir la variable y data.framecon do.call/ rbindpara volver a colocar los datos en un data.frame. La mejora incremental adicional es el uso de setNamesagregar nombres de variables al data.frame.

lmo
fuente
6

Esta pregunta es bastante antigua, pero agregaré la solución que encontré es la más simple en la actualidad.

library(reshape2)
before = data.frame(attr = c(1,30,4,6), type=c('foo_and_bar','foo_and_bar_2'))
newColNames <- c("type1", "type2")
newCols <- colsplit(before$type, "_and_", newColNames)
after <- cbind(before, newCols)
after$type <- NULL
after
Swifty McSwifterton
fuente
Este es, con mucho, el más fácil cuando se trata de administrar vectores df
Apricot
5

Desde R versión 3.4.0, puede usarlo strcapture()desde el paquete utils (incluido con las instalaciones base R), vinculando la salida a la (s) otra (s) columna (s).

out <- strcapture(
    "(.*)_and_(.*)",
    as.character(before$type),
    data.frame(type_1 = character(), type_2 = character())
)

cbind(before["attr"], out)
#   attr type_1 type_2
# 1    1    foo    bar
# 2   30    foo  bar_2
# 3    4    foo    bar
# 4    6    foo  bar_2
Rich Scriven
fuente
4

Otro enfoque si desea seguir strsplit()es utilizar el unlist()comando. Aquí hay una solución en ese sentido.

tmp <- matrix(unlist(strsplit(as.character(before$type), '_and_')), ncol=2,
   byrow=TRUE)
after <- cbind(before$attr, as.data.frame(tmp))
names(after) <- c("attr", "type_1", "type_2")
ashaw
fuente
4

base pero probablemente lenta:

n <- 1
for(i in strsplit(as.character(before$type),'_and_')){
     before[n, 'type_1'] <- i[[1]]
     before[n, 'type_2'] <- i[[2]]
     n <- n + 1
}

##   attr          type type_1 type_2
## 1    1   foo_and_bar    foo    bar
## 2   30 foo_and_bar_2    foo  bar_2
## 3    4   foo_and_bar    foo    bar
## 4    6 foo_and_bar_2    foo  bar_2
jpmorris
fuente
1

Aquí hay otra solución base R. Podemos usarlo, read.tablepero dado que solo acepta un separgumento de un byte y aquí tenemos un separador de varios bytes que podemos usar gsubpara reemplazar el separador de varios bytes a cualquier separador de un byte y usarlo como separgumento enread.table

cbind(before[1], read.table(text = gsub('_and_', '\t', before$type), 
                 sep = "\t", col.names = paste0("type_", 1:2)))

#  attr type_1 type_2
#1    1    foo    bar
#2   30    foo  bar_2
#3    4    foo    bar
#4    6    foo  bar_2

En este caso, también podemos acortarlo reemplazándolo con un separgumento predeterminado para que no tengamos que mencionarlo explícitamente

cbind(before[1], read.table(text = gsub('_and_', ' ', before$type), 
                 col.names = paste0("type_", 1:2)))
Ronak Shah
fuente