1 Giải phẫu và sinh lý hàm số trong R

## function (X, FUN, ...)

2 Hàm số vòng lặp

Vòng lặp for hữu ích trong lập trình nhưng không dễ để sử dụng. Vì thế, R xây dựng một số hàm giúp cho vòng lặp trở nên dễ dàng hơn

3 lapply()

3.1 Ôn tập

Trong bài này, ta sẽ làm việc với bộ dữ liệu airqualityđược tích hợp sẵn trong R.

Câu hỏi: Bộ dữ liệu airquality có 6 biến. Hãy dùng vòng lặp for, tạo một vector 6 thành phần, mà mỗi thành phần là số trung bình của từng biến số trong airquality.

Giải đáp:

Cách 1

##      Ozone    Solar.R       Wind       Temp      Month        Day 
##  42.129310 185.931507   9.957516  77.882353   6.993464  15.803922

Cách 2

## [1]  42.129310 185.931507   9.957516  77.882353   6.993464  15.803922

Để giải quyết câu hỏi trên, ta cần gọi nhiều hàm số và cần quen với tư duy sử dụng vòng lặp for. Các công đoạn trên có thể được làm ngắn lại với lapply().

3.2 Hàm số lapply() hoạt động theo từng bước sau:

  1. Lặp qua từng thành phần của list đó

  2. Áp dụng hàm số vào mỗi thành phần của list đó (một hàm số được chỉ định)

  3. Trả về một list

Hàm số này nhận 3 đối số: (1) list X; (2) hàm số (hoặc tên của hàm số) FUN; (3) những đối số khác trong đối số .... Nếu X không phải là list, nó sẽ được chuyển dạng sang list bằng hàm số as.list().

Một điều quan trọng là lapply() luôn trả về list, cho dù đối số X được truyền vào là nhóm nào đi nữa.

## $a
## [1] 3
## 
## $b
## [1] -0.5941557

Ta có thể sử dụng lapply() để đánh giá một hàm số nhiều lần với những đối số khác nhau.

## [[1]]
## [1] 0.239534
## 
## [[2]]
## [1] 0.9044524 0.5214814
## 
## [[3]]
## [1] 0.6431543 0.3466180 0.5789678
## 
## [[4]]
## [1] 0.6118357 0.4173622 0.9518173 0.6476397

Khi ta truyền một hàm số tới lapply(), lapply() lấy từng thành phần của danh sách và truyền chúng vào đối số đầu tiên của hàm số được chỉ định. Ở ví dụ trên, đối số đầu tiên của runif()n, và do đó từng thành phần của chuỗi 1:4 được truyền vào đối số n của runif().

Hàm số mà ta truyền vào lapply() có những đối số của nó. Ví dụ, hàm runif() có đối số minmax. Những đối số này sẽ được đặt ở đối số ... của hàm lapply().

Ở trường hợp này, đối số min = 0max = 10 được truyền vào hàm runìf() mỗi khi nó được gọi.

## [[1]]
## [1] 4.159846
## 
## [[2]]
## [1] 3.455416 1.893661
## 
## [[3]]
## [1] 7.162459 1.927981 7.113403
## 
## [[4]]
## [1] 2.470389 8.222483 1.679206 1.868843

Do đó, thay vì những con số ngẫu nhiên từ 0 đến 1 (mặc định), chúng dao động từ 0 đến 10.

Cuối cùng, bài toán trên có thể được giải theo cách khác.

## $Ozone
## [1] 42.12931
## 
## $Solar.R
## [1] 185.9315
## 
## $Wind
## [1] 9.957516
## 
## $Temp
## [1] 77.88235
## 
## $Month
## [1] 6.993464
## 
## $Day
## [1] 15.80392

Ta có được kết quả là một list với từng thành phần là số trung bình của mỗi biến số của airquality.

Tuy nhiên, đầu ra vẫn không phải là một vector.

Vì thế ta cần một cách khác, đó là sapply().

4 sapply()

Hàm sapply() hoạt động tương tự như lapply(); sự khác biệt duy nhất là giá trị trả về. sapply() sẽ làm đơn giản kết quả khi có thể. Về bản chất sapply() hoạt động như sau:

## $a
## [1] 2.5
## 
## $b
## [1] -0.8675274
## 
## $c
## [1] 1.322438
## 
## $d
## [1] 5.077016

Lưu ý rằng lapply() trả về list như mọi khi. Mỗi thành phần của list này có độ dài là 1.

Đây là kết quả khi ta gọi sapply() trên cùng một list.

##          a          b          c          d 
##  2.5000000 -0.8675274  1.3224383  5.0770161

Bởi vì kết quả của lapply() là list với mỗi thành phần có độ dài là 1, sapply() thu gọn đầu ra thành một vector kiểu numeric.

Ta áp dụng sapply() để giải quyết bài toán trên. Vì kết quả từ lapply() là một list với độ dài mỗi thành phần là 1, đầu ra của sapply() sẽ là một vector.

##      Ozone    Solar.R       Wind       Temp      Month        Day 
##  42.129310 185.931507   9.957516  77.882353   6.993464  15.803922

5 split()

5.1 Ôn tập

Đề bài: Sử dụng vòng lặp for, hãy tính số trung bình Ozone theo từng tháng.

Cách trên giúp ta đạt được mục tiêu, nhưng trông không gọn gàng và hơi phức tạp. Với hàm split(), ta có thể làm cho chúng gọn nhẹ hơn.

5.2 Hàm split()

Hàm split() nhận vào một vector và tách nó thành nhiều nhóm theo một factor, hoặc list của những factor.

Các đối số của split() bao gồm

## function (x, f, drop = FALSE, ...)

Trong đó

  • x là vector hoặc list hoặc data frame.

  • f là factor (hoặc được ép kiểu thành factor) hoặc một list của những factor.

  • drop chỉ rằng factor trống có nên được bỏ hay không.

Sự kết hợp giữa split và hàm số như lapply() hoặc sapply() là thường gặp trong R. Về cơ bản, ta muốn nhận một cấu trúc dữ liệu, phân tách nó thành những tập hợp con theo một biến số khác, và áp dụng một hàm vào những tập hợp con đó.

## $`1`
##  [1] -1.74163069 -1.17232295  1.41421991 -0.68943470  0.15135942  1.31186861
##  [7] -0.01650557  0.01440528 -0.05464505  0.43808892
## 
## $`2`
##  [1] 0.25957667 0.33551607 0.91171408 0.64484045 0.55282049 0.04975694
##  [7] 0.47542621 0.20121836 0.91547593 0.22188966
## 
## $`3`
##  [1]  0.51749099  1.65492296  0.76318368 -0.07187649  0.61071065  1.51427794
##  [7] -0.62854999  0.49869919  1.29134474  0.93867488

Sự kết hợp thường là split() được truyền vào lapply().

## $`1`
## [1] -0.03445968
## 
## $`2`
## [1] 0.4568235
## 
## $`3`
## [1] 0.7088879

Bằng sự kết hợp giữa sapply()split(). Ta giải quyết bài toán ở đầu phần này như sau.

##        5        6        7        8        9 
## 23.61538 29.44444 59.11538 59.96154 31.44828

5.3 Phân tách một Data Frame

##   Ozone Solar.R Wind Temp Month Day
## 1    41     190  7.4   67     5   1
## 2    36     118  8.0   72     5   2
## 3    12     149 12.6   74     5   3
## 4    18     313 11.5   62     5   4
## 5    NA      NA 14.3   56     5   5
## 6    28      NA 14.9   66     5   6

Ta có thể tách data frame airquality theo biến số Month. Từ đó ta có những data frame nhỏ cho mỗi tháng.

## List of 5
##  $ 5:'data.frame':   31 obs. of  6 variables:
##   ..$ Ozone  : int [1:31] 41 36 12 18 NA 28 23 19 8 NA ...
##   ..$ Solar.R: int [1:31] 190 118 149 313 NA NA 299 99 19 194 ...
##   ..$ Wind   : num [1:31] 7.4 8 12.6 11.5 14.3 14.9 8.6 13.8 20.1 8.6 ...
##   ..$ Temp   : int [1:31] 67 72 74 62 56 66 65 59 61 69 ...
##   ..$ Month  : int [1:31] 5 5 5 5 5 5 5 5 5 5 ...
##   ..$ Day    : int [1:31] 1 2 3 4 5 6 7 8 9 10 ...
##  $ 6:'data.frame':   30 obs. of  6 variables:
##   ..$ Ozone  : int [1:30] NA NA NA NA NA NA 29 NA 71 39 ...
##   ..$ Solar.R: int [1:30] 286 287 242 186 220 264 127 273 291 323 ...
##   ..$ Wind   : num [1:30] 8.6 9.7 16.1 9.2 8.6 14.3 9.7 6.9 13.8 11.5 ...
##   ..$ Temp   : int [1:30] 78 74 67 84 85 79 82 87 90 87 ...
##   ..$ Month  : int [1:30] 6 6 6 6 6 6 6 6 6 6 ...
##   ..$ Day    : int [1:30] 1 2 3 4 5 6 7 8 9 10 ...
##  $ 7:'data.frame':   31 obs. of  6 variables:
##   ..$ Ozone  : int [1:31] 135 49 32 NA 64 40 77 97 97 85 ...
##   ..$ Solar.R: int [1:31] 269 248 236 101 175 314 276 267 272 175 ...
##   ..$ Wind   : num [1:31] 4.1 9.2 9.2 10.9 4.6 10.9 5.1 6.3 5.7 7.4 ...
##   ..$ Temp   : int [1:31] 84 85 81 84 83 83 88 92 92 89 ...
##   ..$ Month  : int [1:31] 7 7 7 7 7 7 7 7 7 7 ...
##   ..$ Day    : int [1:31] 1 2 3 4 5 6 7 8 9 10 ...
##  $ 8:'data.frame':   31 obs. of  6 variables:
##   ..$ Ozone  : int [1:31] 39 9 16 78 35 66 122 89 110 NA ...
##   ..$ Solar.R: int [1:31] 83 24 77 NA NA NA 255 229 207 222 ...
##   ..$ Wind   : num [1:31] 6.9 13.8 7.4 6.9 7.4 4.6 4 10.3 8 8.6 ...
##   ..$ Temp   : int [1:31] 81 81 82 86 85 87 89 90 90 92 ...
##   ..$ Month  : int [1:31] 8 8 8 8 8 8 8 8 8 8 ...
##   ..$ Day    : int [1:31] 1 2 3 4 5 6 7 8 9 10 ...
##  $ 9:'data.frame':   30 obs. of  6 variables:
##   ..$ Ozone  : int [1:30] 96 78 73 91 47 32 20 23 21 24 ...
##   ..$ Solar.R: int [1:30] 167 197 183 189 95 92 252 220 230 259 ...
##   ..$ Wind   : num [1:30] 6.9 5.1 2.8 4.6 7.4 15.5 10.9 10.3 10.9 9.7 ...
##   ..$ Temp   : int [1:30] 91 92 93 93 87 84 80 78 75 73 ...
##   ..$ Month  : int [1:30] 9 9 9 9 9 9 9 9 9 9 ...
##   ..$ Day    : int [1:30] 1 2 3 4 5 6 7 8 9 10 ...

Tiếp theo, ta có thể tính số trung bình cho biến Ozone, Solar.R, và Wind ở mỗi data frame nhỏ.

## $`5`
##    Ozone  Solar.R     Wind 
##       NA       NA 11.62258 
## 
## $`6`
##     Ozone   Solar.R      Wind 
##        NA 190.16667  10.26667 
## 
## $`7`
##      Ozone    Solar.R       Wind 
##         NA 216.483871   8.941935 
## 
## $`8`
##    Ozone  Solar.R     Wind 
##       NA       NA 8.793548 
## 
## $`9`
##    Ozone  Solar.R     Wind 
##       NA 167.4333  10.1800

Sử dụng sapply() để có đầu ra dễ nhìn hơn.

##                5         6          7        8        9
## Ozone         NA        NA         NA       NA       NA
## Solar.R       NA 190.16667 216.483871       NA 167.4333
## Wind    11.62258  10.26667   8.941935 8.793548  10.1800

Dẫu vậy, có NA ở bộ dữ liệu nên một số kết quả số trung bình trả lại NA. Lúc này, ta thêm đối số na.rm = TRUE của colMeans bằng cách truyền vào đối số ... của sapply().

##                 5         6          7          8         9
## Ozone    23.61538  29.44444  59.115385  59.961538  31.44828
## Solar.R 181.29630 190.16667 216.483871 171.857143 167.43333
## Wind     11.62258  10.26667   8.941935   8.793548  10.18000

Thi thoảng, ta muốn tách đối tượng R thành những cấp độ được xác định bởi hơn 1 biến. Ta có thể sử dụng hàm interaction().

##  [1] 1 1 1 1 1 2 2 2 2 2
## Levels: 1 2
##  [1] 1 1 2 2 3 3 4 4 5 5
## Levels: 1 2 3 4 5
##  [1] 1.1 1.1 1.2 1.2 1.3 2.3 2.4 2.4 2.5 2.5
## Levels: 1.1 2.1 1.2 2.2 1.3 2.3 1.4 2.4 1.5 2.5

Việc này có thể tạo ra nhiều cấp độ mà ở đó không có dữ liệu.

## List of 10
##  $ 1.1: num [1:2] 0.618 -0.505
##  $ 2.1: num(0) 
##  $ 1.2: num [1:2] 0.298 1.081
##  $ 2.2: num(0) 
##  $ 1.3: num 0.236
##  $ 2.3: num 0.3
##  $ 1.4: num(0) 
##  $ 2.4: num [1:2] 0.428 0.261
##  $ 1.5: num(0) 
##  $ 2.5: num [1:2] -0.21 0.799

Lưu ý rằng có 4 nhóm/cấp độ không có dữ liệu. Để loại bỏ những cấp độ trống, ta thêm vào đối số drop = TRUE.

## List of 6
##  $ 1.1: num [1:2] 0.618 -0.505
##  $ 1.2: num [1:2] 0.298 1.081
##  $ 1.3: num 0.236
##  $ 2.3: num 0.3
##  $ 2.4: num [1:2] 0.428 0.261
##  $ 2.5: num [1:2] -0.21 0.799

6 tapply()

tapply() áp dụng một hàm số vào những tập hợp con của một vector. tapply() là kết hợp giữa split()sapply() chỉ dành cho vector.

## function (X, INDEX, FUN = NULL, ..., default = NA, simplify = TRUE)

Các đối số của tapply() bao gồm

Đối với một vector những con số, chỉ cần một phép tính đơn giản để ra được số trung bình mỗi nhóm.

##  [1] 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3
## Levels: 1 2 3
##          1          2          3 
## -0.5453991  0.4677234  0.8644814

Ta có thể tính số trung bình từng nhóm mà không đơn giản hóa kết quả. Phép tính này trả về list.

## $`1`
## [1] -0.5453991
## 
## $`2`
## [1] 0.4677234
## 
## $`3`
## [1] 0.8644814

Ta có thể áp dụng hàm số mà trả về hơn 1 giá trị. Lúc này, tapply() sẽ không đơn giản hóa kết quả và trả về list.

## $`1`
## [1] -3.200912  1.391144
## 
## $`2`
## [1] 0.1602670 0.9965533
## 
## $`3`
## [1] -0.5259968  2.0865810

7 apply()

Hàm apply() giúp đánh giá một hàm số theo lề của một array. Ta sử dụng nó để áp dụng một hàm số theo các hàng, hoặc các cột của một matrix (một dạng array 2 chiều).

## function (X, MARGIN, FUN, ...)

Các đối số của apply() bao gồm

Ở đây, ta tính số trung bình của mỗi cột.

##  [1] -0.02161585 -0.29224971 -0.08093589 -0.27779348 -0.17595958 -0.09126083
##  [7]  0.05675241  0.10946790 -0.14345166 -0.10677869

Ta cũng có thể tính tổng của mỗi hàng.

##  [1] -1.0599644  0.2033402 -1.3316518  3.9335807 -2.0345666  1.3139554
##  [7] -3.8891804 -4.0311925  1.9417970  0.7039226 -1.2854550 -8.1881165
## [13] -2.5078882  4.1156069  0.1856165 -5.2364001  0.5832373  0.8171430
## [19] -5.6262757  0.9159837

Lưu ý rằng ở cả 2 lần gọi hàm apply(), đầu ra là vector số.

8 Tính tổng hoặc số trung bình theo hàng/cột

Ta có các cách nhanh hơn để tính tổng hoặc số trung bình theo hàng/cột.

9 Tổng kết