Rassemblez plusieurs ensembles de colonnes

108

J'ai des données provenant d'un sondage en ligne où les répondants parcourent une boucle de questions 1 à 3 fois. Le logiciel d'enquête (Qualtrics) enregistre ces données dans plusieurs colonnes qui est, Q3.2 dans l'enquête aura des colonnes Q3.2.1., Q3.2.2.et 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
...

Je veux combiner toutes les colonnes QN.N * dans des colonnes QN.N individuelles bien rangées, pour finalement aboutir à quelque chose comme ceci:

   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 tidyrbibliothèque a la gather()fonction, qui fonctionne très bien pour combiner un ensemble de colonnes:

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

La base de données résultante a 30 lignes, comme prévu (10 individus, 3 boucles chacun). Cependant, la collecte d'un deuxième ensemble de colonnes ne fonctionne pas correctement - cela réussit à créer les deux colonnes combinées Q3.2et Q3.3, mais finit avec 90 lignes au lieu de 30 (toutes les combinaisons de 10 individus, 3 boucles de Q3.2 et 3 boucles de Q3 .3; les combinaisons augmenteront considérablement pour chaque groupe de colonnes dans les données réelles):

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-t-il un moyen d'utiliser plusieurs appels pour gather()aimer ceci, en combinant de petits sous-ensembles de colonnes comme celui-ci tout en conservant le nombre correct de lignes?

Andrew
la source
quel est le problème avecdf %>% gather(loop_number, Q3.2, starts_with("Q3."))
Alex
Cela me donne une colonne consolidée avec 60 lignes. Je suppose que cela pourrait fonctionner si seperate()j'incluais ensuite une sorte d'appel pour diviser les valeurs Q3.3 (et au-delà) dans leurs propres colonnes. Mais cela semble toujours être une solution de piratage vraiment détournée ...
Andrew
utiliser spreadje travaille sur une solution maintenant: p
Alex
essaye ça! 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 le
Ooh, cela fonctionne vraiment bien pour les deux variables. Je suis curieux de savoir s'il est évolutif - dans mes données réelles, j'ai Q3.2-Q3.30, il faudrait donc un tas d'appels individuels spread(). Bien que plusieurs appels semblent inévitables de toute façon, que ce soit un groupe de generate()s qui fonctionnent ou imbriqués spread()
Andrew

Réponses:

146

Cette approche me semble assez naturelle:

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

Rassemblez d'abord toutes les colonnes de questions, utilisez extract()pour les séparer en questionet loop_number, puis spread()questionnez de nouveau dans les colonnes.

#>    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
la source
5
Bonjour. J'ai de nombreuses colonnes dont les noms se terminent par 1 et 2, comme age1, age2, weight1, weight2, blood1, blood2 .... Comment appliquer votre méthode ici?
skan
4
Que signifie cette partie: "(Q. \\ ..) \\. (.)" Que rechercherais-je pour décoder ce qui se passe là-bas?
mob
3
@mob Expressions régulières
hadley
1
@mob "(Q. \\ ..) \\. (.)" est une expression régulière avec des parenthèses qui définissent les groupes de l'expression régulière à extraire dans "question" et "loop_number". Plus précisément, dans cet exemple, les éléments en clé avec l'expression "Q. \\ .." vont dans la colonne "question" (c'est-à-dire "Q3.2" et "Q3.3"), puis la partie après la suivante point, exprimé par ".", va dans la colonne "loop_number".
LC-datascientist le
31

Cela pourrait être fait en utilisant reshape. C'est possible avec dplyrcependant.

  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 en utilisant 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

Mettre à jour

Avec tidyr_0.8.3.9000, nous pouvons utiliser pivot_longerpour remodeler plusieurs colonnes. (En utilisant les noms de colonne modifiés gsubci-dessus)

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

REMARQUE: les valeurs sont différentes car il n'y avait pas de valeur de départ définie lors de la création du jeu de données d'entrée

Akrun
la source
Whoa, cela fonctionne parfaitement. tidyr est ostensiblement un remplacement / mise à niveau pour remodeler - Je me demande si @hadley connaît un moyen de faire la même chose avec dplyr ou tidyr…
Andrew
C'est de la pure magie. La seule chose que j'ai ajoutée était mutate(loop_number = as.numeric(L2))avant de tomber L2, et c'est parfait.
Andrew
1
@Andrew Je préfère personnellement la reshapeméthode pour son code compact, bien qu'elle dplyrpuisse être plus rapide pour les grands ensembles de données.
akrun
1
Je n'ai jamais été en mesure de comprendre la reshape()fonction, voir ma solution pour ce qui me semble être une implémentation tidyr assez propre.
hadley
22

Avec la récente mise à jour de melt.data.table, nous pouvons désormais fondre plusieurs colonnes. Avec cela, nous pouvons faire:

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

Vous pouvez obtenir la version de développement à partir d' ici .

Arun
la source
Bonjour. J'ai de nombreuses colonnes dont les noms se terminent par 1 et 2, comme age1, age2, weight1, weight2, blood1, blood2 .... Comment appliquer votre méthode ici?
skan
skan, vérifiez la vignette de remodelage . Bonne chance!
Arun
Je l'ai fait mais je ne sais pas comment incorporer correctement des expressions régulières pour diviser les noms de colonnes et les transmettre à fondre. Il n'y a qu'un seul exemple avec des motifs, et c'est trop simple. Dans mon cas, j'aurais besoin d'inclure de nombreux noms de colonnes dans pattern ()
skan
Imaginez que vous ayez ces colonnes: paste0 (rep (LETTERS, each = 3), 1: 3) et que vous vouliez obtenir le long tableau défini par une lettre et un nombre
skan
C'est de loin le plus succinct et le plus facile à interpréter.
Michael Bellhouse
10

Ce n'est pas du tout lié à "tidyr" et "dplyr", mais voici une autre option à considérer: à merged.stackpartir de mon package "splitstackshape" , V1.4.0 et supérieur.

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
la source
1
Bonjour. J'ai de nombreuses colonnes dont les noms se terminent par 1 et 2, comme age1, age2, weight1, weight2, blood1, blood2 .... Comment appliquer votre méthode ici?
skan
6

Si vous êtes comme moi et que vous ne parvenez pas à utiliser une expression régulière avec des groupes de capture extract, le code suivant réplique la extract(...)ligne dans la réponse 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)

Le problème ici est que le regroupement initial forme une colonne clé qui est en fait une combinaison de deux clés. J'ai choisi d'utiliser mutatedans ma solution d'origine dans les commentaires pour diviser cette colonne en deux colonnes avec des informations équivalentes, une loop_numbercolonne et une question_numbercolonne. spreadpeuvent ensuite être utilisées pour transformer les données de forme longue, qui sont des paires de valeurs clés, (question_number, value)en données de forme large.

Alex
la source