Hướng dẫn cải tiến function step-by-step

Biên soạn: Duc Nguyen | Chuyên đào tạo kỹ năng xử lý dữ liệu sử dụng phần mềm R | Website: www.tuhocr.com

Tình huống

Bạn xây dựng function filter theo chỉ tiêu ở các file .csv như sau. Ý tưởng ban đầu là function này (click here)

  • Download dataset example 2007.rar gồm 10 file .csv, đồng nhất cấu trúc mercury-nitrite-id.

  • Download dataset example 2008.rar gồm 8 file .csv, không đồng nhất cấu trúc: khi file 2, 6, 8 có thêm cột COD.

## FUNCTION_MIN_CHI_TIEU

"%min_chi_tieu%" <- function(a, b) {
  # import dataset
  files_list <- list.files(b, full.names = TRUE)

  tmp_working <- vector(mode = "list", length = length(files_list))

  for (i in seq_along(files_list)) {
    tmp_working[[i]] <- read.csv(files_list[i])
    tmp_working[[i]]$Date <- as.Date(tmp_working[[i]]$Date, format = "%m/%d/%Y")
  }
  # find min value

  ket_qua_min <- data.frame()

  for (min_i in seq_along(tmp_working)) {
    gia_tri_min <- min(get(chi_tieu, tmp_working[[min_i]]), na.rm = TRUE)

    ### trích xuất ra vị trí index
    vi_tri <- which(get(chi_tieu, tmp_working[[min_i]]) == gia_tri_min)

    ket_qua_min <- rbind(ket_qua_min, tmp_working[[min_i]][vi_tri, ])
  }
  print(paste("Kết quả lọc theo giá trị MIN của", chi_tieu, "là:"))
  return(ket_qua_min)
}

## FUNCTION_MAX_CHI_TIEU

"%max_chi_tieu%" <- function(a, b) {
  # import dataset
  files_list <- list.files(b, full.names = TRUE)

  tmp_working <- vector(mode = "list", length = length(files_list))

  for (i in seq_along(files_list)) {
    tmp_working[[i]] <- read.csv(files_list[i])
    tmp_working[[i]]$Date <- as.Date(tmp_working[[i]]$Date, format = "%m/%d/%Y")
  }
  # find max value

  ket_qua_max <- data.frame()

  for (max_i in seq_along(tmp_working)) {
    gia_tri_max <- max(get(chi_tieu, tmp_working[[max_i]]), na.rm = TRUE)

    ### trích xuất ra vị trí index
    vi_tri <- which(get(chi_tieu, tmp_working[[max_i]]) == gia_tri_max)

    ket_qua_max <- rbind(ket_qua_max, tmp_working[[max_i]][vi_tri, ])
  }
  print(paste("Kết quả lọc theo giá trị MAX của", chi_tieu, "là:"))
  return(ket_qua_max)
}

## FUNCTION RANGE CHỈ TIÊU

"%range_chi_tieu%" <- function(a, b) {
  # import dataset
  files_list <- list.files(b, full.names = TRUE)

  tmp_working <- vector(mode = "list", length = length(files_list))

  for (i in seq_along(files_list)) {
    tmp_working[[i]] <- read.csv(files_list[i])
    tmp_working[[i]]$Date <- as.Date(tmp_working[[i]]$Date, format = "%m/%d/%Y")
  }

  # tìm trong khoảng

  ket_qua_range <- data.frame()

  for (range_i in seq_along(tmp_working)) {
    ket_qua_tim_trong_khoang <- which(get(chi_tieu, tmp_working[[range_i]]) >= a[1] & 
                                          get(chi_tieu, tmp_working[[range_i]]) <= a[2])

    ket_qua_range <- rbind(ket_qua_range, tmp_working[[range_i]][ket_qua_tim_trong_khoang, ])
  }
  print(paste("Kết quả lọc trong khoảng, từ", a[1], "đến", a[2], "của", chi_tieu, "là:"))
  return(ket_qua_range)
} 

## FUNCTION GHÉP CHUNG

"%chi_tieu%" <- function(a, b) {
  if (length(a) == 1) {
    switch(a,
      "min" = a %min_chi_tieu% b,
      "max" = a %max_chi_tieu% b
    )
  } else {
    a %range_chi_tieu% b
  }
}  

## FUNCTION FINAL

filter_theo_chi_tieu <- function(chi_tieu = "mercury", a = "min", b = "2007") {
  chi_tieu <<- chi_tieu ### sử dụng super assign
  a %chi_tieu% b
}

Test OK với các file trong folder 2007 vì nó cùng cấu trúc.

filter_theo_chi_tieu(chi_tieu = "mercury", a = "max", b = "2007")
## [1] "Kết quả lọc theo giá trị MAX của mercury là:"
##             Date mercury nitrite ID
## 987   2005-09-13   19.10   0.149  1
## 2437  2007-09-03   27.90   1.320  2
## 1717  2005-09-13   22.50   0.133  3
## 17171 2005-09-13   23.60   0.347  4
## 9871  2005-09-13   20.40   0.887  5
## 1004  2004-09-30   12.80   0.241  6
## 1352  2005-09-13   19.80   0.432  7
## 13521 2005-09-13   20.20   0.496  8
## 256   2005-09-13   16.20   0.452  9
## 497   2003-05-12    2.27   0.367 10

Test không OK ở các file trong folder 2008. Lệnh rbind báo lỗi ghép không khớp số cột ở các data frame.

filter_theo_chi_tieu(chi_tieu = "mercury", a = "max", b = "2008")
## Error in rbind(deparse.level, ...): numbers of columns of arguments do not match

Lý do

Function ban đầu hoạt động tốt với các file .csv trong folder 2007 có cùng số cột như sau nên khi ghép lại bằng lệnh rbind thì ổn.

water_id1_2007 <- read.csv("2007/water001.csv")
head(water_id1_2007)
##       Date mercury nitrite ID
## 1 1/1/2003      NA      NA  1
## 2 1/2/2003      NA      NA  1
## 3 1/3/2003      NA      NA  1
## 4 1/4/2003      NA      NA  1
## 5 1/5/2003      NA      NA  1
## 6 1/6/2003      NA      NA  1
water_id2_2007 <- read.csv("2007/water002.csv")
head(water_id2_2007)
##       Date mercury nitrite ID
## 1 1/1/2001      NA      NA  2
## 2 1/2/2001      NA      NA  2
## 3 1/3/2001      NA      NA  2
## 4 1/4/2001      NA      NA  2
## 5 1/5/2001      NA      NA  2
## 6 1/6/2001      NA      NA  2

Tuy nhiên nếu có 1 vài file, ở đây ví dụ là file water002.csv trong folder 2008 thì có chèn thêm 1 cột mới, là cột COD, dẫn đến cấu trúc file khác với các file .csv còn lại. Do đó, để function rbind() nhận biết được ghép các data frame khác cột khi thực hiện lệnh for loop thì cần dùng lệnh bind_rows() trong package dplyr thay thế, vì lệnh này có khả năng ghép theo hàng, các data frame có số cột không đồng nhất.

water_id1_2008 <- read.csv("2008/water001.csv")
head(water_id1_2008)
##       Date mercury nitrite ID
## 1 1/1/2003      NA      NA  1
## 2 1/2/2003      NA      NA  1
## 3 1/3/2003      NA      NA  1
## 4 1/4/2003      NA      NA  1
## 5 1/5/2003      NA      NA  1
## 6 1/6/2003      NA      NA  1
water_id2_2008 <- read.csv("2008/water002.csv")
head(water_id2_2008)
##            Xuất hiện cột mới ở đây
##                       ↓
##       Date mercury   COD nitrite ID
## 1 1/1/2001      NA 0.699      NA  2
## 2 1/2/2001      NA    NA      NA  2
## 3 1/3/2001      NA    NA      NA  2
## 4 1/4/2001      NA    NA      NA  2
## 5 1/5/2001      NA    NA      NA  2
## 6 1/6/2001      NA    NA      NA  2

Cải tiến lần 1

Function ban đầu có tên là filter_theo_chi_tieu().

Function cải tiến lần 1, có tên là filter_theo_chi_tieu_enhanced() tăng khả năng filter các dataset khác số lượng cột. Những chỗ highlight là được cải tiến so với function ban đầu.

library(dplyr) # Load package này để dùng lệnh bind_rows() thay lệnh rbind()

## FUNCTION_MIN_CHI_TIEU

"%min_chi_tieu%" <- function(a, b) {
 # import dataset
 files_list <- list.files(b, full.names = TRUE)

 tmp_working <- vector(mode = "list", length = length(files_list))

 for (i in seq_along(files_list)) {
   tmp_working[[i]] <- read.csv(files_list[i])
   tmp_working[[i]]$Date <- as.Date(tmp_working[[i]]$Date, format = "%m/%d/%Y")
 }
 # find min value

 ket_qua_min <- data.frame()

 for (min_i in seq_along(tmp_working)) {
   if (chi_tieu %in% names(tmp_working[[min_i]])) { # Đưa lệnh `if` để check tên cột, nếu không trùng thì `next`
     gia_tri_min <- min(get(chi_tieu, tmp_working[[min_i]]), na.rm = TRUE)

     ### trích xuất ra vị trí index
     vi_tri <- which(get(chi_tieu, tmp_working[[min_i]]) == gia_tri_min)

     ket_qua_min <- bind_rows(ket_qua_min, tmp_working[[min_i]][vi_tri, ])
   }
   next
 }
 #### THÊM DÒNG THÔNG BÁO
 print(paste("Kết quả lọc theo giá trị MIN của", chi_tieu, "ở folder", b, "là:"))
 #### TRẢ KẾT QUẢ
 return(ket_qua_min)
}

## FUNCTION_MAX_CHI_TIEU

"%max_chi_tieu%" <- function(a, b) {
 # import dataset
 files_list <- list.files(b, full.names = TRUE)

 tmp_working <- vector(mode = "list", length = length(files_list))

 for (i in seq_along(files_list)) {
   tmp_working[[i]] <- read.csv(files_list[i])
   tmp_working[[i]]$Date <- as.Date(tmp_working[[i]]$Date, format = "%m/%d/%Y")
 }
 # find max value

 ket_qua_max <- data.frame()

 for (max_i in seq_along(tmp_working)) {
   if (chi_tieu %in% names(tmp_working[[max_i]])) {
     gia_tri_max <- max(get(chi_tieu, tmp_working[[max_i]]), na.rm = TRUE)

     ### trích xuất ra vị trí index
     vi_tri <- which(get(chi_tieu, tmp_working[[max_i]]) == gia_tri_max)

     ket_qua_max <- bind_rows(ket_qua_max, tmp_working[[max_i]][vi_tri, ])
   }
   next
 }
 print(paste("Kết quả lọc theo giá trị MAX của", chi_tieu, "ở folder", b, "là:"))
 return(ket_qua_max)
}

## FUNCTION RANGE CHỈ TIÊU

"%range_chi_tieu%" <- function(a, b) {
 # import dataset
 files_list <- list.files(b, full.names = TRUE)

 tmp_working <- vector(mode = "list", length = length(files_list))

 for (i in seq_along(files_list)) {
   tmp_working[[i]] <- read.csv(files_list[i])
   tmp_working[[i]]$Date <- as.Date(tmp_working[[i]]$Date, format = "%m/%d/%Y")
 }

 # tìm trong khoảng

 ket_qua_range <- data.frame()

 for (range_i in seq_along(tmp_working)) {
   if (chi_tieu %in% names(tmp_working[[range_i]])) {
     ket_qua_tim_trong_khoang <- which(get(chi_tieu, tmp_working[[range_i]]) >= a[1] &
                                           get(chi_tieu, tmp_working[[range_i]]) <= a[2])

     ket_qua_range <- bind_rows(ket_qua_range, tmp_working[[range_i]][ket_qua_tim_trong_khoang, ])
   }
   next
 }
 print(paste("Kết quả lọc trong khoảng, từ", a[1], "đến", a[2], "của", chi_tieu, "ở folder", b, "là:"))
 return(ket_qua_range)
}

## FUNCTION GHÉP CHUNG

"%chi_tieu%" <- function(a, b) {
 if (length(a) == 1) {
   switch(a,
     "min" = a %min_chi_tieu% b,
     "max" = a %max_chi_tieu% b
   )
 } else {
   a %range_chi_tieu% b
 }
}

## FUNCTION FINAL

filter_theo_chi_tieu_enhanced <- function(chi_tieu = "mercury", a = "min", b = "2007") {
 chi_tieu <<- chi_tieu ### sử dụng super assign
 a %chi_tieu% b
}

Test hàm enhanced

Trường hợp 1: Khi filter theo chỉ tiêu nitrite đều có trong các file .csv thì những file nào có cột COD sẽ thể hiện NA (missing value) thay thế ở dataset kết quả xuất ra.

filter_theo_chi_tieu_enhanced("nitrite", c(0.89, 0.9), "2007")
## [1] "Kết quả lọc trong khoảng, từ 0.89 đến 0.9 của nitrite ở folder 2007 là:"
##          Date mercury nitrite ID
## 1  2005-02-15    3.61   0.895  2
## 2  2005-04-22    3.54   0.896  2
## 3  2010-02-25    2.40   0.894  2
## 4  2002-05-08    7.12   0.898  4
## 5  2003-05-21    5.16   0.898  4
## 6  2005-10-19    6.48   0.892  4
## 7  2007-12-08    6.71   0.891  4
## 8  2007-04-24    4.66   0.896  5
## 9  2008-04-18    3.07   0.890  5
## 10 2008-11-02    4.44   0.893  5
## 11 2003-12-05    3.94   0.896  6
## 12 2006-01-05    2.08   0.895  6
## 13 2002-02-13    2.64   0.896  7
## 14 2002-03-15    4.59   0.899  7
## 15 2005-01-10    3.78   0.896  7
## 16 2003-04-09    4.43   0.900  8
filter_theo_chi_tieu_enhanced("nitrite", c(0.89, 0.9), "2008")
## [1] "Kết quả lọc trong khoảng, từ 0.89 đến 0.9 của nitrite ở folder 2008 là:"
##          Date mercury nitrite ID  COD
## 1  2005-02-15    3.61   0.895  2 2.23
## 2  2005-04-22    3.54   0.896  2   NA
## 3  2002-05-08    7.12   0.898  4   NA
## 4  2003-05-21    5.16   0.898  4   NA
## 5  2005-10-19    6.48   0.892  4   NA
## 6  2007-12-08    6.71   0.891  4   NA
## 7  2007-04-24    4.66   0.896  5   NA
## 8  2008-04-18    3.07   0.890  5   NA
## 9  2008-11-02    4.44   0.893  5   NA
## 10 2003-12-05    3.94   0.896  6   NA
## 11 2006-01-05    2.08   0.895  6   NA
## 12 2002-02-13    2.64   0.896  7   NA
## 13 2002-03-15    4.59   0.899  7   NA
## 14 2005-01-10    3.78   0.896  7   NA
## 15 2003-04-09    4.43   0.900  8 1.51

Trường hợp 2: Khi filter theo chỉ tiêu COD chỉ có ở một số file .csv trong folder 2008, còn folder 2007 không có. Kết quả trả về là các file chỉ chứa cột COD rất gọn.

filter_theo_chi_tieu_enhanced("COD", c(1.7, 1.9), "2007")
## [1] "Kết quả lọc trong khoảng, từ 1.7 đến 1.9 của COD ở folder 2007 là:"
## data frame with 0 columns and 0 rows
filter_theo_chi_tieu_enhanced("COD", c(1.7, 1.9), "2008")
## [1] "Kết quả lọc trong khoảng, từ 1.7 đến 1.9 của COD ở folder 2008 là:"
##          Date mercury  COD nitrite ID
## 1  2001-02-18    2.05 1.79   3.570  2
## 2  2001-09-16    8.55 1.79   0.803  2
## 3  2002-02-13    1.90 1.79   1.510  2
## 4  2002-07-13    4.00 1.79   0.563  2
## 5  2003-03-21      NA 1.79      NA  2
## 6  2003-11-17    4.35 1.79   0.987  2
## 7  2004-07-15      NA 1.79      NA  2
## 8  2004-12-04      NA 1.73      NA  2
## 9  2004-12-13      NA 1.82      NA  2
## 10 2005-01-08      NA 1.79      NA  2
## 11 2005-06-08      NA 1.79      NA  2
## 12 2005-11-06    4.69 1.79   0.474  2
## 13 2006-04-06      NA 1.79      NA  2
## 14 2002-07-04      NA 1.79      NA  6
## 15 2003-03-02      NA 1.79      NA  6
## 16 2003-07-22      NA 1.73      NA  6
## 17 2003-07-31      NA 1.82      NA  6
## 18 2003-08-26      NA 1.79      NA  6
## 19 2004-01-24      NA 1.79      NA  6
## 20 2004-06-23      NA 1.79      NA  6
## 21 2004-11-21      NA 1.79      NA  6
## 22 2004-02-23      NA 1.70      NA  8
## 23 2004-04-05      NA 1.73      NA  8
## 24 2005-06-11      NA 1.72      NA  8

filter_theo_chi_tieu_enhanced() hoạt động OK. Tuy nhiên kết quả xuất ra thì vị trí các cột không theo thứ tự như mong muốn. Có thể sẽ gây nhầm lẫn. Vì vậy chúng ta cải tiến hàm này để làm sao kết quả xuất ra giữ cột DateID ở đầu và cuối, còn ở giữa thì các chỉ tiêu sẽ được xếp theo alphabet.

Cải tiến lần 2

Function cải tiến lần 2, có tên là filter_theo_chi_tieu_super() giúp xử lý dataset kết quả thu được “user-friendly” hơn. Cách tiếp cận là ta sẽ lồng ghép/nested filter_theo_chi_tieu_enhanced() vào trong function sau cùng.

filter_theo_chi_tieu_super <- function(chi_tieu = "mercury", a = "min", b = "2007") {
    
  # Logic là: lấy kết quả từ function enhanced để sắp xếp thứ tự cột    
  data_1 <- filter_theo_chi_tieu_enhanced(chi_tieu, a, b)
  
  if (length(data_1) != 0) {
    kq_sort <- sort(names(data_1)[!(names(data_1) %in% c("Date", "ID"))])

    ok <- c("Date", kq_sort, "ID")

    data_2 <- data_1[ok]

    return(data_2)
  }
  return("Không có kết quả tìm kiếm") # Nếu không có kết quả thì in thông báo rõ ràng
}

Test hàm super

filter_theo_chi_tieu_super(, ,) # test với default value (folder "2007", min của mercury)
## [1] "Kết quả lọc theo giá trị MIN của mercury ở folder 2007 là:"
##          Date mercury nitrite ID
## 1  2005-12-12  0.6130  0.3260  1
## 2  2006-09-29  0.0000  0.0000  2
## 3  2005-12-12  0.7130  0.2940  3
## 4  2009-07-12  0.0283  0.0199  4
## 5  2008-11-08  0.5160  0.4210  5
## 6  2003-11-29  0.5210  0.2240  6
## 7  2008-11-26  0.6450  1.2500  7
## 8  2003-01-15  1.0900  2.8500  8
## 9  2009-04-01  0.4130  0.3910  9
## 10 2004-04-27  0.1170  0.0636 10
filter_theo_chi_tieu_super(, , "2008") # tương tự, nhưng là folder "2008
## [1] "Kết quả lọc theo giá trị MIN của mercury ở folder 2008 là:"
##         Date   COD mercury nitrite ID
## 1 2005-12-12    NA  0.6130  0.3260  1
## 2 2006-09-29 0.772  0.0000  0.0000  2
## 3 2005-12-12    NA  0.7130  0.2940  3
## 4 2009-07-12    NA  0.0283  0.0199  4
## 5 2008-11-08    NA  0.5160  0.4210  5
## 6 2003-11-29    NA  0.5210  0.2240  6
## 7 2008-11-26    NA  0.6450  1.2500  7
## 8 2003-01-15    NA  1.0900  2.8500  8
filter_theo_chi_tieu_super("COD", "max", "2007")
## [1] "Kết quả lọc theo giá trị MAX của COD ở folder 2007 là:"
## [1] "Không có kết quả tìm kiếm"
filter_theo_chi_tieu_super("COD", "max", "2008")
## [1] "Kết quả lọc theo giá trị MAX của COD ở folder 2008 là:"
##          Date  COD mercury nitrite ID
## 1  2004-10-26 11.2      NA      NA  2
## 2  2005-04-21 11.2      NA      NA  2
## 3  2005-09-19 11.2    9.69   0.607  2
## 4  2006-02-17 11.2      NA      NA  2
## 5  2006-07-18 11.2      NA      NA  2
## 6  2006-12-18 11.2      NA      NA  2
## 7  2007-05-05 11.2      NA      NA  2
## 8  2007-09-20 11.2      NA      NA  2
## 9  2008-02-05 11.2      NA      NA  2
## 10 2008-06-22 11.2      NA      NA  2
## 11 2008-11-07 11.2      NA      NA  2
## 12 2009-03-25 11.2      NA      NA  2
## 13 2009-08-10 11.2      NA      NA  2
## 14 2003-06-13 11.2      NA      NA  6
## 15 2003-12-07 11.2      NA      NA  6
## 16 2004-05-06 11.2      NA      NA  6
## 17 2004-10-04 11.2      NA      NA  6
## 18 2005-03-04 11.2      NA      NA  6
## 19 2005-08-04 11.2      NA      NA  6
## 20 2005-12-20 11.2      NA      NA  6
## 21 2006-05-07 11.2      NA      NA  6
## 22 2006-09-22 11.2      NA      NA  6
## 23 2005-10-26 11.8      NA      NA  8

Như vậy sau 2 cải tiến thì filter_theo_chi_tieu_super() đã hoàn chỉnh. Khi có nhu cầu bổ sung thêm các chức năng khác thì chúng ta cũng thêm dòng code tương tự như vậy. Việc xây dựng function trong R không khó, chỉ là chúng ta cần có tư duy hệ thống và bình tĩnh thử và sai một chút để hiểu quy luật trong R.

Nhiều trường hợp chạy function bị lỗi vì bị thiếu dấu () hay {} đóng mở ngoặc, vì vậy cần nắm chắc logic viết function để đảm bảo bạn viết code đến đâu là khi test hàm sẽ đúng đến đó.

Sơ kết

Trên đây là ví dụ minh họa cho bài giảng Cách xây dựng hàm số trong khóa học “HDSD R để xử lý dữ liệu | Chuyên đề Coding in R”, sau 20 giờ học, bạn sẽ có nền tảng vững chắc về R căn bản và cách xây dựng function cho riêng mình!

Nội dung khóa học: www.tuhocr.com

Hành trình ngàn dặm bắt đầu từ bước chân đầu tiên.

ĐĂNG KÝ NGAY: https://www.tuhocr.com/register