本工具使用的開源套件

本工具程式碼中捆綁了 1 個開源套件。

R 速查 — 簡明參考

R 4.4 語法、數據結構與最常用 base R / tidyverse 速查手冊,覆蓋約 80% 日常場景。

R

R R 4.4

R (GNU R / 交互式解釋器) · 函數式 · 向量化 · OO (S3 / S4 / R6) · 動態

學習路徑

先學會 Rscript 運行與賦值(<-)→ 掌握向量、data.frame 等數據結構 → 用 if/for/apply 家族寫流程 → 學會函數、閉包與管道 |> → 深入 apply 集合操作 → 理解 S3/R6 面向對象與錯誤處理 → 再按需學文件 I/O、並行、網絡與正則。FAQ 節適合回頭避坑;包管理與 renv 見構建章節。

1.Hello World 與運行環境

Rscript 運行、REPL 交互、打印輸出與幫助文檔。

最小程序

R 以表達式為單位求值。print 打印對象,cat 直接輸出。腳本可用 Rscript 運行。

1
2
3
4
5
print('Hello, world!') # 打印到控制台
cat('Hello, world!\n') # 直接輸出,無引號
# 保存為 hello.R 後運行:
# $ Rscript hello.R
# 或在 R / RStudio 裏 source('hello.R')

賦值與輸出

賦值用 <- 或 =。print 顯示對象結構,cat 適合拼接輸出。message 寫 stderr。

1
2
3
4
5
x <- 42 # 賦值(推薦 <-)
y = 42 # = 也合法,風格上少用
print(x) # 顯示 [1] 42
cat('x =', x, '\n') # 拼接輸出:x = 42
message('這是提示') # 寫往 stderr

Rscript 運行

Rscript 以非交互方式運行腳本,適合批處理與命令行。source 在會話內運行腳本文件。

1
2
3
4
5
6
7
8
9
10
# 腳本 hello.R:
# print('hello')
# 運行腳本:
# $ Rscript hello.R
# 運行並傳參:
# $ Rscript hello.R a b
# 會話內運行文件:
source('hello.R')
# 直接執行字符串:
eval(parse(text = '1 + 1'))

命令行參數

commandArgs(trailingOnly = TRUE) 取腳本後的命令行參數。參數均為字符串。

1
2
3
4
5
6
7
8
9
# 運行:Rscript app.R a b c
args <- commandArgs(trailingOnly = TRUE)
print(args) # [1] "a" "b" "c"
# 是否含腳本名(FALSE 時 args[1] 是腳本路徑):
commandArgs() # 含 Rscript 與腳本路徑
# 用 args[1] 當輸入路徑:
if (length(args) > 0) {
input <- args[1]
}

打印輸出

print 顯示對象與結構,cat 無引號拼接,sprintf 格式化。invisible 抑制自動打印。

1
2
3
4
5
6
7
8
x <- c(1, 2, 3)
print(x) # [1] 1 2 3
cat(x, sep = ', ', '\n') # 1, 2, 3
sprintf('Pi = %.2f', pi) # "Pi = 3.14"
# 表達式自動打印(頂層才有):
1 + 1 # [1] 2
# 函數內最後表達式自動返回並打印:
invisible(1) # 不打印

幫助文檔

? 打開幫助,?? 全文搜索。args 看函數形參,example 運行示例。

1
2
3
4
5
6
7
8
?mean # mean 的幫助頁
??regression # 全文搜索
args(mean) # 形參列表
example(mean) # 運行內置示例
apropos('dist') # 搜索含 dist 的對象
help.search('聚類') # 按主題搜索
# 查看源碼:
mean # 打印函數體

加載包

library 加載包到搜索路徑,require 返回邏輯值。:: 顯式取包內函數。

1
2
3
4
5
6
7
8
9
10
11
library(dplyr) # 加載包
library('ggplot2') # 字符串形式亦可
if (!requireNamespace('pkg', quietly = TRUE)) {
install.packages('pkg')
}
# 顯式命名空間調用:
dplyr::select(df, x)
# 查看已加載:
search()
# 已安裝包列表:
rownames(installed.packages())

運行腳本

source 在當前會話執行腳本文件,可定義函數與變量。echo 顯示執行過程。

1
2
3
4
5
6
7
source('utils.R') # 運行 utils.R
source('utils.R', encoding = 'UTF-8')
# 顯示每行與結果:
source('demo.R', echo = TRUE)
# 常用:加載自建函數集:
# utils.R 裏定義 f <- function(x) x * 2
f(5) # 10

2.變量與常量

賦值、基本類型、NA/NULL 與類型轉換。

賦值符號

<- 是慣用賦值;= 也可。<<- 在函數內寫外層變量。全局賦值用 assign。

1
2
3
4
5
6
7
8
9
10
11
x <- 5 # 推薦
y = 5 # 也合法
x <- y <- z <- 1 # 鏈式賦值,三者皆 1
# 函數內改外層變量:
f <- function() {
g <<- 99 # 寫到全局環境
}
# 按名字賦值:
assign('name', 'Rex')
name # "Rex"
# 與 = 的區別:= 不進入嵌套函數環境

基本類型

原子向量有 numeric、integer、character、logical。整數用 L 後綴。typeof 看存儲類型。

1
2
3
4
5
6
7
8
9
10
x <- 3.14 # numeric (double)
i <- 1L # integer(L 後綴)
s <- 'hi' # character
b <- TRUE # logical
typeof(x) # "double"
typeof(i) # "integer"
# 長度:所有標量都是長度為 1 的向量:
length(s) # 1
# 複合類型:
typeof(1:5) # "integer"

NA 與 NULL

NA 表示缺失值,NULL 表示空對象。is.na / is.null 判斷。NaN 是非數值,Inf 無窮。

1
2
3
4
5
6
7
8
9
10
11
12
13
v <- c(1, NA, 3)
is.na(v) # FALSE TRUE FALSE
any(is.na(v)) # TRUE
# NULL 表示對象不存在:
x <- NULL
is.null(x) # TRUE
# 數值上的特殊值:
0 / 0 # NaN
1 / 0 # Inf
is.nan(NaN) # TRUE
is.finite(Inf) # FALSE
# 統計時忽略 NA:
mean(c(1, NA), na.rm = TRUE) # 1

類型轉換

as.* 顯式轉換。c() 拼接不同類時自動向最通用類型轉換。判斷用 is.*。

1
2
3
4
5
6
7
8
9
10
11
12
as.numeric('42') # 42
as.character(42) # "42"
as.logical(1) # TRUE
as.factor(c('a', 'b')) # 因子
# 混合拼接自動轉換:
c(1, 'a') # "1" "a"(變 character)
c(1, TRUE) # 1 1(變 numeric)
# 轉換失敗產生 NA:
as.numeric('abc') # NA(有警告)
# 安全轉換 + 檢查:
x <- suppressWarnings(as.numeric('abc'))
is.na(x) # TRUE

創建向量

c 拼接、冒號生成序列、seq 定製步長、rep 重複。names 給元素命名。

1
2
3
4
5
6
7
8
9
10
c(1, 2, 3) # 拼接
1:10 # 1 2 ... 10
seq(0, 1, by = 0.25) # 0 0.25 ... 1
seq(1, 10, length.out = 5) # 5 個等距點
rep(1, 3) # 1 1 1
rep(c('a', 'b'), each = 2) # a a b b
# 命名元素:
x <- c(a = 1, b = 2)
names(x) # "a" "b"
x['a'] # 1

命名約定

變量名允許字母、數字、點、下劃線,不能以數字開頭。點號在 R 中不是運算符。

1
2
3
4
5
6
7
8
9
10
11
12
my_var <- 1 # 下劃線
my.var <- 2 # 點號合法
.var <- 3 # 以點開頭
# 不能以數字開頭:
# 1var <- 1 # 語法錯誤
# 保留字不可作變量名:
# if <- 1 # 錯誤
# 駝峯或蛇形均可,保持一致:
totalCount <- 10
total_count <- 20
# 點號在 R 裏是普通字符:
a.b <- 1 # 合法

變量管理

ls 列出變量,rm 刪除。exists 判斷存在。rm(list = ls()) 清空環境。

1
2
3
4
5
6
7
8
9
10
11
x <- 1; y <- 2
ls() # "x" "y"
exists('x') # TRUE
rm(x) # 刪除 x
rm(list = ls()) # 清空當前環境
# 只刪特定模式:
rm(list = ls(pattern = '^tmp'))
# 查看對象結構:
str(y) # num 2
# 環境信息:
ls(all.names = TRUE) # 含隱藏對象

內置常量

R 內置常用常量:pi、letters、month.name,以及 .Machine 平台細節。

1
2
3
4
5
6
7
8
9
10
11
pi # 3.141593
letters # a b ... z
LETTERS # A B ... Z
letters[1:3] # "a" "b" "c"
month.name # January ...
month.abb # Jan Feb ...
# 機器精度等:
.Machine$double.eps # 2.22e-16
.Machine$integer.max # 2147483647
# 其他內置:
date() # 當前日期時間字符串

作用域基礎

R 詞法作用域:函數內找變量逐級向外。全局與局部同名會遮蔽。get 指定環境取值。

1
2
3
4
5
6
7
8
9
10
11
12
13
x <- 'global'
f <- function() {
x <- 'local' # 遮蔽全局
x
}
f() # "local"
# 不賦值則取全局:
g <- function() x
g() # "global"
# 顯式環境取值:
get('x', envir = .GlobalEnv)
# 環境鏈:
parent.env(globalenv())

3.數據類型與結構

向量、因子、矩陣、列表、數據框與 tibble,R 的數據結構體系。

向量

原子向量同質且一維,是 R 最基礎結構。c() 創建,索引從 1 開始。

1
2
3
4
5
6
7
8
9
10
11
12
v <- c(1, 2, 3)
v[1] # 1(索引從 1 起)
v[c(1, 3)] # 1 3
v[-1] # 去掉第 1 個
v > 1 # FALSE TRUE TRUE
v[v > 1] # 邏輯篩選:2 3
length(v) # 3
# 整數序列:
1:5
# 命名訪問:
x <- c(a = 1, b = 2)
x['a']

因子

因子存分類變量,levels 固定類別。有序因子表示順序。as.factor / factor 創建。

1
2
3
4
5
6
7
8
9
10
11
f <- factor(c('低', '中', '高'))
levels(f) # "低" "中" "高"
table(f) # 計數
# 固定類別順序:
f2 <- factor(c('低', '高'),
levels = c('低', '中', '高'))
# 有序因子:
of <- ordered(c('低', '高'),
levels = c('低', '中', '高'))
of[2] > of[1] # TRUE
# 數值轉因子再取值要小心(見 FAQ):

矩陣

matrix 二維同質。byrow 控制填充方向。行名/列名與維度名 dimnames。

1
2
3
4
5
6
7
8
9
10
11
12
m <- matrix(1:6, nrow = 2, ncol = 3)
m # 2 行 3 列
m[1, 2] # 第 1 行第 2 列
m[1, ] # 第 1 行
# 按行填充:
matrix(1:6, nrow = 2, byrow = TRUE)
# 行/列運算:
rowSums(m); colMeans(m)
# 維度:
dim(m) # 2 3
# 命名:
dimnames(m) <- list(c('r1', 'r2'), c('a', 'b', 'c'))

數組

array 任意維同質結構。dim 指定各維長度。適用多維張量。

1
2
3
4
5
6
7
8
9
10
11
a <- array(1:24, dim = c(2, 3, 4))
dim(a) # 2 3 4
a[1, 2, 3] # 切片取值
# 索引後變低維:
a[1, , ] # 3x4 矩陣
# 沿維度求和:
apply(a, 3, sum) # 每個第三維切片求和
# 變維:
dim(a) <- c(6, 4) # 重塑為 6x4
# 與 matrix 的關係:
is.matrix(a) # TRUE(當 dim 為 2 時)

列表

list 異構可嵌套。$ 與 [[ 取元素,[ 返回子列表。length 統計元素數。

1
2
3
4
5
6
7
8
9
10
11
l <- list(name = 'Rex', age = 5, tags = c('a', 'b'))
l$name # "Rex"
l[[2]] # 5
l[1] # 子列表(含 name)
length(l) # 3
names(l) # "name" "age" "tags"
# 嵌套列表:
l2 <- list(a = list(x = 1), b = 2)
l2$a$x # 1
# 遞歸遍歷:
unlist(l2) # 拍平為向量

數據框

data.frame 表格型數據,列可異質。默認 stringsAsFactors=FALSE(R 4.0+)。str/head 查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
df <- data.frame(
name = c('alice', 'bob'),
age = c(30, 25),
admin = c(TRUE, FALSE)
)
df$age # 取列(向量)
df[2, ] # 第 2 行
df[['name']] # 取列(精確名)
nrow(df); ncol(df) # 2 3
str(df) # 結構概覽
head(df, 2) # 前 2 行
# 增列:
df$score <- c(88, 92)

tibble

tibble 是 tidyverse 的現代數據框:惰性計算列、打印友好、不轉字符串為因子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# library(tibble)
tb <- tibble(
name = c('a', 'b'),
value = c(1, 2)
)
tb # 打印友好,顯示類型
# 列可引用前面列:
tb2 <- tibble(x = 1:3, y = x * 2)
# 不轉因子:
class(tb$name) # character
# 與 data.frame 互轉:
df <- as.data.frame(tb)
tb3 <- as_tibble(df)
# 提取:
tb$value; tb[['value']]

屬性

attributes 攜帶元信息:names、dim、class 等。attr 單獨讀取/設置。structure 一步構造。

1
2
3
4
5
6
7
8
9
10
11
12
x <- 1:3
attr(x, 'unit') <- 'cm' # 自定義屬性
attributes(x) # 列出全部
attr(x, 'unit') # "cm"
# structure 一步構造:
y <- structure(1:3, unit = 'cm')
# 常用內置屬性:
dim(m); names(df); class(x)
# 移除屬性:
attributes(x) <- NULL
# 查看對象類:
class(c(1, 2)) # "numeric"

類型判斷

is.* 判斷類型,class/typeof/mode 看類型。as.* 轉換。is.data.frame 等用於分支。

1
2
3
4
5
6
7
8
9
10
11
12
is.numeric(1) # TRUE
is.integer(1L) # TRUE
is.character('a') # TRUE
is.logical(TRUE) # TRUE
is.list(list()) # TRUE
is.data.frame(df) # TRUE
is.matrix(m) # TRUE
# class vs typeof:
class(data.frame()) # "data.frame"
typeof(list()) # "list"
# 判斷 null / na:
is.null(NULL); is.na(NA)

4.引用與對象語義

R 無裸指針:copy-on-modify 值語義與 environment 引用語義。

寫時複製

R 採用複製時修改(copy-on-modify):賦值共享數據,僅在被修改時才真正複製。大對象賦別名幾乎零成本。

1
2
3
4
5
6
7
8
9
10
11
x <- 1:1e6
y <- x # 不復制,共享數據
# 修改 y 才觸發複製:
y[1] <- 99
# 此時 x 不受影響:
x[1] # 1
# tracemem 可觀察複製時機:
tracemem(x)
y2 <- x # 共享,不復制
y2[1] <- 1 # 此處發生複製
untracemem(x)

tracemem 追蹤拷貝

tracemem 標記對象,R 在真正複製該對象時打印地址變化。用來診斷意外拷貝與內存開銷。

1
2
3
4
5
6
7
8
9
10
x <- 1:1e6
tracemem(x) # 開始追蹤
y <- x # 無輸出:未複製
y[1] <- 0 # 輸出 tracemem: 複製
untracemem(x) # 停止追蹤
# 大數據框修改整列會複製:
df <- data.frame(a = 1:1e5, b = rnorm(1e5))
tracemem(df)
df$a <- df$a + 1 # 整框被複制
untracemem(df)

環境引用

environment 是 R 中的引用對象:傳參給函數不會複製,函數內修改直接生效。相當於 R 的「指針」。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
e <- new.env() # 新建環境
e$x <- 1
e[['y']] <- 2
# 函數內修改環境(無需 <<-):
inc <- function(env) {
env$x <- env$x + 1
}
inc(e)
e$x # 2(引用語義生效)
# 讀取:
get('x', envir = e)
ls(e) # "x" "y"
# 環境也是哈希表:
e$name <- 'Rex'

可變狀態容器

環境作可變狀態容器:計數器、緩存、累加器。避免全局變量污染,狀態顯式傳遞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
counter <- function() {
e <- new.env()
e$n <- 0
function() {
e$n <- e$n + 1 # 修改被閉包捕獲的環境
e$n
}
}
next_num <- counter()
next_num() # 1
next_num() # 2
# 緩存:按 key 存結果
cache <- new.env(hash = TRUE)
cache$key <- list(result = 42)

淺拷貝與深拷貝

列表賦值是淺拷貝:共享子對象,改嵌套元素會複製該層。深拷貝需顯式實現。

1
2
3
4
5
6
7
8
9
10
11
12
l1 <- list(x = 1:3, y = list(a = 1))
l2 <- l1 # 淺拷貝,共享子對象
# 修改 l1 頂層觸發該層複製:
l1$x[1] <- 99 # x 被複制,y 仍共享
# 深拷貝示例(遞歸複製):
deep_copy <- function(obj) {
if (is.list(obj)) lapply(obj, deep_copy)
else obj
}
l3 <- deep_copy(l1)
# data.table 有顯式 copy():
# dt2 <- copy(dt)

引用類 R6

R6 是引用語義的類:對象按引用傳遞,方法修改原對象而非副本。適合狀態管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# library(R6)
Counter <- R6Class('Counter',
public = list(
n = 0,
increment = function() self$n <- self$n + 1
)
)
c1 <- Counter$new()
c2 <- c1 # 引用,指向同一對象
c2$increment()
c1$n # 1(c1 也被改了)
# R5 / setRefClass 類似引用語義:
# setRefClass 定義 field + methods
# 相比 S3/S4,R6 修改無 copy-on-modify

data.table 引用

data.table 用 := 原地修改(引用語義),data.frame 是複製語義。setDT 原地轉 data.table。

1
2
3
4
5
6
7
8
9
10
11
12
# library(data.table)
dt <- data.table(a = 1:3, b = 4:6)
dt[, c := a + b] # 原地新增列 c,不復制
dt
# data.frame 修改是複製:
df <- as.data.frame(dt)
df$d <- 1 # 產生副本
# 原地設置/刪除:
dt[, d := NULL] # 刪列
setDT(df) # 原地轉 data.table
# 顯式複製:
dt2 <- copy(dt) # data.table::copy

對象大小

object.size 看單個對象佔用,gc 看整體內存。lobstr::obj_size 能算共享量。

1
2
3
4
5
6
7
8
9
object.size(1:1e6) # ~8 MB
format(object.size(1:1e6), units = 'MB')
# 整體內存:
gc() # 使用/峯值(MB)
memory.size() # Windows 專用
# 共享對象實際佔用:
# lobstr::obj_size(x, y) # 重複部分只算一次
# 大數據集:
# 可用 data.table 減少複製

顯式複製

需要獨立副本時顯式複製:列表用 lapply 遞歸複製,data.table 用 copy,向量用 []。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 列表深複製:
l <- list(a = 1:3, b = list(x = 1))
l_copy <- lapply(l, function(x) {
if (is.list(x)) lapply(x, function(y) y)
else x
})
# data.table 副本:
# dt2 <- copy(dt)
# 向量複製:
v <- c(1, 2, 3)
v2 <- v[] # 顯式複製
# 修改 v2 不影響 v:
v2[1] <- 99
v[1] # 1
# 檢查是否共享:
# lobstr::ref(v, v2)

5.流程控制

if / else、向量化 ifelse、for / while、switch 與邏輯運算。

if / else

if 條件必須是長度 1。非 1 時取首元素並告警。else 與 if 同行結尾括號一致。

1
2
3
4
5
6
7
8
9
10
11
12
x <- 5
if (x > 0) {
print('正數')
} else if (x == 0) {
print('零')
} else {
print('負數')
}
# 條件長度為 1 才安全:
if (c(TRUE, FALSE)) print('x') # 警告並取首元素
# 賦值表達式:
y <- if (x > 0) 'pos' else 'neg'

ifelse 向量化

ifelse 對向量逐元素條件判斷,返回與條件同長的結果。適合批量替換。

1
2
3
4
5
6
7
8
9
10
11
x <- c(-1, 0, 1, 2)
ifelse(x > 0, 'pos', 'non-pos')
# 嵌套:
ifelse(x > 0, 'pos',
ifelse(x == 0, 'zero', 'neg'))
# 注意 ifelse 會強制返回類型:
ifelse(x > 0, 1L, 0) # 都轉 double
# 保留 NA:
ifelse(x > 0, 'pos', NA_character_)
# tidyverse 替代:
# dplyr::case_when(x > 0 ~ 'pos', TRUE ~ 'neg')

for 循環

for 遍歷向量或序列。seq_along 按索引遍歷,避免用 1:length 的空序列陷阱。

1
2
3
4
5
6
7
8
9
10
11
for (i in 1:5) print(i)
# 遍歷向量元素:
for (ch in c('a', 'b')) print(ch)
# 按索引遍歷:
x <- c(10, 20, 30)
for (i in seq_along(x)) {
x[i] <- x[i] * 2
}
x # 20 40 60
# 避免 1:length(x) 當 x 為空時出錯
# 儘量向量化替代循環

while 與 repeat

while 先判條件再執行;repeat 無條件循環,靠 break 退出。二者都注意防止死循環。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
n <- 0
while (n < 3) {
n <- n + 1
}
n # 3
# repeat 至少執行一次:
total <- 0
repeat {
total <- total + 1
if (total >= 5) break
}
total # 5
# 死循環保護:設最大次數
# 條件永不滿足時用 break/return

next 與 break

next 跳到下一輪,break 退出循環。適合跳過與提前終止。

1
2
3
4
5
6
7
8
9
10
11
12
for (i in 1:10) {
if (i %% 2 == 0) next # 跳過偶數
if (i > 7) break # 大於 7 終止
print(i) # 1 3 5 7
}
# 嵌套循環中 break 只退內層:
for (i in 1:3) {
for (j in 1:3) {
if (j == 2) break
cat(i, j, '\n')
}
}

switch

switch 按位置或名字分發。數字取第 n 個,字符匹配命名分支,未匹配返回 NULL。

1
2
3
4
5
6
7
8
9
10
11
12
13
switch(2, 'a', 'b', 'c') # "b"(位置)
op <- 'add'
switch(op,
add = 1 + 1, # 2
mul = 2 * 3,
'未知'
)
# 無默認時未匹配返回 NULL:
switch('nope', add = 1)
# 數字 + 缺失返回 NULL:
switch(5, 'a', 'b')
# match.arg 校驗參數:
# match.arg(choice, c('a', 'b'))

向量化優於循環

R 用向量化替代顯式循環:整向量一次運算,更快更簡潔。循環留給不可向量化場景。

1
2
3
4
5
6
7
8
9
10
11
x <- 1:1e6
y <- x * 2 # 整向量乘,快
z <- x^2 + sqrt(x)
# 等價循環(慢,不推薦):
for (i in seq_along(x)) x[i] * 2
# 常用向量化函數:
sum(x); mean(x); cumsum(x)
which(x > 5) # 滿足條件的索引
x[x > 5] # 篩選值
# 比較向量化:
any(x > 5); all(x > 0)

邏輯運算

& | 是向量化邏輯運算,&& || 只評估第一個元素(短路)。any/all 彙總。

1
2
3
4
5
6
7
8
9
10
11
12
13
x <- c(TRUE, FALSE, TRUE)
y <- c(FALSE, TRUE, TRUE)
x & y # FALSE FALSE TRUE
x | y # TRUE TRUE TRUE
!x # 取反
# 短路版本(單值):
TRUE && FALSE # FALSE
TRUE || stop('不執行') # 短路,不報錯
# 彙總:
any(x); all(x)
# 比較鏈需要逐段寫:
# x > 1 && x < 5
# 注意 && || 只取第一個元素

索引遍歷

seq_along / seq_len 生成索引,rev 逆序。which 取位置。split 分組循環。

1
2
3
4
5
6
7
8
9
10
11
12
13
x <- c(3, 1, 4, 1, 5)
seq_along(x) # 1 2 3 4 5
seq_len(3) # 1 2 3
# 逆序遍歷:
for (i in rev(seq_along(x))) print(x[i])
# which:
which(x == 1) # 2 4
which.max(x) # 5
# 按組循環:
grp <- split(x, x > 2)
lapply(grp, sum)
# 逐元素輸出:
# sapply(x, function(v) ...)

6.函數與閉包

函數定義、參數、惰性求值、可變參數、閉包與管道。

函數定義

function 定義函數,函數體最後表達式為返回值。匿名函數直接用於參數。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
square <- function(x) {
x * x
}
square(5) # 25
# 單表達式可簡寫:
add1 <- function(x) x + 1
add1(2) # 3
# 匿名函數:
(function(x) x * 2)(10) # 20
# 函數即對象,可賦值:
f <- add1
f(1) # 2
# 查看函數體:
body(square)

參數與默認值

函數參數可設默認值。調用可省略參數名按位置傳,也可命名傳參。missing 判斷是否提供。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
greet <- function(name, greeting = '你好') {
paste(greeting, name)
}
greet('張三') # 你好 張三
greet('張三', '早上好')
greet(greeting = '嗨', name = '李四')
# 檢測參數是否缺省:
show <- function(x) {
if (missing(x)) return('未提供')
x
}
show() # 未提供
# ... 傳透參數:
f <- function(x, ...) paste(x, ...)
f('a', 'b', 'c')

惰性求值

R 參數惰性求值:用到才求值,未用不報錯。force 強制提前求值。這在閉包中很關鍵。

1
2
3
4
5
6
7
8
9
10
11
f <- function(a, b) a
f(1, stop('這行不執行')) # 1,b 未用
# 強制求值:
g <- function(a, b) {
force(b) # 立刻求值 b
a
}
g(1, stop('會報錯')) # 拋錯
# 惰性求值配合默認參數引用前面參數:
h <- function(x, y = x * 2) y
h(5) # 10

可變參數

... 捕獲任意多參數。list(...) 收集,do.call 用列表動態調用函數。

1
2
3
4
5
6
7
8
9
10
11
12
13
sum_all <- function(...) sum(...)
sum_all(1, 2, 3) # 6
# 收集為列表:
capture <- function(...) list(...)
capture(1, 'a', TRUE)
# do.call:動態傳參
args <- list(x = 1:3, y = 1:3)
do.call(pmax, args)
# 向量元素拆開傳入:
vals <- c(1, 2, 3)
do.call(sum, as.list(vals)) # 6
# ... 轉發給其它函數:
wrap <- function(...) plot(...)

返回值

函數返回最後表達式。return 提前返回。invisible 返回值不自動打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
f <- function(x) x * 2 # 最後表達式即返回
f(3) # 6
# return 提前退出:
g <- function(x) {
if (x < 0) return('負數')
x * 2
}
g(-1) # 負數
# 多值用列表返回:
h <- function(x) {
list(sq = x^2, rt = sqrt(x))
}
h(4) # 列表含 sq rt
# 不打印的返回值:
invisible(42)

閉包

函數捕獲創建它的環境,可攜帶私有狀態。工廠函數生成帶狀態的新函數。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
make_adder <- function(n) {
function(x) x + n # 閉包捕獲 n
}
add5 <- make_adder(5)
add5(3) # 8
# 計數器閉包:
counter <- local({
n <- 0
function() {
n <<- n + 1 # 修改環境中的 n
n
}
})
counter(); counter() # 1 2
# 環境是閉包的載體:
environment(add5)

匿名函數

匿名函數即函數字面量,常用於 apply 家族與 purrr。R 4.1+ 支持 \\(x) 簡寫。

1
2
3
4
5
6
7
8
9
10
11
lapply(1:3, function(x) x^2) # 1 4 9
sapply(1:3, function(x) x * 2)
# \\(x) 簡寫(R >= 4.1):
lapply(1:3, \(x) x^2)
# 直接調用:
(function(x) x + 1)(5) # 6
# purrr 管道風格:
# purrr::map(1:3, ~ .x * 2)
# 匿名函數在排序中的用法:
sort(c(3, 1, 2))
sort(c('b', 'a'), decreasing = TRUE)

高階函數

函數可作為參數傳給函數,也可返回函數。Map / Reduce / Filter 等內建高階函數。

1
2
3
4
5
6
7
8
9
10
11
12
13
Filter(is.numeric, list(1, 'a', 2))
Reduce(`+`, 1:5) # 15
Map(function(x, y) x + y, 1:3, 4:6)
# 返回函數的函數(柯里化):
curry_add <- function(a) {
function(b) a + b
}
curry_add(10)(5) # 15
# 常見模式:給數據框的每列應用函數
sapply(df, mean, na.rm = TRUE)
# 自定義高階:
apply_twice <- function(f, x) f(f(x))
apply_twice(function(x) x + 1, 5) # 7

管道

|> 原生管道把左側結果傳給右側函數首參,讓嵌套調用變線性。%>% 是 magrittr 版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
x <- 1:10
x |> sum() # 55
# 鏈式管道:
1:10 |> mean() |> round(2)
# 傳至非首參用佔位符:
1:10 |> paste(collapse = '-') # 錯誤演示
# 原生管道佔位符:
1:10 |> paste(_, collapse = '-')
# tidyverse 風格:
df |> dplyr::filter(age > 18) |>
dplyr::select(name) |>
dplyr::arrange(name)
# magrittr 管道 . 佔位:
library(magrittr)
1:10 %>% sum()

7.字符串

字符向量、拼接、截取、分割、正則與格式化。

字符串基礎

字符向量用引號,雙引號與單引號等價。nchar 長度,[] 取字符。反斜槓需轉義。

1
2
3
4
5
6
7
8
9
10
11
12
s <- 'hello'
"hi" # 等價
toupper('abc') # "ABC"
tolower('ABC') # "abc"
nchar('中文') # 2(字符數)
# 反斜槓轉義:
cat('a\tb') # a b
# 原始字符串:
raw_s <- r"(C:\path\file)"
# 拼接:
paste('a', 'b') # "a b"
paste0('a', 'b') # "ab"

拼接

paste 默認空格分隔,paste0 無分隔。collapse 把整個向量合成一個字符串。向量化拼接。

1
2
3
4
5
6
7
8
9
10
11
12
paste('a', 'b', 'c') # "a b c"
paste0('a', 'b') # "ab"
paste('a', 'b', sep = '-') # "a-b"
# collapse:向量合成單串
paste(1:3, collapse = ',') # "1,2,3"
# 向量化組合:
paste(letters[1:3], 1:3) # "a 1" "b 2" "c 3"
# 循環回收組合:
paste(letters[1:2], 1:3) # 長度補成 3
# 字符數與字節:
nchar('你好') # 2
nchar('你好', type = 'bytes') # 6

截取

substr / substring 按位置截取,[] 中括號取單個字符。strsplit 分割。

1
2
3
4
5
6
7
8
9
10
s <- 'hello world'
substr(s, 1, 5) # "hello"
substring(s, 7) # "world"(到末尾)
# 取單個字符:
strsplit(s, '')[[1]] # 逐字符
# 替換子串:
substr(s, 7, 11) <- 'R!'
s # "hello R!"
# 按位置取多段:
substr('abcdef', c(1, 4), c(2, 6))

分割

strsplit 按分隔符拆成列表。unlist 拍平。正則也可作分隔。

1
2
3
4
5
6
7
8
9
10
11
s <- 'a,b,c'
strsplit(s, ',') # 列表 ["a" "b" "c"]
unlist(strsplit(s, ',')) # 向量
# 向量化分割:
strsplit(c('a-b', 'c-d'), '-')
# 正則分隔:
strsplit('a1b22c', '[0-9]+')
# 固定字符串匹配(非正則):
strsplit('a.b.c', '.', fixed = TRUE)
# 合併回來:
paste(unlist(strsplit(s, ',')), collapse = ';')

正則函數

grep / grepl 匹配,gsub / sub 替換。vectorized 全部按元素。詳細見正則章節。

1
2
3
4
5
6
7
8
9
10
x <- c('apple', 'banana', 'cherry')
grepl('^a', x) # TRUE FALSE FALSE
grep('a', x) # 索引 1 2
# 替換:
gsub('a', 'o', x) # opple bonono cherory
sub('a', 'o', x) # 只換第一個
# 提取匹配:
regmatches(x, gregexpr('a', x))
# 大小寫:
grepl('APPLE', x, ignore.case = TRUE)

格式化

sprintf 仿 C printf 格式化。%s 字符串,%d 整數,%f 浮點。formatC / round 控制數字。

1
2
3
4
5
6
7
8
9
10
11
sprintf('%s is %d', 'R', 4) # "R is 4"
sprintf('%.2f', pi) # "3.14"
sprintf('%5.1f', pi) # " 3.1"
sprintf('%05d', 42) # "00042"
# 百分號轉義:
sprintf('100%%')
# 向量化:
sprintf('x%d', 1:3) # "x1" "x2" "x3"
# 數字格式:
format(pi, digits = 3)
formatC(12345, big.mark = ',')

大小寫與修剪

toupper / tolower 轉換大小寫,trimws 去空白。chartr 字符替換。工具名處理常用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
toupper('hello') # "HELLO"
tolower('HELLO') # "hello"
# 首字母大寫:
to_title <- function(s) {
paste0(toupper(substr(s, 1, 1)), substr(s, 2, nchar(s)))
}
to_title('hello') # "Hello"
# 去空白:
trimws(' hi ') # "hi"
trimws('\nhi\t', which = 'both')
# 字符級替換:
chartr('a-c', 'A-C', 'abc')
# 包名轉下劃線:
# gsub('([A-Z])', '_\\L\\1', s, perl = TRUE)

編碼處理

R 字符串為 UTF-8 為主。Encoding 查看編碼,enc2utf8 轉換。iconv 轉碼。

1
2
3
4
5
6
7
8
9
10
11
12
13
s <- '你好'
Encoding(s) # "UTF-8"
enc2utf8(s) # 轉 UTF-8
enc2native(s) # 轉本機編碼
# 編碼轉換:
iconv(s, from = 'UTF-8', to = 'GB18030')
# 字節視圖:
charToRaw('A') # 41
rawToChar(as.raw(0x41)) # "A"
# 十六進制串:
paste(as.hexmode(charToRaw('R')), collapse = ' ')
# 讀取文件指定編碼:
# readLines('f.txt', encoding = 'UTF-8')

stringr

tidyverse 字符串處理包。str_ 前綴函數統一命名,str_detect / str_replace / str_extract。

1
2
3
4
5
6
7
8
9
10
11
12
# library(stringr)
str_detect(c('a1', 'b2'), '[0-9]')
str_replace('ab-cd', '-', '_')
str_replace_all('a-b-c', '-', '')
str_extract('price 99', '[0-9]+')
str_extract_all('a1 b2', '[0-9]')
str_remove('xx-abc', 'xx-')
str_split('a,b', ',')
str_pad('5', 3, pad = '0') # "005"
str_trim(' hi ')
str_to_title('hello world')
str_length('你好') # 2

8.集合與 apply 家族

apply / lapply / sapply 批量遍歷,排序、去重、集合運算與 dplyr 數據操作。

lapply 與 sapply

lapply 對列表/向量逐元素應用函數並返回列表。sapply 嘗試簡化成向量或矩陣。

1
2
3
4
5
6
7
8
9
10
l <- list(a = 1:3, b = 4:5)
lapply(l, sum) # 列表,含 $a $b
lapply(l, function(x) x * 2)
# sapply 簡化:
sapply(l, sum) # a=6 b=9 具名向量
sapply(1:4, sqrt)
# vapply 指定返回類型(更穩):
vapply(1:4, sqrt, numeric(1))
# unlist 拍平列表結果:
unlist(lapply(1:3, function(x) x^2))

apply

apply 沿矩陣/數組維度批量運算。MARGIN=1 按行、2 按列。返回值按維度拼。

1
2
3
4
5
6
7
8
9
10
m <- matrix(1:9, nrow = 3)
apply(m, 1, sum) # 每行和
apply(m, 2, sum) # 每列和
apply(m, 1, mean)
apply(m, 2, function(x) x / max(x))
# 3 維數組沿第三維:
a <- array(1:24, c(2, 3, 4))
apply(a, 3, sum)
# 附帶索引的分組:
apply(m, 1, which.max)

mapply 多參

mapply 並行地對多個參數應用函數,對應 Map。短參數循環補齊。SIMPLIFY 控制簡化。

1
2
3
4
5
6
7
8
9
mapply(paste, c('a', 'b'), c(1, 2))
# 對應 pmax 等:
mapply(max, c(1, 5), c(3, 2)) # 3 5
# Map 不簡化,永遠列表:
Map(paste, c('a', 'b'), c(1, 2))
# 多參數向量化:
mapply(function(x, y) x^y, 1:3, 1:3)
# 簡化成矩陣:
mapply(function(x, y) c(x, y), 1:2, 3:4, SIMPLIFY = TRUE)

split 分組

split 按因子把向量拆成組列表。配合 lapply 做分組統計,等價 group-by。

1
2
3
4
5
6
7
8
9
10
11
12
13
x <- c(1, 2, 3, 4, 5)
g <- c('a', 'a', 'b', 'b', 'b')
split(x, g) # 列表 $a $b
# 分組求均值:
lapply(split(x, g), mean)
# 數據框按列分組:
df <- data.frame(grp = c('a', 'a', 'b'), val = 1:3)
split(df, df$grp)
# 統計概要:
sapply(split(df$val, df$grp), summary)
# tapply:一步到位
tapply(df$val, df$grp, mean)
# 對應 dplyr group_by + summarise

排序與排名

sort 排向量,order 返回排序索引,rank 排名。decreasing 控制方向。數據框按列排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
x <- c(3, 1, 2)
sort(x) # 1 2 3
order(x) # 2 3 1(索引)
x[order(x)] # 排序後
rev(sort(x)) # 降序
rank(c(3, 1, 2)) # 3 1 2
# 數據框排序:
df <- data.frame(age = c(30, 20), name = c('b', 'a'))
df[order(df$age), ]
# 多列排序:
df2 <- data.frame(a = c(1, 1, 2), b = c(2, 1, 3))
df2[order(df2$a, df2$b), ]
# dplyr:
# dplyr::arrange(df, desc(age))

去重

unique 去重,duplicated 標記重複項。任何重複行取第一個。all.equal 比較向量。

1
2
3
4
5
6
7
8
9
10
11
12
x <- c(1, 2, 1, 3, 2)
unique(x) # 1 2 3
duplicated(x) # FALSE FALSE TRUE FALSE TRUE
!duplicated(x) # 保留首現
x[!duplicated(x)]
# 數據框去重行:
df <- data.frame(a = c(1, 1, 2), b = c(1, 1, 3))
unique(df)
# 去重計數:
table(x)
# dplyr:
# dplyr::distinct(df, a, .keep_all = TRUE)

集合運算

union / intersect / setdiff 集合運算。%in% 判斷元素存在。unique 後取交併差。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
a <- c(1, 2, 3); b <- c(2, 3, 4)
union(a, b) # 1 2 3 4
intersect(a, b) # 2 3
setdiff(a, b) # 1
setdiff(b, a) # 4
# 成員判斷:
2 %in% a # TRUE
# 保留重複的並集:
c(a, b)
# 相等比較:
identical(a, b)
setequal(a, c(3, 2, 1)) # TRUE(忽略順序)
# 用 %in% 過濾:
a[a %in% b] # 2 3

列表操作

組合列表、展平、批量改名。unlist 遞歸拍平,do.call 拼接為數組。purrr 類型化操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
l1 <- list(a = 1); l2 <- list(b = 2)
c(l1, l2) # 組合
unlist(l1) # 具名向量
# 列表每個元素加函數:
lapply(l1, function(x) x + 1)
# 改列表名:
names(l) <- c('x', 'y')
# 按條件篩元素:
l <- list(1, 'a', TRUE)
Filter(is.numeric, l)
# 拍平一層:
list_of_lists <- list(list(1, 2), list(3))
unlist(list_of_lists, recursive = TRUE)
# purrr:
# purrr::map(l, ~ .x * 2)
# purrr::keep(l, is.numeric)

dplyr 數據操作

select/filter/mutate/arrange/summarise 管道式操作數據框,group_by 分組。核心 tidyverse。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# library(dplyr)
df <- data.frame(
name = c('a', 'b', 'c'),
age = c(25, 35, 40),
salary = c(50, 60, 80)
)
df |> filter(age > 30)
df |> select(name, salary)
df |> mutate(salary2 = salary * 2)
df |> arrange(desc(age))
# 分組彙總:
df |> group_by(age > 30) |>
summarise(avg = mean(salary))
# 去重:
df |> distinct(age)
# 新增計算列並篩選:
df |> mutate(ratio = salary / age) |>
filter(ratio > 1.5)

9.內存管理

垃圾回收、對象大小、拷貝成本、預分配與性能分析。

垃圾回收

R 自動引用計數 + 週期 GC。gc 手動回收並報告。大數據循環後可調用。

1
2
3
4
5
6
7
8
9
10
11
gc() # 強制回收,返回統計
# gc 返回 Ncells/Vcells 使用與峯值:
gc(reset = TRUE) # 重置峯值統計
# 大對象不再用時置 NULL:
big <- 1:1e8
big <- NULL # 可回收
gc()
# 查看 gc 觸發閾值:
gc(verbose = TRUE) # 打印回收明細
# 內存佔用函數:
ls() # 查看當前對象

對象大小

object.size 看單個對象字節數。format 轉可讀單位。大數據集需預估內存。

1
2
3
4
5
6
7
8
9
10
11
object.size(1:1e6)
format(object.size(1:1e6), units = 'MB')
# 列表大小:
object.size(list(1:1e5, 1:1e5))
# 數據框:
df <- data.frame(x = 1:1e5, y = rnorm(1e5))
format(object.size(df), units = 'MB')
# 共享對象實際佔用:
# lobstr::obj_size(x, y)
# 大數據集預估:
# 1e7 行數據框約幾百 MB

拷貝成本

R 值語義在修改時複製整個對象,大對象修改代價高。避免頻繁修改大向量元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
x <- 1:1e7
# 整對象運算快(向量化):
y <- x * 2
# 循環內逐元素改:
for (i in 1:1000) x[i] <- x[i] + 1 # 觸發大量複製
# 預分配替代:
out <- numeric(1e5) # 先分配
for (i in seq_along(out)) out[i] <- i
# 避免 c() 在循環裏增長:
# res <- c() # 慢
# 用向量化或預分配
# 查看是否複製:
# tracemem(x)

預分配

先分配好長度的結果向量,再循環填充,避免反覆拼接複製。numeric/character 預分配。

1
2
3
4
5
6
7
8
9
10
11
12
13
n <- 1e4
# 預分配 numeric 向量:
out <- numeric(n)
for (i in 1:n) out[i] <- i^2
# 預分配字符:
res <- character(n)
# 預分配列表:
results <- vector('list', n)
for (i in seq_len(n)) results[[i]] <- i * 2
# 避免方式(慢):
# res <- c(); for (i in 1:n) res <- c(res, i)
# 理想:完全向量化
sq <- (1:n)^2

向量化與性能

儘量整向量運算。行綁定 rbind 慢,用 do.call(rbind, list) 或 data.table。微基準測試。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
x <- 1:1e6
# 快:整向量
system.time(y <- x * 2 + sin(x))
# 慢:循環
# for (i in seq_along(x)) y[i] <- x[i] * 2
# 行綁定累積慢:
# for (...) df <- rbind(df, row) # 差
# 正確做法:收集列表再 do.call
rows <- lapply(1:100, function(i) data.frame(i = i))
big <- do.call(rbind, rows)
# 計時:
system.time(mean(x))
# 微基準:
# microbenchmark::microbenchmark(a, b)

內存上限

memory.limit(Windows)查看/調整上限。object.size 檢查。ulimit 影響會話。

1
2
3
4
5
6
7
8
9
10
11
memory.limit() # Windows 內存上限 MB
# 可能需要:
# memory.limit(size = 8192)
# 當前已用:
memory.size()
# 其它平台無 memory.limit:
# 用系統資源工具監控
# 檢查大對象:
ls() |> sapply(function(x) object.size(get(x)))
# 排序看最大:
sort(sapply(ls(), function(x) object.size(get(x))), decreasing = TRUE)[1:5]

性能分析

Rprof 記錄函數調用耗時。summaryRprof 彙總。system.time 粗略計時。profvis 可視化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Rprof('prof.out')
slow <- function() {
for (i in 1:1e5) sqrt(i)
}
slow()
Rprof(NULL)
summaryRprof('prof.out')
# 簡單計時:
system.time(slow())
# 精細計時:
# tictoc::tic(); slow(); tictoc::toc()
# 可視化:
# profvis::profvis(slow())
# 基準比較:
# bench::mark(a, b)

data.table 內存

data.table 用引用語義減少複製::= 原地改、setDT 轉換、copy 顯式複製。大數據集更省內存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# library(data.table)
dt <- data.table(a = 1:1e5, b = rnorm(1e5))
# 原地新增/修改列(不復制):
dt[, c := a * 2]
# set 系列:
setkey(dt, a) # 排序鍵
setorder(dt, a) # 原地排序
# 取子集時避免複製:
sub <- dt[a > 100] # 複製子集
# 顯式複製:
copy(dt)
# 共享列引用:
# set(dt, j = 'd', value = dt$a)
# 內存對比:
# df 修改整列會複製整框,dt 不會

10.面向對象 (S3 / S4 / R6)

S3 簡易泛型、S4 形式化類、R6 引用類,三種 OO 系統各司其職。

S3 類基礎

S3 是 R 的簡易 OO:class 屬性 + 泛型函數。unclass 查看底層。最常用、最輕量。

1
2
3
4
5
6
7
8
9
10
11
12
x <- 1:3
class(x) # "integer"
# 自定義類:
obj <- structure(list(a = 1), class = 'myclass')
class(obj) # "myclass"
# 泛型分派:
print(obj) # 調用 print.myclass
# 通用函數列表:
methods('print')
# 查看分派目標:
class(1:3)
unclass(1:3) # 去掉類屬性

S3 方法

定義 class.method 函數即可實現泛型。UseMethod 分派。NextMethod 調父方法。

1
2
3
4
5
6
7
8
9
10
11
12
shape <- function(x) UseMethod('shape')
shape.default <- function(x) paste('default:', class(x)[1])
shape.circle <- function(x) paste('圓,半徑', x$r)
# 構造對象:
c1 <- structure(list(r = 3), class = 'circle')
shape(c1) # 圓,半徑 3
shape(1:3) # default
# 覆蓋 print:
print.circle <- function(x) cat('Circle r =', x$r, '\n')
print(c1)
# 查看所有方法:
methods('shape')

S3 構造

自定義類需提供構造器與校驗。structure 一步設類。print / summary 自定義輸出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new_point <- function(x, y) {
stopifnot(is.numeric(x), is.numeric(y))
structure(list(x = x, y = y), class = 'point')
}
p <- new_point(1, 2)
# 泛型 print:
print.point <- function(obj, ...) {
cat('point(', obj$x, ',', obj$y, ')\n')
}
print(p)
# 泛型 summary:
summary.point <- function(obj, ...) {
cat('均值:', mean(c(obj$x, obj$y)), '\n')
}
summary(p)
# 泛型 + 校驗:
# new_ 前綴是 tidyverse 習慣

S4 類

S4 是形式化 OO:setClass 定義槽,validity 校驗,setGeneric / setMethod 泛型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
setClass('Person',
slots = c(name = 'character', age = 'numeric'),
validity = function(obj) {
if (obj@age < 0) 'age 不能為負' else TRUE
}
)
p <- new('Person', name = 'Rex', age = 5)
p@age # 5
slot(p, 'name') # "Rex"
# 泛型:
setGeneric('describe', function(x) standardGeneric('describe'))
setMethod('describe', 'Person', function(x) paste(x@name, x@age))
describe(p)
# 檢查:
isS4(p)
# 繼承:
# setClass('Student', contains = 'Person')

R6 類

R6 是引用語義封裝類,方法裏用 self$ 訪問。面向對象編程更貼近其它語言。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# library(R6)
Animal <- R6Class('Animal',
public = list(
name = NULL,
initialize = function(name) self$name <- name,
speak = function() paste(self$name, '叫')
)
)
a <- Animal$new('狗')
a$speak() # "狗 叫"
# 私有成員:
Class <- R6Class('Class',
public = list(init = NULL),
private = list(hidden = 42)
)
# 繼承:
Dog <- R6Class('Dog',
inherit = Animal,
public = list(speak = function() paste(self$name, '汪汪'))
)
d <- Dog$new('旺財')
d$speak()

泛型分派

分派基於對象的 class 屬性,UseMethod 按 class 找對應方法。舊類(S3)層層嘗試。

1
2
3
4
5
6
7
8
9
10
11
12
13
f <- function(x) UseMethod('f')
f.default <- function(x) '默認'
f.numeric <- function(x) '數字'
f.factor <- function(x) '因子'
f(1L) # 數字
f(factor('a')) # 因子
f('a') # 默認
# 多參數分派(S4):
# setMethod(..., signature = c('x', 'y'))
# 查看分派方法表:
methods('mean')
# class 可以是向量:
class(x) <- c('myclass', 'numeric')

繼承

S3 用 class 屬性繼承(NextMethod 鏈),S4 用 contains,R6 用 inherit。

1
2
3
4
5
6
7
8
9
10
11
12
13
# S3 繼承:
structure(list(a = 1), class = c('sub', 'base'))
# NextMethod 調父類方法:
f.base <- function(x) 'base 實現'
f.sub <- function(x) paste('sub:', NextMethod())
f(structure(list(), class = c('sub', 'base')))
# R6 繼承已見 r6 主題:
# 用 super$ 訪問父類方法
# S4 繼承:
# setClass('Base')
# setClass('Derived', contains = 'Base')
# 檢查繼承:
# inherits(obj, 'base')

三套系統對比

S3 輕量用於大部分包;S4 用於嚴謹定義;R6 引用語義適合狀態對象。實際多混用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# S3:類屬性 + 泛型,無需聲明
# 快速、無強制、多數包用
# S4:setClass 嚴格槽與校驗
# 適合正式 API 與大型框架
# R6:引用語義、方法封裝
# 適合可變狀態、建模對象
# 混用示例:
# ggplot2 用 S3;
# Bioconductor 用 S4;
# 許多新包用 R6
# 選擇建議:
# 簡單數據對象用 S3,
# 複雜正式接口用 S4,
# 需要修改原對象用 R6

class 與屬性

class 是特殊屬性,泛型按它分派。attr 可加自定義屬性,不影響分派。class<- 直接設類。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
x <- 1:3
class(x) # "integer"
class(x) <- 'myclass' # 直接改類
x
# 查看全部屬性:
attributes(x)
# 自定義屬性:
attr(x, 'note') <- '説明'
attr(x, 'note')
# 判斷類:
inherits(x, 'myclass') # TRUE
is.numeric(1:3)
# 泛型尋找 class 順序:
# 先找 class[1],再 class[2]...,最後 default
# 移除類:
unclass(x)

11.錯誤處理

stop 報錯、warning 警告、tryCatch 捕獲與條件系統。

stop 報錯

stop 拋出錯誤終止執行。stopifnot 快速校驗條件。錯誤消息建議寫清原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
divide <- function(a, b) {
if (b == 0) stop('除數不能為 0')
a / b
}
divide(1, 0) # 拋錯
# stopifnot:
check <- function(x) {
stopifnot(is.numeric(x), length(x) > 0)
x
}
check('a') # 拋錯
# 自定義錯誤類:
stop('自定義錯誤', call. = FALSE)
# 附條件對象:
# stop(simpleError('msg'))

warning 警告

warning 給出非致命提示,不中斷。suppressWarnings 靜默。options(warn=2) 轉錯誤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
warn_if <- function(x) {
if (any(is.na(x))) warning('存在 NA')
x
}
warn_if(c(1, NA))
# 靜默:
suppressWarnings(warn_if(c(NA)))
# 把所有警告變錯誤:
options(warn = 2)
# 還原:
options(warn = 0)
# 一次性抓取警告:
withCallingHandlers(
expr,
warning = function(w) print('有警告')
)

tryCatch

tryCatch 捕獲錯誤/警告並返回處理結果。錯誤、警告、finally 三部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
safe_div <- function(a, b) {
tryCatch(
a / b,
error = function(e) paste('錯誤:', conditionMessage(e)),
warning = function(w) paste('警告:', conditionMessage(w)),
finally = cat('完成\n')
)
}
safe_div(1, 0) # 錯誤: 除數不能為 0
safe_div(1, 2) # 0.5
# 只看錯誤:
tryCatch(stop('boom'), error = function(e) '捕獲')
# 把結果與錯誤都保留:
tryCatch(list(ok = TRUE, val = 1), error = function(e) list(ok = FALSE))

try 容錯

try 返回表達式結果,出錯時返回 try-error 類對象,不中斷整體。批量處理常用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
out <- try(log('a'), silent = TRUE)
class(out) # "try-error"
inherits(out, 'try-error') # TRUE
# 容錯循環:
res <- vector('list', 3)
for (i in 1:3) {
res[[i]] <- try(log(i - 2), silent = TRUE)
}
# 過濾失敗項:
failed <- sapply(res, inherits, 'try-error')
# 成功項:
res[!failed]
# 出錯時給默認值:
out2 <- tryCatch(log(-1), error = function(e) NA)
out2

條件系統

R 錯誤/警告/消息都是條件對象。signalCondition 觸發,withCallingHandlers 捕獲並繼續。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
signalCondition(simpleError('出錯了'))
# 條件類:
stopifnot(identical(conditionMessage(simpleError('x')), 'x'))
# 消息:
message('普通提示')
# 捕獲消息:
withCallingHandlers(
message('hi'),
message = function(m) cat('捕獲:', conditionMessage(m), '\n')
)
# 條件包含調用棧:
f <- function() stop('deep')
# tryCatch 捕獲:
tryCatch(f(), error = function(e) conditionCall(e))
# 條件對象可攜帶自定義字段:
# simpleError('msg', call = sys.call())

警告攔截

withCallingHandlers 捕獲警告但不停止執行,可記錄並繼續。配合 invokeRestart。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
log_warnings <- function(expr) {
ws <- list()
res <- withCallingHandlers(
expr,
warning = function(w) {
ws[[length(ws) + 1]] <<- conditionMessage(w)
invokeRestart('muffleWarning')
}
)
list(result = res, warnings = ws)
}
out <- log_warnings({ warning('w1'); 42 })
out$result # 42
out$warnings # "w1"
# 與 tryCatch 區別:
# withCallingHandlers 捕獲後可繼續執行原表達式

restarts 恢復點

信號處理的高級機制:調用者可提供恢復動作。invokeRestart 在捕獲端調用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 定義恢復點:
withRestarts(
{ signalCondition(simpleError('x')); '繼續' },
my_restart = function() '恢復動作'
)
# 捕獲並恢復:
withCallingHandlers(
withRestarts(stop('err'), abort = function() 'aborted'),
error = function(e) invokeRestart('abort')
)
# 常見恢復點:
# muffleWarning、muffleMessage
# 自定義:用户可在 error handler 裏選擇
# 適合交互式重試邏輯

循環內錯誤

批量處理時單個失敗不應中斷全部。tryCatch 逐項容錯並保留結果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
items <- list(1, 'a', 3, -1)
process <- function(x) if (x < 0) stop('負數') else x * 2
# 安全處理:
out <- lapply(items, function(x) {
tryCatch(process(x), error = function(e) NA)
})
unlist(out) # 2 NA 6 NA
# 記錄失敗原因:
out2 <- lapply(items, function(x) {
tryCatch(list(ok = TRUE, v = process(x)),
error = function(e) list(ok = FALSE, msg = conditionMessage(e)))
})
# 繼續處理:
# 可用 purrr::possibly / safely

自定義錯誤

自定義錯誤類攜帶額外字段。simpleError / structure 構造,class 標識類型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
my_error <- function(msg, code = 500) {
structure(
list(message = msg, call = NULL, code = code),
class = c('my_error', 'error', 'condition')
)
}
stop(my_error('餘額不足', code = 400))
# 捕獲並讀取字段:
fail <- function() stop(my_error('boom'))
tryCatch(
fail(),
my_error = function(e) paste('自定義:', e$code),
error = function(e) '其他錯誤'
)
# 創建條件類:
# condition <- structure(list(), class = c('x', 'condition'))

12.文件與數據 I/O

CSV、readr、逐行讀取、RDS、JSON、連接對象與二進制。

CSV 讀寫

read.csv / write.csv 讀寫 CSV。stringsAsFactors=FALSE 防止轉因子。check.names 處理列名。

1
2
3
4
5
6
7
8
9
10
11
12
13
df <- data.frame(a = 1:3, b = c('x', 'y', 'z'))
write.csv(df, 'out.csv', row.names = FALSE)
# 讀回:
d <- read.csv('out.csv')
# 避免因子:
read.csv('out.csv', stringsAsFactors = FALSE)
# 自定義分隔:
write.table(df, 'out.tsv', sep = '\t', row.names = FALSE)
read.delim('out.tsv')
# 無表頭:
read.csv('f.csv', header = FALSE)
# 指定編碼:
# read.csv('f.csv', fileEncoding = 'UTF-8')

readr 讀寫

readr 是 tidyverse 的快速 CSV 讀取。read_csv 自動推斷類型,write_csv 快速寫出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# library(readr)
write_csv(df, 'out.csv')
read_csv('out.csv')
# 類型自動推斷,更快:
read_csv('out.csv', col_types = 'di') # d=double i=integer
# 大數據集:
read_csv('big.csv', show_col_types = FALSE)
# 指定列名:
read_csv('f.csv', col_names = c('x', 'y'))
# 寫回:
write_csv(df, 'out2.csv')
# 其它:
# read_tsv / write_tsv 製表符
# read_delim(file, delim = '|')
# 進度與類型見 README

逐行讀取

readLines 按行讀成字符向量。writeLines 寫出。處理大文件時逐塊讀。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
writeLines(c('第一行', '第二行'), 'f.txt')
lines <- readLines('f.txt')
lines # "第一行" "第二行"
# 指定編碼:
readLines('f.txt', encoding = 'UTF-8')
# 只讀前幾行:
readLines('f.txt', n = 1)
# 大文件分塊:
con <- file('f.txt', 'r')
while (length(chunk <- readLines(con, n = 10)) > 0) {
cat('塊大小', length(chunk), '\n')
}
close(con)
# 寫出帶編碼:
writeLines(lines, 'out.txt', useBytes = FALSE)

read.table 讀表

read.table 通用表格讀取,read.csv 是其變體。header / sep / na.strings 常用參數。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 製表符分隔:
read.table('data.tsv', header = TRUE, sep = '\t')
# 無表頭:
read.table('f.txt', sep = ',')
# 自定義 NA 值:
read.table('f.csv', header = TRUE, sep = ',', na.strings = c('NA', ''))
# 跳過行:
read.table('f.csv', skip = 2)
# 指定列類型:
read.table('f.csv', colClasses = c('numeric', 'character'))
# 行名:
read.table('f.csv', row.names = 1)
# 數量限制:
read.table('f.csv', nrows = 100)

RDS 存取

saveRDS 存單個 R 對象,readRDS 讀回。保留類型與屬性,比 CSV 完整。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
saveRDS(df, 'df.rds')
d2 <- readRDS('df.rds')
identical(df, d2) # TRUE
# 存多個對象:
save(df, x, file = 'all.RData')
load('all.RData') # 恢復 df 和 x
# 壓縮:
saveRDS(df, 'df.rds', compress = TRUE)
# 讀其它工具:
# readRDS 支持讀取之前版本
# 大數據建議:
# data.table::fread / fwrite 更高效
# 檢查文件:
# file.info('df.rds')

JSON 讀寫

jsonlite 是主流 JSON 包。fromJSON 解析,toJSON 序列化。行式 JSON 處理 API 返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# library(jsonlite)
json <- '{"name":"R","ver":4.4}'
fromJSON(json) # 具名列表
# 數據框轉 JSON:
df <- data.frame(a = 1:2, b = c('x', 'y'))
toJSON(df)
# 嵌套結構:
fromJSON('{"x":[1,2],"y":{"z":true}}')
# 行式 JSON(每行一個對象):
stream_in(file('rows.jsonl'))
# 數組:
fromJSON('[1,2,3]')
# 寫文件:
write(toJSON(df), 'out.json')
# 複雜結構轉數據框:
# fromJSON('...', flatten = TRUE)

連接對象

連接是文件/網絡等數據源抽象。file 打開、readLines / writeLines 讀寫、close 關閉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
con <- file('f.txt', 'w')
writeLines('一行', con)
close(con)
# 讀連接:
con <- file('f.txt', 'r')
content <- readLines(con)
close(con)
# 逐行處理:
con <- file('big.txt', 'r')
while (length(line <- readLines(con, n = 1)) > 0) {
# 處理 line
}
close(con)
# 文本/二進制模式:
file('f.bin', 'rb')
# url 連接:
url('https://example.com')
# 自動關閉:
# on.exit(close(con))

二進制讀寫

readBin / writeBin 讀寫二進制。raw 類型存字節。大文件與圖像數據用二進制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
x <- as.raw(c(0x48, 0x49)) # "H" "I"
writeBin(x, 'f.bin')
back <- readBin('f.bin', 'raw', n = 2)
# 讀整數:
writeBin(1:3, 'ints.bin')
readBin('ints.bin', 'integer', n = 3)
# 字節長度:
length(x)
# 文件大小:
file.info('f.bin')$size
# 圖像:
# png::readPNG / jpeg::readJPEG
# 讀寫大數據:
# 用 readBin 分塊
# 檢查 raw 視圖:
charToRaw('A') # 41

其它格式

readxl 讀 Excel,haven 讀 SPSS/Stata,feather/parquet 列式格式。按需加載。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Excel:
# library(readxl)
# read_excel('book.xlsx', sheet = 1)
# readxl::excel_sheets('book.xlsx')
# SPSS / Stata:
# library(haven)
# read_sav('data.sav'); read_dta('data.dta')
# 列式存儲:
# library(arrow)
# write_parquet(df, 'df.parquet')
# read_parquet('df.parquet')
# feather:
# arrow::write_feather(df, 'df.feather')
# 數據庫:
# library(DBI); dbConnect(RSQLite::SQLite())
# dbReadTable(con, 'tbl')

13.常見坑

R 日常最常踩的坑與正確寫法。

條件長度不為 1

if 需要長度為 1 的條件,向量條件只判第一個並告警。用 any/all 彙總或 ifelse。

1
2
3
4
5
6
7
8
x <- c(1, 2, 3)
// BAD if 只判第一個元素,且有警告
if (x == 2) print('找到')
// GOOD 用 any / all 判斷整個向量
if (any(x == 2)) print('找到')
if (all(x > 0)) print('全正')
// GOOD 逐元素條件用 ifelse
ifelse(x == 2, '命中', '未命中')

因子轉數值

因子轉數值直接 as.numeric 得到內部編碼。先轉字符再轉數值才是真實值。

1
2
3
4
5
6
7
8
9
f <- factor(c('10', '20', '30'))
// BAD 得到內部編碼 1 2 3
as.numeric(f)
// GOOD 先轉字符再轉數值
as.numeric(as.character(f))
// GOOD 或先 levels 索引
as.numeric(levels(f))[f]
# 讀取 CSV 時可 stringsAsFactors = FALSE 預防
read.csv('f.csv', stringsAsFactors = FALSE)

維度丟失

矩陣取單行/單列默認降維成向量。drop = FALSE 保持維度。

1
2
3
4
5
6
7
8
9
10
11
12
m <- matrix(1:9, nrow = 3)
m[1, ] # 向量 1 4 7
// BAD 想按行拼接時維度丟
rbind(m[1, ], m[2, ])
// GOOD 保留行維度
rbind(m[1, , drop = FALSE], m[2, , drop = FALSE])
# 子集後想保留矩陣:
m[1:2, , drop = FALSE]
# 數據框取單列返回向量:
df <- data.frame(a = 1:3, b = 4:6)
df[1] # 仍是 data.frame
df[[1]] # 向量

循環增長向量

循環裏 c() 拼接每次複製整個向量,極慢。先預分配或向量化。

1
2
3
4
5
6
7
8
9
10
11
n <- 5000
// BAD 每次 c() 都複製,O(n^2)
res <- numeric()
for (i in 1:n) res <- c(res, i)
// GOOD 預分配長度
res <- numeric(n)
for (i in 1:n) res[i] <- i
// GOOD 直接向量化(最快)
res <- 1:n
# 收集列表再拼接:
# do.call(c, lapply(1:n, function(i) i))

NA 與比較

NA 參與比較得到 NA,不會產生 FALSE。排除 NA 用 is.na 判斷。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
x <- c(1, NA, 3)
x == NA # NA NA NA(不要這樣)
// BAD 過濾 NA 後比較
x[x == 1] # NA 會漏進
// GOOD 先排除 NA
x[!is.na(x) & x == 1]
// GOOD 用 which 可忽略 NA
which(x == 1)
# 判斷存在 NA:
any(is.na(x))
# NA 與 NaN 不同:
is.nan(NaN); is.na(NaN) # TRUE TRUE
# 統計時跳過:
sum(x, na.rm = TRUE)

部分匹配

$ 與 [[ 會做唯一前綴匹配。列名縮寫的靜默行為易藏 bug,用完整名或 dplyr。

1
2
3
4
5
6
7
8
9
10
11
12
13
df <- data.frame(age_group = 1:3, score = 4:6)
// BAD 靜默匹配到 age_group
df$age_g
// GOOD 用完整列名
df$age_group
// GOOD 精確匹配 [[
df[['age_group']]
# 關閉部分匹配:
df[['age_g', exact = FALSE]] # 仍會模糊匹配
# dplyr 用 tidyselect,不模糊:
# dplyr::select(df, age_g) # 會報錯提示
# 查看列名:
names(df)

循環回收

短向量參與運算會循環補齊到長向量長度。長度不成倍數時只有警告,易出隱蔽 bug。

1
2
3
4
5
6
7
8
9
10
11
x <- c(1, 2, 3, 4)
y <- c(10, 20)
// BAD 回收:y 補成 c(10, 20, 10, 20)
x + y # 11 22 13 24
// 不成倍數才告警:
x + c(1, 2) # 長度 4 vs 2,無警告
x + c(1, 2, 3) # 警告:4 不整除 3
// GOOD 顯式確認長度一致
stopifnot(length(x) == length(y))
# 比較時循環回收同樣隱蔽:
# x > c(1, 100)

列表扁平化

c() 對列表會嘗試拍平頂層;把列表元素放列表裏用 list()。unlist 遞歸拍平有隱憂。

1
2
3
4
5
6
7
8
9
10
11
12
13
l <- list(a = list(x = 1), b = list(y = 2))
// BAD c() 拍平了頂層列表
c(l$a, l$b)
// GOOD 保持嵌套
list(l$a, l$b)
// GOOD 需要拍平時明確 intent
unlist(l, recursive = FALSE)
# 拼接兩個列表:
c(list(a = 1), list(b = 2))
# 查看結構:
str(c(l$a, l$b))
# 向量化元素收集:
# do.call(list, l)

包遮蔽

library 加載的包會遮蔽同名函數,search 順序後面的先隱藏。用 :: 消除歧義。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 兩個包都導出 filter:
# library(stats)
# library(dplyr) # dplyr 遮蔽 stats::filter
// BAD 遮蔽後 filter 含義隨加載順序漂移
filter(x, rep(1, 3)) # 可能是 dplyr::filter
// GOOD 顯式命名空間調用
stats::filter(x, rep(1, 3))
dplyr::filter(df, a > 1)
# 查看遮蔽:
# find('filter')
# 卸載包:
# detach('package:dplyr')
# 只取特定函數:
# dplyr::select
# 用 conflicted 包檢測:
# library(conflicted); conflict_prefer('filter', 'dplyr')

14.並行與併發

parallel、mclapply、foreach、future:R 的多進程並行。

parallel 概覽

R 並行基於多進程(fork)或套接字。parallel 包內置 mcapply 與集羣。並行有啓動開銷,任務要夠大才划算。

1
2
3
4
5
6
7
8
9
10
library(parallel)
detectCores() # CPU 核數
detectCores(logical = FALSE) # 物理核
# 比較串行與並行耗時:
f <- function(i) sqrt(i)
system.time(lapply(1:1e5, f))
# 並行版本:
# system.time(mclapply(1:1e5, f))
# 小任務並行反而更慢(進程啓動開銷)
# 合適場景:耗時函數、大數據批處理

mclapply 並行

mclapply 是 parallel 的並行 lapply,fork 實現。僅 Unix 可用(Windows 需集羣)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
library(parallel)
# Unix/macOS:
f <- function(i) {
Sys.sleep(0.01); i^2
}
# res <- mclapply(1:8, f, mc.cores = 4)
# Windows 用 mc.cores = 1 或集羣
res <- lapply(1:8, f) # 串行兜底
# mc.preschedule:批量調度
# mclapply(x, f, mc.cores = 2, mc.preschedule = TRUE)
# 返回類型同 lapply:
unlist(res)
# 出錯檢查:
# is.atomic(res)

集羣並行

makeCluster 建進程集羣,parLapply 並行應用。Windows 也支持。用後 stopCluster 回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
library(parallel)
cl <- makeCluster(2) # 2 個 worker
# 各 worker 獨立環境,需傳數據:
clusterExport(cl, 'shared_data')
res <- parLapply(cl, 1:4, function(i) i * 2)
stopCluster(cl)
unlist(res)
# 每 worker 加載包:
# clusterEvalQ(cl, library(dplyr))
# 預設變量:
# clusterExport(cl, varlist = c('x', 'y'))
# 隨機種子:
# clusterSetRNGStream(cl)
# Windows 下並行必用集羣

foreach 並行

foreach 循環收集結果,%dopar% 並行執行(需 doParallel)。%do% 是串行版。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# library(foreach)
# library(doParallel)
# registerDoParallel(cores = 2)
res <- foreach(i = 1:4) %do% {
i * 2 # 串行
}
# 並行版:
# res <- foreach(i = 1:4, .combine = c) %dopar% i * 2
# 組合結果:
# .combine = c / rbind / list
# 傳包與依賴:
# .packages = 'dplyr', .export = 'fun'
# 停止:
# stopImplicitCluster()
# 結果合併:
unlist(res)

future 異步

future 包把並行抽象為「未來值」。future() 起異步任務,value() 取結果。plan 選策略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# library(future)
plan(multisession) # 多會話策略
f <- future({
Sys.sleep(0.1)
42
}) # 立即返回 future
value(f) # 42,阻塞取結果
# 批量:
# library(furrr)
# plan(multisession)
# future_map(1:4, ~ .x * 2)
# 策略切換:
# plan(sequential) # 回串行
# 錯誤傳遞:
# try(value(future(stop('x'))))
# 全局變量自動傳:
# future({ x + 1 }) 中的 x 自動帶入

共享狀態

並行 worker 不共享內存:各自拷貝環境。寫文件要防併發衝突,結果用 return 收集。

1
2
3
4
5
6
7
8
9
10
11
12
# 數據要顯式傳給 worker:
shared <- 1:10
# mclapply(shared, function(x) x * 2)
# 用 clusterExport 或 future 自動捕獲
# 併發寫同一文件會衝突:
# 每 worker 寫獨立文件:
# file <- paste0('out_', i, '.csv')
# 彙總收集:
res <- parLapply(cl, 1:4, function(i) i^2)
final <- Reduce(`+`, res)
# 全局變量在 worker 不可見:
# 必須顯式導出,否則報找不到對象

隨機數種子

並行時隨機數需每 worker 獨立種子。set.seed 全局 + clusterSetRNGStream 或 future.seed 保證可復現。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
set.seed(42)
rnorm(3) # 可復現串行
# 集羣並行種子:
# clusterSetRNGStream(cl)
# 或:
# clusterSetRNGStream(cl, 123)
# foreach 並行種子:
# registerDoParallel(); set.seed(1)
# future 自動管理:
# future.seed = TRUE
# 驗證可復現:
set.seed(7)
a <- rnorm(5)
set.seed(7)
b <- rnorm(5)
identical(a, b) # TRUE

並行性能

並行收益受任務粒度、核數與通信開銷限制。先 profile 串行瓶頸,再並行最大塊。

1
2
3
4
5
6
7
8
9
10
11
12
# 並行開銷遠大於任務本身時不要並行
f <- function(i) sqrt(i)
system.time(lapply(1:1e5, f))
# system.time(mclapply(1:1e5, f, mc.cores = 4))
# 合適:每個任務耗時 >= 幾十 ms
# 線性擴展觀察:
# time(1 core) vs time(4 cores)
# 通信瓶頸:大數據來回複製慢
# 建議:把結果整理成小對象返回
# 避免共享大對象頻繁傳
# 測速工具:
# microbenchmark::microbenchmark(...)

15.網絡請求

下載、httr2/httr 請求、JSON API、URL 處理與網頁抓取。

下載文件

download.file 下載文件。mode 指定二進制。R.utils 支持斷點續傳。慢速連接可設 timeout。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
download.file(
'https://example.com/data.csv',
'data.csv',
mode = 'wb'
)
# 指定超時:
# download.file(url, dest, timeout = 60)
# 檢查結果:
file.info('data.csv')$size
# 讀入下載的數據:
read.csv('data.csv')
# 二進制數據:
# download.file(url, 'img.png', mode = 'wb')
# 鏡像相關:
# setInternet2(TRUE) # 舊 Windows

httr2 請求

httr2 是新一代 HTTP 客户端。req_perform 執行,resp_body_json 解析。管道式構建請求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# library(httr2)
req <- request('https://api.example.com')
req <- req %>%
req_url_path_append('v1') |>
req_url_query(q = 'rlang') |>
req_headers('Authorization' = 'Bearer xx')
# resp <- req_perform(req)
# resp_body_json(resp)
# 錯誤處理:
# tryCatch(req_perform(req), error = function(e) ...)
# 限速:
# req_throttle(req, rate = 10)
# 認證:
# req_auth_bearer_token(req, 'token')

GET 請求

GET 讀取資源。base 的 readLines 可拉文本;httr GET + content 解析。query 參數拼 URL。

1
2
3
4
5
6
7
8
9
10
11
12
13
# base 簡單 GET:
txt <- readLines('https://example.com')
# library(httr)
resp <- httr::GET('https://api.example.com/items')
httr::status_code(resp) # 200
httr::content(resp, 'text')
# 帶查詢參數:
httr::GET('https://api.example.com/search',
query = list(q = 'rlang', page = 2))
# 響應頭:
httr::headers(resp)
# 連接超時:
httr::GET(url, httr::timeout(30))

POST 請求

POST 提交數據。body 傳 JSON 或表單。httr POST 與 httr2 req_body_json 兩種風格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# library(httr)
httr::POST(
'https://api.example.com/login',
body = list(user = 'a', pass = 'b'),
encode = 'form'
)
# JSON 體:
httr::POST(
'https://api.example.com/submit',
body = '{"x":1}',
content_type_json()
)
# httr2 風格:
# req_body_json(req, list(x = 1))
# 響應解析:
# resp <- httr::POST(...)
# httr::content(resp, 'parsed')
# 上傳文件:
# httr::POST(url, body = list(f = upload_file('f.csv')))

JSON API

調用 JSON API:請求 + jsonlite 解析。結果常是嵌套列表,flatten 轉數據框。分頁與錯誤處理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# library(httr)
# library(jsonlite)
url <- 'https://api.github.com/repos/tidyverse/dplyr'
resp <- httr::GET(url)
stopifnot(httr::status_code(resp) == 200)
js <- httr::content(resp, 'text')
info <- jsonlite::fromJSON(js)
info$full_name
info$stargazers_count
# 列表轉數據框:
# fromJSON(js, flatten = TRUE)
# 分頁:
# 用 query 參數 page / per_page 循環
# 統一錯誤處理:
# tryCatch(..., error = ...)

URL 處理

parse_url 拆分 URL 組件,URLencode 編碼。URLdecode 還原。base 內置處理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# library(httr)
u <- parse_url('https://user:[email protected]/a?b=1&c=2')
u$scheme; u$hostname; u$path
u$query # b=1 c=2
# 修改後重組:
u$query$d <- 3
build_url(u)
# URL 編碼:
URLencode('a b/c') # a%20b%2Fc
URLdecode('%E4%BD%A0%E5%A5%BD')
# 編碼中文:
URLencode('你好')
# 相對路徑解析:
# url_absolute('/a', 'https://x.com')
# 查詢字符串解析:
# parse_url('https://x.com/?a=1')$query

網頁抓取

rvest 解析 HTML。html_elements 選節點,html_text 取文本。遵守 robots 與頻率限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# library(rvest)
url <- 'https://example.com'
page <- read_html(url)
page %>% html_elements('h1') %>% html_text()
page %>% html_elements('a') %>%
html_attr('href')
# 表格:
# page %>% html_table()
# CSS 選擇器:
page %>% html_elements('#main p')
page %>% html_elements('.item')
# XPath:
page %>% html_elements(xpath = '//div[@class="x"]')
# 尊重網站規則:
# 頻率限制、User-Agent 標識
# 合法用途:公開數據分析

TCP 套接字

socketConnection 建立 TCP 連接,讀寫字符串。適合簡單協議與內網服務。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
con <- socketConnection(
host = 'localhost',
port = 9999,
server = FALSE, # 客户端
open = 'r+b'
)
# 寫入:
writeLines('hello', con)
# 讀取:
line <- readLines(con, n = 1)
close(con)
# 起服務端:
# srv <- socketConnection(host = 'localhost',
# port = 9999, server = TRUE)
# 注意:阻塞等待
# 複雜協議建議用 httpuv / plumber
# 生產接口建議用 API 框架而非裸 socket

16.時間與日期

Sys.Date / POSIXct、格式化、lubridate 與時區。

當前時間

Sys.Date 今天日期,Sys.time 當前日期時間。date 當前時間字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
Sys.Date() # 2026-08-02
Sys.time() # POSIXct 日期時間
format(Sys.time())
date() # "Sat Aug ... 2026"
# 取組件:
as.POSIXlt(Sys.time())
unclass(as.POSIXlt(Sys.time()))
# 時間戳:
unclass(Sys.time()) # 秒
# 自定時區:
Sys.timezone()
# 手動設置:
# Sys.setenv(TZ = 'Asia/Shanghai')

日期基礎

Date 類是日期類型。as.Date 轉換字符串,加減天數。unclass 看天數數值。

1
2
3
4
5
6
7
8
9
10
11
12
13
d <- as.Date('2026-08-02')
class(d) # "Date"
d + 1 # 明天
Sys.Date() - d # 相差天數
difftime(Sys.Date(), d, units = 'days')
# 解析其它格式:
as.Date('2026/08/02')
as.Date('02-08-2026', format = '%d-%m-%Y')
# 序列:
seq(as.Date('2026-01-01'), by = 'day', length.out = 3)
# 組件:
format(d, '%Y-%m-%d')
format(d, '%A') # 星期幾

POSIXct

POSIXct 存秒級時間戳,POSIXlt 拆分組件。as.POSIXct 解析字符串,處理時區。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
t <- as.POSIXct('2026-08-02 10:30:00')
class(t) # POSIXct POSIXt
unclass(t) # 自 1970 起秒數
# 指定時區:
as.POSIXct('2026-08-02 10:30:00', tz = 'UTC')
# 轉組件:
lt <- as.POSIXlt(t)
lt$year + 1900 # 2026
lt$mon + 1 # 8 月
lt$mday; lt$hour; lt$min
# 格式化為字符串:
format(t, '%Y-%m-%d %H:%M:%S')
# 加減:
t + 3600 # +1 小時
# 與 Date 互轉:
as.Date(t)

格式化

strftime / format 按佔位符輸出。%Y 年 %m 月 %d 日 %H 時 %M 分 %S 秒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
d <- Sys.Date()
t <- Sys.time()
format(d, '%Y-%m-%d')
format(d, '%d/%m/%Y')
format(d, '%A') # 星期名
format(t, '%H:%M:%S')
format(t, '%Y 年第 %j 天')
# 解析:
as.Date('02/08/2026', format = '%d/%m/%Y')
# strptime:
strptime('2026-08-02 10:00', format = '%Y-%m-%d %H:%M')
# 佔位符速查:
# %Y 四位年 %y 兩位年 %m 月 %d 日
# %H %M %S 時/分/秒 %A 星期名
# %j 一年中的第幾天

lubridate 簡介

lubridate 提供友好日期函數:ymd / ymd_hms 解析,year / month 取組件,加減間隔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# library(lubridate)
ymd('20260802')
ymd('2026-08-02')
ymd_hms('2026-08-02 10:30:00')
# 取組件:
year(t); month(t); day(t)
hour(t); minute(t); second(t)
wday(t) # 星期(數字)
# 加減:
t + days(1); t - weeks(2)
months(3) # 3 個月
# 間隔:
interval(d, Sys.Date()) |> as.duration()
# 時區設置:
with_tz(t, 'UTC')
# 四捨五入:
round_date(t, 'hour')
floor_date(t, 'day')

日期序列

seq 生成日期序列:by 日/月/年。任意間隔工作日用 seq.Date。

1
2
3
4
5
6
7
8
9
10
11
seq(as.Date('2026-01-01'), as.Date('2026-01-10'), by = 'day')
seq(as.Date('2026-01-01'), by = 'month', length.out = 3)
seq(as.Date('2026-01-01'), by = 'year', length.out = 2)
# 指定數量:
seq(as.Date('2026-01-01'), as.Date('2026-12-31'), length.out = 5)
# seq.Date 更明確:
seq.Date(as.Date('2026-01-01'), by = '2 days', length.out = 3)
# 與工作日:
# 過濾週末:
x <- seq(as.Date('2026-01-01'), by = 'day', length.out = 30)
x[!weekdays(x) %in% c('Saturday', 'Sunday')]

時間差

日期相減得 difftime。units 指定單位。as.numeric 取數值。用於耗時統計。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
t0 <- Sys.time()
Sys.sleep(0.5)
Sys.time() - t0 # Time difference
# 指定單位:
difftime(Sys.time(), t0, units = 'secs')
difftime(Sys.time(), t0, units = 'mins')
# 取數值:
as.numeric(Sys.time() - t0)
# 日期差:
Sys.Date() - as.Date('2026-01-01')
# 平均耗時:
# mean 對 difftime 有效
# 自定義單位:
# difftime(t2, t1, units = 'hours')
# 計時工具:
# system.time(expr)
# tictoc::tic() / toc()

時區

tz 參數指定時區。Sys.timezone 當前時區。OlsonNames 列出合法時區。比較跨時區時刻。

1
2
3
4
5
6
7
8
9
10
11
12
Sys.timezone() # 當前時區
OlsonNames() # 全部合法時區名
# 指定時區解析:
as.POSIXct('2026-08-02 10:00', tz = 'Asia/Shanghai')
# 同一時刻不同時區顯示:
t <- as.POSIXct('2026-08-02 10:00', tz = 'UTC')
format(t, tz = 'Asia/Shanghai')
format(t, tz = 'America/New_York')
# 時區換算:
with_tz(t, 'Asia/Shanghai') # lubridate
# UTC 時間戳:
# as.integer(as.POSIXct('2026-08-02', tz = 'UTC'))

17.進程與環境

運行系統命令、環境變量、命令行參數、平台信息與路徑。

運行命令

system 運行系統命令並返回退出碼。system2 傳參更安全。輸出捕獲用 capture 或 intern。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
system('echo hello')
system2('echo', 'hello')
# 捕獲輸出:
system('date', intern = TRUE)
system2('date', stdout = TRUE)
# 退出碼:
code <- system('ls')
code # 0 成功
# 傳參避免 shell 注入:
# system2('cp', c('a.txt', 'b.txt'))
# 忽略輸出:
system('echo x', ignore.stdout = TRUE)
# 超時:
# system2('cmd', timeout = 10)

環境變量

Sys.getenv 讀、Sys.setenv 寫。unsetenv 刪除。PATH / R 相關變量常用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Sys.getenv('PATH')
Sys.setenv(MY_VAR = 'hello')
Sys.getenv('MY_VAR') # "hello"
Sys.unsetenv('MY_VAR')
# 列出全部:
Sys.getenv()
# 帶默認值:
Sys.getenv('NOPE', unset = 'default')
# 常見變量:
Sys.getenv('HOME')
Sys.getenv('R_VERSION') # 或:
R.version.string
# 條件判斷:
if (nzchar(Sys.getenv('CI'))) print('在 CI 環境')

命令行參數

commandArgs 取腳本參數。trailingOnly 去掉 R 自身參數。解析用 optparse 或 base 手寫。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 運行:Rscript app.R --name Rex
args <- commandArgs(trailingOnly = TRUE)
args # "--name" "Rex"
# 簡易解析:
get_arg <- function(name) {
i <- which(args == name)
if (length(i)) args[i + 1] else NULL
}
get_arg('--name')
# 專業解析:
# library(optparse)
# parser <- OptionParser(option_list = list(
# make_option('--name', type = 'character')))
# opts <- parse_args(parser)
# 默認值:
# 參數個數校驗:
stopifnot(length(args) >= 1)

退出狀態

quit 退出 R。q('no') 不保存工作區。錯誤退出用 quit(status = 1) 供腳本判斷。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
quit(save = 'no') # 退出,不保存
# 指定狀態碼:
# quit(status = 1) # 失敗退出
# 條件退出:
if (!file.exists('data.csv')) {
message('缺文件')
quit(status = 1)
}
# 正常結束腳本:
# Rscript 腳本末尾自然結束
# 捕獲退出碼(shell):
# Rscript app.R; echo $?
# 阻止退出用 browse:
# browser() # 調試暫停

平台信息

R.version 版本,Sys.info 系統信息,.Platform 平台細節。路徑分隔等跨平台處理。

1
2
3
4
5
6
7
8
9
10
11
12
13
R.version.string # R 4.4.x
version # 完整版本
Sys.info() # 系統信息列表
Sys.info()['sysname'] # Windows/Linux/Darwin
.Platform$file.sep # / 或 \
.Platform$OS.type # "windows" / "unix"
# 條件判斷:
if (.Platform$OS.type == 'windows') 'Win' else 'Unix'
# 架構:
R.version$arch
# 路徑分隔符:
# file.path 自動處理:
file.path('a', 'b', 'c.txt') # 平台正確分隔

路徑管理

file.path 拼接路徑,dirname / basename 拆分,normalizePath 規範絕對路徑。file.exists 判斷。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
file.path('data', 'sub', 'f.csv')
basename('a/b/f.csv') # "f.csv"
dirname('a/b/f.csv') # "a/b"
file.exists('f.csv')
dir.exists('data')
# 工作目錄:
getwd()
setwd('data') # 謹慎修改
# 規範路徑:
normalizePath('..')
# 展開 ~:
path.expand('~/R')
# 列出目錄:
list.files('.')
list.files('.', pattern = '\\.csv$')
# 創建目錄:
dir.create('out', showWarnings = FALSE)

延時等待

Sys.sleep 暫停指定秒數。可用於限速或等待外部資源就緒。單位是秒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Sys.sleep(1) # 暫停 1 秒
t0 <- Sys.time()
Sys.sleep(0.5)
Sys.time() - t0
# 循環限速:
for (i in 1:3) {
cat(i, '\n')
Sys.sleep(0.2) # 每 0.2s 一次
}
# 等待文件出現:
# while (!file.exists('done.txt')) Sys.sleep(1)
# 等待網絡資源:
# 配合 httr timeout 使用
# 注意:阻塞當前進程

Rscript 腳本

Rscript 是非交互執行入口,適合定時任務與批處理。腳本開頭可加 shebang。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 腳本開頭(Unix):
# #!/usr/bin/env Rscript
# 運行:
# $ Rscript script.R arg1
# 使用:
args <- commandArgs(trailingOnly = TRUE)
message('開始處理: ', args[1])
# 輸出結構化結果:
cat('RESULT:', mean(1:100), '\n')
# 退出碼:
# 出錯時 quit(status = 1)
# 批處理管道:
# 讀 stdin:
input <- readLines(file('stdin'), n = 1)
# 寫 stdout 即可管道接力

18.正則表達式

grep / grepl、gsub 替換、regexpr 定位、stringr 與常用模式。

grep 與 grepl

grep 返回匹配索引,grepl 返回邏輯向量。ignore.case 忽略大小寫。value 返回匹配值。

1
2
3
4
5
6
7
8
9
10
11
12
13
x <- c('apple', 'banana', 'apricot')
grepl('^ap', x) # TRUE FALSE TRUE
grep('^ap', x) # 1 3
grep('^ap', x, value = TRUE) # "apple" "apricot"
grep('^AP', x, ignore.case = TRUE)
# 固定匹配:
grepl('a.', x, fixed = TRUE) # 字面 .
# 計數:
sum(grepl('a', x))
# 反選:
x[!grepl('^ap', x)]
# 多模式:
grepl('a|b', c('x', 'a'))

gsub 替換

gsub 替換全部匹配,sub 只替換第一個。\\1 引用捕獲組。perl=TRUE 擴展語法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gsub('a', 'o', 'banana') # "bonono"
sub('a', 'o', 'banana') # "bonana"
# 刪除匹配:
gsub('[0-9]', '', 'a1b2')
# 捕獲組引用:
gsub('(\\d{4})-(\\d{2})', '\\2/\\1', '2026-08')
# 多個替換:
gsub('a|b', 'x', 'abacus')
# 忽略大小寫:
gsub('a', 'o', 'ABC', ignore.case = TRUE)
# 固定字面:
gsub('a.b', 'x', 'a.b', fixed = TRUE)
# 向量化:
gsub('s', 'S', c('sun', 'sea'))

regexpr 定位

regexpr 返回首個匹配位置與長度,gregexpr 返回全部。regmatches 提取匹配文本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
s <- 'price is 99 dollars'
m <- regexpr('[0-9]+', s)
m # 位置與長度
regmatches(s, m) # "99"
# 全部匹配:
ms <- gregexpr('[a-z]+', 'a1bc2def')
regmatches('a1bc2def', ms) # 列表
# 提取匹配與捕獲:
# 正則匹配後 unlist:
unlist(regmatches('x12y34', gregexpr('[0-9]+', 'x12y34')))
# 無匹配時:
regexpr('z', 'abc') # -1
# 匹配位置作為索引:
# 結合 substring 用

語法基礎

基礎正則元字符:^ 開頭 $ 結尾 . 任意 . 字符類 [] 分組 () 量詞 * + ? {} 轉義 \\。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
'^abc' # 以 abc 開頭
'abc$' # 以 abc 結尾
'^[a-z]+$' # 全小寫字母
'[0-9]{2,4}' # 2 到 4 位數字
'colou?r' # colour/color
'a.c' # a?c 任一字符
'\\.' # 字面點號
'[^0-9]' # 非數字
'(ab|cd)' # 分組或
'\\d' '\\w' '\\s' # 數字/字母/空白
# R 裏反斜槓要雙寫:
# 正則 \d 在 R 字符串寫成 \\d
# 驗證:
grepl('^[0-9]+$', '123') # TRUE

常用模式

常用模式速查:郵箱、電話、日期、空白清理、提取數字。按需求微調。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 提取數字:
regmatches('v1.2.3', regexpr('[0-9]+', 'v1.2.3'))
# 去掉空白:
gsub('[[:space:]]', '', 'a b c')
# 郵箱簡配:
pat <- '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$'
grepl(pat, '[email protected]') # TRUE
# 日期 YYYY-MM-DD:
pat <- '^\\d{4}-\\d{2}-\\d{2}$'
grepl(pat, '2026-08-02')
# 中文:
grepl('[\\u4e00-\\u9fa5]', '你好')
# 提取括號內容:
sub('.*\\((.+)\\).*', '\\1', 'name(value)')
# 分隔:
strsplit('a,b;c', '[,;]')

stringr 匹配

stringr 正則語法一致但接口統一。str_detect / str_extract / str_match 及 _all 版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# library(stringr)
str_detect(c('a1', 'b2'), '\\d')
str_extract('price 99', '\\d+')
str_extract_all('a1 b22', '\\d+')
str_match('a=1;b=2', 'a=(\\d+)')
str_match_all('a1b2', '([ab])(\\d)')
# 位置:
str_locate('abcabc', 'b')
str_locate_all('abcabc', 'b')
# 替換:
str_replace('a-b', '-', '_')
str_replace_all('a-b-c', '-', '+')
# 拼接正則邊界:
str_detect('apple', regex('^ap'))

stringr 替換

str_replace / str_replace_all 替換。regexp 可用 fixed / perl。str_remove 刪除匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# library(stringr)
str_replace('one-two', '-', '_')
str_replace_all('a-b-c', '-', '')
# 捕獲引用:
str_replace('2026-08', '(\\d{4})-(\\d{2})', '\\2/\\1')
# 刪除:
str_remove('xx-abc', 'xx-')
str_remove_all('a1b2', '\\d')
# 固定字面:
str_replace_all('a.b', fixed('.'), 'X')
# 大小寫無關:
str_replace('ABC', 'a', 'x', regex(ignore_case = TRUE))
# 向量化自動:
str_replace_all(c('a1', 'b2'), '\\d', '#')
# 邊界匹配:
str_replace('abc', '^a', 'X')

標誌與擴展

perl=TRUE 啓用 PCRE 擴展:前瞻、命名組。fixed=TRUE 字面匹配。ignore.case。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 前瞻:
grepl('a(?=b)', 'ab', perl = TRUE) # TRUE
# 非前瞻:
grepl('a(?!b)', 'ac', perl = TRUE) # TRUE
# 命名組:
# gsub('(?<y>\\d+)', '\\k<y>', s, perl = TRUE)
# 非貪婪:
regmatches('a<b>c<b>', regexpr('<.+?>', 'a<b>c<b>', perl = TRUE))
# 多行 / 點匹配換行:
# grepl('a.b', 'a\nb', perl = TRUE)
# fixed 字面:
grepl('a.b', 'a.b', fixed = TRUE)
# 全部標誌組合:
# grepl(pat, x, ignore.case = TRUE, perl = TRUE)

19.包管理與構建

CRAN 安裝、renv 依賴管理、包結構與 R CMD、testthat、roxygen。

安裝包

install.packages 從 CRAN 安裝。update.packages 更新。library 加載。開發版用 devtools / remotes。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
install.packages('dplyr')
# 從鏡像安裝:
install.packages('ggplot2', repos = 'https://cloud.r-project.org')
# 更新:
# update.packages()
# 開發版:
# remotes::install_github('tidyverse/dplyr')
# 本地:
# install.packages('path/pkg_0.1.0.tar.gz', repos = NULL)
# 查看已裝:
rownames(installed.packages())
# 加載:
library(dplyr)
# 依賴安裝:
# install.packages(c('dplyr', 'tidyr'))

CRAN 與倉庫

CRAN 是官方包倉庫。repos 指定鏡像。available.packages 列出可安裝。CRAN 政策約束髮布。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 當前倉庫:
getOption('repos')
# 全部可用包:
ap <- available.packages()
ap[1:5, 'Package']
# 搜索包:
# available.packages() 按字段 grep
# 包信息:
# packageDescription('dplyr')
# 依賴:
# packageDescription('dplyr')$Depends
# CRAN 檢查:
# R CMD check 通過才可上架
# 鏡像列表:
# https://cran.r-project.org/mirrors.html
# 中國鏡像:
# https://mirrors.tuna.tsinghua.edu.cn/CRAN/

renv 依賴管理

renv 鎖定項目依賴版本,類似 Python venv。renv::init 初始化,snapshot / restore 同步鎖文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 初始化:
# renv::init()
# 安裝依賴:
# renv::install('dplyr')
# 記錄依賴:
# renv::snapshot() # 寫 renv.lock
# 換機恢復:
# renv::restore()
# 查看狀態:
# renv::status()
# 隔離項目庫:
# renv::activate()
# 升級:
# renv::update()
# 使用注意:
# .Rprofile 會自動加載 renv
# 提交 renv.lock 到版本控制

包結構

R 包標準結構:DESCRIPTION、R/、man/、tests/、NAMESPACE。R/ 放源碼,man/ 放文檔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# mypkg/
# ├── DESCRIPTION 元數據:包名/版本/依賴
# ├── NAMESPACE 導出哪些函數
# ├── R/ 源碼文件 *.R
# ├── man/ 文檔 *.Rd
# ├── tests/ 測試
# └── data/ 內置數據
# DESCRIPTION 關鍵字段:
# Package: mypkg
# Version: 0.1.0
# Imports: dplyr
# 用 usethis 生成骨架:
# usethis::create_package('mypkg')
# 文檔:
# usethis::use_roxygen_md()

R CMD 命令

R CMD 系列命令構建/檢查包:build 打包、check 檢查、INSTALL 安裝。發佈前必經 check。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 構建 tar.gz:
# R CMD build mypkg
# 檢查(關鍵步驟):
# R CMD check mypkg_0.1.0.tar.gz
# 安裝:
# R CMD INSTALL mypkg
# 常用檢查項:
# --as-cran 模擬 CRAN 嚴格檢查
# 運行時等價:
# system('R CMD INSTALL --help')
# 在 R 會話內:
# system2('Rscript', c('-e', '1+1'))
# devtools 封裝:
# devtools::check()

testthat 測試

testthat 是主流測試框架。expect_equal 等斷言,test_that 組織用例。usethis 生成測試文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# library(testthat)
f <- function(x) x * 2
test_that('f 正確加倍', {
expect_equal(f(2), 4)
expect_identical(f(0), 0)
expect_true(f(-1) < 0)
expect_error(f('a'))
})
# 運行:
# testthat::test_dir('tests')
# 或開發時:
# devtools::test()
# 常用斷言:
# expect_equal / expect_identical
# expect_true / expect_false
# expect_warning / expect_message
# 與 CI 結合自動跑

格式與檢查

styler 統一代碼風格,lintr 靜態檢查。配合 RStudio Addins 或 pre-commit 保持整潔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# library(styler)
# 格式化文件:
# styler::style_file('R/foo.R')
# 格式化整個包:
# styler::style_pkg()
# 檢查風格差異:
# styler::style_file('R/foo.R', dry = 'only')
# lintr 檢查:
# lintr::lint('R/foo.R')
# 常用配置:
# .lintr 文件指定 linters
# 自動執行:
# pre-commit hooks
# CI 集成:
# lintr 作為 check 步驟

roxygen 文檔

roxygen2 用註釋生成 .Rd 文檔。#' 開頭,@param / @return / @export 標籤。函數文檔與源碼同處。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#' 兩數相加
#'
#' @param a 第一個數
#' @param b 第二個數
#' @return
#' @export
add2 <- function(a, b) a + b
# 生成文檔:
# devtools::document()
# 或:
# roxygen2::roxygenise()
# 生成 NAMESPACE 導出:
# @export 自動寫入 NAMESPACE
# 包級文檔:
# @keywords internal
# 檢查文檔:
# R CMD check

會話復現

sessionInfo 記錄版本與依賴,保證可復現。renv.lock 鎖依賴。R 版本也要記錄。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sessionInfo() # R 版本 + 已加載包
R.version.string
# 輸出示例:
# R version 4.4.x
# Platform: ...
# 附加包及其版本
# 復現要點:
# 1. 記錄 sessionInfo() 輸出
# 2. renv::snapshot() 鎖依賴
# 3. 固定 R 版本(Docker 鏡像)
# 檢查缺失:
# 依賴清單:
# renv::dependencies()
# 報告問題時附上:
# sessionInfo() + 最小可復現代碼

官方鏈接

直達官方文檔與資源。

關於本速查

本頁是 R 4.4 的自包含速查手冊,覆蓋 base R 與 tidyverse 在真實數據分析中最常用的約 80% 場景。內容偏向現代慣用法:原生管道 `|>`、匿名函數 `\(x)` 簡寫、`data.frame` 配 `stringsAsFactors = FALSE`、向量化 `ifelse`、`purrr::map_*` 類型化集合操作,以及 R 獨特的複製時修改(copy-on-modify)與 environment 引用語義。權威參考見官方 R 手冊與 R for Data Science。 19 個章節各自聚焦一個主題——從第一個程序、變量與類型到 apply 家族、S3/R6 面向對象、並行與網絡。每節拆成 8–9 個帶示例的小節(每個 5–20 行),共約 160 個主題。代碼片段刻意短小、自解釋,複製即可粘貼進 R 或 RStudio 運行。 所有處理都在瀏覽器中完成——無上傳、無追蹤。本頁是 GuruToolkit 免費開發者工具集的一部分;代碼片段可自由使用,無任何擔保。

版本 2.1.0