Cách xem source và edit function

Trong R có hai dạng function là S3 và S4 method, ngoài ra còn các dạng function khác. Phổ biến nhất là dạng S3 method (dạng căn bản), vì vậy các lệnh trong bài này được áp dụng cho việc truy vấn các function S3 method, nhằm hỗ trợ việc tùy chỉnh, đọc hiểu cấu trúc câu lệnh được tốt hơn, từ đó giúp ta dễ dàng truy ra phương pháp tính toán cho các tình huống cụ thể.

Để nắm chắc kiến thức về function trong R, các bạn xem ở section 25 về cách xây dựng function trong R nhé. Bài giảng

Các lệnh truy vấn function

Ta xét trường hợp lệnh subset() được dùng để trích xuất các thành phần trong data frame theo cột.

# trích xuất giá trị cyl == 6 ở cột cyl
df <- mtcars |> subset(cyl == 6)
df
                mpg cyl  disp  hp drat    wt  qsec vs am gear carb
Mazda RX4      21.0   6 160.0 110 3.90 2.620 16.46  0  1    4    4
Mazda RX4 Wag  21.0   6 160.0 110 3.90 2.875 17.02  0  1    4    4
Hornet 4 Drive 21.4   6 258.0 110 3.08 3.215 19.44  1  0    3    1
Valiant        18.1   6 225.0 105 2.76 3.460 20.22  1  0    3    1
Merc 280       19.2   6 167.6 123 3.92 3.440 18.30  1  0    4    4
Merc 280C      17.8   6 167.6 123 3.92 3.440 18.90  1  0    4    4
Ferrari Dino   19.7   6 145.0 175 3.62 2.770 15.50  0  1    5    6

Giờ ta muốn xem ruột/source của function này thì cách làm như sau.

# gõ trực tiếp tên function
subset
function (x, ...) 
UseMethod("subset")
<bytecode: 0x000001806026a728>
<environment: namespace:base>

Khi gõ riêng tên function, ta thấy có UseMethod("subset") nghĩa là function này là 1 function chung (generic), nó còn có các function con (method) tương ứng từng loại (class) đối tượng để subset. Ta truy vấn tiếp lệnh này.

methods("subset")
[1] subset.data.frame subset.default    subset.matrix     subset.news_db*  
see '?methods' for accessing help and source code

Kết quả trả về sẽ là các function con cụ thể, function nào có dấu hoa thị * là dạng function bị ẩn, các function này ta gọi trực tiếp bằng lệnh ten_package:::ten_function() để truy đến package gốc chứa function đó.

Khi gõ tên function cụ thể ra, ta đọc được source của function này. Đặc điểm function trong R sẽ tạo ra các đối tượng hoạt động trong môi trường riêng biệt (local environment) sẽ không hiển thị ở (global environment), do đó cụm bytecode: 0x0000012babb1b2d8 chính là mã ID của vùng local environment mà tại đó các đối tượng trong ruột function sẽ tồn tại để thực hiện các bước tính toán.

# ở đây ta có `mtcars` thuộc class(mtcars) là data.frame
# nên lệnh subset (dấu chấm) data.frame sẽ là lệnh
# thực hiện chính công việc subset dữ liệu này
subset.data.frame
function (x, subset, select, drop = FALSE, ...) 
{
    chkDots(...)
    r <- if (missing(subset)) 
        rep_len(TRUE, nrow(x))
    else {
        e <- substitute(subset)
        r <- eval(e, x, parent.frame())
        if (!is.logical(r)) 
            stop("'subset' must be logical")
        r & !is.na(r)
    }
    vars <- if (missing(select)) 
        rep_len(TRUE, ncol(x))
    else {
        nl <- as.list(seq_along(x))
        names(nl) <- names(x)
        eval(substitute(select), nl, parent.frame())
    }
    x[r, vars, drop = drop]
}
<bytecode: 0x0000018060a23078>
<environment: namespace:base>
base:::subset.data.frame

Giờ ta muốn can thiệp/thay đổi function subset này ngay trong phiên làm việc/session thì ta dùng lệnh trace

# có thể để tên function trong dấu "" hoặc không cần cũng được.
# tốt nhất để trực tiếp tên function without quoted
trace(what = subset.data.frame,
      tracer = FALSE,
      edit = TRUE)

Thay đổi nội dung function ngay trong phiên làm việc (nếu ta restart RStudio thì function sẽ trở lại trạng thái ban đầu, không bị ghi đè các thay đổi này), phương pháp này rất hữu ích để ta debug/dò ra các biến trung gian và cơ chế hoạt động của function để modify lại theo ý của mình.

Gọi lại function, lúc này trên global environment xuất hiện đối tượng x_ok từ trong local environment của lệnh subset.data.frame.

df <- mtcars |> subset(cyl == 6)
df
                mpg cyl  disp  hp drat    wt  qsec vs am gear carb
Mazda RX4      21.0   6 160.0 110 3.90 2.620 16.46  0  1    4    4
Mazda RX4 Wag  21.0   6 160.0 110 3.90 2.875 17.02  0  1    4    4
Hornet 4 Drive 21.4   6 258.0 110 3.08 3.215 19.44  1  0    3    1
Valiant        18.1   6 225.0 105 2.76 3.460 20.22  1  0    3    1
Merc 280       19.2   6 167.6 123 3.92 3.440 18.30  1  0    4    4
Merc 280C      17.8   6 167.6 123 3.92 3.440 18.90  1  0    4    4
Ferrari Dino   19.7   6 145.0 175 3.62 2.770 15.50  0  1    5    6

Truy vấn function từ package

Cách làm cũng tương tự như trên, ta lưu ý khi dùng function từ package nào thì cần gọi trực tiếp đến tên function gốc theo cú pháp :::.

library(car)
# vẽ lần 1 bằng lệnh qqPlot
car:::qqPlot(trees$Height)

qqPlot
function (x, ...) 
{
    UseMethod("qqPlot")
}
<bytecode: 0x000001805f14d180>
<environment: namespace:car>
methods("qqPlot")
[1] qqPlot.default* qqPlot.formula* qqPlot.glm*     qqPlot.lm*     
see '?methods' for accessing help and source code
# khi dò được function gốc rồi ta dễ dàng xem được
# source của function này để chỉnh sửa
car:::qqPlot.default(trees$Height)
# chỉnh sửa trực tiếp trong phiên làm việc
trace(what = car:::qqPlot.default,
      tracer = F,
      edit = TRUE)

Nếu muốn tìm xem function nào thuộc package nào, các bạn có thể dùng các lệnh sau.

library(car)

# tìm xem function này thuộc package nào
getAnywhere(qqPlot)

# tìm xem function này thuộc package nào (chỉ áp dụng cho các function đã được load/library trong phiên làm việc)
find("qqPlot")

# tìm xem function nayù thuộc package nào
environment(qqPlot)
environmentName(environment(qqPlot))

# tìm các đối tượng có cùng tên này 
apropos("qqPlot")

# xác định xem function này có tồn tại
exists("qqPlot", mode = "function")

Câu 1

Dựa trên kiến thức về function, bạn hãy tùy chỉnh lại function car:::qqPlot.default để khi vẽ ra các điểm outlier thì sẽ tô màu đỏ ở phần label. Kết quả giống như hình này.

Gợi ý cách làm:

  1. Ta sẽ gọi riêng lệnh car:::qqPlot.default này ra ngoài console để copy source của function, lưu trong 1 file script đặt tên là qqPlot_enhanced.R

  2. Ta chỉnh sửa ruột function trong file script này và test xem hoạt động được như yêu cầu đề bài hay không. Cách tiếp cận là dò lần lượt từng cụm code và check, function nào trong ruột của function này mà có liên quan đến package khác thì ta sửa câu lệnh theo kiểu ::: để gọi trực tiếp function đó ra. minh họa

  3. Sau khi xong hết bạn kiểm tra hoạt động 2 function car:::qqPlot.default(trees$Height)qqPlot_enhanced(trees$Height) nếu ra cùng kết quả thì ta đã biết cách chỉnh sửa function theo ý của mình.

par(mfrow = c(1,2))
car:::qqPlot.default(trees$Height,
                     main = "Lệnh gốc")

# các bạn xem file source ở đây và chạy lại trên máy tính nhé
source("dataset/qqPlot_enhanced.R")
qqPlot_enhanced(trees$Height,
                main = "Lệnh được cải tiến")

Để thuận tiện đối chiếu giữa hai file code, các bạn copy source file qqPlot_ori.RqqPlot_enhanced.R, dùng chương trình compare code để đối chiếu xem đã có những thay đổi nào trong function cải tiến nhé. Mình thường dùng trang web này để đối chiếu file code diffchecker.