Reúna varios conjuntos de columnas


108

Tengo datos de una encuesta en línea donde los encuestados realizan un ciclo de preguntas de 1 a 3 veces. El software de la encuesta (Qualtrics) registra estos datos en varias columnas, es decir, Q3.2 en la encuesta tendrá columnas Q3.2.1., Q3.2.2.y Q3.2.3.:

df <- data.frame(
  id = 1:10,
  time = as.Date('2009-01-01') + 0:9,
  Q3.2.1. = rnorm(10, 0, 1),
  Q3.2.2. = rnorm(10, 0, 1),
  Q3.2.3. = rnorm(10, 0, 1),
  Q3.3.1. = rnorm(10, 0, 1),
  Q3.3.2. = rnorm(10, 0, 1),
  Q3.3.3. = rnorm(10, 0, 1)
)

# Sample data

   id       time    Q3.2.1.     Q3.2.2.    Q3.2.3.     Q3.3.1.    Q3.3.2.     Q3.3.3.
1   1 2009-01-01 -0.2059165 -0.29177677 -0.7107192  1.52718069 -0.4484351 -1.21550600
2   2 2009-01-02 -0.1981136 -1.19813815  1.1750200 -0.40380049 -1.8376094  1.03588482
3   3 2009-01-03  0.3514795 -0.27425539  1.1171712 -1.02641801 -2.0646661 -0.35353058
...

Quiero combinar todas las columnas QN.N * en columnas QN.N individuales ordenadas, y finalmente terminaré con algo como esto:

   id       time loop_number        Q3.2        Q3.3
1   1 2009-01-01           1 -0.20591649  1.52718069
2   2 2009-01-02           1 -0.19811357 -0.40380049
3   3 2009-01-03           1  0.35147949 -1.02641801
...
11  1 2009-01-01           2 -0.29177677  -0.4484351
12  2 2009-01-02           2 -1.19813815  -1.8376094
13  3 2009-01-03           2 -0.27425539  -2.0646661
...
21  1 2009-01-01           3 -0.71071921 -1.21550600
22  2 2009-01-02           3  1.17501999  1.03588482
23  3 2009-01-03           3  1.11717121 -0.35353058
...

La tidyrbiblioteca tiene la gather()función, que funciona muy bien para combinar un conjunto de columnas:

library(dplyr)
library(tidyr)
library(stringr)

df %>% gather(loop_number, Q3.2, starts_with("Q3.2")) %>% 
  mutate(loop_number = str_sub(loop_number,-2,-2)) %>%
  select(id, time, loop_number, Q3.2)


   id       time loop_number        Q3.2
1   1 2009-01-01           1 -0.20591649
2   2 2009-01-02           1 -0.19811357
3   3 2009-01-03           1  0.35147949
...
29  9 2009-01-09           3 -0.58581232
30 10 2009-01-10           3 -2.33393981

El marco de datos resultante tiene 30 filas, como se esperaba (10 individuos, 3 bucles cada uno). Sin embargo, la recopilación de un segundo conjunto de columnas no funciona correctamente: crea correctamente las dos columnas combinadas Q3.2y Q3.3, pero termina con 90 filas en lugar de 30 (todas las combinaciones de 10 personas, 3 bucles de Q3.2 y 3 bucles de Q3 .3; las combinaciones aumentarán sustancialmente para cada grupo de columnas en los datos reales):

df %>% gather(loop_number, Q3.2, starts_with("Q3.2")) %>% 
  gather(loop_number, Q3.3, starts_with("Q3.3")) %>%
  mutate(loop_number = str_sub(loop_number,-2,-2))


   id       time loop_number        Q3.2        Q3.3
1   1 2009-01-01           1 -0.20591649  1.52718069
2   2 2009-01-02           1 -0.19811357 -0.40380049
3   3 2009-01-03           1  0.35147949 -1.02641801
...
89  9 2009-01-09           3 -0.58581232 -0.13187024
90 10 2009-01-10           3 -2.33393981 -0.48502131

¿Hay alguna manera de usar múltiples llamadas para gather()así, combinando pequeños subconjuntos de columnas como esta mientras se mantiene el número correcto de filas?


qué pasa condf %>% gather(loop_number, Q3.2, starts_with("Q3."))
Alex

Eso me da una columna consolidada con 60 filas. Supongo que eso podría funcionar si luego incluyo algún tipo de llamada seperate()para dividir los valores de Q3.3 (y más allá) en sus propias columnas. Pero eso todavía parece una solución hacky realmente indirecta…
Andrew

uso spreadEstoy trabajando en una solución ahora: p
Alex

¡prueba esto! df %>% gather(question_number, Q3.2, starts_with("Q3.")) %>% mutate(loop_number = str_sub(question_number,-2,-2), question_number = str_sub(question_number,1,4)) %>% select(id, time, loop_number, question_number, Q3.2) %>% spread(key = question_number, value = Q3.2)
Alex

Ooh, eso funciona muy bien para las dos variables. Sin embargo, tengo curiosidad por saber si es escalable; en mis datos reales, tengo Q3.2-Q3.30, por lo que necesitaría un montón de llamadas individuales spread(). Aunque las llamadas múltiples parecen inevitables de todos modos, ya sea que se trate de un montón de correos generate()electrónicos que funcionan o de correos spread()electrónicos anidados …
Andrew

Respuestas:


146

Este enfoque me parece bastante natural:

df %>%
  gather(key, value, -id, -time) %>%
  extract(key, c("question", "loop_number"), "(Q.\\..)\\.(.)") %>%
  spread(question, value)

Primero reúna todas las columnas de preguntas, use extract()para separar en questiony loop_number, luego spread()pregunte nuevamente en las columnas.

#>    id       time loop_number         Q3.2        Q3.3
#> 1   1 2009-01-01           1  0.142259203 -0.35842736
#> 2   1 2009-01-01           2  0.061034802  0.79354061
#> 3   1 2009-01-01           3 -0.525686204 -0.67456611
#> 4   2 2009-01-02           1 -1.044461185 -1.19662936
#> 5   2 2009-01-02           2  0.393808163  0.42384717

5
Hola. Tengo muchas columnas con nombres que terminan en 1 y 2, como edad1, edad2, peso1, peso2, sangre1, sangre2 ... ¿Cómo aplicaría su método aquí?
skan

4
¿Qué significa esta parte: "(Q. \\ ..) \\. (.)" ¿Qué buscaría para decodificar lo que está sucediendo allí?
mob

3
@mob Expresiones regulares
hadley

1
@mob "(Q. \\ ..) \\. (.)" es una expresión regular entre paréntesis que define los grupos de la expresión regular para extraer en "pregunta" y "número_bucle". Más específicamente, en este ejemplo, los elementos en clave con la expresión "Q. \\ .." van a la columna "pregunta" (es decir, "Q3.2" y "Q3.3"), luego la parte después de la siguiente punto, expresado como ".", va a la columna "número_bucle".
LC-datascientist

31

Esto se puede hacer usando reshape. Sin dplyrembargo, es posible .

  colnames(df) <- gsub("\\.(.{2})$", "_\\1", colnames(df))
  colnames(df)[2] <- "Date"
  res <- reshape(df, idvar=c("id", "Date"), varying=3:8, direction="long", sep="_")
  row.names(res) <- 1:nrow(res)

   head(res)
  #  id       Date time       Q3.2       Q3.3
  #1  1 2009-01-01    1  1.3709584  0.4554501
  #2  2 2009-01-02    1 -0.5646982  0.7048373
  #3  3 2009-01-03    1  0.3631284  1.0351035
  #4  4 2009-01-04    1  0.6328626 -0.6089264
  #5  5 2009-01-05    1  0.4042683  0.5049551
  #6  6 2009-01-06    1 -0.1061245 -1.7170087

O usando dplyr

  library(tidyr)
  library(dplyr)
  colnames(df) <- gsub("\\.(.{2})$", "_\\1", colnames(df))

  df %>%
     gather(loop_number, "Q3", starts_with("Q3")) %>% 
     separate(loop_number,c("L1", "L2"), sep="_") %>% 
     spread(L1, Q3) %>%
     select(-L2) %>%
     head()
  #  id       time       Q3.2       Q3.3
  #1  1 2009-01-01  1.3709584  0.4554501
  #2  1 2009-01-01  1.3048697  0.2059986
  #3  1 2009-01-01 -0.3066386  0.3219253
  #4  2 2009-01-02 -0.5646982  0.7048373
  #5  2 2009-01-02  2.2866454 -0.3610573
  #6  2 2009-01-02 -1.7813084 -0.7838389

Actualizar

Con tidyr_0.8.3.9000, podemos usar pivot_longerpara remodelar varias columnas. (Usando los nombres de columna cambiados de gsubarriba)

library(dplyr)
library(tidyr)
df %>% 
    pivot_longer(cols = starts_with("Q3"), 
          names_to = c(".value", "Q3"), names_sep = "_") %>% 
    select(-Q3)
# A tibble: 30 x 4
#      id time         Q3.2    Q3.3
#   <int> <date>      <dbl>   <dbl>
# 1     1 2009-01-01  0.974  1.47  
# 2     1 2009-01-01 -0.849 -0.513 
# 3     1 2009-01-01  0.894  0.0442
# 4     2 2009-01-02  2.04  -0.553 
# 5     2 2009-01-02  0.694  0.0972
# 6     2 2009-01-02 -1.11   1.85  
# 7     3 2009-01-03  0.413  0.733 
# 8     3 2009-01-03 -0.896 -0.271 
#9     3 2009-01-03  0.509 -0.0512
#10     4 2009-01-04  1.81   0.668 
# … with 20 more rows

NOTA: Los valores son diferentes porque no hubo una semilla establecida al crear el conjunto de datos de entrada


Vaya, esto funciona perfectamente. tidyr es aparentemente un reemplazo / actualización para remodelar - Me pregunto si @hadley sabe de una manera de hacer lo mismo con dplyr o tidyr…
Andrew

Eso es pura magia. Lo único que agregué fue mutate(loop_number = as.numeric(L2))antes de dejarlo caer L2, y es perfecto.
Andrew

1
@Andrew Personalmente prefiero el reshapemétodo por su código compacto, aunque dplyrpuede ser más rápido para grandes conjuntos de datos.
Akrun

1
Nunca he podido entender la reshape()función, veo mi solución para lo que me parece una implementación tidyr bastante limpia.
hadley

22

Con la actualización reciente de melt.data.table, ahora podemos fundir varias columnas. Con eso, podemos hacer:

require(data.table) ## 1.9.5
melt(setDT(df), id=1:2, measure=patterns("^Q3.2", "^Q3.3"), 
     value.name=c("Q3.2", "Q3.3"), variable.name="loop_number")
 #    id       time loop_number         Q3.2        Q3.3
 # 1:  1 2009-01-01           1 -0.433978480  0.41227209
 # 2:  2 2009-01-02           1 -0.567995351  0.30701144
 # 3:  3 2009-01-03           1 -0.092041353 -0.96024077
 # 4:  4 2009-01-04           1  1.137433487  0.60603396
 # 5:  5 2009-01-05           1 -1.071498263 -0.01655584
 # 6:  6 2009-01-06           1 -0.048376809  0.55889996
 # 7:  7 2009-01-07           1 -0.007312176  0.69872938

Puede obtener la versión de desarrollo desde aquí .


Hola. Tengo muchas columnas con nombres que terminan en 1 y 2, como edad1, edad2, peso1, peso2, sangre1, sangre2 ... ¿Cómo aplicaría su método aquí?
skan

skan, mira la viñeta de remodelación . ¡Buena suerte!
Arun

Lo hice, pero no sé cómo incrustar correctamente expresiones regulares para dividir los nombres de las columnas y pasarlas a fundir. Solo hay un ejemplo con patrones y es demasiado simple. En mi caso, necesitaría incluir muchos nombres de columnas dentro del patrón ()
skan

Imagine que tiene estas columnas: paste0 (rep (LETTERS, each = 3), 1: 3) y desea obtener la tabla larga definida por una letra y un número
skan

Este es sin duda el más conciso y fácil de interpretar.
Michael Bellhouse

10

No está relacionado en absoluto con "tidyr" y "dplyr", pero aquí hay otra opción a considerar: merged.stackde mi paquete "splitstackshape" , V1.4.0 y superior.

library(splitstackshape)
merged.stack(df, id.vars = c("id", "time"), 
             var.stubs = c("Q3.2.", "Q3.3."),
             sep = "var.stubs")
#     id       time .time_1       Q3.2.       Q3.3.
#  1:  1 2009-01-01      1. -0.62645381  1.35867955
#  2:  1 2009-01-01      2.  1.51178117 -0.16452360
#  3:  1 2009-01-01      3.  0.91897737  0.39810588
#  4:  2 2009-01-02      1.  0.18364332 -0.10278773
#  5:  2 2009-01-02      2.  0.38984324 -0.25336168
#  6:  2 2009-01-02      3.  0.78213630 -0.61202639
#  7:  3 2009-01-03      1. -0.83562861  0.38767161
# <<:::SNIP:::>>
# 24:  8 2009-01-08      3. -1.47075238 -1.04413463
# 25:  9 2009-01-09      1.  0.57578135  1.10002537
# 26:  9 2009-01-09      2.  0.82122120 -0.11234621
# 27:  9 2009-01-09      3. -0.47815006  0.56971963
# 28: 10 2009-01-10      1. -0.30538839  0.76317575
# 29: 10 2009-01-10      2.  0.59390132  0.88110773
# 30: 10 2009-01-10      3.  0.41794156 -0.13505460
#     id       time .time_1       Q3.2.       Q3.3.

1
Hola. Tengo muchas columnas con nombres que terminan en 1 y 2, como edad1, edad2, peso1, peso2, sangre1, sangre2 ... ¿Cómo aplicaría su método aquí?
skan

6

En caso de que sea como yo y no pueda averiguar cómo usar "expresión regular con grupos de captura" extract, el siguiente código replica la extract(...)línea en la respuesta de Hadleys:

df %>% 
    gather(question_number, value, starts_with("Q3.")) %>%
    mutate(loop_number = str_sub(question_number,-2,-2), question_number = str_sub(question_number,1,4)) %>%
    select(id, time, loop_number, question_number, value) %>% 
    spread(key = question_number, value = value)

El problema aquí es que la recopilación inicial forma una columna clave que en realidad es una combinación de dos claves. Elegí usar mutateen mi solución original en los comentarios para dividir esta columna en dos columnas con información equivalente, una loop_numbercolumna y una question_numbercolumna. spreadluego se puede utilizar para transformar los datos de formato largo, que son pares clave-valor (question_number, value)en datos de formato amplio.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.