1 Các hàm trong tidyverse

Bộ dữ liệu sử dụng là iris. Kiểm tra sơ bộ về bộ dữ liệu này

library(dplyr)
head(iris)
str(iris)
## 'data.frame':    150 obs. of  5 variables:
##  $ Sepal.Length: num  5.1 4.9 4.7 4.6 5 5.4 4.6 5 4.4 4.9 ...
##  $ Sepal.Width : num  3.5 3 3.2 3.1 3.6 3.9 3.4 3.4 2.9 3.1 ...
##  $ Petal.Length: num  1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 ...
##  $ Petal.Width : num  0.2 0.2 0.2 0.2 0.2 0.4 0.3 0.2 0.2 0.1 ...
##  $ Species     : Factor w/ 3 levels "setosa","versicolor",..: 1 1 1 1 1 1 1 1 1 1 ...

Giải thích các Argument được áp dụng trong công thức:

  • tbl: Tên của bảng.
  • funs: List các function được gọi vào để biến đổi dữ liệu. Có thể là vector function names, hoặc một function.
  • predicate: là function logic (chỉ trả về TRUE hoặc FALSE) sử dụng để xác định trường hợp nào công thức sẽ được áp dụng.
  • vars: tên các cột, có thể sử dụng vị trí hoặc tên.

3 kiểu đuôi hàm với ý nghĩa áp dụng

  • _all: biến đổi ảnh hưởng đến mọi trường trong hàm
  • _at: biến đổi chỉ ảnh hưởng đến được lựa chọn trong argument vars
  • _if: biến đổi chỉ ảnh hưởng đến những trường thỏa mãn predicate function.

Các hàm cơ bản trong tidyverse

  • mutate(tbl,…): Tạo trường mới vào bảng sẵn có theo công thức được ta tự định nghĩa
  • transmute(tbl,…: Tạo thêm trường mới vào bảng sẵn có theo công thức được ta tự định nghĩa và drop các trường cũ trong bảng.
#Tạo thêm trường Sepal.NuaChuvi = Sepal.Length+Sepal.Width đính kèm vào bảng cũ
iris %>% mutate(Sepal.NuaChuVi = Sepal.Length + Sepal.Width) %>% head()
#Chỉ tạo thêm trường Sepal.NuaChuvi và drop các trường thuộc bảng cũ.
iris %>% transmute(Sepal.NuaChuVi = Sepal.Length + Sepal.Width) %>% head()

Các hàm thuộc nhóm transmute kết quả hoàn toàn tương tự như mutate chỉ khác ở điểm duy nhất là có giữa nguyên các trường cũ hay không nên các ví dụ sau sẽ không trình bày về transmute. Bạn đọc tự rút ra từ mutate.

  • summarise(tbl,…): Được sử dụng với grouped data được sử dụng bởi công thức group_by(). Kết quả output là mỗi dòng tại mỗi nhóm. Argument ‘…’ được truyền vào là các cặp name-value function. Trong đó name là tên trường, value function là các hàm chẳng hạn như mean(x),n() ,sum(is.na(0)),….
#Muốn biết bình quân Sepal.Length và số dòng ứng với mỗi group Species()
iris %>% group_by(Species) %>% 
  summarise(mean = mean(Sepal.Length), n = n())
#Khi đó dữ liệu đã được group nên các thống kê sẽ thực hiện cho mỗi group và kết quả trả về ứng với mỗi dòng là 1 group.
#Muốn biết đang group theo biến nào dùng groups()
iris %>% group_by(Species, Petal.Length) %>% 
  groups()
## [[1]]
## Species
## 
## [[2]]
## Petal.Length
#Muốn không group các biến nữa dùng ungroup()
iris %>% group_by(Species, Petal.Length) %>% 
  ungroup() %>% 
  groups()
## NULL
#Muốn biết bình quân Sepal.Length và số dòng cho toàn bộ các quan sát
iris %>% summarise(mean = mean(Sepal.Length), n = n())
  • summarise_all(tbl,funs,…): Biến đổi toàn bộ các trường theo funs.Điểm khác biệt giữa summarise_all và summarise đó là summarise all sẽ luôn thực hiện cho toàn bộ các trường, còn summarise bạn sẽ phải tự định nghĩa các trường cần tính toán theo cặp name-value function.
  • summarise_at(tbl,vars,funs,…): Biến đổi các trường trong danh sách vars theo funs.
#Tính mean toàn bộ các trường:
iris[,1:4] %>% summarise_all(mean)
#TÍnh thêm sum nữa:
iris[,1:4] %>% summarise_all(c('mean','sum'))
#Nếu bạn không biết công dụng tuyệt vời của summarise_all trong thống #kê cho toàn bộ các trường bạn sẽ phải dùng lapply 2 lần
iris[,1:4] %>% lapply(mean)
## $Sepal.Length
## [1] 5.843333
## 
## $Sepal.Width
## [1] 3.057333
## 
## $Petal.Length
## [1] 3.758
## 
## $Petal.Width
## [1] 1.199333
iris[,1:4] %>% lapply(sum)
## $Sepal.Length
## [1] 876.5
## 
## $Sepal.Width
## [1] 458.6
## 
## $Petal.Length
## [1] 563.7
## 
## $Petal.Width
## [1] 179.9
#TÍnh mean các trường theo group
iris %>% group_by(Species) %>% 
  summarise_all(mean)
#Chỉ tính mean cho một số trường thuộc về Sepal
iris %>% summarise_at(vars(matches("Sepal")),mean)
#Tính mean theo group cho các trường thuộc về Sepal
iris %>% group_by(Species) %>% 
  summarise_at(vars(matches("Sepal")),mean)
  • summarise_if(predicate, funs,…): Biến đổi dữ liệu nếu thỏa mãn predicate. Về bản chất nó chính là hàm summarise_all kết hợp với Filter (của hàm base) theo predicate.
#Tính mean các trường là is.numeric
iris %>% summarise_if(is.numeric,mean)
#Về bản chất là kết hợp giữa Filter và summarise_all
iris %>% Filter(is.numeric,.) %>% summarise_all(mean)
#Hoặc muốn chậm hơn thì dùng select_if trong cùng hệ sinh thái.
iris %>% select_if(is.numeric) %>% summarise_all(mean)
  • mutate_all(tbl,funs,…): Biến đổi dữ liệu tại toàn bộ các trường.
#Tính log của toàn bộ các trường
iris[,1:4] %>% mutate_all(log) %>% head()
#Nếu muốn tự định nghĩa các trường không sẵn có thì truyền vào funs
iris[,1:4] %>% mutate_all(funs((.-min(.))/sd(.))) %>% head()
#Hoặc lưu hàm vào Global Environment trước rồi gọi sau:
chuan_hoa_sd <- function(x) {(x-min(x))/sd(x)}
iris[,1:4] %>% mutate_all(chuan_hoa_sd) %>% head()
#Ai không biết đến có thể dùng lapply
#iris[,1:4] %>% lapply(chuan_hoa_sd)
#Hoặc dùng apply để trả về matrix
iris[,1:4] %>% apply(2,chuan_hoa_sd) %>% head()
##      Sepal.Length Sepal.Width Petal.Length Petal.Width
## [1,]    0.9661064    3.441422    0.2265906   0.1311927
## [2,]    0.7245798    2.294282    0.2265906   0.1311927
## [3,]    0.4830532    2.753138    0.1699430   0.1311927
## [4,]    0.3622899    2.523710    0.2832383   0.1311927
## [5,]    0.8453431    3.670851    0.2265906   0.1311927
## [6,]    1.3283963    4.359135    0.3965336   0.3935780
  • mutate_at(tbl,vars,funs,…): Biến đổi dữ liệu tại các trường trong vars theo funs.
#Chuyển trường Sepal.Length sang giá trị mean của nó
iris %>% mutate_at(c("Sepal.Length"),mean) %>% head()
#Hoặc sử dụng thứ tự của trường
iris %>% mutate_at(c(1),mean) %>% head()
#Hoặc sử dụng vars(mathes('fieldName')) nhưng rất hiếm, chủ yếu dùng #trong lập trình nâng cao
iris %>% mutate_at(vars(matches("Sepal")), log) %>% head()
#Những trường thỏa mãn có tên chứa Sepal sẽ được lựa chuyển sang log
iris %>% mutate_at(vars(matches("Sepal")), log) %>% head()
  • mutate_if(tbl,predicate,funs,…): Biến đổi dữ liệu thỏa mãn predicate theo funs. Chủ yếu sử dụng trong convert định dạng.
#Định dạng của Species đang là Factor
iris %>% str()
## 'data.frame':    150 obs. of  5 variables:
##  $ Sepal.Length: num  5.1 4.9 4.7 4.6 5 5.4 4.6 5 4.4 4.9 ...
##  $ Sepal.Width : num  3.5 3 3.2 3.1 3.6 3.9 3.4 3.4 2.9 3.1 ...
##  $ Petal.Length: num  1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 ...
##  $ Petal.Width : num  0.2 0.2 0.2 0.2 0.2 0.4 0.3 0.2 0.2 0.1 ...
##  $ Species     : Factor w/ 3 levels "setosa","versicolor",..: 1 1 1 1 1 1 1 1 1 1 ...
#Chuyển định dạng của trường Species từ factor sang character
iris %>% mutate_if(is.factor,as.character) %>% str()
## 'data.frame':    150 obs. of  5 variables:
##  $ Sepal.Length: num  5.1 4.9 4.7 4.6 5 5.4 4.6 5 4.4 4.9 ...
##  $ Sepal.Width : num  3.5 3 3.2 3.1 3.6 3.9 3.4 3.4 2.9 3.1 ...
##  $ Petal.Length: num  1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 ...
##  $ Petal.Width : num  0.2 0.2 0.2 0.2 0.2 0.4 0.3 0.2 0.2 0.1 ...
##  $ Species     : chr  "setosa" "setosa" "setosa" "setosa" ...
#Chuyển sang dạng log đối với các trường numerics
iris %>% mutate_if(is.numeric,log) %>% head()

2 Các hàm join của dplyr

Giả sử ta có 2 data.frame mẫu về student và province được tạo như bên dưới.

student <- data.frame(STUDENT_ID = c(1:5), 
                      NAME = c("Khanh", "Linh", "Ngoc", "Toan", "Hanh"), 
                      CLASS = c("7A","8B","7A","8B","7E"), 
                      MATH_POINT = c(10,9,8,9,9), PROVINCE_ID = c(1,2,4,2,1))
student 
province <- data.frame(PROVINCE_ID = c(1,2,3), 
                       PROVINCE_NAME = c("Ha Noi", "Thanh Hoa","Da Nang"))
province

Đối với bảng student key sẽ là STUDENT_ID và bảng province có key là PROVINCE_ID. Ta sẽ dùng 2 bảng này để tìm hiểu về các lệnh join của dplyr.

2.1 Hàm left_join

  • Cú pháp: left_join(left_table,right_table, by = “key_name”)

  • left join: lấy toàn các record gốc ở bảng bên trái join với các record ở bảng bên phải theo điều kiện của key. Nếu thỏa mãn điều kiện bảng sẽ được join, nếu không thỏa mãn điều kiện bảng sẽ để NULL các trường thuộc bảng bên phải.

library(dplyr)
left_join(student, province, by = "PROVINCE_ID")

2.2 Hàm right_join

  • Cú pháp: right_join(left_table,right_table, by = “key_name”)

  • right join: tương tự như left join nhưng các record gốc được xét là bảng bên phải và bảng bên trái sẽ để null nếu không thỏa mãn điều kiện join.

library(dplyr)
right_join(student, province, by = "PROVINCE_ID")

2.3 Hàm inner_join

  • Cú pháp: inner_join(left_table,right_table, by = “key_name”)

  • inner join: sẽ chỉ lấy các record thỏa mãn điều kiện join của hai bảng, nếu không thỏa mãn điều kiện sẽ bị loại bỏ khỏi kết quả.

library(dplyr)
inner_join(student, province, by = "PROVINCE_ID")

2.4 Hàm full_join

  • Cú pháp: full_join(left_table,right_table, by = “key_name”)

  • full join: sẽ lấy toàn bộ record của bảng bên trái ghép với toàn bộ record của bảng bên phải. Nếu không thỏa mãn điều kiện join thì bảng bên trái hoặc bên phải dữ liệu sẽ bị null.

library(dplyr)
full_join(student, province, by = "PROVINCE_ID")

2.5 Hàm anti_join

  • Cú pháp: anti_join(left_table,right_table, by = “key_name”)

  • anti join: lấy toàn bộ record ở bảng bên trái làm gốc join với dữ liệu bảng bên phải đối với dữ liệu không thỏa mãn điều kiện join sẽ được lựa chọn vào kết quả trả về.

#lay nhung record cua student ma province khong co trong bang province
library(dplyr)
anti_join(student, province, by = "PROVINCE_ID")
#lay nhung record cua province ma province khong co trong bang student
anti_join(province,student,by = "PROVINCE_ID")

Hàm anti join là một hàm mà chỉ R mới có. Thậm chí ngôn ngữ truy vấn dữ liệu như SQL cũng không có bất kì một hàm nào thực hiện chức năng này.

2.6 Hàm merge của base

Đây là một hàm của base chứ không nằm trong dplyr. Cú pháp của hàm như sau:

  • merge(tbl.x, tbl.y, by.x=“x_key”, by.y = “y.key”):
    Trong đó tbl.x, tbl.y là các bảng bên trái và bên phải. by.x và by.y là tên của 2 key sử dụng để merge. Hàm merge() có chức năng tương tự như inner_join() khi thực hiện join 2 bảng theo key.
merge(student,province, by.x="PROVINCE_ID", by.y = "PROVINCE_ID")
#Trường hợp 2 trường key ở 2 bảng trùng tên có thể viết ngắn gọn:
merge(student,province, by = "PROVINCE_ID")

3 Các hàm reshape data frame

Dữ liệu được sử dụng trong ví dụ này là ship. Bộ dữ liệu thuộc package MASS tổng hợp thông tin vê số vụ tai nạn vận chuyển theo ngành dịch vụ.

library(MASS)
ships %>% head(10)

4 Sử dụng câu lệnh SQL

4.1 Package RODBC

Nếu bạn là người mới đối với ngôn ngữ R và chưa thành thạo hết các hàm xứ lý dữ liệu của R thì có thể sử dụng package RODBC để kết nối với SQL và sử dụng câu lệnh như của SQL. Quá trình xử lý gồm 2 bước:

  1. Kết nối dữ liệu: Tạo dựng một đường kết nối đến server thông qua odbcDriverConnect:
  • odbcDriverConnect(‘driver={SQL Server};
    server=serverName;
    database=dbName;
    trusted_connection=true’):
    Trong đó driver là tên của driver sử dụng để kết nối, mặc định là {SQL Server}; server là tên server; database là tên dbName; trusted_connection=true là kết nối không sử dụng mật khẩu.
library(RODBC)
dbhandler <- odbcDriverConnect('driver={SQL Server};
                               server=192.168.91.2;
                               database=AdventureWorks;
                               trusted_connection=true')
  1. Sử dụng kết nối vừa tạo để truy vấn dữ liệu:
  • sqlQuery(dbhandler,sqlQuery): Trong đó dbhandler là kết nối được khởi tạo ở bước 1, sqlQuery là câu lệnh SQL sử dụng để truy vấn dữ liệu.
TransactHistory <- sqlQuery(dbhandler,
  "select TransactionDate,Quantity,ActualCost from AdventureWorks.Production.TransactionHistory"
)
TransactHistory %>% head(10)

4.2 Package sqldf

Ngoài ra nếu không kết nối tới SQL mà chỉ cần truy vấn dữ liệu trong Global Enviroment bằng câu lệnh SQL thì có thể sử dụng package sqldf

library(sqldf)
#Tính trung bình độ rộng tán, chiều dài tán và số lượng hoa ở mỗi loài trong bộ dữ liệu iris
sqldf("select Species, 
        AVG([Sepal.Length]) AvgSepalLength,
        AVG([Sepal.Width]) AvgSepalWidth, 
        COUNT(*) n    
      from iris group by Species"
)
## Note: no visible binding for global variable 'ref'
#Sử dụng dplyr thì đơn giản như sau:
iris %>% group_by(Species) %>% 
  summarise(AvgSepalLength = mean(Sepal.Length), 
            AvgSepalWidth = mean(Sepal.Width), 
            n = n())

5 Các hàm reshape dataframe trong package reshape

5.1 Hàm melt()

  • melt(tbl, id): Trong đó tbl là dataframe, id là vectors tên các key được sử dụng để tổ cThống kê Trung bình h Sal.Lengthê , Sepal.Width và số lượng các cádfmẫu hoa tcheo từng loại hoa ttrong bộ dữ liệu irisrường key để sắp xSpecies, average(Sepal.Length) AvgSepalLength, average(Sepal.Width) AvgSepalWidth, count(*) n whereiri> group by Speciestừng iris trường còn lại. Trong kết quả trả về, các giá trị ở trường value sẽ là giá trị tương ứng của trường gốc có tên được xác định ở trường variable.
library(reshape)
melt.ships <- melt(ships,id = c("type", "year"))
melt.ships

Khi đó tên các trường không phải key được sử dụng để xác định label cho record trong trường variable. Trường value là giá trị ở bảng gốc ứng với tên trường và nhóm key.

5.2 Hàm cast()

  • cast(tbl, formula = …~variable, funs,…) : Trong đó tbl là data.frame và formula là công thức sử dụng để reshape dữ liệu dựa trên biến variable. Trong công thức của formula phần … là các trường key được truyền vào. funs là hàm sử dụng để biến đổi. Hàm cast() có thể coi là biến đổi ngược của hàm melt(). Giả sử chúng ta muốn trở về dạng bảng ban đầu dựa trên bảng đã được reshape ở ví dụ trên là melt.ships
cast(melt.ships,type+year~variable, sum)

Kết quả cho thấy các nhóm thuộc cột variable đã được trải ra thành các trường ở data.frame mới dựa trên công thức sum.

Một ví dụ khác cho thấy tiện ích của hàm cast() so với SQL.
Chúng ta có bảng AdventureWorks.Production.TransactionHistory là bảng lịch sử giao dịch mua hàng của khác hàng đã được ví dụ trong phần SQL.

TransactHistory %>% str()
## 'data.frame':    113443 obs. of  3 variables:
##  $ TransactionDate: POSIXct, format: "2007-09-01" "2007-09-01" ...
##  $ Quantity       : int  2 1 1 1 1 1 1 1 1 1 ...
##  $ ActualCost     : num  0 0 0 0 0 0 0 0 0 0 ...

Chẳng hạn chúng ta cần biết tổng chi phí của từng nhóm số lượng Quantity theo ngày giao dịch trong đó các nhóm Quantity này được trải theo dạng cột. Trên SQL chúng ta sẽ phải sử dụng hàm pivot khá phức tạp:

SummaryTransCost <- sqlQuery(dbhandler,
  "SELECT TransactionDate, [1],[2],[3],[4],[5] FROM 
    (SELECT TransactionDate, Quantity, ActualCost FROM AdventureWorks.Production.TransactionHistory) A
      PIVOT (
        SUM(ActualCost)
        FOR Quantity IN ([1],[2],[3],[4],[5])
      ) as PivotTable;
  "
)
SummaryTransCost %>% head(10)

Tuy nhiên sử dụng hàm cast() trên R đơn giản hơn rất nhiều:

cast(TransactHistory,TransactionDate ~ Quantity,sum) %>% head(10)

Điều đó cho thấy các xử lý dữ liệu trên R rất phong phú và hoàn toàn có thể đáp ứng được các yêu cầu xử lý chuyên sâu. Tuy nhiên hạn chế là tốc độ xử lý của R so với SQL thì đang kém gấp vài nghìn lần.