Reúna vários conjuntos de colunas

108

Tenho dados de uma pesquisa online em que os entrevistados fazem uma série de perguntas de 1 a 3 vezes. O software de pesquisa (Qualtrics) registra esses dados em várias colunas, ou seja, Q3.2 na pesquisa terá colunasQ3.2.1. , Q3.2.2.e 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
...

Quero combinar todas as colunas QN.N * em colunas QN.N individuais organizadas, no final das contas acabando com algo assim:

   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
...

A tidyrbiblioteca tem a gather()função, que funciona muito bem para combinar um conjunto de colunas:

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

O quadro de dados resultante tem 30 linhas, como esperado (10 indivíduos, 3 loops cada). No entanto, reunir um segundo conjunto de colunas não funciona corretamente - torna as duas colunas combinadas Q3.2eQ3.3 , mas termina com 90 linhas em vez de 30 (todas as combinações de 10 indivíduos, 3 loops de Q3.2 e 3 loops de Q3 .3; as combinações aumentarão substancialmente para cada grupo de colunas nos dados reais):

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

Existe uma maneira de usar várias chamadas para gather()assim, combinando pequenos subconjuntos de colunas como este, mantendo o número correto de linhas?

Andrew
fonte
o que há de errado comdf %>% gather(loop_number, Q3.2, starts_with("Q3."))
Alex
Isso me dá uma coluna consolidada com 60 linhas. Eu acho que isso poderia funcionar se eu incluísse algum tipo de chamada seperate()para dividir os valores Q3.3 (e além) em suas próprias colunas. Mas isso ainda parece uma solução realmente indireta ...
Andrew,
uso, spreadestou trabalhando em uma solução agora: p
Alex
tente isso! 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, isso funciona muito bem para as duas variáveis. Estou curioso para saber se ele é escalável - em meus dados reais, tenho Q3.2-Q3.30, então precisaria de um monte de chamadas individuais para spread(). Embora várias chamadas pareçam inevitáveis ​​de qualquer maneira, sejam vários generate()s que funcionam ou spread()s ...
Andrew

Respostas:

146

Essa abordagem parece bastante natural para mim:

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

Primeiro reúna todas as colunas de perguntas, use extract()para separar em questione loop_number, em seguida, spread()questione de volta nas colunas.

#>    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
Hadley
fonte
5
Olá. Tenho muitas colunas com nomes que terminam em 1 e 2, como idade1, idade2, peso1, peso2, sangue1, sangue2 ... Como eu aplicaria seu método aqui?
skan
4
O que esta parte significa: "(Q. \\ ..) \\. (.)" O que eu procuraria para decodificar o que está acontecendo lá?
mob de
3
@mob Expressões regulares
hadley
1
@mob "(Q. \\ ..) \\. (.)" é uma expressão regular com parênteses que definem os grupos da expressão regular a serem extraídos em "question" e "loop_number". Mais especificamente, neste exemplo, os itens principais com a expressão "Q. \\ .." vão para a coluna "pergunta" (ou seja, "Q3.2" e "Q3.3"), e então a parte após a próxima período, expresso como ".", vai para a coluna "loop_number".
LC-datascientist
31

Isso pode ser feito usando reshape. É possível com dplyrembora.

  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

Ou 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

Atualizar

Com tidyr_0.8.3.9000, podemos usar pivot_longerpara remodelar várias colunas. (Usando os nomes de coluna alterados gsubacima)

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: Os valores são diferentes porque não houve nenhuma semente definida na criação do conjunto de dados de entrada

Akrun
fonte
Uau, isso funciona perfeitamente. tidyr é ostensivamente uma substituição / atualização para remodelar - eu me pergunto se @hadley conhece uma maneira de fazer isso com dplyr ou tidyr ...
Andrew
Isso é pura magia. A única coisa que adicionei foi mutate(loop_number = as.numeric(L2))antes de cair L2, e é perfeito.
Andrew
1
@Andrew Eu pessoalmente prefiro o reshapemétodo para seu código compacto, embora dplyrpossa ser mais rápido para grandes conjuntos de dados.
akrun
1
Eu nunca fui capaz de entender a reshape()função, veja minha solução para o que me parece uma implementação bem limpa.
hadley
22

Com a atualização recente do melt.data.table, agora podemos derreter várias colunas. Com isso, podemos fazer:

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

Você pode obter a versão de desenvolvimento aqui .

Uma corrida
fonte
Olá. Tenho muitas colunas com nomes que terminam em 1 e 2, como idade1, idade2, peso1, peso2, sangue1, sangue2 ... Como eu aplicaria seu método aqui?
skan
skan, verifique a vinheta de remodelagem . Boa sorte!
Arun
Eu fiz, mas não sei como incorporar corretamente expressões regulares para dividir nomes de colunas e passá-los para derreter. Existe apenas um exemplo com padrões e é muito simples. No meu caso, eu precisaria incluir muitos nomes de coluna dentro de pattern ()
skan
Imagine que você tem estas colunas: paste0 (rep (LETTERS, each = 3), 1: 3) e deseja obter a tabela longa definida por uma letra e um número
skan
Este é sem dúvida o mais sucinto e fácil de interpretar.
Michael Bellhouse
10

Não está relacionado com "tidyr" e "dplyr", mas aqui está outra opção a considerar: merged.stackdo meu pacote "splitstackshape" , V1.4.0 e 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.
A5C1D2H2I1M1N2O1R2T1
fonte
1
Olá. Tenho muitas colunas com nomes que terminam em 1 e 2, como idade1, idade2, peso1, peso2, sangue1, sangue2 ... Como eu aplicaria seu método aqui?
skan
6

Caso você seja como eu e não consiga descobrir como usar "expressão regular com grupos de captura" extract, o código a seguir replica a extract(...)linha da resposta 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)

O problema aqui é que a coleta inicial forma uma coluna-chave que, na verdade, é uma combinação de duas chaves. Optei por usar mutatena minha solução original nos comentários para dividir esta coluna em duas colunas com informações equivalentes, uma loop_numbercoluna e uma question_numbercoluna. spreadpode então ser usado para transformar os dados de formato longo, que são pares de valores-chave, (question_number, value)em dados de formato amplo.

Alex
fonte