Zbierz wiele zestawów kolumn

108

Mam dane z ankiety internetowej, w której respondenci 1-3 razy przechodzą przez pętlę pytań. Program badania (Qualtrics) zapisuje te dane w wielu kolumnach, to znaczy Q3.2 w badaniu będą miały kolumny Q3.2.1., Q3.2.2.oraz 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
...

Chcę połączyć wszystkie kolumny QN.N * w uporządkowane oddzielne kolumny QN.N, ostatecznie kończąc na czymś takim:

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

tidyrBiblioteka posiada gather()funkcję, która działa świetnie na łączenie jeden zestaw kolumn:

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

Wynikowa ramka danych ma 30 wierszy, zgodnie z oczekiwaniami (10 osobników, 3 pętle każda). Jednak gromadzenie drugiego zestawu kolumn nie działa poprawnie - pomyślnie tworzy dwie połączone kolumny Q3.2i Q3.3, ale kończy się na 90 wierszach zamiast 30 (wszystkie kombinacje 10 osobników, 3 pętle Q3.2 i 3 pętle Q3 .3; kombinacje znacznie wzrosną dla każdej grupy kolumn w rzeczywistych danych):

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

Czy istnieje sposób na użycie wielu wywołań w gather()tym celu, łącząc małe podzbiory kolumn, jak to, przy jednoczesnym zachowaniu prawidłowej liczby wierszy?

Andrzej
źródło
co jest nie takdf %>% gather(loop_number, Q3.2, starts_with("Q3."))
Alex
To daje mi jedną skonsolidowaną kolumnę z 60 wierszami. Myślę, że mogłoby to zadziałać, gdybym następnie zawarł jakiś rodzaj wywołania, seperate()aby podzielić wartości Q3.3 (i nie tylko) na ich własne kolumny. Ale to wciąż wydaje się być naprawdę okrężnym, hakerskim rozwiązaniem…
Andrew
użyj spreadPracuję teraz nad rozwiązaniem: p
Alex,
Spróbuj tego! 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,
Och, to działa naprawdę dobrze dla dwóch zmiennych. Jestem jednak ciekawy, czy jest skalowalny - w moich prawdziwych danych mam Q3.2-Q3.30, więc wymagałoby to kilku indywidualnych wywołań spread(). Chociaż wielokrotne połączenia i tak wydają się nieuniknione, niezależnie od tego, czy działa kilka połączeń generate(), czy zagnieżdżone spread()
Andrew

Odpowiedzi:

146

To podejście wydaje mi się całkiem naturalne:

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

Najpierw zbierz wszystkie kolumny pytań, użyj ich extract()do rozdzielenia na questioni loop_number, a następnie spread()zapytaj z powrotem do kolumn.

#>    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
źródło
5
Dzień dobry. Mam wiele kolumn z nazwami kończącymi się na 1 i 2, np. Wiek1, wiek2, waga1, waga2, krew1, krew2 .... Jak zastosować tutaj twoją metodę?
skan
4
Co oznacza ta część: „(Q. \\ ..) \\. (.)” Czego szukałbym, aby zdekodować to, co się tam dzieje?
mob
3
@mob Wyrażenia regularne
hadley
1
@mob "(Q. \\ ..) \\. (.)" jest wyrażeniem regularnym z nawiasami, które definiują grupy wyrażenia regularnego do wyodrębnienia do "pytanie" i "numer_pętli". Dokładniej, w tym przykładzie elementy w kluczu z wyrażeniem „P. \\ ..” trafiają do kolumny „Pytanie” (tj. „P3.2” i „P3.3”), a następnie część po następnej kropka, wyrażona jako „.”, trafia do kolumny „loop_number”.
LC-datascientist
31

Można to zrobić za pomocą reshape. Jest to jednak możliwe dplyr.

  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

Lub używając 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

Aktualizacja

Za pomocą tidyr_0.8.3.9000możemy pivot_longerzmienić kształt wielu kolumn. (Używając zmienionych nazw kolumn z gsubgóry)

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

UWAGA: Wartości są różne, ponieważ podczas tworzenia wejściowego zestawu danych nie było ustawionego ziarna

akrun
źródło
Whoa, to działa doskonale. tidyr jest rzekomo zamiennikiem / ulepszeniem dla zmiany kształtu - zastanawiam się, czy @hadley zna sposób na zrobienie tego samego z dplyr lub tidyr…
Andrew
To jest czysta magia. Jedyne, co dodałem, to mutate(loop_number = as.numeric(L2))przed upuszczeniem L2i jest idealne.
Andrew,
1
@Andrew Osobiście wolę tę reshapemetodę ze względu na jej zwarty kod, chociaż dplyrmoże być szybsza w przypadku dużych zbiorów danych.
akrun
1
Nigdy nie byłem w stanie zrozumieć tej reshape()funkcji, zobacz moje rozwiązanie, które wydaje mi się całkiem czystą implementacją Tidyr.
hadley
22

Dzięki ostatniej aktualizacji do melt.data.table, możemy teraz stopić wiele kolumn. Dzięki temu możemy:

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

Możesz pobrać wersję rozwojową stąd .

Bieg
źródło
Dzień dobry. Mam wiele kolumn z nazwami kończącymi się na 1 i 2, np. Wiek1, wiek2, waga1, waga2, krew1, krew2 .... Jak zastosować tutaj twoją metodę?
skan
skan, sprawdź przekształcającą winietę . Powodzenia!
Arun
Zrobiłem, ale nie wiem, jak poprawnie osadzić wyrażenia regularne, aby podzielić nazwy kolumn i przekazać je do topienia. Jest tylko jeden przykład z wzorami i jest on zbyt prosty. W moim przypadku musiałbym dołączyć wiele nazw kolumn do wzorca ()
skan
Wyobraź sobie, że masz te kolumny: paste0 (rep (LETTERS, each = 3), 1: 3) i chcesz uzyskać długą tabelę zdefiniowaną przez literę i liczbę
skan
Jest to z pewnością najbardziej zwięzłe i łatwe do zinterpretowania.
Michael Bellhouse
10

Wcale nie jest to związane z "tidyr" i "dplyr", ale oto inna opcja do rozważenia: merged.stackz mojego pakietu "splitstackshape" , wersja 1.4.0 i nowsze.

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
źródło
1
Dzień dobry. Mam wiele kolumn z nazwami kończącymi się na 1 i 2, np. Wiek1, wiek2, waga1, waga2, krew1, krew2 .... Jak zastosować tutaj twoją metodę?
skan
6

Jeśli jesteś podobny do mnie i nie możesz wymyślić, jak użyć „wyrażenia regularnego z grupami przechwytującymi” dla extract, następujący kod replikuje extract(...)wiersz w odpowiedzi 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)

Problem polega na tym, że początkowe gromadzenie tworzy kolumnę klucza, która w rzeczywistości jest kombinacją dwóch kluczy. Zdecydowałem się użyć mutatew moim oryginalnym rozwiązaniu w komentarzach, aby podzielić tę kolumnę na dwie kolumny z równoważnymi informacjami, loop_numberkolumną i question_numberkolumną. spreadmożna następnie użyć do przekształcenia danych w postaci długiej, które są parami klucz-wartość (question_number, value)w dane w szerokim formularzu.

Alex
źródło