Tình huống

Bạn có folder này chứa 332 file .csv tương ứng 332 cảm biến theo dõi chỉ tiêu sulfate và nitrate chất lượng nước ở các địa điểm khác nhau (số liệu mô phỏng, không có thực). Nguồn dataset từ Coursera/Data Science Specialization.

Download dataset sau đó giải nén ra folder specdata rồi đặt trong folder project R.

Cấu trúc của 1 file .csv như sau:

data_020 <- read.csv("specdata/020.csv")
head(data_020) # xem 6 dòng đầu tiên
##         Date sulfate nitrate ID
## 1 2002-01-01      NA      NA 20
## 2 2002-01-02      NA      NA 20
## 3 2002-01-03      NA      NA 20
## 4 2002-01-04      NA      NA 20
## 5 2002-01-05      NA      NA 20
## 6 2002-01-06      NA      NA 20
dim(data_020) # kiểm tra số hàng và cột
## [1] 1461    4
sapply(data_020, class) # kiểm tra `class` từng cột
##        Date     sulfate     nitrate          ID 
## "character"   "numeric"   "numeric"   "integer"
summary(data_020) # tóm tắt dataset
##      Date              sulfate          nitrate             ID    
##  Length:1461        Min.   : 0.472   Min.   :0.1080   Min.   :20  
##  Class :character   1st Qu.: 2.147   1st Qu.:0.3032   1st Qu.:20  
##  Mode  :character   Median : 3.180   Median :0.4740   Median :20  
##                     Mean   : 3.625   Mean   :0.7780   Mean   :20  
##                     3rd Qu.: 4.890   3rd Qu.:0.8140   3rd Qu.:20  
##                     Max.   :14.400   Max.   :6.7700   Max.   :20  
##                     NA's   :1337     NA's   :1333

Câu hỏi

Trong dataset này có hai chỉ tiêu sulfatenitrate được cảm biến đo độc lập nhau. Có những ngày ghi nhận được giá trị sulfate hoặc nitrate, có những ngày không ghi nhận giá trị gì cả (missing value), cũng như có ngày ghi nhận đầy đủ cả 2 giá trị sulfatenitrate.

Bạn hãy xây dựng 2 function khác nhau để tìm ra có bao nhiêu giá trị quan sát đầy đủ tương ứng ở từng cảm biến, để làm cơ sở đánh giá xem các cảm biến ở những vị trí khác nhau thì số lượng kết quả theo dõi có đồng đều nhau hay không.

Sau đó so sánh tốc độ xử lý, trả kết quả của hai function này.

Cách 1

Cách tiếp cận function complete_1 này sử dụng hàm for loop để đọc file .csv gom trong 1 list master sau đó gom chung lại thành 1 dataset master rồi mới lọc dữ liệu những dòng nào chứa đủ cả 2 giá trị quan sát ở từng dataset con, sau đó trả về kết quả chung.

complete_1 <- function(directory = "specdata", id) {
    
  # import dataset    
  files_list <- list.files(directory, full.names = TRUE)
  tmp <- files_list[c(id)] # Đây là chỗ tùy chọn
  tmp_working <- vector(mode = "list", length = length(tmp))

  for (i in seq_along(tmp)) {
    tmp_working[[i]] <- read.csv(tmp[[i]]) # Kiểu subset của tmp_working là dạng list tách ra dạng bảng
  }
  
  # gom chung các dataset lại làm 1 dataset master 
  output_tmp_working <- do.call(rbind, tmp_working)

  y <- unique(output_tmp_working$ID) # Đếm số lần xuất hiện (number of occurences for each unique value)

  extract_data <- data.frame(matrix(ncol = 2, nrow = 0))

  for (uu in seq_along(y)) {
    happy <- output_tmp_working[which(output_tmp_working[, "ID"] == y[uu]), ]

    sul <- happy[, 2]
    nit <- happy[, 3]
    tot <- sul + nit
    sum(is.na(tot)) # Hàm đếm missing values trong vector
    nobs <- length(tot) - sum(is.na(tot))
    extract_data <- rbind(extract_data, c(y[uu], nobs))
  }
  # trả kết quả
  x <- c("cam_bien", "full_quan_sat")
  colnames(extract_data) <- x
  return(extract_data)
  
}

Test hàm complete_1 kiểm tra các dataset id từ 20 đến 40.

complete_1(directory = "specdata", id = 20:40)
##    cam_bien full_quan_sat
## 1        20           124
## 2        21           426
## 3        22           135
## 4        23           492
## 5        24           885
## 6        25           463
## 7        26           586
## 8        27           338
## 9        28           475
## 10       29           711
## 11       30           932
## 12       31           483
## 13       32           616
## 14       33           466
## 15       34           165
## 16       35           509
## 17       36           495
## 18       37           497
## 19       38           491
## 20       39           734
## 21       40            21

Cách 2

Cách tiếp cận function complete_2 sử dụng hàm lapply loop để đọc file .csv gom trong 1 list master. Sau đó tạo 1 hàm con trong này để đếm ngày quan sát chỉ tiêu sulfatenitrate trùng nhau (tức là quan sát full chỉ tiêu), rồi xuất ra kết quả thông qua lệnh sapply. Như vậy sẽ tiết kiệm thời gian hơn gom chung 1 dataset master như cách 1.

complete_2 <- function(directory = "specdata", id) {
    
  # import dataset
  files_list <- list.files(directory,
    full.names = TRUE,
    pattern = ".csv"
  ) # set này để chọn riêng file .csv

  tmp_working <- lapply(files_list[id], read.csv)

  # function con để check giá trị full quan sát 2 chỉ tiêu
  check_full_quan_sat <- function(check_id) {
    raw <- tmp_working[[check_id]]

    check_1 <- which(!is.na(raw$nitrate))
    check_2 <- which(!is.na(raw$sulfate))

    ra_soat <- intersect(check_1, check_2)

    length(ra_soat)
  }

  # áp dụng sapply cho seq_along(tmp_working)
  full_quan_sat <- sapply(seq_along(tmp_working), check_full_quan_sat)

  full_quan_sat <- as.data.frame(full_quan_sat) # sapply trả về matrix, cần convert qua lại data frame
  
  # trả kết quả
  ket_qua <- cbind(cam_bien = id, full_quan_sat)

  return(ket_qua)
}

Test hàm complete_2 kiểm tra các dataset id từ 20 đến 40.

complete_2(directory = "specdata", id = 20:40)
##    cam_bien full_quan_sat
## 1        20           124
## 2        21           426
## 3        22           135
## 4        23           492
## 5        24           885
## 6        25           463
## 7        26           586
## 8        27           338
## 9        28           475
## 10       29           711
## 11       30           932
## 12       31           483
## 13       32           616
## 14       33           466
## 15       34           165
## 16       35           509
## 17       36           495
## 18       37           497
## 19       38           491
## 20       39           734
## 21       40            21

So sánh 2 function

Việc giải bài toán trong R thường đi từ các cách tiếp cận khác nhau phụ thuộc vào mức độ am hiểu với những câu lệnh nào mà chúng ta biết. Mục tiêu vẫn là trả được kết quả như yêu cầu đề bài. Tuy nhiên khi xử lý những tình huống phức tạp, thì các function nào xây dựng dựa trên những dòng lệnh an toàn (ví dụ dùng các lệnh họ apply thay vì dùng for loop để tránh bị trùng biến trung gian) và thông minh (xử lý theo yêu cầu thay vì gom lại tổng thể rồi subset) sẽ giúp tăng tốc độ trả về kết quả, nhanh chóng có câu trả lời.

Vì vậy việc đánh giá hiệu suất giữa hai function là cần thiết để chúng ta có cơ sở nên dùng function nào, và cách tiếp cận nào phù hợp với từng tình huống cụ thể.

Tiêu chí đánh giá function trong R, theo mình, dựa vào các chỉ tiêu sau:

1/ Các câu lệnh sử dụng không xuất hiện biến trung gian (nếu có), vì khi xây dựng một đoạn code lớn thì khả năng trùng tên biến khá cao. Nếu am hiểu lệnh họ apply thì các bạn có thể dùng thay thế for loop.

2/ Cách giải quyết bài toán trực tiếp theo kiểu lazy function, tức là khi nào cần thì mới gọi ra xử lý (như cách 2, vì dataset nằm trong list master), thay vì gom lại 1 dataset master (như cách 1) thì gây tốn thời gian và CPU máy tính.

3/ Độ dài ngắn của script code thì không nói lên điều gì về mức độ đoạn code đó hiệu quả hay không. Mà chủ yếu cách trình bày format script code có comment dễ hiểu, giúp chúng ta nhìn vào hiểu được ý tưởng của từng code block trong đó, thay vì phải mất time suy luận coi đoạn code này giải quyết vấn đề gì. Tất nhiên, một đoạn code ngắn gọn, súc tích, đáp ứng tiêu chí 1/ và 2/ sẽ hiệu quả hơn là cách viết code rườm rà (vì càng cồng kềnh thì khả năng xảy ra lỗi càng cao, đôi khi chỉ là thiếu dấu {} đóng mở ngoặc).

Sử dụng lệnh identical() so sánh kết quả

Sau khi xây dựng function thì việc test hàm ở các tình huống khác nhau là rất cần thiết để dự trù trước hầu như mọi chuyện sẽ xảy ra khi áp dụng function trong thực tế. Nếu bạn có hai function với cách tiếp cận khác nhau nhưng để đảm bảo cả 2 function này hoạt động trả về cùng kết quả y chang nhau (nghĩa là nó có cùng chức năng) thì việc dùng hàm identical sẽ là khâu confirm quyết định sự thành công của function cải tiến so với function chuẩn ban đầu.

Ở đây chúng ta sẽ test các tình huống đọc dữ liệu các dataset từ 1 đến 332 (toàn bộ file .csv) để xem kết quả của hai function này ra sao.

library(DT) # xuất kết quả dạng bảng pagination

full_1 <- complete_1(directory = "specdata", id = 1:332)

full_2 <- complete_2(directory = "specdata", id = 1:332)

datatable(full_1, options = list(pageLength = 5))
datatable(full_2, options = list(pageLength = 5))
# nếu kết quả lệnh `identical()` là TRUE thì cho thấy kết quả xuất ra từ hai function này y chang nhau.
identical(complete_1(directory = "specdata", id = 1:332),
          complete_2(directory = "specdata", id = 1:332))
## [1] TRUE

Sử dụng lệnh system.time() để đo tốc độ xử lý

Sau khi xác định được function complete_1complete_2 có khả năng như nhau, thì việc lựa chọn function nào phụ thuộc vào thời gian trả kết quả của function đó nhanh hay chậm.

Ta áp dụng một trong hai cách sau lệnh system.time() hoặc R profiler để đánh giá hiệu suất xử lý.

# thời gian để thực thi function `complete_1`
time_1 <- system.time(complete_1(directory = "specdata", id = 1:332))
time_1
##    user  system elapsed 
##    6.00    1.65    9.54
# thời gian để thực thi function `complete_2`
time_2 <- system.time(complete_2(directory = "specdata", id = 1:332))
time_2
##    user  system elapsed 
##    2.23    0.50    2.81

Các bạn so sánh user time là thời gian CPU máy tính chạy để thực thi function cũng như elapsed time là tổng thời gian thực thi câu lệnh của hai function này, kết quả là function complete_2 chạy nhanh hơn 3.4 lần so với function complete_1.

Sử dụng lệnh Rprof() để phân tích tốc độ xử lý

Nếu chúng ta muốn biết chính xác trong nội bộ từng lệnh nhỏ ở trong từng function hoạt động nhanh, chậm ra sao, khâu nào là chậm nhất (để có cơ sở cải tiến đoạn code đó), thì ta áp dụng R profiler, với cách tiếp cận là:

1/ Gọi lệnh Rprof() để báo cho R console biết là sẽ theo dõi các function sắp gọi ra. 2/ Gọi function cần thực thi 3/ Xuất kết quả profiling (theo dõi function) bằng lệnh summaryRprof()

Ở đây, ta sẽ kiểm tra xem ở trong từng function complete_1complete_2 thì cụ thể những lệnh nào thực thi bao lâu, tổng thời gian thực thi như thế nào (kết quả này sẽ khác đôi chút với cách tiếp cận dùng lệnh system.time() ở trên.

Kết quả profiling function complete_1

Rprof() # theo dõi function
profile_1 <- complete_1(directory = "specdata", id = 1:332) # gọi function, đưa kết quả vào dataset
report_1 <- summaryRprof() # xuất báo cáo tốc độ xử lý từng hàm con trong function
report_1
## $by.self
##                         self.time self.pct total.time total.pct
## "=="                         1.14    27.94       1.14     27.94
## "scan"                       1.06    25.98       1.06     25.98
## "rbind"                      0.50    12.25       0.56     13.73
## ".External2"                 0.40     9.80       0.40      9.80
## "which"                      0.36     8.82       1.50     36.76
## "read.table"                 0.08     1.96       1.84     45.10
## "file"                       0.08     1.96       0.08      1.96
## "[.data.frame"               0.06     1.47       1.64     40.20
## "anyDuplicated.default"      0.06     1.47       0.06      1.47
## "eval"                       0.04     0.98       4.08    100.00
## "%in%"                       0.04     0.98       0.04      0.98
## ".External"                  0.04     0.98       0.04      0.98
## "close.connection"           0.04     0.98       0.04      0.98
## "type.convert"               0.02     0.49       0.46     11.27
## "type.convert.default"       0.02     0.49       0.44     10.78
## "make.names"                 0.02     0.49       0.06      1.47
## "match.arg"                  0.02     0.49       0.06      1.47
## "attr"                       0.02     0.49       0.02      0.49
## "lengths"                    0.02     0.49       0.02      0.49
## "Rprof"                      0.02     0.49       0.02      0.49
## "structure"                  0.02     0.49       0.02      0.49
## "unique.default"             0.02     0.49       0.02      0.49
## 
## $by.total
##                           total.time total.pct self.time self.pct
## "eval"                          4.08    100.00      0.04     0.98
## "eval_with_user_handlers"       4.08    100.00      0.00     0.00
## "handle"                        4.08    100.00      0.00     0.00
## "timing_fn"                     4.08    100.00      0.00     0.00
## "withCallingHandlers"           4.08    100.00      0.00     0.00
## "withVisible"                   4.08    100.00      0.00     0.00
## "block_exec"                    4.06     99.51      0.00     0.00
## "call_block"                    4.06     99.51      0.00     0.00
## "complete_1"                    4.06     99.51      0.00     0.00
## "eng_r"                         4.06     99.51      0.00     0.00
## "evaluate"                      4.06     99.51      0.00     0.00
## "evaluate::evaluate"            4.06     99.51      0.00     0.00
## "evaluate_call"                 4.06     99.51      0.00     0.00
## "in_dir"                        4.06     99.51      0.00     0.00
## "in_input_dir"                  4.06     99.51      0.00     0.00
## "knitr::knit"                   4.06     99.51      0.00     0.00
## "process_file"                  4.06     99.51      0.00     0.00
## "process_group"                 4.06     99.51      0.00     0.00
## "process_group.block"           4.06     99.51      0.00     0.00
## "rmarkdown::render"             4.06     99.51      0.00     0.00
## "read.table"                    1.84     45.10      0.08     1.96
## "read.csv"                      1.84     45.10      0.00     0.00
## "[.data.frame"                  1.64     40.20      0.06     1.47
## "["                             1.64     40.20      0.00     0.00
## "which"                         1.50     36.76      0.36     8.82
## "=="                            1.14     27.94      1.14    27.94
## "scan"                          1.06     25.98      1.06    25.98
## "rbind"                         0.56     13.73      0.50    12.25
## "<Anonymous>"                   0.50     12.25      0.00     0.00
## "do.call"                       0.50     12.25      0.00     0.00
## "type.convert"                  0.46     11.27      0.02     0.49
## "type.convert.default"          0.44     10.78      0.02     0.49
## ".External2"                    0.40      9.80      0.40     9.80
## "file"                          0.08      1.96      0.08     1.96
## "anyDuplicated.default"         0.06      1.47      0.06     1.47
## "make.names"                    0.06      1.47      0.02     0.49
## "match.arg"                     0.06      1.47      0.02     0.49
## "anyDuplicated"                 0.06      1.47      0.00     0.00
## "%in%"                          0.04      0.98      0.04     0.98
## ".External"                     0.04      0.98      0.04     0.98
## "close.connection"              0.04      0.98      0.04     0.98
## "close"                         0.04      0.98      0.00     0.00
## "order"                         0.04      0.98      0.00     0.00
## "attr"                          0.02      0.49      0.02     0.49
## "lengths"                       0.02      0.49      0.02     0.49
## "Rprof"                         0.02      0.49      0.02     0.49
## "structure"                     0.02      0.49      0.02     0.49
## "unique.default"                0.02      0.49      0.02     0.49
## "[["                            0.02      0.49      0.00     0.00
## "[[.data.frame"                 0.02      0.49      0.00     0.00
## "evaluate_call                  0.02      0.49      0.00     0.00
## "unique"                        0.02      0.49      0.00     0.00
## 
## $sample.interval
## [1] 0.02
## 
## $sampling.time
## [1] 4.08

Kết quả profiling function complete_2

Rprof()
profile_2 <- complete_2(directory = "specdata", id = 1:332)
report_2 <- summaryRprof()
report_2
## $by.self
##                    self.time self.pct total.time total.pct
## "scan"                  1.02    50.50       1.04     51.49
## ".External2"            0.34    16.83       0.34     16.83
## "file"                  0.18     8.91       0.20      9.90
## "read.table"            0.12     5.94       1.90     94.06
## "close.connection"      0.06     2.97       0.06      2.97
## "character"             0.04     1.98       0.04      1.98
## "pmatch"                0.04     1.98       0.04      1.98
## "handle"                0.02     0.99       2.00     99.01
## "FUN"                   0.02     0.99       1.94     96.04
## "match.arg"             0.02     0.99       0.06      2.97
## "order"                 0.02     0.99       0.04      1.98
## "%in%"                  0.02     0.99       0.02      0.99
## "getOption"             0.02     0.99       0.02      0.99
## "is.array"              0.02     0.99       0.02      0.99
## "numeric"               0.02     0.99       0.02      0.99
## "strsplit"              0.02     0.99       0.02      0.99
## "which"                 0.02     0.99       0.02      0.99
## lers"                   0.02     0.99       0.02      0.99
## 
## $by.total
##                           total.time total.pct self.time self.pct
## "handle"                        2.00     99.01      0.02     0.99
## "evaluate"                      2.00     99.01      0.00     0.00
## "evaluate::evaluate"            2.00     99.01      0.00     0.00
## "evaluate_call"                 2.00     99.01      0.00     0.00
## "in_dir"                        2.00     99.01      0.00     0.00
## "timing_fn"                     2.00     99.01      0.00     0.00
## "withCallingHandlers"           2.00     99.01      0.00     0.00
## "block_exec"                    1.98     98.02      0.00     0.00
## "call_block"                    1.98     98.02      0.00     0.00
## "eng_r"                         1.98     98.02      0.00     0.00
## "eval"                          1.98     98.02      0.00     0.00
## "eval_with_user_handlers"       1.98     98.02      0.00     0.00
## "in_input_dir"                  1.98     98.02      0.00     0.00
## "knitr::knit"                   1.98     98.02      0.00     0.00
## "process_file"                  1.98     98.02      0.00     0.00
## "process_group"                 1.98     98.02      0.00     0.00
## "process_group.block"           1.98     98.02      0.00     0.00
## "rmarkdown::render"             1.98     98.02      0.00     0.00
## "withVisible"                   1.98     98.02      0.00     0.00
## "FUN"                           1.94     96.04      0.02     0.99
## "complete_2"                    1.94     96.04      0.00     0.00
## "lapply"                        1.94     96.04      0.00     0.00
## "read.table"                    1.90     94.06      0.12     5.94
## "scan"                          1.04     51.49      1.02    50.50
## "type.convert"                  0.44     21.78      0.00     0.00
## "type.convert.default"          0.44     21.78      0.00     0.00
## ".External2"                    0.34     16.83      0.34    16.83
## "file"                          0.20      9.90      0.18     8.91
## "close.connection"              0.06      2.97      0.06     2.97
## "match.arg"                     0.06      2.97      0.02     0.99
## "close"                         0.06      2.97      0.00     0.00
## "character"                     0.04      1.98      0.04     1.98
## "pmatch"                        0.04      1.98      0.04     1.98
## "order"                         0.04      1.98      0.02     0.99
## "make.names"                    0.04      1.98      0.00     0.00
## "sapply"                        0.04      1.98      0.00     0.00
## "summaryRprof"                  0.04      1.98      0.00     0.00
## "%in%"                          0.02      0.99      0.02     0.99
## "getOption"                     0.02      0.99      0.02     0.99
## "is.array"                      0.02      0.99      0.02     0.99
## "numeric"                       0.02      0.99      0.02     0.99
## "strsplit"                      0.02      0.99      0.02     0.99
## "which"                         0.02      0.99      0.02     0.99
## lers"                           0.02      0.99      0.02     0.99
## "intersect"                     0.02      0.99      0.00     0.00
## "vapply"                        0.02      0.99      0.00     0.00
## "withCallingHand                0.02      0.99      0.00     0.00
## 
## $sample.interval
## [1] 0.02
## 
## $sampling.time
## [1] 2.02

Kết quả thời gian sampling.time xem như đại diện cho tổng thời gian thực thi câu lệnh trong function complete_1 là 4.08 (tính theo giây) lâu hơn trong function complete_2 là 2.02, hay nói cách khác function complete_2 có thời gian xử lý kết quả nhanh hơn function complete_1.

Biểu đồ so sánh tốc độ xử lý

Gom kết quả vào matrix để vẽ đồ thị cột

# tạo matrix

f1 <- c(unclass(time_1)[1], unclass(time_1)[2], unclass(time_1)[3], report_1$sampling.time)

f2 <- c(unclass(time_2)[1], unclass(time_2)[2], unclass(time_2)[3], report_2$sampling.time)

barplot_data <- cbind(f1, f2)

colnames(barplot_data) <- c("complete_1", "complete_2")
rownames(barplot_data) <- c("user time", "system time", "elapsed time", "sampling time")

barplot_data
##               complete_1 complete_2
## user time           6.00       2.23
## system time         1.65       0.50
## elapsed time        9.54       2.81
## sampling time       4.08       2.02

Vẽ đồ thị cột

Lưu ý là mỗi lần ta render file .Rmd thì R sẽ chạy lại toàn bộ các lệnh đo lường tốc độ xử lý nên kết quả thu được sẽ khác biệt nhau tùy vào tốc độ CPU ở thời điểm đó.

# Define a set of colors
my_colors <- c("palegreen1", "yellow1", "salmon1", "cornsilk")

# Bar plot
barplot(barplot_data, 
  col = my_colors,
  beside = TRUE,
  ylim = c(0, yes),
  xlab = "Chỉ tiêu theo dõi",
  ylab = "Thời gian (giây)",
  main = "So sánh tốc độ xử lý giữa hai function | tuhocr.com",
  las = 1
)

# Add legend
legend("top",
  legend = rownames(barplot_data),
  horiz = TRUE,
  fill = my_colors,
  box.lty = 0,
  cex = 1
)

# Đưa đường baseline
abline(h = max(barplot_data[, 2]), col = "red", lty = 2, lwd = 2)

# Ghi thêm text trên abline
text(8,
     max(barplot_data[, 2]),
     "Thời gian xử lý của complete_2 \n luôn thấp hơn complete_1",
     srt = 0,
     pos = 3,
     cex = 1,
     col = "blue")

Như vậy, function complete_2 có tốc độ xử lý nhanh hơn function complete_1 nhờ áp dụng các lệnh apply cũng như tiếp cận theo cách subset dữ liệu từ list master thay vì gom chung thành data frame master.

Sơ kết

Trên đây là ví dụ minh họa cho bài giảng Cách đánh giá tốc độ function 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

Session info

sessionInfo()
## R version 4.2.1 (2022-06-23 ucrt)
## Platform: x86_64-w64-mingw32/x64 (64-bit)
## Running under: Windows 10 x64 (build 19041)
## 
## Matrix products: default
## 
## locale:
## [1] LC_COLLATE=English_United States.utf8 
## [2] LC_CTYPE=English_United States.utf8   
## [3] LC_MONETARY=English_United States.utf8
## [4] LC_NUMERIC=C                          
## [5] LC_TIME=English_United States.utf8    
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] DT_0.26
## 
## loaded via a namespace (and not attached):
##  [1] digest_0.6.29     R6_2.5.1          jsonlite_1.8.0    magrittr_2.0.3   
##  [5] evaluate_0.16     highr_0.9         stringi_1.7.8     cachem_1.0.6     
##  [9] rlang_1.0.6       cli_3.4.0         rstudioapi_0.14   jquerylib_0.1.4  
## [13] bslib_0.4.0       rmarkdown_2.16    tools_4.2.1       stringr_1.4.1    
## [17] htmlwidgets_1.5.4 crosstalk_1.2.0   xfun_0.33         yaml_2.3.5       
## [21] fastmap_1.1.0     compiler_4.2.1    htmltools_0.5.3   knitr_1.40       
## [25] sass_0.4.2