Lấy và làm sạch dữ liệu với R: Dọn dẹp dữ liệu với tidyr

Data Cleaning

Data Cleaning

Trong bài viết này, ta sẽ học cách dọn dẹp dữ liệu với tidyr package. Các phần trong bài viết này đòi hỏi sử dụng dplyr. Nếu bạn không có kiến thức căn bản về dplyr, bạn nên đọc trước loạt bài dplyr trước khi bắt đầu bài viết này.

Để bắt đầu, chúng ta cần load thư viện tidyr.

library(tidyr)

Tác giả của tidyr, Hadley Wickham, có thảo luận về triết lý đằng sau việc dọn dẹp dữ liệu (tidy data) trong bài báo: http://vita.had.co.nz/papers/tidy-data.pdf. Bài báo này nên đọc nếu bạn làm việc với phân tích dữ liệu.

Tidy data là định dạng chuẩn cho các phân tích exploration và bước tiền xử lý cho các công cụ khác làm việc mượt mà hơn trên dữ liệu đã được dọn dẹp. Cụ thể, tidy data thỏa mãn ba điều kiện.

  • Mỗi thuộc tính chỉ thuộc một cột dữ liệu
  • Mỗi đối tượng chỉ chứa trên một dòng dữ liệu
  • Mỗi loại đối tượng chỉ thuộc một bảng dữ liệu

Mỗi thuộc tính chỉ thuộc một cột dữ liệu

Những tập dữ liệu không thỏa các điều kiện trên được xem là dữ liệu hỗn độn (messy data). Vấn đề đầu tiên đó là cột dữ liệu chỉ chứa giá trị chứ không có tên thuộc tính. Ta lấy tập dữ liệu ‘students’ sau làm ví dụ cho trường hợp này.

students
  grade male female
1     A    1      5
2     B    5      0
3     C    5      2
4     D    5      5
5     E    7      4

Cột đầu tiên biểu diễn xếp hạng có thể có của từng học sinh. Cột thứ hai và ba thể hiện số lượng nữ sinh và nam sinh tương ứng với xếp hạng này. Tập dữ liệu này thật ra có ba thuộc tính: grade, sex, và count. Thuộc tính đầu tiên là grade đã thỏa điều kiện với cột đầu tiên. Thuộc tính thứ hai sex được biểu diễn bởi hai cột dữ liệu hai và ba. Thuộc tính thứ ba count là số lượng học sinh cho mỗi kết hợp của grade và sex.

Đễ dọn dẹp tập dữ liệu students, ta cần một cột dữ liệu cho từng thuộc tính trên. Chúng ta sẽ dùng hàm gather() từ thư viện tidyr để hoàn thành tác vụ này.

# Gather takes multiple columns and collapses into key-value pairs,
# duplicating all other columns as needed. You use ‘gather()’ when
# you notice that you have columns that are not variables.
gather(students, sex, count, -grade)
   grade    sex count
1      A   male     1
2      B   male     5
3      C   male     5
4      D   male     5
5      E   male     7
6      A female     5
7      B female     0
8      C female     2
9      D female     5
10     E female     4

Mỗi dòng dữ liệu bây giờ chỉ thể hiện một đối tượng duy nhất được đặc trưng bởi kết hợp giữa thuộc tính grade và sex. Mỗi thuộc tính (grade, sex, và count) được lưu bởi duy nhất một cột dữ liệu. Ta đã có tidy data.

Điều quan trọng chúng ta cần hiểu được ý nghĩa của các đối số truyền vào hàm gather(). Đối số data ở đây là students. Đối số key và value gồm có sex và count để đặt tên cho cột thuộc tính của tidy dataset. Đối số sau cùng là grade, ta muốn gom tất cả các cột dữ liệu ngoại trừ cột dữ liệu grade (vì grade đã thảo điều kiện tidy data).

Tập dữ liệu lộn xộn thứ hai ta sẽ quan sát có nhiều thuộc tính được lưu bên trong duy nhất một cột thuộc tính. Ta có tập dữ liệu student2 như sau:

students2
  grade male_1 female_1 male_2 female_2
1     A      3        4      3        4
2     B      6        4      3        5
3     C      7        4      3        8
4     D      4        0      8        1
5     E      1        1      2        7

Tập dữ liệu này khá tương đồng với tập dữ liệu ban đầu, ngoại trừ ta có thêm hai lớp phân biệt 1 và 2. Chúng ta có thêm tổng số lượng cho từng lớp. students2 bị xếp vào tập dữ liệu lộn xộn (messy data) khi cột thuộc tính là các giá trị (male_1, female_1,…) mà không phải là tên thuộc tính (sex, class, và count).

Tuy nhiên, nó cũng chứa nhiều thuộc tính được lưu trong mỗi cột (sex và class) là dấu hiệu khác của dữ liệu lộn xộn. Dọn dẹp tập dữ liệu này gồm hai bước. Ta hãy dùng hàm gather() để nạp chồng các cột dữ liệu của students2 như lúc nãy. Lần này, đặt tên cho đối số ‘key’ là sex_class và ‘value’ là count. Ta lưu kết quả trả về vào biến res.

res <- gather(students2, sex_class, count, -grade)
res
   grade sex_class count
1      A    male_1     3
2      B    male_1     6
3      C    male_1     7
4      D    male_1     4
5      E    male_1     1
6      A  female_1     4
7      B  female_1     4
8      C  female_1     4
9      D  female_1     0
10     E  female_1     1
11     A    male_2     3
12     B    male_2     3
13     C    male_2     3
14     D    male_2     8
15     E    male_2     2
16     A  female_2     4
17     B  female_2     5
18     C  female_2     8
19     D  female_2     1
20     E  female_2     7
[/code]

Ta đã hoàn thành nửa bước dọn dẹp dữ liệu, nhưng chúng ta vẫn còn hai thuộc tính sex và class được lưu trong cùng một cột sex_class. tidyr cung cấp hàm separate() hữu dụng cho việc chia một cột dữ liệu thành nhiều cột. Ta gọi hàm này cho res để chia cột sex_class thành sex và class.

[code language="r"]separate(res, sex_class, c("sex", "class"))
   grade    sex class count
1      A   male     1     3
2      B   male     1     6
3      C   male     1     7
4      D   male     1     4
5      E   male     1     1
6      A female     1     4
7      B female     1     4
8      C female     1     4
9      D female     1     0
10     E female     1     1
11     A   male     2     3
12     B   male     2     3
13     C   male     2     3
14     D   male     2     8
15     E   male     2     2
16     A female     2     4
17     B female     2     5
18     C female     2     8
19     D female     2     1
20     E female     2     7
[/code]

Thật tiện lợi, hàm separate() có khả năng biết được cách để chia cột sex_class. Mặc định hàm này chia dựa trên dấu gạch dưới "_", ta có thể chỉ định kí tự phân chia bằng đối số 'sep'.

Dọn dẹp tập dữ liệu students2 cần hai hàm gather() và separate(). Như vậy, ta có thể sử dụng kĩ thuật chaining bên dplyr bằng toán tử %&gt;% để kết nhiều hàm cùng lúc.

[code language="r"]students2 %>%
  gather(sex_class, count, -grade) %>%
  separate(sex_class, c("sex", "class")) %>%
  print

Mỗi đối tượng chỉ chứa trên một dòng dữ liệu

Dấu hiệu thứ ba của tập dữ liệu hỗn độn là khi các thuộc tính được lưu trên cả dòng và cột. Ta lấy ví dụ ở tập dữ liệu students3 sau:

students3
    name    test class1 class2 class3 class4 class5
1  Sally midterm      A         B      
2  Sally   final      C         C      
3   Jeff midterm         D         A   
4   Jeff   final         E         C   
5  Roger midterm         C            B
6  Roger   final         A            A
7  Karen midterm            C      A   
8  Karen   final            C      A   
9  Brian midterm      B               A
10 Brian   final      B               C

Trong students3, ta có giá trị xếp hạng midterm và final exam cho năm học sinh, mỗi học sinh đăng ký học năm lớp tương ứng.

Thuộc tính đầu tiên, name, ta sẽ giữ nguyên. Năm cột dữ liệu còn lại từ class1 đến class5 nên được gom thành cột thuộc tính class. Giá trị trong cột test là midterm và final cũng nên được gom thành các cột thuộc tính tương ứng.

students3 %>%
  gather(class, grade, class1:class5, na.rm = TRUE) %>%
  print

    name    test  class grade
1  Sally midterm class1     A
2  Sally   final class1     C
3  Brian midterm class1     B
4  Brian   final class1     B
5   Jeff midterm class2     D
6   Jeff   final class2     E
7  Roger midterm class2     C
8  Roger   final class2     A
9  Sally midterm class3     B
10 Sally   final class3     C
11 Karen midterm class3     C
12 Karen   final class3     C
13  Jeff midterm class4     A
14  Jeff   final class4     C
15 Karen midterm class4     A
16 Karen   final class4     A
17 Roger midterm class5     B
18 Roger   final class5     A
19 Brian midterm class5     A
20 Brian   final class5     C

Bước tiếp theo ta sẽ cần đến hàm spread().

# Spread a key-value pair across multiple columns.
# spread(data, key, value, fill = NA, convert = FALSE, drop = TRUE)
students3 %>%
  gather(class, grade, class1:class5, na.rm = TRUE) %>%
  spread(test, grade) %>%
  print

    name  class final midterm
1  Brian class1     B       B
2  Brian class5     C       A
3   Jeff class2     E       D
4   Jeff class4     C       A
5  Karen class3     C       C
6  Karen class4     A       A
7  Roger class2     A       C
8  Roger class5     A       B
9  Sally class1     C       A
10 Sally class3     C       B

Sau cùng, chúng ta muốn các giá trị trong cột class đơn giản là 1, 2, …, 5 thay vì class1, class2, …, class5. Chúng ta có thể dùng hàm extract_numeric() của tidyr để làm việc này.

students3 %>%
  gather(class, grade, class1:class5, na.rm = TRUE) %>%
  spread(test, grade) %>%
  mutate(class = extract_numeric(class)) %>%
  print

    name class final midterm
1  Brian     1     B       B
2  Brian     5     C       A
3   Jeff     2     E       D
4   Jeff     4     C       A
5  Karen     3     C       C
6  Karen     4     A       A
7  Roger     2     A       C
8  Roger     5     A       B
9  Sally     1     C       A
10 Sally     3     C       B

Mỗi loại đối tượng chỉ thuộc một bảng dữ liệu

Trường hợp thứ tư của dữ liệu hỗn độn đó là nhiều loại đối tượng khác nhau được lưu chung một bảng. Ta xét ví dụ này với students4

students4
    id  name sex class midterm final
1  168 Brian   F     1       B     B
2  168 Brian   F     5       A     C
3  588 Sally   M     1       A     C
4  588 Sally   M     3       B     C
5  710  Jeff   M     2       D     E
6  710  Jeff   M     4       A     C
7  731 Roger   F     2       C     A
8  731 Roger   F     5       B     A
9  908 Karen   M     3       C     C
10 908 Karen   M     4       A     A

students4 khá giống students3. Điểm khác biệt duy nhất đó là students4 có thêm cột id cho mỗi học sinh cùng với giới tính (M = male; F = female).

Nhìn sơ qua, ta không thấy gì bất ổn với students4. Tất cả các cột đều là thuộc tính và tất cả các đối tượng đều thuộc một dòng. Tuy nhiên, để ý cột id, name, và sex lặp lại hai lần, điều này có vẻ dư thừa. Đây là dấu hiệu dữ liệu của chúng ta chứa nhiều loại đối tượng trên cùng một bảng dữ liệu.

Giải pháp của chúng ta chia students4 thành hai bảng tách biệt — một bảng chứa thông tin cơ bản của student (id, name, và sex) bảng còn lại chứa grades (id, class, midterm, final).

# bảng dữ liệu chứa thông tin học sinh 
student_info %
  select(id, name, sex) %>%
  unique %>%
  print

   id  name sex
1 168 Brian   F
3 588 Sally   M
5 710  Jeff   M
7 731 Roger   F
9 908 Karen   M

# bảng dữ liệu chứa thông tin grade 
gradebook %
  select(id, class, midterm, final) %>%
  print

    id class midterm final
1  168     1       B     B
2  168     5       A     C
3  588     1       A     C
4  588     3       B     C
5  710     2       D     E
6  710     4       A     C
7  731     2       C     A
8  731     5       B     A
9  908     3       C     C
10 908     4       A     A

Điều quan trọng cần lưu ý là chúng ta đặt cột id ở cả hai bảng dữ liệu. id ở đây là ‘primary key’ để tham chiếu đến thông tin học sinh. Không có chỉ định cột id cho bảng gradebook, ta không thể xác định được đây là xếp hạng của học sinh nào.

Trường hợp cuối cùng của dữ liệu hỗn độn đó là một loại đối tượng lại được lưu ở nhiều bảng khác nhau. Trái ngược với trường hợp thứ tư. Ta lấy ví dụ sau:

# danh sách đậu 
passed
   name class final
1 Brian     1     B
2 Roger     2     A
3 Roger     5     A
4 Karen     4     A

# danh sách rớt 
failed
   name class final
1 Brian     5     C
2 Sally     1     C
3 Sally     3     C
4  Jeff     2     E
5  Jeff     4     C
6 Karen     3     C

Các giáo viên quyết định xem xếp hạng final như thế nào để quyết định học sinh đâu hay rớt. Như bạn có thể suy luận từ tập dữ liệu trên, những học sinh nào có xếp loại A hoặc B thì đậu và ngược lại thì rớt.

Tên của các tập dữ liệu này sẽ là giá trị của cột thuộc tính mới gọi là ‘status’. Trước khi kết hai bảng này lại với nhau, ta sẽ thêm một cột dữ liệu mới để chứa thông tin này.

Sử dụng hàm mutate() để thêm cột thuộc tính mới cho bảng passed. Cột thuộc tính này có tên là status với giá trị là passed (kiểu character string). Tương tự cho cột thuộc tính ở bảng failed.

passed <- passed %>% mutate(status = "passed")
   name class final status
1 Brian     1     B passed
2 Roger     2     A passed
3 Roger     5     A passed
4 Karen     4     A passed

failed <- failed %>% mutate(status = "failed")
   name class final status
1 Brian     5     C failed
2 Sally     1     C failed
3 Sally     3     C failed
4  Jeff     2     E failed
5  Jeff     4     C failed
6 Karen     3     C failed

Ta sử dụng hàm bind_rows() để kết hai bảng này lại

bind_rows(passed, failed)
Source: local data frame [10 x 4]

    name class final status
1  Brian     1     B passed
2  Roger     2     A passed
3  Roger     5     A passed
4  Karen     4     A passed
5  Brian     5     C failed
6  Sally     1     C failed
7  Sally     3     C failed
8   Jeff     2     E failed
9   Jeff     4     C failed
10 Karen     3     C failed

Tất nhiên, chúng ta có thể sắp xếp các dòng dữ liệu theo ý của mình, nhưng điều quan trọng ở đây là mỗi dòng dữ liệu thể hiện một loại đối tượng, mỗi cột dữ liệu là một thuộc tính, và một bảng chỉ chứa một loại đối tượng duy nhất. Như vậy, tập dữ liệu của chúng ta là tidy data.

Chúng ta đã khảo sát khá nhiều ở bài viết này. Bây giờ là lúc tổng hợp mọi thứ để làm việc với dữ liệu thực tế. SAT là một chứng chỉ thông dụng kiểm tra trình độ toán học ở Mĩ gồm: kĩ năng đọc hiểu, toán, và kĩ năng viết luận. Các sinh viên có thể đạt tối đa 800 điểm ở mỗi phần. Tập dữ liệu sau chứa thông tin các sinh viên thực hiện cuộc kiểm tra này. Lấy từ tập dữ liệu ‘Total Group Report 2013’. Có thể download tại: http://research.collegeboard.org/programs/sat/data/cb-seniors-2013.

sat
Source: local data frame [6 x 10]

  score_range read_male read_fem read_total math_male math_fem math_total
1     700-800     40151    38898      79049     74461    46040     120501
2     600-690    121950   126084     248034    162564   133954     296518
3     500-590    227141   259553     486694    233141   257678     490819
4     400-490    242554   296793     539347    204670   288696     493366
5     300-390    113568   133473     247041     82468   131025     213493
6     200-290     30728    29154      59882     18788    26562      45350
Variables not shown: write_male (int), write_fem (int), write_total (int)
# 1. select() all columns that do NOT contain the word "total",
# since if we have the male and female data, we can always
# recreate the total count in a separate column, if we want it.
# Hint: Use the contains() function, which you'll
# find detailed in 'Special functions' section of ?select.
#
# 2. gather() all columns EXCEPT score_range, using
# key = part_sex and value = count.
#
# 3. separate() part_sex into two separate variables (columns),
# called "part" and "sex", respectively. You may need to check
# the 'Examples' section of ?separate to remember how the 'into'
# argument should be phrased.
# 4. Use group_by() (from dplyr) to group the data by part and
# sex, in that order.
#
# 5. Use mutate to add two new columns, whose values will be
# automatically computed group-by-group:
#
#   * total = sum(count)
#   * prop = count / total

sat %>%
  select(-contains("total")) %>%
  gather(part_sex, count, -score_range) %>%
  separate(part_sex, c("part", "sex")) %>%
  group_by(part, sex) %>%
  mutate(total = sum(count),
         prop = count / total
  ) %>% print

Source: local data frame [36 x 6]
Groups: part, sex

   score_range part  sex  count  total       prop
1      700-800 read male  40151 776092 0.05173485
2      600-690 read male 121950 776092 0.15713343
3      500-590 read male 227141 776092 0.29267278
4      400-490 read male 242554 776092 0.31253253
5      300-390 read male 113568 776092 0.14633317
6      200-290 read male  30728 776092 0.03959324
7      700-800 read  fem  38898 883955 0.04400450
8      600-690 read  fem 126084 883955 0.14263622
9      500-590 read  fem 259553 883955 0.29362694
10     400-490 read  fem 296793 883955 0.33575578
..         ...  ...  ...    ...    ...        ...

Ở bài viết này, chúng ta đã học được các dọn dẹp dữ liệu với tidyr và dplyr. Các công cụ này sẽ giúp bạn dành ít thời gian và công sức trong việc lấy và làm sạch dữ liệu để có nhiều thời gian hơn chuẩn bị cho các các bước phân tích kế tiếp.

Nguồn tham khảo: http://swirlstats.com/

Tham khảo thêm: Introduction to data quality

Advertisements

Trả lời

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Đăng xuất / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Đăng xuất / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Đăng xuất / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Đăng xuất / Thay đổi )

Connecting to %s