25  Kỹ thuật reshape dữ liệu

25.1 Sử dụng lệnh reshape của base R

Lệnh stats::reshape này là căn bản của base R giúp chuyển dữ liệu từ long sang wide và ngược lại. Hạn chế là nếu dữ liệu unbalanced thì quá trình chuyển đổi sẽ phức tạp.

Lưu ý là khái niệm semi-longsemi-wide data là nói về các dạng dữ liệu ở trạng thái longwide chưa triệt để. Còn longwide nói về dạng dữ liệu ở trạng thái longwide triệt để. Như vậy cùng 1 bộ dataset khi reshape sẽ có nhiều hơn 1 cách sắp xếp dữ liệu tùy vào nhu cầu phân tích của người dùng (chủ yếu để phù hợp với đặc điểm dữ liệu mà các function trong R yêu cầu khi vẽ đồ thị hay phân tích thống kê).

25.1.1 Áp dụng lệnh reshape của base R cho imbalanced dataset

Ta có dataset ở dạng long, ký hiệu là survey với thông tin như sau. Trong đây thì cột id là mã số người tham gia khảo sát, gender là giới tính, age là tuổi, income là thu nhập và year là năm.

id <- c("111", "111", "112", "112", "112", 
        "114", "315", "315", "315", "539")

gender <- c("male", "male", "female", "female", "female", 
            "female", "male", "male", "male", "female")

age <- c(56, 58, 29, 31, 33, 
         NA, 86, 88, 90, NA)

income <- c(1000, 2500, NA, 400, 540, 
            200, 440, NA, NA, 300)

year <- c(2012, 2014, 2012, 2014, 2016,
          2016, 2012, 2014, 2016, 2016)

survey <- data.frame(id, gender, age, income, year)

survey
    id gender age income year
1  111   male  56   1000 2012
2  111   male  58   2500 2014
3  112 female  29     NA 2012
4  112 female  31    400 2014
5  112 female  33    540 2016
6  114 female  NA    200 2016
7  315   male  86    440 2012
8  315   male  88     NA 2014
9  315   male  90     NA 2016
10 539 female  NA    300 2016

idgender không thay đổi qua các năm ở cùng 1 cá nhân nên khi reshape từ long sang wide ta sẽ giữ lại 2 cột này.

survey_wide_1 <- reshape(survey, 
                         direction = "wide", # định vị kiểu spread dữ liệu dạng wide
                         idvar = c("id", "gender"), # giữ lại 2 cột này
                         timevar = "year", # spread ra theo cột `year`
                         v.names = c("age", "income") # chọn 2 cột để spread dữ liệu
                         ) 

survey_wide_1 # dữ liệu sau khi reshape
    id gender age.2012 income.2012 age.2014 income.2014 age.2016 income.2016
1  111   male       56        1000       58        2500       NA          NA
3  112 female       29          NA       31         400       33         540
6  114 female       NA          NA       NA          NA       NA         200
7  315   male       86         440       88          NA       90          NA
10 539 female       NA          NA       NA          NA       NA         300
row.names(survey_wide_1) <- NULL

attributes(survey_wide_1)$reshapeWide <- NULL

# dataset đã làm sạch attributes 
# có thể sắp xếp thứ tự cột cho chuẩn về 1 trật tự thuận tiện cho việc subset sau này
survey_wide_1 
   id gender age.2012 income.2012 age.2014 income.2014 age.2016 income.2016
1 111   male       56        1000       58        2500       NA          NA
2 112 female       29          NA       31         400       33         540
3 114 female       NA          NA       NA          NA       NA         200
4 315   male       86         440       88          NA       90          NA
5 539 female       NA          NA       NA          NA       NA         300

Như vậy ta có dữ liệu ở dạng wide, giờ nếu chuyển về dạng long thì R sẽ chuyển về dạng long triệt để, rồi sau đó ta mới tìm cách chuyển về dạng semi-long như là dataset survey ban đầu.

survey_long_1 <- reshape(survey_wide_1, 
                         direction = "long", # định vị kiểu spread dữ liệu dạng long
                         varying = list(3:8), # xác định các cột được rút lại khi reshape về dạng long
                         v.names = "value", # tên cột mới chứa giá trị tương ứng
                         timevar = "variable", # tên cột mới chứa tên của các cột được rút lại theo varying
                         times = names(survey_wide_1)[3:8], # R căn cứ vào đây để đưa tên cột `variable`
                         idvar = c("id") # nếu trong dataset ban đầu có cột id thì chỉ định luôn, 
                         #nếu không thì ta cần đặt tên cột `sample_id` 
                         #để R chỉ ra vị trí các cột sau khi gather
                      )

row.names(survey_long_1) <- NULL

attributes(survey_long_1)$reshapeLong <- NULL

survey_long_1
    id gender    variable value
1  111   male    age.2012    56
2  112 female    age.2012    29
3  114 female    age.2012    NA
4  315   male    age.2012    86
5  539 female    age.2012    NA
6  111   male income.2012  1000
7  112 female income.2012    NA
8  114 female income.2012    NA
9  315   male income.2012   440
10 539 female income.2012    NA
11 111   male    age.2014    58
12 112 female    age.2014    31
13 114 female    age.2014    NA
14 315   male    age.2014    88
15 539 female    age.2014    NA
16 111   male income.2014  2500
17 112 female income.2014   400
18 114 female income.2014    NA
19 315   male income.2014    NA
20 539 female income.2014    NA
21 111   male    age.2016    NA
22 112 female    age.2016    33
23 114 female    age.2016    NA
24 315   male    age.2016    90
25 539 female    age.2016    NA
26 111   male income.2016    NA
27 112 female income.2016   540
28 114 female income.2016   200
29 315   male income.2016    NA
30 539 female income.2016   300

Tiếp tục chuyển survey_long_1 về dạng cấu trúc y chang dataset survey ban đầu bằng cách reshape qua dạng wide cho các biến ageincome.

Bước 1: Tách ra thành các cột riêng

strsplit(survey_long_1$variable, split = "\\.") -> ok

ok
[[1]]
[1] "age"  "2012"

[[2]]
[1] "age"  "2012"

[[3]]
[1] "age"  "2012"

[[4]]
[1] "age"  "2012"

[[5]]
[1] "age"  "2012"

[[6]]
[1] "income" "2012"  

[[7]]
[1] "income" "2012"  

[[8]]
[1] "income" "2012"  

[[9]]
[1] "income" "2012"  

[[10]]
[1] "income" "2012"  

[[11]]
[1] "age"  "2014"

[[12]]
[1] "age"  "2014"

[[13]]
[1] "age"  "2014"

[[14]]
[1] "age"  "2014"

[[15]]
[1] "age"  "2014"

[[16]]
[1] "income" "2014"  

[[17]]
[1] "income" "2014"  

[[18]]
[1] "income" "2014"  

[[19]]
[1] "income" "2014"  

[[20]]
[1] "income" "2014"  

[[21]]
[1] "age"  "2016"

[[22]]
[1] "age"  "2016"

[[23]]
[1] "age"  "2016"

[[24]]
[1] "age"  "2016"

[[25]]
[1] "age"  "2016"

[[26]]
[1] "income" "2016"  

[[27]]
[1] "income" "2016"  

[[28]]
[1] "income" "2016"  

[[29]]
[1] "income" "2016"  

[[30]]
[1] "income" "2016"  
do.call(rbind, ok) -> ok_1

ok_1
      [,1]     [,2]  
 [1,] "age"    "2012"
 [2,] "age"    "2012"
 [3,] "age"    "2012"
 [4,] "age"    "2012"
 [5,] "age"    "2012"
 [6,] "income" "2012"
 [7,] "income" "2012"
 [8,] "income" "2012"
 [9,] "income" "2012"
[10,] "income" "2012"
[11,] "age"    "2014"
[12,] "age"    "2014"
[13,] "age"    "2014"
[14,] "age"    "2014"
[15,] "age"    "2014"
[16,] "income" "2014"
[17,] "income" "2014"
[18,] "income" "2014"
[19,] "income" "2014"
[20,] "income" "2014"
[21,] "age"    "2016"
[22,] "age"    "2016"
[23,] "age"    "2016"
[24,] "age"    "2016"
[25,] "age"    "2016"
[26,] "income" "2016"
[27,] "income" "2016"
[28,] "income" "2016"
[29,] "income" "2016"
[30,] "income" "2016"
survey_long_1$chi_tieu_theo_doi <- ok_1[, 1]

survey_long_1$year <- ok_1[, 2]

survey_long_1
    id gender    variable value chi_tieu_theo_doi year
1  111   male    age.2012    56               age 2012
2  112 female    age.2012    29               age 2012
3  114 female    age.2012    NA               age 2012
4  315   male    age.2012    86               age 2012
5  539 female    age.2012    NA               age 2012
6  111   male income.2012  1000            income 2012
7  112 female income.2012    NA            income 2012
8  114 female income.2012    NA            income 2012
9  315   male income.2012   440            income 2012
10 539 female income.2012    NA            income 2012
11 111   male    age.2014    58               age 2014
12 112 female    age.2014    31               age 2014
13 114 female    age.2014    NA               age 2014
14 315   male    age.2014    88               age 2014
15 539 female    age.2014    NA               age 2014
16 111   male income.2014  2500            income 2014
17 112 female income.2014   400            income 2014
18 114 female income.2014    NA            income 2014
19 315   male income.2014    NA            income 2014
20 539 female income.2014    NA            income 2014
21 111   male    age.2016    NA               age 2016
22 112 female    age.2016    33               age 2016
23 114 female    age.2016    NA               age 2016
24 315   male    age.2016    90               age 2016
25 539 female    age.2016    NA               age 2016
26 111   male income.2016    NA            income 2016
27 112 female income.2016   540            income 2016
28 114 female income.2016   200            income 2016
29 315   male income.2016    NA            income 2016
30 539 female income.2016   300            income 2016

Bước 2: Sắp xếp dataset gọn gàng trước khi reshape qua dạng wide. Ta thấy R đã chuẩn lại về cho ballance bộ dataset, tức là các giá trị id đều đầy đủ số liệu theo năm.

library(dplyr)

survey_long_2 <- survey_long_1[, -3]

survey_long_2 |> dplyr::arrange(id, year) -> survey_long_2

survey_long_2[, c(1, 2, 4, 3, 5)] -> survey_long_2

survey_long_2
    id gender chi_tieu_theo_doi value year
1  111   male               age    56 2012
2  111   male            income  1000 2012
3  111   male               age    58 2014
4  111   male            income  2500 2014
5  111   male               age    NA 2016
6  111   male            income    NA 2016
7  112 female               age    29 2012
8  112 female            income    NA 2012
9  112 female               age    31 2014
10 112 female            income   400 2014
11 112 female               age    33 2016
12 112 female            income   540 2016
13 114 female               age    NA 2012
14 114 female            income    NA 2012
15 114 female               age    NA 2014
16 114 female            income    NA 2014
17 114 female               age    NA 2016
18 114 female            income   200 2016
19 315   male               age    86 2012
20 315   male            income   440 2012
21 315   male               age    88 2014
22 315   male            income    NA 2014
23 315   male               age    90 2016
24 315   male            income    NA 2016
25 539 female               age    NA 2012
26 539 female            income    NA 2012
27 539 female               age    NA 2014
28 539 female            income    NA 2014
29 539 female               age    NA 2016
30 539 female            income   300 2016

Bước 3: Reshape trở về dạng wide để giống như dataset survey ban đầu.

survey_wide_ok <- reshape(survey_long_2, 
                         direction = "wide", # định vị kiểu spread dữ liệu dạng wide
                         idvar = c("id", "gender", "year"), # giữ lại 3 cột này
                         timevar = "chi_tieu_theo_doi", # spread ra theo cột `chi_tieu_theo_doi`
                         v.names = c("value") # chọn cột này để spread dữ liệu
                         ) 

row.names(survey_wide_ok) <- NULL

attributes(survey_wide_ok)$reshapeWide <- NULL

survey_wide_ok
    id gender year value.age value.income
1  111   male 2012        56         1000
2  111   male 2014        58         2500
3  111   male 2016        NA           NA
4  112 female 2012        29           NA
5  112 female 2014        31          400
6  112 female 2016        33          540
7  114 female 2012        NA           NA
8  114 female 2014        NA           NA
9  114 female 2016        NA          200
10 315   male 2012        86          440
11 315   male 2014        88           NA
12 315   male 2016        90           NA
13 539 female 2012        NA           NA
14 539 female 2014        NA           NA
15 539 female 2016        NA          300

Bước 4: Sửa lại tên cột.

survey_wide_ok_1 <- survey_wide_ok[, c(1, 2, 4, 5, 3)]

names(survey_wide_ok_1)[3] <- "age"
names(survey_wide_ok_1)[4] <- "income"

survey_wide_ok_1
    id gender age income year
1  111   male  56   1000 2012
2  111   male  58   2500 2014
3  111   male  NA     NA 2016
4  112 female  29     NA 2012
5  112 female  31    400 2014
6  112 female  33    540 2016
7  114 female  NA     NA 2012
8  114 female  NA     NA 2014
9  114 female  NA    200 2016
10 315   male  86    440 2012
11 315   male  88     NA 2014
12 315   male  90     NA 2016
13 539 female  NA     NA 2012
14 539 female  NA     NA 2014
15 539 female  NA    300 2016

Bước 5: Loại bỏ missing value và kiểm tra identical với survey ban đầu.

complete.cases(survey_wide_ok_1$age) -> check_1
complete.cases(survey_wide_ok_1$income) -> check_2

check_1 | check_2 -> check_3 # dùng OR để chọn cả 2 FALSE
# để loại missing value ở 2 cột `age` và `income`

survey_wide_ok_1[check_3, ] -> survey_wide_ok_2

row.names(survey_wide_ok_2) <- NULL

# lưu ý là cột `year` cần chuyển về numeric
survey_wide_ok_2$year <- as.numeric(survey_wide_ok_2$year)

# setdiff(survey, survey_wide_ok_2)

survey_wide_ok_2
    id gender age income year
1  111   male  56   1000 2012
2  111   male  58   2500 2014
3  112 female  29     NA 2012
4  112 female  31    400 2014
5  112 female  33    540 2016
6  114 female  NA    200 2016
7  315   male  86    440 2012
8  315   male  88     NA 2014
9  315   male  90     NA 2016
10 539 female  NA    300 2016
survey
    id gender age income year
1  111   male  56   1000 2012
2  111   male  58   2500 2014
3  112 female  29     NA 2012
4  112 female  31    400 2014
5  112 female  33    540 2016
6  114 female  NA    200 2016
7  315   male  86    440 2012
8  315   male  88     NA 2014
9  315   male  90     NA 2016
10 539 female  NA    300 2016
# nếu ở bước này nhìn dataset đã giống nhau rồi mà 
# identical vẫn false thì ta sắp xếp toàn bộ 
#các cột trong dataset về cùng 1 cấu trúc sau đó mới so sánh
identical(survey, survey_wide_ok_2)
[1] TRUE

Bước 6 (nếu kỹ hơn): Chuẩn lại toàn bộ thứ tự thành phần trong dataset

survey |> dplyr::arrange(!!! rlang::syms(names(survey))) -> survey_chuan

survey_chuan
    id gender age income year
1  111   male  56   1000 2012
2  111   male  58   2500 2014
3  112 female  29     NA 2012
4  112 female  31    400 2014
5  112 female  33    540 2016
6  114 female  NA    200 2016
7  315   male  86    440 2012
8  315   male  88     NA 2014
9  315   male  90     NA 2016
10 539 female  NA    300 2016
survey_wide_ok_2 |> dplyr::arrange(!!! rlang::syms(names(survey_wide_ok_2))) -> survey_wide_ok_2_chuan

identical(survey_chuan, survey_wide_ok_2_chuan)
[1] TRUE

25.1.2 Tham khảo

  • https://www.magesblog.com/post/2012-02-09-reshape-function/

  • https://search.r-project.org/CRAN/refmans/splitstackshape/html/Reshape.html