Lập trình hàm với map

Tác giả: Hoàng Đức Anh | 2019-04-15

Khi phân tích dữ liệu phức tạp, ta thường xuyên phải thực hiện một nhóm các phân tích tương tự nhau cho các nhóm dữ liệu khác nhau. Việc sử dụng các hàm làm đơn vị thao tác cơ bản và phối hợp các hàm với nhau được gọi là lập trình chức năng hàm (functional programming). Để đơn giản, ta xét ví dụ sau.

Sử dụng tập dữ liệu iris, với mỗi nhóm của Species, xây dựng mô hình hồi quy giữa Sepal.LengthPetal.Length, so sánh giá trị r.squared giữa các mô hình.

Với cách làm thông thường, ta sẽ phải thức hiện theo thứ tự sau:

  • Tạo các data.frame cho từng giá trị của Species
  • Với mỗi data.frame vừa tạo, xây dựng mô hình lm
  • Với mỗi mô hình vừa tạo, chiết xuất giá trị r.squared và lưu vào một data.frame

Cách triển khai trên có thể sử dụng vòng lặp trong R với phương án như sau

library(dplyr)
library(purrr)

category <- iris$Species %>% levels %>% as.character()
model_result <- data.frame()
for (i in category){
  df <- iris %>% filter(Species == i)
  model <- lm(Sepal.Length ~ Sepal.Width, data = df)
  model_summary <- summary(model)
  df_temp <- data.frame(species = i,
                        r.square = model_summary$r.squared)
  model_result <- bind_rows(model_result, df_temp)
}

Tuy nhiên, với lập trình chức năng hàm, ta có thể làm rất đơn giản như sau.

library(purrr)
iris %>% 
  split(.$Species) %>% 
  map(~lm(Sepal.Length ~ Sepal.Width, data = .)) %>% 
  map(summary) %>% 
  map_dbl("r.squared")
##     setosa versicolor  virginica 
##  0.5513756  0.2765821  0.2090573

Trong bài viết này, chúng ta sẽ tìm hiểu các cách thức cơ bản lập trình chức năng hàm cơ bản với map qua package purrr. Việc nắm vững kiến thức và kỹ năng lập trình hàm có rất nhiều ứng dụng trong công việc phân tích, giúp giảm thiểu rất lớn thời gian phân tích, làm cho quá trình phân tích mạch lạc hơn rất nhiều trong các bài toán khám phá dữ liệu


Công thức tổng quát của nhóm hàm map

map(.x, .f, ...)

Giải thích: Với mỗi giá trị của .x, thực hiện .f. Trong đó, x là một list.

Hàm map làm hàm tổng quát, ngoài ra, map còn có các biến thể chính sau

Câu lênh Kết quả
map list
map_dbl vector dạng double
map_int vector dạng int
map_chr vector dạng character
map_df data.frame
# Dạng list
iris %>% map(class)
## $Sepal.Length
## [1] "numeric"
## 
## $Sepal.Width
## [1] "numeric"
## 
## $Petal.Length
## [1] "numeric"
## 
## $Petal.Width
## [1] "numeric"
## 
## $Species
## [1] "factor"
# Dạng char
iris %>% map_chr(class)
## Sepal.Length  Sepal.Width Petal.Length  Petal.Width      Species 
##    "numeric"    "numeric"    "numeric"    "numeric"     "factor"
# Dạng data.frame
iris %>% map_df(class)
## # A tibble: 1 x 5
##   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
##   <chr>        <chr>       <chr>        <chr>       <chr>  
## 1 numeric      numeric     numeric      numeric     factor

Map theo điều kiện với map_ifmap_at

Tương tự với map, nhóm map_ifmap_at cho phép tính toán theo điều kiện hoặc vị trí của list. Xem ví dụ sau.

# map_if
iris %>% 
  map_if(is.numeric, as.character) %>% 
  as.data.frame %>% 
  str
## 'data.frame':    150 obs. of  5 variables:
##  $ Sepal.Length: Factor w/ 35 levels "4.3","4.4","4.5",..: 9 7 5 4 8 12 4 8 2 7 ...
##  $ Sepal.Width : Factor w/ 23 levels "2","2.2","2.3",..: 15 10 12 11 16 19 14 14 9 11 ...
##  $ Petal.Length: Factor w/ 43 levels "1","1.1","1.2",..: 5 5 4 6 5 8 5 6 5 6 ...
##  $ Petal.Width : Factor w/ 22 levels "0.1","0.2","0.3",..: 2 2 2 2 2 4 3 2 2 1 ...
##  $ Species     : Factor w/ 3 levels "setosa","versicolor",..: 1 1 1 1 1 1 1 1 1 1 ...
# map_at
iris %>% 
  map_at(c(1,2), as.character) %>% 
  str
## List of 5
##  $ Sepal.Length: chr [1:150] "5.1" "4.9" "4.7" "4.6" ...
##  $ Sepal.Width : chr [1:150] "3.5" "3" "3.2" "3.1" ...
##  $ Petal.Length: num [1:150] 1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 ...
##  $ Petal.Width : num [1:150] 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 ...

Lưu ý: Với trường hợp có hai biến đầu vào, có thể sử dụng nhóm hàm map2. Ví dụ

# Không chạy
map_dbl(1:3, 4:6, sum)
map2_dbl(1:3, 4:6, sum)
## [1] 5 7 9

Với các trường hợp phức tạp, ta cần vận dụng linh hoạt.

Ví dụ: Với mỗi dòng trong iris , tách thành dataframe riêng và xoay chiều dữ liêu. Tên các cột trở thành biến attribute, giá trị các cột trở thành biến value.

library(tidyverse)
get_data <- function(data, i){
 df <- data %>% 
    slice(i) %>% t %>% 
    as.data.frame
 result <- data.frame(attribute = rownames(df),
                      value = df[,1])
 rownames(result) <- NULL
 return(result)
}

get_data(mtcars, 3)
##    attribute  value
## 1        mpg  22.80
## 2        cyl   4.00
## 3       disp 108.00
## 4         hp  93.00
## 5       drat   3.85
## 6         wt   2.32
## 7       qsec  18.61
## 8         vs   1.00
## 9         am   1.00
## 10      gear   4.00
## 11      carb   1.00
get_data(iris, 1)
##      attribute  value
## 1 Sepal.Length    5.1
## 2  Sepal.Width    3.5
## 3 Petal.Length    1.4
## 4  Petal.Width    0.2
## 5      Species setosa
map2(replicate(3, iris, simplify = F),
     c(1:3), get_data)
## [[1]]
##      attribute  value
## 1 Sepal.Length    5.1
## 2  Sepal.Width    3.5
## 3 Petal.Length    1.4
## 4  Petal.Width    0.2
## 5      Species setosa
## 
## [[2]]
##      attribute  value
## 1 Sepal.Length    4.9
## 2  Sepal.Width      3
## 3 Petal.Length    1.4
## 4  Petal.Width    0.2
## 5      Species setosa
## 
## [[3]]
##      attribute  value
## 1 Sepal.Length    4.7
## 2  Sepal.Width    3.2
## 3 Petal.Length    1.3
## 4  Petal.Width    0.2
## 5      Species setosa

Như vậy, chúng ta đã vừa được học cách sử dụng nhóm hàm map cơ bản với purrr. Chúc các bạn học tập và làm việc hiệu quả với Ranalytics.vn!

comments powered by Disqus