Kumpulkan beberapa set kolom

108

Saya memiliki data dari survei online di mana responden melewati putaran pertanyaan 1-3 kali. Perangkat lunak survei (Qualtrics) mencatat data ini dalam beberapa kolom-yaitu, Q3.2 dalam survei akan memiliki kolom Q3.2.1., Q3.2.2.dan 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
...

Saya ingin menggabungkan semua kolom QN.N * menjadi kolom QN.N individual yang rapi, yang pada akhirnya berakhir dengan sesuatu seperti ini:

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

The tidyrperpustakaan memiliki gather()fungsi, yang bekerja bagus untuk menggabungkan satu set kolom:

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

Bingkai data yang dihasilkan memiliki 30 baris, seperti yang diharapkan (10 individu, masing-masing 3 loop). Namun, mengumpulkan kumpulan kolom kedua tidak berfungsi dengan benar — ini berhasil membuat dua kolom gabungan Q3.2dan Q3.3, tetapi berakhir dengan 90 baris, bukan 30 (semua kombinasi 10 individu, 3 loop Q3.2, dan 3 loop Q3 .3; kombinasi akan meningkat secara substansial untuk setiap kelompok kolom dalam data aktual):

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

Adakah cara untuk menggunakan beberapa panggilan untuk gather()seperti ini, menggabungkan subkumpulan kecil kolom seperti ini sambil mempertahankan jumlah baris yang benar?

Andrew
sumber
apa yang salah dengandf %>% gather(loop_number, Q3.2, starts_with("Q3."))
Alex
Itu memberi saya satu kolom terkonsolidasi dengan 60 baris. Saya kira itu bisa berhasil jika saya kemudian memasukkan semacam panggilan seperate()untuk membagi nilai Q3.3 (dan seterusnya) ke dalam kolom mereka sendiri. Tapi itu masih tampak seperti solusi yang tidak benar-benar bundar…
Andrew
gunakan spreadsaya sedang mengerjakan solusi sekarang: p
Alex
coba ini! 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, itu bekerja dengan sangat baik untuk kedua variabel. Saya ingin tahu apakah ini dapat diskalakan — dalam data asli saya, saya memiliki Q3.2-Q3.30, jadi ini akan membutuhkan banyak panggilan individu spread(). Meskipun beberapa panggilan tampaknya tak terhindarkan, baik itu sekumpulan panggilan generate()yang berhasil atau bertingkat spread()...
Andrew

Jawaban:

146

Pendekatan ini tampaknya cukup alami bagi saya:

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

Pertama-tama kumpulkan semua kolom pertanyaan, gunakan extract()untuk memisahkan menjadi questiondan loop_number, kemudian spread()pertanyaan kembali ke kolom.

#>    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
sumber
5
Halo. Saya memiliki banyak kolom dengan nama yang diakhiri dengan 1 dan 2, seperti age1, age2, weight1, weight2, blood1, blood2 .... Bagaimana saya menerapkan metode Anda di sini?
skan
4
Apa arti bagian ini: "(Q. \\ ..) \\. (.)" Apa yang akan saya cari untuk memecahkan kode apa yang terjadi di sana?
mafia
3
@mob Kalimat biasa
hadley
1
@mob "(Q. \\ ..) \\. (.)" adalah ekspresi reguler dengan tanda kurung yang menentukan grup dari ekspresi reguler yang akan diekstrak menjadi "pertanyaan" dan "nomor_ulang". Lebih khusus lagi, dalam contoh ini, item dalam kunci dengan ekspresi "Q. \\ .." masuk ke kolom "pertanyaan" (yaitu, "Q3.2" dan "Q3.3"), lalu bagian setelah berikutnya titik, diekspresikan sebagai ".", masuk ke kolom "loop_number".
LC-datacientist
31

Ini bisa dilakukan dengan menggunakan reshape. Itu mungkin saja 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

Atau menggunakan 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

Memperbarui

Dengan tidyr_0.8.3.9000, kita bisa menggunakan pivot_longeruntuk membentuk kembali beberapa kolom. (Menggunakan nama kolom yang diubah dari gsubatas)

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

CATATAN: Nilainya berbeda karena tidak ada benih yang ditetapkan dalam membuat set data masukan

akrun
sumber
Wah, ini bekerja dengan sempurna. tidyr seolah-olah merupakan pengganti / peningkatan untuk membentuk kembali - Saya ingin tahu apakah @hadley tahu cara untuk melakukan hal yang sama ini dengan dplyr atau tidyr…
Andrew
Itu adalah sihir murni. Satu-satunya hal yang saya tambahkan adalah mutate(loop_number = as.numeric(L2))sebelum menjatuhkan L2, dan itu sempurna.
Andrew
1
@Andrew Saya pribadi lebih suka reshapemetode untuk kode ringkasnya, meskipun dplyrmungkin lebih cepat untuk kumpulan data besar.
akrun
1
Saya tidak pernah bisa memahami reshape()fungsinya, lihat solusi saya untuk apa yang menurut saya implementasi tidyr yang cukup bersih.
hadley
22

Dengan pembaruan terkini ke melt.data.table, kami sekarang dapat mencairkan banyak kolom. Dengan itu, kita bisa melakukan:

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

Anda bisa mendapatkan versi pengembangan dari sini .

Arun
sumber
Halo. Saya memiliki banyak kolom dengan nama yang diakhiri dengan 1 dan 2, seperti age1, age2, weight1, weight2, blood1, blood2 .... Bagaimana saya menerapkan metode Anda di sini?
skan
skan, periksa sketsa pembentukan kembali . Semoga berhasil!
Arun
Saya melakukannya tetapi saya tidak tahu cara menyematkan ekspresi reguler dengan benar untuk memisahkan nama kolom dan meneruskannya hingga meleleh. Hanya ada satu contoh dengan pola, dan itu terlalu sederhana. Dalam kasus saya, saya perlu memasukkan banyak nama kolom di dalam pattern ()
skan
Bayangkan Anda memiliki kolom ini: paste0 (rep (LETTERS, each = 3), 1: 3) dan Anda ingin tabel panjang yang ditentukan oleh huruf dan angka
skan
Ini adalah yang paling ringkas dan mudah untuk ditafsirkan.
Michael Bellhouse
10

Ini sama sekali tidak terkait dengan "tidyr" dan "dplyr", tetapi berikut adalah opsi lain untuk dipertimbangkan: merged.stackdari paket "splitstackshape" saya , V1.4.0 dan yang lebih baru.

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
sumber
1
Halo. Saya memiliki banyak kolom dengan nama yang diakhiri dengan 1 dan 2, seperti age1, age2, weight1, weight2, blood1, blood2 .... Bagaimana saya menerapkan metode Anda di sini?
skan
6

Jika Anda seperti saya, dan tidak dapat mengetahui cara menggunakan "ekspresi reguler dengan menangkap grup" untuk extract, kode berikut mereplikasi extract(...)baris dalam jawaban Hadley:

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)

Masalahnya di sini adalah bahwa pengumpulan awal membentuk kolom kunci yang sebenarnya merupakan kombinasi dari dua kunci. Saya memilih untuk menggunakan mutatesolusi asli saya di komentar untuk membagi kolom ini menjadi dua kolom dengan info yang setara, loop_numberkolom dan question_numberkolom. spreadkemudian dapat digunakan untuk mengubah data bentuk panjang, yang merupakan pasangan nilai kunci (question_number, value)menjadi data bentuk lebar.

Alex
sumber